Rust のコアに組み込んでいた MIDIFabric のアルペジエーターを、Lua エフェクトとして書き直しました。
コードはコメント込みで約 400 行です。これに操作パネルの定義を組み合わせています。利用者が作るエフェクトと同じ形式で、アプリ内のエディタで読めます。付属テンプレートからエフェクトを作れば、コードの変更もできます。時間の予約と取り消し、MIDI の送信はアプリ(Rust のコア)が受け持ち、Lua は「次にどの音を、いつ鳴らすか」だけを決めています。
操作パネル
ルーティングの画面でアルペジエーターを選ぶと、操作パネルが開きます。

このアルペジエーターが加工するのは MIDI 1.0 のノートです。MIDI 2.0 のメッセージはそのまま通るため、画面の MIDI 2.0 出力をつないだ状態ではアルペジオになりません。
- 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 のタブで、コードを読めます。

入口は二つです。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 の再現性。 Random は、置いたエフェクトごとに独立した、決まった数列を使います。置き直した直後から同じように弾けば、同じ順で鳴ります。
- 停止時に Hold の状態を解除。 ルーティングを止めて再開しても、前に Hold していた音は鳴り始めません。All Notes Off(CC 123)と All Sound Off(CC 120)を受けると、その入力の系列を止めます。
- 新しい音の前に、前の音を止める。 前の音の予約済みの Note Off が、後から新しい音を切ってしまわないようにしています。
確かめたこと
自動テストで、全モード、全パターン、全ルート、全音階、全度数、全アクセントと、音域の端、ノート番号 0、短い音、押し直し、設定変更後の Note Off、クロックがないときと途切れたとき、入力の分離、All Notes Off を確かめています。ルーティングを止めて再開した後に、Hold していた古い音が鳴らないことも、実際のルーティングの経路で確かめました。
なぜ Lua で書いたか
付属のアルペジエーターも、利用者がコードを読んで変更できるようにしたかったためです。Lua に移したことで、テンプレートからコピーして、自分の好みに合わせたエフェクトを作れるようになりました。AI にエフェクトを頼むときも、AI は付属のエフェクトを手がかりにできます(「MIDIFabric と AI を MCP でつなぐ」)。
機器固有の情報はデバイス定義に、エフェクトの処理は Lua に分け、どちらも利用者が編集できる構成にしています。この考え方は「CC 102 が「OP1 Level」と読めるまで」に書きました。