9月の開発記録(2023)

近況: 多忙です

mastodon.cloud

9月はAAPの開発以外ではちょっとsfizzにパッチを作る程度の活動しかしていないので、AAPの話だけを書きます。

AAPは現時点で開発中のGUIサポートもMIDI 2.0出力を使った通知機能も拡張機能のリアルタイムサポートも完成していない状態ですが、新しいプラグインマネージャーを含み、かつ0.7.7と互換性を維持しているバージョンとして、0.7.8リリースをリリースしました。

github.com

0.7.8の主な機能はv0.7.9あるいはv0.8.0に向けて後方互換性を破壊できることなのですが、新プラグインマネージャーと、部分的な拡張機能のリアルタイムサポートが中心です(これについてはすぐ説明します)。

今月はまだ上記の各種タスクが仕掛中ステータスのままなのですが、その設計の話をまとめます。今月はその過程でState拡張とPresets拡張の設計にかかる話を少しだけ切り出してまとめたものも書きました。

atsushieno.hatenablog.com

拡張機能のリアルタイムサポート 前提: AAPXS

AAPでは、モダンなオーディオプラグインフォーマットで一般的に見られる拡張機能の仕組みをサポートしていますが、Androidのようなモバイル環境ではホストでプラグインを動的ロードできず、拡張機能の呼び出し = プロセス境界越え、となるので、単に「他所のプラグインフォーマットAPIを真似して決め打ちで定義する」だけでは実現しません。(いや、まあ最初はそのレベルの認識で始めたわけですが…!)

もしCヘッダーレベルのAPIだけを定義して「実装は実装者の責任」ということになると、各プラグインと各DAWがそれぞれAAPのようなBinder IPCを実装することになって、実装コストがえらいことになりますし、それはおそらく相互運用できないレベルのものになるでしょう。そういうわけで、プラグインDAWの開発者には、一般的なプラグインフォーマットにおけるCヘッダーレベルのAPIの実装を求め、それより先のIPC部分は「拡張機能の開発者がホストとプラグインの橋渡し部分を実装する」というかたちで責務を分散しています。

そのおかげで、AAPの実装はその後2, 3年ほど変遷を経ていますが、拡張機能のIPCにかかる部分は、aap-lv2、aap-juceとも、個別のプラグイン実装を変更する必要が無かっただけでなく、LV2拡張機能へのマッピングやJUCEの機能の実装をフレームワークの変更に追従させるという意味での変更も必要ありませんでした。拡張機能にかかる変更は、全てその下のレイヤーで吸収していた、ということになります。

AAPの標準拡張機能の開発者は自分1人なので(そもそもAAPプラグインもホストも自分しか開発していませんが…!)、実質的に「自分が実装する」とイコールだけど、たとえばLV2のexternal UIみたいな独自拡張を作ろうと思ったら、その開発者はatsushienoと同じ作業をすることになります。

この橋渡し部分は、AAPXS (extension service) と呼ばれ、AAPにはAAPXSの実装をロードするためのC APIがホスト側・プラグイン側の両方についてあります。この部分の舵取りで難しいのは、拡張機能の「実装」は、自分が実装しているAAPの「リファレンス実装」とは切り離されていなければならない、というところです。そうしないと、リファレンス実装のバージョンアップがあって実装の非互換が生じた時に、サードパーティ拡張機能が古いライブラリへの依存関係を引きずってロードできなくなってしまいます。

また、C++ APIではなくC APIとしておくことで、将来コンパイラツールチェインの変更などに伴ってABIに破壊的変更が生じたとしても、その影響を受けなくなります(これに失敗してmingwが必須になったのがC++APIを規定するVCVRackだと言われています)。AAPのリファレンス実装におけるAAPXS実装では、C++が使われています。

AAPXS and RT safety

現在開発中ではない、従来のAAPXSを支えるのは、Android Binderを用いたIPCの仕組みです。Binderそのものはrealtime safetyを実現しているものの、アプリケーションには解放されていないというのが現状で、vendor libraryを作れるなら対応可能、みたいなレベルではあります。それはそれで問題ですが、いずれにせよ、その上で動作するRT safeな仕組みを作ることが重要で、それが無いとベンダーサポートやAOSPへの変更を交渉するとしても単なる画餅になります。

拡張機能の呼び出しは、共有メモリ上に引数と戻り値を配置したうえでextension()というAIDL定義されたメソッドを呼び出す、というかたちで実現しています。このメモリ配置のレイアウトは拡張機能の開発者が自分で規定すれば良いので、ホストやプラグイン開発者、あるいはフレームワーク開発者(atsushieno)が関知することはありません。

ホストがいつ呼び出すかわからない拡張機能の関数から呼び出されるこのメソッドを呼び出すのは、オーディオスレッドというわけにはいかないので、非オーディオスレッドで呼び出すことになります。そのため、AAP 0.7.7までは、拡張機能のサポートは本質的にnon-RT safeということになっています。

また、仮にextension()がnon-RT safeで良いとしても、プラグインがアクティブな状態でオーディオ処理がオーディオスレッド上でリアルタイムで動作しているときに、non-RT-safeなextension()の呼び出しスレッドがそのBinderの制御を奪い取ると、リアルタイムで動作しているオーディオスレッドもブロックされてしまうことになります。これを防ぐには、リアルタイム処理中はオーディオ処理のprocess()のみを許容し、extension()その他すべてのBinderメソッド呼び出しを禁止しなければなりません。

AAPのMIDI 2.0チャンネルの活用

モダンなプラグインフォーマットでは、オーディオ処理において、パラメータの変更などをsample accurateにプラグインが処理できるようなかたちで、イベントリストをオーディオバッファと合わせて渡してやる仕組みが一般的です(イベントリストを渡す仕組みが無い場合は、オーディオ処理とは別のサイクルでパラメーターを設定する拡張機能などを呼び出すことになります)。

この、イベントリストでの拡張機能の呼び出し等を可能にするために、LV2にはAtomが存在します。CLAPにはLV2のような「エレガントな」設計はありませんが、clap_event_*_tの各構造体をclap_plugin_tprocess関数(関数ポインタの呼び出し)に渡せるようになっていて、同様の目的を果たせています。

AAPでも、MIDIのノートイベントなどは、オーディオ処理中のMIDI 2.0入出力チャンネルで、UMPとして送受信することになっています。このチャンネル上でLV2 Atomのような構造を実現すれば、ある程度の拡張機能の呼び出しがRT safeなかたちで実現できることになります。AAPの場合、パラメーターの変更はAAPのMIDI拡張を定義するaap/ext/midi.h上でParameter SysEx8メッセージとして定義されていますが(他にもNRPNやCCを使うことも設定次第で可能です)、これと同様のSysEx8メッセージによって、任意の拡張機能の呼び出しを可能にできるはずです。拡張機能の引数と戻り値は共有メモリ上に書き込まれているメモリブロックなので、これをそのままSysEx8などのデータにしてしまえばよいわけです。この仕組みは、ラベルとしてわかりやすくするためにAAPXS SysEx8と呼ばれています。

そういうわけで、今月は主にこのAAPXS SysEx8の設計作業を行っていました。コードでいうとこの辺:

github.com

(ちなみに、パラメーター変更をNRPNやCCにマッピングできるAAP MIDI拡張の機能を実現していて、残った課題が「プログラムチェンジのメッセージをset_preset()の呼び出しに差し替える」という項目で、それを実現するというのが今回のrealtime safeな拡張機能の呼び出しに繋がったわけですが、枝葉の話なので省略します。)

"set preset" in 0.7.8 plugin manager UI

AAP 0.7.8では、拡張機能の呼び出しを、「部分的に」このMIDI 2.0チャンネル上でシステムエクスクルーシブ メッセージとしてやり取りできる基盤が追加されています。8月に実装した新しいplugin manager UI上でプリセットを変更する処理は、リアルタイムモードの場合はAAP本体の拡張機能呼び出しの機能をスキップして、直接MIDIチャンネルにプリセット呼び出しのSysEx8を追加する仕組みを作って呼び出しています。

これはProof of Conceptをdogfoodingするためのやっつけ仕事でしかないのですが、それはこの仕組みをきちんと導入するとABI breakageが発生して従来のバージョンのAAPプラグインと互換性が損なわれてしまうために、妥協せざるを得なかった部分です。0.7.8リリースの主な機能は後方互換性を破壊…などと書いたのはこのためで、このリリースが終わった今、この部分を一般化する作業に着手できるわけです。

「破壊的変更」としたのは、Binderのextension()呼び出しでは明示的に要求していなかったリクエストとレスポンスのバイナリをUMPにシリアライズするときに必要になりそうだったからです(が、現状では必要なく回っています)。

Reply, and Async Reply

Binderの呼び出しをリアルタイムMIDI2 UMPメッセージで置き換える仕組みは作りましたが、ここに来て面倒な問題が残っていることに気づきました。AAPXSのホスト = プラグイン間のやり取りは同期的なAPI定義に基づいて実装されているということです。

AAPXSを呼び出すスレッドがオーディオスレッドと別であったときは、それぞれの呼び出しスレッドが同期的に呼び出されて結果を返せていればよかったのですが、オーディオスレッドでMIDI2 UMP化してリクエストが送信された結果を、(どれくらいかわからないけど)後になってレスポンスで返されるまで待機して、それから結果をコールバック等で返す、という仕組みにするためには、AAPXSのAPIが非同期呼び出し前提で設計されている必要があります。

この非同期APIの実装において重要なのはスレッディングモデルの切り替えです。非同期呼び出しの拡張機能のリクエストはリアルタイムに送信されて来ますが、そのスレッドで拡張機能をそのまま呼び出すわけにはいかないので、一度non-RTなワーカースレッドに処理を移譲する必要があります。それが完了したら、その結果をAAPXS SysEx8としてホスト側に送り返すことになります。

非同期処理はこのようなモデルになっていますが、一方でリアルタイム呼び出し1回で完結する前提の拡張機能もあって、それらは非同期呼び出しへのディスパッチと結果の収集を前提に設計すると、目的を果たせないことになります。そのため、AAPXSのAPIを非同期呼び出し「のみ」にすることもできません。どちらかを設定することになります。

こんな複雑な設計が求められる時点で、AAPの拡張機能の開発はだいぶ難易度が高くなったな(atsushieno にしかできなそう)と思いましたが(あと「AppleもAUv3で同じくらいややこしい問題をかかえて破綻してほしい〜」とか思いましたが)、まあリアルタイム拡張機能呼び出しを実現するためには超えないといけない壁なので、やるしかないですね。

とはいえ、冒頭に書いた通り、今は異世界の仕事をこなすので無職は手一杯なので、そっちの仕事が落ち着いたらこっちに戻ってきます。