オーディオプラグインの動的ポートに関する覚書

最近自分がやっていることは多分もう自分以外の人にとっては完全にイミフだと思うが、後で歴史を発掘する作業も必要になってくると思うので、記録を残しておこうと思う。自分のオーディオプラグインフレームワーク最近ずっと悩みつつ進めている動的ポートのサポートについてだ。

目次

オーディオプラグインのポート

一般的なオーディオプラグインにはオーディオやMIDIの入出力チャンネルが存在し、DAWなどのホストはフィルターグラフとかオーディオグラフとかチェインとかいったものを形成して、そこにオーディオやMIDIのデータを流し込んで処理させる。プラグインにはそれぞれにさまざまなパラメーターが存在するが、一般的にはプラグインのパラメーターの数は状況に応じて増減したりはしない。

(LV2の場合はパラメーターという存在は全てポートとして処理されるので少し話がややこしくなって、ポートの数はDAWなどのホスト側の「バス」(Bus)の設定に依存してくる(ステレオとか5.1chとかambisonicとかいろいろある)のだけど、いったんバスが決まってしまえば、ポートの数はやはり普通は固定値となる。)

VSTAUでは、プラグインのパラメーターはプラグインインスタンス(ここではvst::IPluginFactoryも含むものとして考えてほしい)から取得するのだけど、LV2はこの点少し違っていてプラグインRDFマニフェストの中に記述する。たとえばGuitarixのマニフェストだとこんな感じだ(長いのでリンクにとどめる)。マニフェストに書いてあると、プラグインのプログラムを実際にロードしなくてもどんなポートが含まれるのか把握できる。ここから、たとえばMIDI入出力のあるプラグインかどうか、といった判断も可能だ(たとえばsfizzにはMIDI入力ポートがあることがわかる)。

動的ポートの存在

もう一度書くが、ポートの数は普通は固定値となる。しかし、このまわりで例外的なプラグインのやつらが存在する。ひとつの典型的なやつはプラグインの「ラッパー」と呼ばれるもの、の一種だ。ラッパーとは、本来サポート対象外である別のプラグイン規格のものをあたかも自分の実装するプラグイン規格のプログラムとして動いているかのように処理するもので、たとえばlv2vstというプログラムはLV2プラグインをまるでVSTプラグインであるかのように処理する。その実態は、ホストから受け取ったデータを単にロードしたLV2プラグインに渡して処理させ、結果をVSTホストに返すだけ、である(だけ、と書いたけど、実際には両規格の仕様には機能的な違いもあってそれなりに大変な作業だ)。ラッパーの多くは静的なものだけど、これが動的にプラグインをロードする仕組みになっていると、必要なパラメーター/ポートの数も変わらざるを得ない。

また、ラッパー以外でも動的ポートが必要になる類のプラグインが存在する。Dexedというプラグインは、往年のYAMAHA DX7などをシミュレートするプラグインだが(DX7部分はGoogleのエンジニアが作成したmusic-synthesizer-for-Androidで、詳しくは音楽ツール・ライブラリ・技術 Advent Calendar 2018の記事を参照されたい)、これはDX7Cartと呼ばれる楽器プログラムリストをロードして楽器として使うプラグインだ。そのパラメーターの数はロードしたCartによって異なってくる。Cartの問題なのか実際にサポートしている音源(DX7以外にもちょいちょい追加されているらしい)なのかは中身を追っていないのでわからないが、重要なのは状況によってポートの数が変わるという事実だ。

LV2 Dynamic Manifest拡張

LV2は事前にポートを列挙する仕組みになっているので、このままでは動的ポートのようなイレギュラーな存在をサポートすることはできない。しかしプラグインラッパーが実現できなかったりするのは不便だ。

LV2はこれを解決するために、Dynamic Manifestという拡張仕様を盛り込むことにした。本来プラグインのプログラムの一部として実装するものではないマニフェストRDFの提供を、プログラム上で動的に行ってホスト側に渡す、という、なんとも奇妙な仕様だ。ポート以外のメタデータも渡せるようだが、DAW側で既にプラグインのリストなどが生成されたりDAW上に表示されていたりするものが、いきなりプラグイン名が変わったりしたら混乱は必至だし、そもそも実現可能かもあやしい。ただ、プラグインのポートが増減する挙動は、ホスト側さえ実装できればこれで対応できる。

それにしても、ポートの数が動的であるのなら、最初からポートの情報はマニフェストに含めないほうが良かったのではないか、という気もするが、そうしないことにはメリットもある。まず、コードで表現すると、プラグインの「種類」を判別するために必要になるメタデータが際限なく増えてしまう。VST3であればPClassInfo, PClassInfo2, あるいはIPluginFactory/2/3と増えている。LV2の場合はPort Propertiesという拡張仕様にプロパティが増えるだけで、APIを増やすことはない(し、APIも拡張として提供してもよいが)。そして、ポート情報をホストとプラグインでやり取りするために、ポートをあらわす型情報が必要になってしまう。また、LV2ではポートについてはPort Propertiesという拡張仕様が規定されているが、もしAPIにしてしまうと、この拡張情報をどのようにやり取りするのか、といった問題も芋づる式に発生する。

こういった煩雑な問題が、「マニフェストのアップデート」であれば何も必要なくなるのである。LV2のDynamic Manifest拡張のAPIでホストからプラグインに渡されるのは、なんとFILE*である。そこに新しいマニフェストを書き出せ、そしたら後はホスト側でよろしく解析する、というわけだ。とんでもないやっつけAPIだな、と思ってしまうわけだけど、実はやっつけではなく、むしろ上記のようなややこしい問題を回避するためのハックなのだ。メタデータのパーサーは当然ホストにはあるはずなので(LV2ならlilvを使えばよい)、実装上のハードルは上がらないといえる。

ポートの追加はプラグインのパラメーターのやり取りの仕組みを利用すればよいのではないか、という考え方もありうるが、オーディオプラグインのパラメーターは、オブジェクト指向言語のプロパティグリッドで設定できるようなさまざまな型が用意されていることは通常はなく、全てfloat配列やbyte配列となっている。LV2のポートも同様で、複雑な構造体についてはAtomという拡張仕様が規定されている。これに既存のポート情報のやり取りを上乗せするAtom構造体を規定する苦労をするくらいなら、既にあるマニフェストのパーサーを使い回せるほうが楽だし覚えることも少なくなる。

プロセス分離モデルでの応用

わたしが開発しているのはAndroid用のフレームワークで、ホストとプラグインは別々のベンダーが開発して別々のアプリケーションとしてインストールするというモデルなので、ポート情報をやり取りする場合にはプログラム上のコードで表されたデータ構造としてそのままやり取りすることはできない(…というのは言い過ぎだが、Kotlin/JVMでもC++でも扱わないといけないので煩雑だ)。メタデータにはLV2同様にポートの情報が記述されているので、動的ポートをサポートするにはLV2のDynamic Manifest類似の拡張機能が必要そうだ。

実のところ、ポート情報をマニフェストに記載する必要はあるのか、という疑問が頭の中をぐるぐる回って、ある程度の解決に至る光明を見出すことがなかなかできなかった。LV2での実現方法とそれにまつわるメリット・デメリット、代替方法などを模索してみて、だいたい同じことをすれば良いだろうと考えるようになった。

実のところ拡張機能まわりは実装できていたと思っていたところがまだ全然足りていない側面があったこととか、LV2みたいに任意のポインタを渡してもらっても、たとえば関数ポインタとか渡されても困るから排除しないといけないということに気づかされた。おそらくプロセス分離モデルを前提とした仕様では同じような回避策を模索しなければならなくなるだろう(AUv3はApp Extensionなのでプログラム情報は共有できそうではある)。

このあたりはUI統合を実現する手法にも影響してくるので、まだ二転三転する可能性はあるのだけど、この辺は技術的に興味のある人が吸い寄せられてコメントしてくるくらい課題としてはけっこう面白いっぽいので、ちまちま実装を進めていこうと思っている。