オーディオプラグインのパラメーター変更と通知に関する覚書

最近自分のプラグインフレームワークプラグインパラメーターのサポートを設計しているのだけど(パラメーターのサポート無かったの!?と思われそうだけど、これまでは古いLV2と同じでパラメーター毎に「ポート」を用意するスタイルで実現していたのを再設計している)、特に変更通知まわりは割と面倒なところがあるなと思ったのでちょっとまとめておきたい。

パラメーター操作の分類

オーディオプラグインのパラメーターを操作する主体は2つ、あるいは3つある。

(1) ホスト(DAW)のシーケンサープラグインprocess()とかrun()とかに相当するオーディオ処理関数が呼び出されたときに、MIDI…に類するイベント…としてパラメーター設定命令が(理想をいえばタイムスタンプ付きでsample accurateに)そのprocess()あるいはrun()のサイクルの分をまとめて渡される。データの元がMIDIマッピングされたMIDIシーケンスであろうと、オートメーションであろうと、APIとしてはこれらの関数に行き着く。これらの関数はMIDI…に類するイベント…のアウトプットも取得でき、ホストはこれを現在処理中のフィルターチェインにある次のプラグインに渡すことができる(これはおそらくは一度MIDIに類する抽象イベントに変換することになる…というのは、たとえば、VST3のチェインの次にAUが繋がっていたらVST3イベントそのものを渡すことはできないからだ)。

(2) プラグインのUI操作。一般的に、GUIはリアルタイムで処理できないので、リアルタイムのオーディオ処理ループからは切り離されていて、したがってスレッドコンテキストも分離しているので、GUI上で生じたパラメーター変更のイベントは、いったんプラグインの内部でキューに溜め込まれて、次のprocess()run()の中で(理想をいえばタイムスタンプ付きでsample accurateに)処理される。ホスト側でプラグインのパラメーターの状態(現在値)を把握しておきたいことを考えると、UIによるパラメーター変更はホスト側に通知されるのが理想的で、したがってプラグインがホストに通知イベントを送信する仕組みが必要になる。

(3) オーディオ処理そのもの。これはパラメーター変更の入力ではなく出力 = 通知の話になるが、パラメーターには他のプラグインパラメーターの変更によって計算される性質のものがあり得る。これらはオーディオ処理に直接影響するとは言い難い(その関連パラメーターの変更は上記のいずれかですでに渡されている)が、これによって生じたパラメーターの再計算は、その変更を通知されるホストにとっては、プラグイン上にに生じたパラメーター変更と同義だ。

ただ、これら(1)〜(3)の処理モデルは、リアルタイムのオーディオ処理ループが稼働していることを前提としている。もしオーディオループが回っていなければ、UIによる変更を反映する次のprocess()run()は回ってこないので、それに伴って生じるべき変更通知はずっとやってこない。オーディオループが回っていない状態でもイベント通知が滞りなく届くようにするには、オーディオ処理が回っていない状態でもパラメーター変更が処理される仕組みが必要だ。そうしないとプラグインGUIで設定したパラメーターの値が、ホストで必要な箇所に反映されなくなってしまう。

UIイベントをプラグインに蓄積させるAPIプラグインにイベントを処理させるAPI

上記のうち(2)に関しては、一般的にはGUIプラグインのプロセスの中で動作しているので、GUIが発生させるイベントは同じプラグインの内部でイベントキューを保持しておいて、オーディオ処理が走るたびに内部で(1)のイベントストリームとマージして処理することになると考えられる。この場合、外部にAPIとして表出させることはない。

一方で、LV2 UIのように、プラグイン規格そのものがGUIをきちんと分離して、外部から入力をデータとして渡す仕組み担っている場合、あるいはその他のプラグインでもUIをきちんと分離する仕組みにしている、あるいはプラグイン本体とプロセス分離せざるを得ない場合(AAPがUIをホスト側で生成するとそうなる)、イベントキューイングのためのAPIプラグイン側から表出している必要がある。LV2 UI拡張のLV2UI_Port_Subscribe LV2UI_Write_Functionはこれを体現したAPIと考えてよいだろう。このAPIはイベントのキューイングのためのAPIであって、イベントを処理するためのAPIではない。

一方で、前述の通り、リアルタイムのオーディオ処理が停止している状態でもパラメーター変更が処理される仕組みが必要で、これはprocess()run()と同様に、イベント出力を生成する必要がある。このAPIの具体例と言えるのがCLAPのparams拡張に含まれるclap_plugin_params_tに含まれるflush()だ。LV2のGUIに変更を反映させるための仕組みとは性格が異なる。(このflush()の位置付けは実のところ曖昧だというissueも立っているが、本質的には書き方の問題であって仕様の設計がぼやけているということではないはずだ。)

別ベクトルの解決策として、そもそもprocess()run()をリアルタイム処理中以外でも呼び出せるようにすれば良いのではないか、そうすればイベント処理の実装はこれらのオーディオ処理関数で一元化できるのではないか、とも考えられるが、それはそれできちんと入力を処理できないプラグイン(すなわちオフラインレンダリングに対応できないプラグイン)が存在する可能性があって、この路線を押し通すには事前に調整されて然るべきということになる(たとえばAAPではまだ仕様が策定中なのでそういう前提を作れる)。

これが多分難しいのは、DSPの処理がリアルタイム前提で時間計算するようになっていたら、オフラインレンダリングには対応できないということと、DSPは特定のプラグインフォーマットから独立して実装されていることが多く、既存のDSPコードがオフラインレンダリングに対応できていない場合、process()run()がオフラインレンダリング対応を前提にしていると、そのDSPそのものが利用不可能になってしまうかもしれない、とかもしれない、といったことが考えられる。

非同期イベントとしての通知出力と、同期イベントとしての通知出力

リアルタイムのオーディオ処理のバックグラウンドで動作するUIを経由したパラメーター変更は、一般的には非同期イベントとなる。これに対して、オーディオ処理を経てのパラメーター変更通知が非同期となるべきかどうかは議論の余地がある。一般的には、UIコードがパラメーター変更のAPIを呼び出しても、前述の通りいったんキューイングされるだけだ。一般的にはキューイングするだけの処理の中で、パラメーター変更通知に値するプラグインの処理が発生するとは考えられず、一般的にはオーディオ処理の「結果」として通知に値するイベントが生成されるはずだ。キューイングの時点で通知イベントが生成されることを期待するようなAPIにしていると、何がしたいのかよくわからないぼやけたAPIになってしまう。

前述のclap_plugin_params_tに含まれるflush()を取り上げると、この関数はリアルタイムオーディオ処理が走っていないときに呼び出されてclap_output_events_tにイベント出力を格納することが想定されており、これは同期的なAPIだ。逆に、LV2 UI拡張にあるLV2UI_Write_Functionport_eventのようなAPIでは、その関数呼び出しから何らかの通知イベントが生成されることは期待されていない非同期的なAPIだ。そもそもGUIからプラグインを経ずにイベントを生成することは推奨されない。

CLAPのparams拡張のような同期的にイベント処理を行う方式については、もうひとつ言及しておくことがある。CLAPの場合、パラメーターの変更はプラグイン側で発生するイベントだが、プラグイン側からホスト側に対してイベント出力を直接送信する仕組みにはなっていない。その代わり、プラグインはホストから事前に渡されているホスト側のコンテキストであるclap_host_params_tのメンバーrequest_flush()を呼び出して、あくまでホスト側からclap_plugin_params_tflush()を呼び出すことを期待している。こうすることで、通常のオーディオ処理とイベント通知による処理のモデル(特にスレッディングモデル)を統一できる。オーディオ処理がリアルタイムで動作している状態であれば、request_flush()を呼び出さなくても、次のサイクルで通知イベントを出力するだけでよい。

パラメーター拡張APIは誰のためのものか (8/31追記)

前回はパラメーター変更と通知のAPIの類型について解説したが、特にLV2とCLAPでの位置付けがだいぶ異なることが見て取れた。なぜそのような違いが生じるのかを、少し踏み込んで考えたい。

一番根本的な違いは、それらのAPIを「誰が」使うのかを明確にすると見えてくるだろう。

  • CLAPの場合、拡張プラグインAPIを呼び出すのは常にホストであり、拡張ホストAPIを呼び出すのは常にプラグインだ(と自分は理解している)。パラメーターのflush()はホストが呼び出すものだし、プラグイン側はプラグインAPIを呼び出すことはない。プラグインGUIコードがプラグインDSPコードで利用するパラメーターを設定するために使用するAPIは存在しない。それはCLAPのスタンスでいえば、プラグイン本体が内部的に解決(提供・実装)すべきものであって、プラグインとホストのインタラクションに影響しないなら規格化されない。オーディオプラグインデザインパターンのようなものは、CLAP全体として標準化されない傾向があるように見える。
  • LV2の場合、LV2 UIの実装はLV2 DSPの実装とは別の共有ライブラリになり、パラメーターの変更と通知は接続されたポート経由で行う。LV2 UIの実装とLV2 DSPの間でインターフェースすなわちAPIが必要になる。

ちなみに、この一連の覚書でCLAPに関してparams拡張の話だけしてgui拡張の話を一切しないのは、CLAPのgui拡張は本当にGUIを扱っているだけで、GUIから発生するであろうパラメーターの変更イベントに関しては一切関知していないからだ。この辺CLAPの切り分けは美しくできている。最初に見たとき「拡張機能が"UI"ではなく"GUI"なのはセンス無いな…」と思ってしまったものだけど、この拡張は本当にGUIのためのものであって、たとえばUIとDSPの分離などを目的としてはいない。