LV2のGUIサポートのトレンドに関する覚書

追記: zrythm作者からいただいたコメントがプラグイン開発者側の最新のトレンドについて詳しく言及しているのでそちらも参照されたい。

目次

オーディオプラグインGUI実装の一般論

一般的に、オーディオプラグインのUIはオーディオ処理の本質ではなく、DSPからは独立して構築できる部品だけど、どのプラグインフレームワークGUIが存在することは想定されている。DSPはリアルタイム処理のサイクルで実行可能であることが要求されるけど、一般論としてGUIをリアルタイムに処理することはできない*1

一般的に、オーディオプラグインはパラメーターやポートで入力を受け取ってDSPの処理に反映する。GUIの操作結果をパラメーターに反映できるようにすれば、オーディオプラグインの機能としては十分だ(パラメーターに反映できない部分はstateとして保存する)。また、一般的にGUIの処理はロジックと分離していることが期待されるが、GUIコードがパラメーターやポートを経由してのみオーディオプラグインの動作を制御する仕組みになっていれば、これは自然と実現できていることになる。すごい。XAMLだけでUIを書いていれば自然にMVVMになります!みたいないかがわしさがチョットある。いずれにせよ、GUI部分はリアルタイムをあまり気にせずに*2、一般的なGUIプログラミングによって実現することになる。*3

一方で、一般的なGUIプログラミングと同じということは、プラットフォームの壁が問題になるということでもある。VSTにはVSTGUIというGUIツールキットがあり(もちろんWindows APIを直接使うこともできる)、AUの場合は最初からApple OSでしか動かないのでCocoa (Touch)が使えることが前提にあると考えられるが、LV2については標準的なGUIツールキットは存在しない。LV2はクロスプラットフォームが前提だし、LinuxだけでもgtkとQtだけでも勢力が二分しているし、他にもX11ベースのツールキットがいくつか存在する(JUCEやWDLもそうだし、FLTKやその他GL系のソリューションもある)。これらを全てフレームワーク側が列挙し対応を表明するのは非現実的だし、それは標準的な技術(de facto standardも含む)で行うべきことではない。

LV2 UI feature

LV2では、コア仕様以外は全て拡張機能としてホストから提供したりプラグインで実装したりする仕組みになっている。LV2 UIについても拡張として仕様がまとめられている。

LV2拡張の仕組みを解説するためには本来RDFやらTurtle Syntaxやらも解説しないといけないのでしんどいのだけど、ここでは要点だけかいつまんで説明する。

LV2の拡張機能は全てマニフェストで判別できる。あるLV2プラグインがUI機能を提供しているかどうかは、http://lv2plug.in/ns/extensions/ui#ui のカテゴリの拡張の有無で判別できる。筆者が開発しているUIありプラグインを例として挙げると:

<https://github.com/atsushieno/aria2web>
  a doap:Project, lv2:Plugin, lv2:InstrumentPlugin ;
  ...
  ui:ui <https://github.com/atsushieno/aria2web#ui> .

<https://github.com/atsushieno/aria2web#ui>
  a ui:Gtk3UI ;
  ui:binary <aria2web-lv2ui.so> .

という感じだ。

doap:, lv2:, ui:プレフィックスで本来は名前空間宣言も載せるべきなのだけど、少しLV2プラグインを開発してみればこれらはほぼ自明なのでここでは省略する。これはlv2:Pluginである(かつdoap:Projectlv2:InstrumentPluginでもある)https://github.com/atsushieno/aria2webというオブジェクトに、ui:uiプロパティとしてhttps://github.com/atsushieno/aria2web#uiというオブジェクトを設定している。後者のオブジェクトはui:Gtk3UIで、aria2web-lv2ui.soというui:binaryに実装が含まれている。

LV2 UI拡張の中では、このGtk3UI以外にも以下のようなプロパティが定義されている:

  • WindowsUI
  • CocoaUI
  • X11UI
  • GtkUI
  • Qt4UI
  • Qt5UI

ひとつのプラグインで複数のGUIフレームワークをサポートする場合は、このui:uiプロパティのオブジェクトを複数定義することになる。

LinuxデスクトップでLV2をサポートするDAWプラグインをホストする場合は、プラグインごとに適切なUIをロードして実装するのが適切なやり方ということになる。Qt5アプリであるQTractorであればQt5UIを、Gtk3アプリであるzrythmであればGtk3UIをロードして使えばよい。

UI拡張API

LV2UI拡張のロード手順はLV2プラグインそのもののロード手順と似ている。共有ライブラリの中にはlv2ui_descriptor()というエントリーポイント関数が定義される。

const LV2_SYMBOL_EXPORT LV2UI_Descriptor *   lv2ui_descriptor (uint32_t index)

LV2UI_Descriptorには、ホストから呼び出され、ホストがUIを制御するために必要な関数などが含まれている。

trydef struct {
  const char *  URI;
  LV2UI_Handle(*    instantiate )(
      const struct LV2UI_Descriptor   *descriptor, 
      const char *plugin_uri, 
      const char *bundle_path,   
      LV2UI_Write_Function write_function, 
      LV2UI_Controller controller, 
      LV2UI_Widget *widget, 
      const LV2_Feature *const *features);
  void(*    cleanup )(LV2UI_Handle ui);
  void(*    port_event )(
      LV2UI_Handle ui, 
      uint32_t port_index,   
      uint32_t buffer_size, 
      uint32_t format, 
      const void *buffer);
  const void *(*    extension_data )(const char *uri);
} LV2UI_Descriptor;

ここでinstantiateで渡されるwrite_functionは、プラグインUIコードがユーザーからの入力をプラグインのポートに出力する時に使う。

LV2UI拡張には、他にもhttp://lv2plug.in/ns/extensions/ui#parentという拡張がある。上記のextension_data()にこのURIを渡すと、ホスト側でプラグインのUIコンポーネントの親コンポーネントが返ってくる(拡張なので、あくまでホストがサポートしている場合に限る)。ホストとプラグインのUI拡張は、これらを使えば最低限の制御が可能だ。この基本的なインターフェースには、GUIフレームワークに固有の部分が何ら存在しないというのがポイントだ。

GUIフレームワークのミスマッチ

オーディオプラグインユビキタスに提供するのは困難な仕事だ。Windowsだけ、Macだけという開発者が多い中、Linux版も頑張って提供する開発者は多くはない。オーディオ処理はクロスプラットフォームで記述できることが多いが、GUIはそうもいかない。なのでJUCEが幅広く使われることにもなるし、VSTGUIなどでクロスプラットフォームで実装することも出来るわけだが、それでは足りずGUIフレームワーク固有の機能を使って実装したい場合も多いかもしれない。

しかし…実のところ、そんな立派な目的でGtkやQtを使っているわけではないことも多いだろう。LV2はLinuxで使われるだけだし、自分の使っているArdourやQTractorで使えれば十分…という開発者は、自分の環境で使えるUIだけ提供することになるだろう。

実際、筆者が開発しているWebViewベースのプラグインUIはwebkitgtk3を簡単に使えるライブラリを使っている都合上、Gtk3UIしか提供できない。Qt5UIを提供するにはQtの使い方を勉強して、Qt5WebViewなどの使い方を調べないといけないし、とてもできるとは思えない*4X11UIを提供するにはもうCEFでも使って再実装するしかない。

もちろん、これはプラグインだけでなくホスト側についても言えることだ。QTractorはQt5UIだけをサポートするしArdourはGtkをホストするのみだろう。つまり実際にはこんな感じになる:

これで最終的に何が起こるかというと、DAWフレームワークでサポートされていないプラグインUIは表示されないことになる。ただでさえ狭いLV2プラグインの世界がさらに分断されているのが現状だ。また、プラグインのエコシステムがGUIフレームワークから切り離せなくなると、GUIフレームワークを乗り換えたり、新しいものを使っていく(たとえばFlutter on Desktopとか)といったチャレンジが難しくなる。

LV2はUIが無くてもポートの定義だけでもそれなりにパラメーター入力が可能なので、困らないことはそれなりにある。たとえば次の画面はsfizzをzrythmでロードした時に表示されるダミーUIだけど、ちゃんとファイル名まで渡せるようになっている。

f:id:atsushieno:20200510103512p:plain
sfizz on zrythm

QTracktorでも似たようなしっかりしたUIが出るし、VSTAUでもある程度は可能だろう。内部的にはプロパティグリッドを作っているようなものだ。しかしWindowsMacに比べて恵まれていない状況にあることは間違いない。

suil

プラグインUIの分断状態は好ましくないが、幸い解決困難な問題ではない。というのは、プラグインプラグインUIのインターフェースはLV2 UI拡張の規定する範囲に限られている。それであれば、他のGUIフレームワークに基づいてLV2 UIの機能を「ラップ」しつつ、ホスト側には対応するフレームワークに基づく情報を渡すような仕組みがあれば、どんなホストであっても任意のフレームワークGUIを呼び出すことが可能になる。プラグインのラッパーとおなじような発想だ。

これを実現しているのがsuilというライブラリだ。

gitlab.com

suilは自身がX11/Qt5/Gtk/Gtk3のLV2UI_Descriptorの内容をロードしUIホストのように振る舞い、X11やQt5やGtk3のプラグインホストが必要とする情報をホストに渡すことができる。suilがサポートするUIをホストする部分が整っていれば、ホスト側は比較的簡単にnon-nativeなプラグインGUIもサポートできるようになるというわけだ。

suilはLV2の開発者(標準仕様のように扱われているが、これを規定しているのは1人の開発者だ)が自ら開発していて、lilvなどと同様、半ば公式SDKの一部のような存在となっている。

ソースの構成を見れば、これがどういう泥臭い仕事をしているかがわかる:

f:id:atsushieno:20200510103540p:plain
wrappers in suil

前節では次のように書いたのだけど

QTractorはQt5UIだけをサポートするしArdourはGtkをホストするのみだろう。

これは実はウソである。数日前にリリースされた最新版のQTractorは、このsuilを組み込むことで、GtkUIとX11UIもサポートするようになったので(リリースノート参照)、いずれパッケージされて各種distroで使えるようになるだろう。それまではウソではない。

Gtk3サポートの課題とexternal UI

suilは銀の弾丸ではなく、ホストと繋ぎこむ部分はまだまだGUI種別ごとに面倒を見てやらなければならない。suilのAPIを使えばある程度問題が緩和できるということにすぎない。また、suilが直ちに全てのGUIフレームワークをサポートできるようにするわけではない。たとえば、最新版のQTractorでもGtk3はサポートされていない。コードならこんな感じでGtk3だけ見当たらない状態だ。

LV2 UIのドキュメントでは、X11UIについてはX11Windowをparentとしてホストから渡すことが想定されている書き方になっているが、Gtk3についてはGtkWindowを渡すとは書かれていない。実際、zrythmがGtk3UIに渡すのはGtkEventBoxになっていて、整合性が無い。zrythmがGtkEventBoxを渡しているのにQTractorがGtkWindowを渡すようになってしまうと、これはややこしい非互換問題になる。想定される挙動が不明なので、QTractorではまだサポートされていないのだろう。

このあたりでやや反則的に特別扱いされているのがCarlaなどを公開しているkxstudioで、zrythmも最新のQTractorも、kxstudioの名前空間を含むUI拡張が定義されていると "external UI" モードになって、独自のアプリケーションループを回す存在として処理されるようだ。

筆者のWebViewベースのプラグインUIも、使っているライブラリがGtkWindowを返すためにzrythmでGtk3UIとしてまともに動作しないので、このexternal UIモードで動作させたいのだけど(それで動くのかどうかはわからない)、LV2 UI標準に含まれていないので困っている状態だ。とはいえ、もともとLV2には存在していてむしろdeprecatedになったものらしい。zrythmが内部的に定義しているexternal UIの型などを見る限り、show/hide/runくらいしか定義されておらず、これではロジックとUIの分離が実現できていなかった、といった当たりの事情で廃止されたのだろう。

いずれにせよ、この辺りはどうやら未整備で、今後状況が変わってくるポイントかもしれない。

まとめ

LV2はGUIフレームワークに依存せずにプラグイン本体を制御するために必要な情報をRDFマニフェスト(ttl)に記載しているので、GUIプラグインの役割を自然に分離できているし、suilのようなラッパーを作って複数のUIフレームワークをサポートすることも可能にしている。もっとも理想と現実の間にまだギャップがあって、Gtk3サポートみたいな部分がこぼれ落ちることがある。この辺は2020年に現在進行形で起こっている出来事であり、近いうちに解決されていく可能性も十分にある。

*1:マルチスレッドで動作するGUIがすでに非現実的で、さらにそのうちのひとつがリアルタイムで排他処理を伴わずに一貫した状態をDSPに提供できなければならない

*2:プラグインのデータを変更する時にatomicな更新を心がける必要があるかもしれない程度だ。これはプラグインによる

*3:2020/5/10追記: 実のところこれはやや過度に一般化しているフシがあって、ホストからのウィンドウ表示への対応など一般的でない考慮事項がちょいちょいあるのだけど、今回はそこは主な話題ではないので割愛したい。

*4:難しくて理解できないという話ではなくて、他にやるべきことがいくらでもある