更新日:
確認環境: MIDIFabric 開発版(macOS)。Lua 5.5。自動テストで全モード・パターン・音階を確認、実機の楽器での演奏は未確認

MIDIFabric のアルペジエーターは、以前は Rust のコアに組み込まれていました。それを、付属の Lua のエフェクトとして書き直しました。

今のアルペジエーターは、コメントを含めて約 400 行の Lua のプログラムと、操作パネルの定義でできています。利用者が作るエフェクトと同じ形式なので、コードはアプリの中で開いて読めますし、付属のテンプレートから作ったエフェクトは書き換えられます。時間の予約と取り消し、MIDI の送信はアプリ(Rust のコア)が受け持ち、Lua は「次にどの音を、いつ鳴らすか」だけを決めています。

操作パネル

ルーティングの画面でアルペジエーターを選ぶと、操作パネルが開きます。

ルーティングの画面で Arpeggiator1 を選び、Time base、Note value、Gate、Octaves、Mode、Accent、Pattern、Hold の操作パネルを開いたところ

  • Time base: 間隔をミリ秒で決めるか、MIDI クロックに合わせるか。クロックに合わせると、テンポの変化に付いていきます。
  • Note value / Time: 1 ステップの長さ。クロックのときは 1/8 などの音価、ミリ秒のときは時間で指定します。
  • Gate: 1 ステップのうち、音を鳴らしている割合。0.6 なら 6 割です。
  • Octaves: 押さえた音を何オクターブ分に広げるか。
  • Mode: Held は押さえた音を順に弾きます。Degree は、最後に押した音を選んだ音階の音に合わせ、1-3-5 などの度数で弾きます。Degree を選ぶと、Root、Scale、度数の操作が現れます。
  • Accent: ステップごとの強弱のパターン。0 のステップは休符になります。
  • Pattern: Up、Down、Up / Down、Random。
  • Hold: 鍵盤を離しても弾き続けます。

パネルの部品と配置も、コードではなく定義(Params と Layout)に書いてあります。パネルの見た目を変えるのに、アプリを作り直す必要はありません。

コードの組み立て

エフェクトの編集画面の Program のタブで、コードを読めます。

エフェクトの編集画面の Program タブに、アルペジエーターの Lua のコードが表示されている。時間の足し方、ノートの解放、系列の停止を行う関数が見える

入口は二つです。MIDI が届いたときに呼ばれる on_midi と、予約した時刻に呼ばれる on_scheduled です。

  • on_midi は、押された鍵と離された鍵を記録します。最初の Note On では、すぐに 1 音目を鳴らします。
  • 1 ステップの処理は tick にまとめています。鳴らす候補を作り、強弱のパターンから休符かどうかを決め、音を 1 つ鳴らして、次のステップを予約します。
local function tick(ctx, stream, id, due)
  local p = ctx.params
  local notes = candidates(stream, p)
  if #notes == 0 then
    stop_stream(ctx, stream, id)
    return
  end

  local now, interval, step_due = step_timing(ctx, stream, due)
  -- A zero accent is a rest: advance the accent step without consuming a pitch.
  local accent = accents[p.accent]
  local gain = accent[stream.step % #accent + 1]
  stream.step = stream.step + 1
  if gain > 0 then
    local index = next_pitch_index(stream, #notes, p.pattern)
    play_note(ctx, stream, notes[index], gain, now, interval)
  end
  schedule_next(ctx, stream, id, now, interval, step_due)
end

押されている鍵の管理は、付属の共通の Lua モジュール note_state に任せています。同じモジュールは、ほかのエフェクトも使えます。

時間はアプリに任せる

Lua は、タイマーを自分で回しません。ctx:schedule で「この時刻にもう一度呼んでほしい」と予約し、ctx:schedule_emit で Note Off を予約し、要らなくなった予約は ctx:cancel で取り消します。予約を管理し、時刻どおりに送るのはアプリの側です。

  • クロックに合わせるときは、時刻を拍で数えます。ミリ秒の時刻とは混ぜません。
  • 呼び出しが遅れたときに、過ぎたステップをまとめて鳴らすことはしません。次の 1 ステップだけを、今から予約し直します。
  • クロックをまだ受け取っていないときは、アルペジオを始めず、弾いた音をそのまま通します。

細部で決めたこと

以前の Rust の処理と、移植する前の定義を読み比べ、良いほうを選んだり直したりしました。

  • 入力ごとに別の系列。 送り元のポート、プロトコル、グループ、チャンネルが違う入力は、1 つのアルペジオに混ぜません。
  • Up / Down で端の音を繰り返さない。 3 音なら、下から上へ、上から下へと往復し、いちばん上と下の音は 2 回続けて鳴りません。
  • 設定の変更は次のステップから。 鳴っている音の長さや、予約済みの次のステップの時刻は変えません。
  • 乱数は再現できるもの。 Random は、置いたエフェクトごとに独立した、決まった数列を使います。置き直した直後から同じように弾けば、同じ順で鳴ります。
  • 止めた後に、古い音を生き返らせない。 ルーティングを止めて再開しても、前に Hold していた音は鳴り始めません。All Notes Off(CC 123)と All Sound Off(CC 120)を受けると、その入力の系列を止めます。
  • 新しい音の前に、前の音を止める。 前の音の予約済みの Note Off が、後から新しい音を切ってしまわないようにしています。

確かめたこと

自動テストで、全モード、全パターン、全ルート、全音階、全度数、全アクセントと、音域の端、ノート番号 0、短い音、押し直し、設定変更後の Note Off、クロックがないときと途切れたとき、入力の分離、All Notes Off を確かめています。ルーティングを止めて再開した後に、Hold していた古い音が鳴らないことも、実際のルーティングの経路で確かめました。

まだできないこと

  • MIDI 2.0 のノートは加工しません。 今のアルペジエーターは MIDI 1.0 のノートだけを扱い、MIDI 2.0 のメッセージはそのまま通します。上の画面では MIDI 2.0 の鍵盤の出力がつながっていますが、この組み合わせではアルペジオになりません。
  • 実機の楽器での弾き心地、クロックが途切れた後の再開の実機での確認、iPad のタッチ操作の確認は、まだです。

何か分かったら、このページを更新します。

なぜ Lua で書いたか

アルペジエーターをアプリに組み込んだままだと、付属のエフェクトだけが特別な部品になります。Lua で書けば、付属のエフェクトも利用者が作るエフェクトも、同じ部品になります。付属のコードはそのまま読める見本になり、コピーして自分の好みに変えることもできます。AI にエフェクトを頼むときも、AI は付属のエフェクトを手がかりにできます(「AI に MIDI アプリを扱わせる、Mac の外へ出さずに」)。

機材の知識をデバイス定義というデータで載せるのと同じく、振る舞いは Lua のプログラムとしてアプリの上に載せています。この考え方は「CC 102 が「OP1 Level」と読めるまで」に書きました。

← 開発ノートの一覧