2025年12月14日日曜日

Web 上で使える Timidity++ っぽい MIDI プレイヤーを作った

Timidity++ っぽい MIDI プレイヤーを作りました。 前回に引き続き、Midy の機能確認やバグ潰しが目的で作ったアプリですが、 割と人気の UI ではあるので使い道もあるかも知れません。 本当は紹介のためにスクリーンレコーダーで録画してみたのですが、 Web Audio API がうまく録音できなかったです。音なしだと面白くないので動画は特になし。



私も割と好きな UI ですが Velocity は鍵盤の色で表現するほうが良いと思っていたので、 64〜191 の間で色を調整して表示しました。これだけでだいぶコンパクトにできます。 それ以外は Timidity++ とだいたい同じですが、簡易的なミキサーアプリとしても使えるようにしています。 スライダーがありきたり過ぎるので改良しようかとも思ったのですが…、 面倒すぎてデフォになりました。スライダーは弄ろうとするとなかなか大変です。

実装では鍵盤をどう光らせるかで多少悩みました。 Web Audio API でやろうとすると、ended event はありますが、started event がないので、意外と難しいです。 AudioBufferSource.start() と同じタイミングで動くようにタイマーを設定すると、 タイマーの数が 2倍になってしまって速度の問題が出てきてしまいます。 Web Audio API を使ったタイマーだと負荷が大きくなってしまうので、 requestAnimationFrame() で描画タイミングと同期しながら、 noteOn などの時間を監視して光らせるのがおそらく一番効率が良いです。 鍵盤を光らせるには Midy 内部で処理するイベントのタイムラインを見ながら処理します。

Midy の再生開始判定と、@marmooo/midi-player の判定は微妙に違っています。 @marmooo/midi-player は再生ボタンを押したときにサウンドフォントの読み込みもします。 このへんの処理をどうすれば楽にできるかを考えていて少しだけハマったので、 再生ボタンを押したときから isPlaying = true な変数を持つように実装を加えたところ、 割と簡単に GUI 拡張を作れるようになりました。 GUI 側でも isPlaying の状態を持つことで、再生の終了判定も容易になりました。 最近の Chrome は AudioContext をつけっぱなしにすると CPU をかなり消費するようになったので、こまめに AudioContext を ON/OFF しています。 Chrome の AudioContext の CPU 負荷はバグとしか思えないので、 そのうち直っているかも知れませんが。

最後に鍵盤を鳴らす実装をしていると、グリッサンドに耐えられない問題が発生しました。 事前にすべてをキャッシュするのも手と思ったのですが、 keyRange/velRange と 2つパラメーターがあるのでちょっとメモリが心配です。 そこで await で音声波形をデコードする前にスケジューリング配列に追加してしまうことで、 問題なく再生できるとわかりました。 ただこれだけだと処理時間が少ないことで運良く実行できているだけだと思うので、 スケジューリング配列に登録するとき note に pending 状態を付けて、 pending 中に noteOff が発生したときは、noteOn の処理内で noteOff も実行するようにしました。 これならリアルタイムな割り込みが発生しても問題なく処理できるはずです。

ここまでの実装で Timidy は動くようになったので公開としましたが、色々と余地はあります。 例えば midy 0.4.0 の noteOn は、SF2 modulation に渡す状態配列や noteOff と整合性を保つために await を付けて処理していますが、波形のデコードが 〜50ms で、 状態配列 (256 x 32bit = 1KB) のコピーが 〜0.02ms であることから、 noteOn から noteOff までの間に状態配列と SF2 modulation に変化がなければ、 await せず状態をコピーしながら処理でき、Worker で並列処理で高速化可能のはずです。 面倒そうだけど並列処理すればグリッサンドに対してより頑強になるはずです。 これもそのうち検証しようとは思いますが、まずはイベント数を減らすほうが確実とは思います。

0 件のコメント: