6月の作業記録

このシリーズのタイトル使うのひさしぶりなんですが、先月書いたaria2webも実質的に作業記録みたいなものなんで、掘り起こして復活させた、みたいな感じではありません…。

6月の、と書き出してしまいましたが、前回からの差分だけにしておきます。3週間くらい?

designing Audio Plugin Framework for Android @ COSCUP 2020

台湾の大規模コミュニティカンファレンスCOSCUPで2年ぶりにしゃべることにしました。内容は今開発しているAndroid向けオーディオプラグインフレームワークの設計アプローチについて、ということになります。7月はその準備作業に着手する予定です。

2018年もlanguage server protocolについてのセッションで、かなり.NET色を排除した内容だったのですが(それまではMonoDevelop -> Monoランタイム -> .NET Coreだった)、今回は完全に過去を切り捨ててオーディオの話をします。どっちも単独で引っ張るとまあまあ素人なのですが、複合領域だからな…!

AndroidiOSもなのですが、モバイルOSは実行可能なコードの制約が強く、プロセス分離から逃げられないので、それを踏まえたオーディオプラグイン機構が必要になります(この話題ここではしょっちゅう出してますが)。オーディオはリアルタイム処理も考慮しないといけないので無理ゲーなのでは…!?となってしまいますが、Android 8.1で「Realtime IPC Binderが実装された<折返し>か!?」みたいな状況になっていたりする…など無駄に深入りした話などを交えつつ、基本的にはどういう設計が求められるかという上澄みの話をする予定です。

プラグインUIの仕組みなども実装しておきたいのですが、時間的に厳しそう…(30分しかないし)。実装基盤もガタガタなので、おそらくその辺を強化して終わりでしょう。

ちなみに台湾のカンファレンスのスピーカーといっても、それだけで業務出張になるわけではないので、渡航資格が発生するわけではありません。そんなわけで日本からのオンラインセッションとなる予定です。準備というのはだいたいそっちの練習という感じです。やってみないとわからないですしね。基本的には会場のネットワークに繋いでビデオ通話するだけなのですが、もしかしたら日本からでも見えるようになるかもしれません。現状何もわかっていません。

aap-sfizz

今月の上旬に書きましたが、ARIAのUIをWebViewで表示できるようになって、sfizzとも組み合わせてオーディオプラグインとして使えるようになったので、今度はこれをAndroidで動かそうと思ったわけですが、LV2プラグインの移植はmda-lv2しか実績がなく、その土台を整備する作業が必要になりました。JUCEのプラグインならほぼProjucerのプロジェクトがあるわけですが、LV2プラグインはプロジェクトによる感じです。

sfizzはCMakeのプロジェクトなので、プロジェクトファイルを取り込むという意味では難易度がやや低いのですが、Android用のLV2開発ライブラリをパッケージとして参照解決できる状態にはなっていません。理想を言えば、aarを参照に追加するだけで解決したいところです。

この目的ではAndroid Studio 4.0に新規追加されたPrefabフォーマットのサポート(4月に書いたやつ)が解決することが期待されたのですが、全くうまくいく様子がなく、AGPのソースを追っかけてもそもそもprefabツールがセットアップされる様子もなく、Prefabパッケージの妥当性を検証する機構が全く無いので(こっちはLV2で使われているwafのビルド結果を手作業でバンドルするしかない)、まだまともに動作させるのは無理だという結論になりました。この辺の機能は今のAndroid NDKのプロジェクトリーダーが仕切っているっぽいのですが、どうも中途半端な機能を無理やりリリースにねじ込んでいる感が否めません…

ともあれ、Prefabは使えないので、これまで通りwafとcerberoのビルド結果をaarに取り込みつつ、参照側でもPkgConfigのパスなどをよろしく解決できるhackに頼るという方向性で、sfizzのCMakeLists.txtにパッチを当ててなんとかビルドしました。

sfizzはそれでビルドまではできたのですが、実際にsfzファイルをロードするところまではまだ実現できていません。それほど難しくはないはずなのですが、続く2つのタスクが先だと判断して先送りしています。

aap-lv2モジュールの切り離しとmda-lv2の再構成

sfizzの移植は、当初は独立リポジトリで行っていたのですが、LV2プラグイン移植の依存関係にフレームワーク本体を追加しないといけない状態はしんどいなあ…と考えていました。

また一方で、そもそもプラグインフレームワーク自体にLV2は必須のものではありません。LV2依存部分がビルドの難易度を爆上げしていた側面はあります。(これは実際にはそこまで大したことはなく、当初lv2リポジトリのサンプルプラグインまでビルドしていた関係でlibsndfileやcairoまでビルドしていたことが原因なのですが、それらを除外してもmda-lv2ではlibsndfileが必須でした。)

mda-lv2はもともとLV2プラグインをビルドする際に必須ではないですし、これだけはGPLv3なのでビルドから切り離しておきたかったこともあって、LV2まわりのビルドを根本的に見直すことにしました。最終的には、aap-lv2というリポジトリが誕生し、プラグイン本体はaap-juceを切り離した時のようにスッキリしました。

いったんLV2依存部分を切り離すと、ここにLV2プラグインの移植を放り込むことでビルドスクリプトの負担がかなり下がったので、sfizz移植はここに追加しました。mda-lv2はLV2サポートのリファレンス実装としてモジュールを再整備されています。

いずれprefabが安定的に使えるようになってaarがMaven経由で使える程度に安定化してきたら、単独の移植プロジェクトでフレームワークを参照するのも平易になるでしょう。(aap-lv2リポジトリ自体はLV2バックエンドの開発とdogfoodingのために現状のままが良いですが。)

Guitarixの移植

前述の4月のエントリーでも言及したのですが、sfizzでインストゥルメントプラグインを実現して、Guitarixで実用的なエフェクターを取り入れることができれば(といってもどれだけ実践的なのかは正直サンプル以上のことはわかりませんが)、とりあえず「波形合成のおもちゃ」以上の、まともな音源で作られた楽曲を再生できる期待値は上がります。

そういうわけでいよいよGuitarixを取り込むことにしたのですが、Guitarixもwafを使ってビルドするので、LV2モジュールをAndroidようにビルドする仕組みを再利用できるようにすることにしました。今後LV2プラグインを誰でも移植できるようにするためには、自分で道を作っておかないといけないわけです。

ビルドスクリプトそのものは再利用がそれほど難しいわけではないはずだったのですが、LV2リポジトリのビルドはautowaf、Guitarixは手書きのwafなので(wafはpythonスクリプトなので何でも書けてしまう)、再利用できる部品だけ再利用しました。同じことを移植作業では毎回気にしないといけないことでしょう…

Guitarixの最難関はハードな依存関係です。glibmm、その依存関係にあるlibsigc++、glibなどが基本にあって、さらにlibsndfile, fftw3, eigen、zita-convolver, zita-resamplerといったものをすべてCerberoでビルドできるようにしました(zita-*は必要なかったので無駄足になってしまいましたが…)。

ネイティブライブラリのビルドシステムのオーバーホールの一環として、Cerberoを置き換えることも検討したのですが(何しろこれはGStreamerのためにしかメンテされないので)、Prefab・ndkportsは前述の通り未熟すぎ、vcpkgはLinuxサポートですら建前でしかなく、パッケージのビルドスクリプトWindows対応のことしか考えていないというのがザラだったので(vcpkgがLinuxで使われるようになる日は一生来ないだろうと思いました)、引き続きCerberoを使っています。

ビルドできたGuitarixは、Androidプロジェクトに取り込むにあたってディレクトリ構成を差し替えないといけないのですが(何しろlib/lv2/GxHogehoge.lv2みたいなディレクトリからターゲットアーキテクチャ向けでもない.soファイルをロードすることはできないので)、それらを自動的に再配置するMakefileスクリプトもaap-lv2で追加されています。誰でも(特に「将来の自分」)同じ手順を追えば任意のプラグインを移植できるようにする、というのが重要です。

ともあれ、Guitarixも無事移植できました。数十種類のエフェクトが利用可能なプラグインのリストに追加されるのを見るとアガりますね。

f:id:atsushieno:20200629230048p:plain
guitarix on Android

Mac環境用ビルド

この3ヶ月くらいメイン開発機のHP Spectre x360のキーボードの故障がひどく、キーが入力できなくなってHPに修理依頼しても、帰ってきてしばらくするとまたキーが入力できなくなる…というのを繰り返していて(実のところ今も修理中)、完全に生産性が下がっていて困っています。

さすがにこのままではまずいと思って、打ち込みマスタリング用のMac環境でも何とかこの辺のリポジトリの開発が継続できるようにビルドを整備しました。GitHub ActionsでMacビルドが通るくらいまではきちんとやってあります…あったのですが、GitHub ActionsはAndroid開発環境のアップデートがまともに行われずAndroid API Level 30がいつまでも入ってこなかったので、CIは放置状態でした。モバイル開発で最前線を走るならGitHub Actionsはやめておいたほうがいいのかも…

ちなみにWindowsでもビルドできました。ただしmakeをWSL環境で走らせてからWindowsAndroid Studioで開いて開発を進める必要があります(ASはWSL2でも動くかもしれない)。GitHub Actions上でWSLのbashを呼び出す方法がわからないので、CIは通さないでしょう。

Next Steps:UI統合

Guitarixの個別のプラグインにはまだ謎のクラッシュを起こすものがあるのですが(要個別調査)、多くが何やら動作はできているっぽいので、「DAWあるいはそれに類するシーケンサーで音楽を演奏できる」状態にするために必要な作業は、インストゥルメント(sfizz)まわりの整備とシーケンサーの整備という感じになりました。

プラグインフレームワーク自体にもLV2バックエンドにもまだまだやることはいっぱいあるので、音楽が演奏できるようになっただけでは完結しないのですが、当面の目標はその辺りです。

aria2webはsfizzをKontaktみたいな商用製品っぽく見せるように構成するのとWeb UIを実現するのが目的でしたが、そのUI統合モデルはそのままこのプラグインフレームワークにも敷衍できます。というドキュメントを先日まとめていたので、この辺に着手しつつ、プレゼンテーションの準備をすることになるでしょう。

シーケンサーはtracktion_engineを使い回せれば十分だと思っています。tracktion_engineがAndroidで期待通りに完全動作するかは不明ですが、juce_emscriptenでもStepSequencerDemoが動いているのでまあ大丈夫ではなかろうか…

tracktion_engineはエディタを伴わず、デスクトップ環境で作成した楽曲データはそのままでは再生できないので、プラグイン情報などを適宜マッピングして再生することになるでしょう。楽曲はMIDIファイルからインポートできるので(デスクトップ向けには実績があります)、Android上でも再生できるような楽曲は作成可能であると踏んでいます。sfizzベースのインストゥルメントであれば、複雑な命令を伴わないので十分に可能でしょう。

aria2web: SFZ ARIA GUI extensions on Web UI (Part II)

前回の続きとなるエントリーです。今回はWeb UIをLV2 UIとしてどう統合するか、という話。

atsushieno.hatenablog.com

だいたい作る予定だった部分はほぼ完成したので、とりあえずこの開発はいったん完了とするつもりです。

github.com

復習と後半の導入

前回はSFZサウンドフォントをGUIからコントロールできるようにするARIA extensionをWeb UIで実現しました。UIだけだと何もできないので、これをsfizzというSFZのサンプラー(UIが特に無い)と組み合わせてホンモノのオーディオプラグインとして機能するものを作ります。

オーディオプラグインフレームワークの選択

sfzサンプラーはいくつか実装があるのですが、今回はOSSで一番期待値が高そうなsfizzを使うことにしました。同じ開発者がsfzformat.comというサイトの中でsfzプレイヤー(ライブラリ)の実装リストを公開しているので、この中にあるものはだいたい見ています。

わたしの開発の主目的は自分のLinuxデスクトップ環境で使えるオーディオプラグインを探すことであり、その次に自前のAndroidフレームワークで使えるサンプラーを用意することにあるので、オーディオプラグインフレームワークとしてはほぼ唯一解としてLV2を選択しています。

…というのが3月頃のわたしの選択だったのですが、sfizzの最近の開発動向を見ていると、どうやらVST3との2本立てになっているようです。VST3ではわたしがDAWに(…ではなくMMLなこともあるので、まあ「制作に」)使っているTracktion Waveformなどが未対応ですし、Android環境でも「まだ」vst3sdkがビルドできる状況ではないので、VST3はまだ選択肢にならないのですが、今後は可能性があるかもしれません。

LV2を使えるDAWは現状生粋のLinux向けDAW(Ardour, QTractor, Zrythmなど)がほぼ全てですが、以前にもちょっと言及したlv2vstを使うとVST環境でも部分的に使えます。

ちなみにsfizzでは以前はJUCEが使われていたのですが、JUCEのLinux環境サポートが貧弱だったこともあってか、JUCEは捨てられてLV2をダイレクトに実装する方向性に変わっていました(Linux系オーディオ開発者の間ではJUCE離れが進んでいたという話を3月に書きましたが、これもそのひとつです)。最近developブランチで開発が進んでいるVST3サポートもダイレクトな実装です。

sfizzにはLV2の特徴のひとつであるCVポートは必要なく、sfzファイル名を除いて全てのポートがfloat1つのパラメーターで表現できるので、技術的な理由でこれらをダイレクトに実装したということは無いはずです。

LV2 UIの独立ライブラリ設計

aria2webのUIをどうsfizzに組み合わせるのか解説する前に、いくつか説明しておくべきことがあります。まずはLV2ホストがLV2プラグインとLV2 UIプラグインを繋げる仕組みです。

LV2 UIは、コードの分離としては優れた設計になっていて、プラグインの共有ライブラリとは別に、プラグインUI用の共有ライブラリを指定するようになっています。これは暗黙的にオーディオプラグインUIの処理をオーディオ処理から分離する役に立っています。プラグインとUIは、次の2つの手段でやり取りします。

  • audio to UI: プラグインUIはプラグイン本体の出力ポートにsubscribeするようmanifest (.ttl) に記述することで、プラグイン本体からのデータ変更通知を受け取れます。
  • UI to audio: ホストがUIプラグインの初期化時に呼び出すinitialize()関数では、プラグインUIから操作などをプラグイン本体へ通知するときに呼び出すwriteFunctionという関数ポインタが渡されるので、UI実装のコードではこれを随時呼び出します。

オーディオプラグインにおいては、オーディオ処理とGUI処理には別々のスレッドが用いられます。オーディオ処理はリアルタイムで行われなければならないので、リアルタイムで処理することが想定できないイベントループをかかえるGUIは、独立して動作しなければならないためです。(LV2にはWorkerという拡張機能があり、オーディオスレッドでの処理が不適切である機能はこれを使用して実装しますが、GUIサポートはホスト側で別スレッドで処理することが最初から想定されているので、プラグイン側が気にすることはありません。)

LV2プラグイン実装のトレンドに関するフォローアップ

4月にLV2のGUIサポートのトレンドに関するメモを公開しました。LV2というかLinuxデスクトップではGUIフレームワークがいくつかあって、QtにもGtkにもバージョンがあるしX11ベースの他のツールキットもあってバベルの塔が崩壊した後の状態になっていて、それをsuilでまとめつつあるみたいな状態だ、という話でした。

このときは「suilを使えばまあ概ねどのGUIバックエンドで実装していても違いを吸収できるようになっているんじゃないか」と考えたわけですが(それで実際ある程度解決するわけですが)、何やらこれがZrythmの作者の目に止まったらしく、いろいろ教えてもらいました。

まずプラグイン開発のトレンドは概ね「X11UIを使わないとダメっぽい」という方向性に収束しつつあるようです。suilはホスト側の実装で、ホスト側のトレンドとしてsuilが使われているのは間違っていないわけですが、X11レベルで実装していれば互換性問題が生じない、というのが理由であるようです。X11でダイレクトに使えるUIフレームワークには、FLUTなどGL系のライブラリ、JUCE GUI、LV2であればpuglという独自のライブラリがあります。(そういえばGuitarixがX11でUIを書き換えているという話を書いたこともありますね。)

suilで何が解決しないかというと、まずそもそもGtk2とGtk3などを1つのアプリケーションで混在して利用することができない(らしい)という話があります。gtk2のglibのアプリケーションループとgtk3のglibのアプリケーションループが共存できない、と考えると、まあ納得感があるでしょう。Qt4とQt5も同様の状況なのかは分かりませんが(Qt4は触ったこともない)、その可能性は十分にあると思います。

上記のトレンドに関するメモでも言及しましたが、そもそもGUIフレームワークごとにホストから渡されてくるGUIウィジェットのparentが何になるのかが明確に規定されず、LV2 UIの仕様自体が詰めきれていないという問題もあります。LV2の仕様ではGtkUIやGtk3UIは「バイナリディストリビューションで使用すべきではない」と記述されているのですが、(わたしの理解としては)バイナリ配布なしに任意のプラグインUIを使用する方法は現実的にあり得ず、要するに仕様として失敗しているところです。LV2仕様は所詮個人で決めているものであり、この種の技術的な回答を提示できない問題が生じるのは仕方のないところでしょう。

LV2 External UI

aria2webのUI統合…というかLV2プラグイン統合…の最初の問題は、Gtk3UIとしてwebview.hからGtkWindowを取得してもホストから渡されるparentにattachできないことでした。以前のUIトレンドの記事も書きましたが、Zrythmから渡されるparentはGtkEventBoxで、event boxにwindowを子として追加することはあり得ないですし、webview.hには親としてGtkWindowしか渡せなかったので、Gtk3UIは早々に諦めました。

とはいえ、X11UIにしたところで親としてはwindowが渡されることに変わりはなく、webview.hとの相性はよくありません。webview.hを諦めて独自実装するのであれば、前述の「suilでも解決しない課題」のことを考えるとX11レベルでやるしかありませんが、WebkitではWebkitWPEくらいしか解決策が無く、これは組み込み環境用にビルドできるという以上のものではない(X11統合があるわけではない)ので、自前で実装するのはしんどいぞ…となりました。WebKit以外の選択肢としてはCEF (Chromium Embedded Framework) がありますが、CEFを組み込むとかなり巨大なバイナリになってしまうので、可能な限り避けたいところです。

それならば、いっそCarlaで採用されているexternal UI方式ではどうだろうか、と考えました。Carlaがどういうアプローチで採用しているのかは分かりませんが、external UIではホストからのコントロール インターフェースとしてshow/hide/runだけを規定するので、それをUIが自前で実装する、というスタイルです。現在aria2webではこれが採用されています。

前述のLV2 GUIのトレンドのメモでは、APIとしてUIとオーディオ部分を分離して処理できるような仕組みを想定していない、と理解していたのですが、実際にコードを組んでみると、この連携部分はmanifestのport subscriptionとwriteFunctionだけで実現できました。

aria2webの初期設計では、UIがinstantiate()の呼び出しによってホストから起動されたら、webview.hを使って手抜きで実装したUIを表示して、そのAPIでJSオブジェクトにC関数コールバックを割り当てて、それが呼び出されるたびにwriteFunctionを呼び出してプラグイン側に反映する、というアプローチで実装しました。これでZrythmからのUI生成が実現できました。

LV2 UIのプロセス分離

Zrythmから表示できるようになったaria2webですが、webview.hでは内部的にwebkitgtkが用いられており、これはQTractorで使えませんでした。webkitgtkがGtkアプリケーションループの存在を前提としていたためです。suilが解決すべき問題ではないかという気はしますが、とりあえず方向転換が必要になりました。そこでUIプロセスを分離することを思いつきました。

オーディオプラグイン機構全般にいえることですが、多くのホスト(DAW)ではユーザーが楽曲の打ち込みで指定したプラグインを、インプロセスでロードします(この辺りの技術的課題については1年前にメモをまとめてあります)。これはLV2ホストの場合も同様です。

ホストとプラグインがひとつのプロセスで動作するということは、ホストがsuilで別々のUIフレームワークを繋ぎ込めたとしても、プラグイン同士で共有ライブラリを動的にロードしていた場合に、その前提バージョンが異なっていると、いわゆるDLL hellのような状態になりかねないでしょう。(プラグインのローカルパスから共有ライブラリをロードできない場合などもあり、静的にリンクすることが多いとは思いますが、それが適切でない場合もあるでしょう*1。)

これは個人的な分析として、どちらかといえば、LV2はプロセス分離モデルに関する意識が低い側面があります。そのひとつがLV2サポートに不可欠なURIDのモデルです。URIDとは、LV2プラグインの様々な場面で使われている「名前空間」を表すURI文字列を、オーディオ処理の過程でも無理なく使えるint32_tに変換する仕組みです。ホストとプラグインは、URIDのmap/unmapという機構(関数ポインタを含むstructで表現される)を用いて、文字列と整数を相互変換します。もしプラグインが分離プロセス上で動作していたら、メンバーとして渡された関数ポインタをそのまま関数として呼び出すことはできません。

プラグインプラグインUIは別々のプロセスで動作できるのでしょうか? 前述したオーディオとUIの双方向のやり取り(特にwriteFunction)をそのままプロセス分離して利用することはできませんが、ホストとUIプラグインのコードそのものは同一プロセスで動作させつつ、UIプラグインのコード自体から分離プロセスをspawnして起動することは不可能ではありません。オーディオポートからの通知はそのままUIプロセスに流せばよく、UIの変更もそのままwriteFunctionの呼び出しに繋げれば良いだけの話です。

aria2webにはWeb UIを単独で表示できる(けど音は何も出さない)aria2web-hostが存在したので、プラグインUIが受け取った通知をstdinから読み取り、UIイベントをstdoutに書き出す、という単純なパイプでプロセス間通信を実現しました。spawnで実装するのが面倒だったので、gitlabで発見したtiny-process-libraryというクロスプラットフォームC++ライブラリを使っています。*2

READMEにも書いているのですが、図面にするとこんな感じです。VSCodeに統合されたdraw.ioで描いたやっつけ図面…!

https://raw.githubusercontent.com/atsushieno/aria2web/051504c/aria2web-ipc.drawio.svg?sanitize=true

何はともあれ、UIプロセスを分離したことによって、QTractorからでもUIがロードできるようになりました。ちなみにここまで進めても、オリジナルのsfizzには名前空間衝突を避けるためのURI変更とui:uiのmanifest要素追加以外では、一切手を入れていません。

オーディオ処理部分とのやり取りとウィンドウ管理

LV2のオーディオプラグインとのやり取りは、通知ポートとwriteFunctionの2つで行われる、という話を書きました。

通知ポートは、オーディオ側では「出力」ポートとなっていて、まず「イベント」はLV2UI_Descriptorで登録したport_eventに届きます。

void aria2web_lv2ui_port_event(LV2UI_Handle ui, uint32_t port_index,
     uint32_t buffer_size, uint32_t format, const void *buffer)
{
    ...
    
    if (port_index == ARIA2WEB_LV2_NOTIFY_PORT) {

また、UIプラグインでmanifestにui:portNotificationを追加すると、指定したport(ここではsymbolで指定)に届いたイベントを受け取れます。

<https://github.com/atsushieno/aria2web#ui>
  a extui:Widget ;
  ui:binary <aria2web-lv2ui.so> ;
  ui:portNotification [
        ui:plugin <https://github.com/atsushieno/aria2web> ;
        lv2:symbol "notify" ;
        ui:protocol atom:eventTransfer
  ] .

AtomとPatch

イベントはAtomというLV2独自のバイナリデータのフォーマットで渡されてきます。Atom形式のデータを構築する方法についてきちんと説明するとそれだけで数千字になってしまうので今回は省略します…(割と大変だったのでホントはちょっと書こうと思っていましたが…)。

writeFunctionは次のような形式で定義されるのですが、

typedef void(* LV2UI_Write_Function) (
    LV2UI_Controller controller, 
    uint32_t port_index, 
    uint32_t buffer_size, 
    uint32_t port_protocol, 
    const void *buffer)

引数が先のport_eventとよく似ています。port_protocolには主にfloatの値1つだけを渡す場合と、Atom Event形式のデータを渡す場合があって、一般的なパラメーターはfloatプロトコルでbufferポインタの先にfloat値が1つ入っているだけです。

sfizzでややこしいやり取りは、sfzファイル名を渡す場合にこのAtom形式にファイルパスを変換して送信する部分だけで、ここにはLV2 PatchというAtom上で成立するパッチDSLのようなものが使われています。これも詳しくは解説する余白が足りないので省略しますが、patch:getというメッセージで「sfzファイル名を送れ」とオーディオの入力ポートに送信すると、sfzファイル名を含むpatch:setというメッセージが通知ポートに届きます。MIDI 2.0のProperty Exchangeっぽい感じです。

ウィンドウとインスタンス管理

プラグインUIは次のようなタイミングでプラグイン本体から情報を受け取る必要があります。

  • ウィンドウの表示・非表示を変更したとき
  • プラグインの削除などに伴ってUIを破棄した場合

これらについては、前半で少し触れた、Carlaを開発しているKXStudioの"External UI"のインターフェースに沿って、show / hideといったコールバックが呼び出されるので、それに従ってウィンドウ表示を調整します。

ウィンドウの表示制御は、aria2webの場合はあくまで「ホストから行う」ことにしており、ユーザーが閉じることは想定していません。ZrythmではGtk3UIなどのウィンドウを自分で閉じることも出来てしまうのですが、これはホスト側からウィンドウの状態を把握できているから出来ることでもあります。external UIではホスト側がこれを把握することは出来ず、またプラグインUIからイベントを受け取ることもありません。想定外の状態を管理することになってしまうので、少なくともexternal UIを使う時はウィンドウを閉じるボタンは消しておいたほうが良いでしょう。

プラグインUIがshow / hideイベントを受け取ったら、子プロセスにパイプのstdioで命令を伝達しています。

プラグイン側からのSFZファイルの変更通知とUIへの反映

プラグインUIの表示で厄介な部分のひとつに、オーディオ部分にSFZファイルの変更があった場合にそれをUIに反映させるまでの流れをどう作るかという課題があります。

基本的には変更が生じたらport_eventで通知ポートから受け取ったファイル名をもとに子プロセスのウィンドウにstdioのパイプで通知を伝達します。現状SFZファイル名の更新くらいしか通知しないので、SFZ (filename)\nと書くだけです。問題は、DAWが楽曲をロードした場合など、プラグインの初期化タイミング(トラックがロードされた時点で初期化)とGUIの初期化タイミング(UI表示が指示された時点で初期化)の、どのような組み合わせについても対応できる必要があるということです。

aria2webの現在の実装では、UIがinstantiateされ子プロセスでのJSのロードが完了した時点で、親プロセス(プラグインUI側)に初期化完了の通知を送ります。プラグインUIはこれをstdioのパイプで受け取ると、sfizz本体にpatch:getを送って、SFZファイル名を通知ポートに送ってもらいます。これが届いたら、それを子プロセスに送って表示を更新します。

このややこしい手順を踏まないと、「子プロセスがまだWeb UIの処理に必要なJSをロードできていない時点で表示更新のためのJS式の評価が呼び出されてしまってエラーになる」といった事態が生じてしまいます。UIプラグインと表示プロセスの間でプロトコルが確立していると、そういった事故を防ぐことができます。

未解決の課題と応用

だいたい以上のような実装を終えた時点で、オーディオプラグインとしての機能はだいたい完成したことになるのですが、課題もいくつか残っています。一番直感的に困るのは、ARIA GUI上にknobやsliderやswitchが存在していてこれでサウンドを調整できるはずなのに、これらはsfizzのパラメーターとしてプラグインのstateに保存されない、ということです。

sfzではこれらはMIDIイベントとして受け取る前提になっていて、stateではなく楽曲のMIDIデータのような部分に保存されていることが前提になっています。そのため、これらをsfizzの今の実装とは別に保存しないと、Kontaktなどを使っているような気分でプラグインのツマミを調整しても記録が残らず、データが失われたような使い勝手になってしまいます。割と重要な課題…

あとはWebKitGtkの問題のせいで、shiftキーを押した状態でwheelイベントを処理しようとすると、なぜかxy軸がひっくり返る(縦wheelなのに横扱いになる)みたいなバグがあったり…

UIの問題とは別に、ひとつ目的にしていたSFZのギター音源をsfizzで鳴らすという試みなのですが、sfizzでサポートされていないSFZ 2.0のopcodesがふんだんに使われているということもあって、まだ音が鳴らないというのが現状です。ただkey switchも適切に入力できているかわかっていないし、サックス音源などは鳴っているので、sfizz本体の開発をもう少し待ってからでも良いだろうと思っています。

他にもいくつかissueとして課題になっているのですが、「プラグインUIを音声処理プロセスから分離しつつホストと協調的に制御してオーディオプラグインらしく振る舞う」という実験にはどうやら成功したと言えるので、これをAndroidフレームワークにも適用していこうと思っています。

*1:たとえば依存ライブラリにsecurity fixがあった場合に必ずアップデートを自前でリリースできるか、などの考慮事項があります

*2:ちなみに今回使っているサードパーティライブラリ、webviewもhttpserver.hもクロスプラットフォームです(自分のコードではGtkWindowにキャストしている部分などはありますが)。

aria2web: SFZ ARIA GUI extensions on Web UI (Part I)

目次

4月頃に、自作オーディオプラグインフレームワークのUI構想についていろいろ検討していて、SFZサウンドフォントのARIAまわりを取り込んでsfizzというSFZサンプラープラグイン)に統合したいという話を書きました

これをaria2webというプロジェクトとして開発して公開しています。まだ実用品ではないのですが、いろいろ知見があったので今のうちに書き連ねておこうと思います。

github.com

aria2webは、ひとことでまとめると、ARIA GUIをHTML(+SVG+JS)化して、WebViewが使える任意の環境でオーディオプラグインGUIとして汎用的に使えるようにしよう、というものです。

動機: なぜSFZを使うのか

オーディオプラグインフレームワークを作る目的は、音楽制作・再生のためのエコシステムを構築することです。古典的なDTMでは「楽器」に相当するのはMIDI音源でしたが、MIDI (1.0)の表現力では自由に思ったとおりの音楽を表現できないことが多いので、現在ではオーディオプラグインを使うのが一般的です。

オーディオプラグインには(ちょうざっくり分けると)楽器とエフェクターがあって、自分のフレームワークでは、とりあえずproof-of-conceptレベルではVSTのサンプルとして使われてきたmdaがLV2に移植されたmda-lv2が、すでに動いています(何しろLV2が実質無修正で動くので)。また、JUCEのプラグインにちょっとモジュールを追加するだけで対応プラグインがビルドできるので、それで取り込めるプラグインもいくつかあります。(UIをホスト側で出すのは現状では無理だけど模索中です。)

楽器のほうもfluidsynthをオーディオプラグイン化したjuicysfpluginsfizzを使えばいけるはずです。juicysfpluginはJUCEソリューションなのですが、内部的にはgtkにも依存していてちょっと使えないし(ここ後で触れる予定)、SF2/SF3のFluidsynthより表現力の高いSFZをサポートしたほうがより本格的な楽器プラグインを実現できる(何しろFluidsynthはMIDI 1.0のレベルなので)と考えてSFZとsfizzのほうを主なターゲットにしています。

SF2/SF3もSFZも同じ「サウンドフォント」ではないのか、何が違うのか、と思われそうですが、SFZにはキースイッチ*1のサポートなどで、SF2/SF3より柔軟な条件式で対応するサンプルを選択できるようになっており、本格的に楽器を表現できるフォーマットとして現在も進化しています。(独自拡張から標準化に進む世界であるようです。)

オーディオプラグインの世界では「サンプラー」というジャンルがあって、サウンドフォントの再生エンジンはこのカテゴリーに属することになります。サンプラーとして有名な音源はKontaktとかVSLとかです。Cubaseに付いてくるHALionとか、わたしが使っているWaveform付属のCollectiveなんかもそうですね。SFZにもこれらの商用音源に匹敵する機能を実現できるポテンシャルがあります。

自分のオーディオプラグインフレームワークKontakt並の音源を実現できるとしたら、だいぶ「エンドユーザーが使いたくなるもの」なのではないかという気がしてくるのではないでしょうか。

ARIA GUIとは何か

SFZはSF2/SF3より詳細な条件式でサンプルを選択できるという話を書きましたが、条件式の記述に用いられるのはopcodeと呼ばれる命令識別子であり、要するにごく単純なプログラムを書いているようなものです(プログラムの著作物とまでは言えないレベル)。SFZのopcodeは特に技術標準とまで言える仕組みではなく、これを各サンプラーのベンダーが独自拡張して使っていることがあります。

ARIAというのは*2、Plogue社のsforzandoというサンプラー製品で使われている独自拡張ですが、これにはSFZのopcode拡張の他に、GUIを定義するXMLマークアップボキャブラリーも含まれています。SFZの仕様にはGUIの定義が含まれていないので、SFZのサンプラー音源をいじるときは数値と格闘するか、標準的なコントロールを操作するしかないのですが、どのパラメーターが重要なのかはユーザーには分かりません。GUIがあればこの問題が緩和されます。

ARIA GUIの内容はごく単純です。

<GUI w="775" h="335">
 
  <StaticImage x="0" y="0" w="775" h="330" image="Main/Control.png" transparent="1" />

  <Knob param="20" x="53" y="168" image="Main/Window1.png" frames="101"   />

  <OnOffButton param="23"  x="660" y="282" w="52" h="21" image="Main/Button.png" />

x, y, w (width), h (height), image 程度のもので、GladeやXAMLなどに比べたら内容も多くが自明です。これで、例えばこんな画面ができます。(Unreal InstrumentsのMETAL-GTX)

f:id:atsushieno:20200601154625p:plain
UI METAL GTXのGUI

この画面では、ツマミの部分と数値入力っぽい部分がKnob要素で表現されています。Knobparamframesimageには説明が必要でしょう。まず簡単なframesのほうを説明しますが、これは数値の範囲に対応します。frames="101"というのは、数値が0〜100の101段階であることを意味します。paramは、サウンドフォントのどのパラメーターを操作するものなのかを、MIDIのコントロールチェンジのように数値で指定します。.sfzファイル側で規定しているはずです。

ツマミの部分は画像を自由に指定できます。それがimage属性の内容なのですが、リンク先を見てみてください。巨大な内容なので埋め込みはしません。

https://aria2web.firebaseapp.com/ui-metal-gtx/gen/GUI/Main/Main_Knob.png

あれ? 見えなくない…?ってなりますが、実はものすごい縦長の画像が、ブラウザウィンドウの幅に合わせて縮尺を調整した結果です。上方のごく一部だけを切り取るとこんな感じになっています。

f:id:atsushieno:20200601154706p:plain
ツマミ画像(抜粋)

ツマミですね…! ひとつひとつをよく見ると、ダイヤルが少しずつ上に移動しているのがわかります。縦長なのは、これが101個ぶんだけ縦に連結しているからです(!)。画面上には「今の値に対応する画像」を表示するだけです。とんでもないやっつけ仕様ですが、確かにこのフォーマットであれば、サンプラーのコードでは何も面倒を見る必要がないので、プラグインを作る側は自由なデザインでツマミを実装することができる、というわけです。

そして数値入力っぽく見えるのも実はダミーで、これはKnobコントロールです。マウスホイールで数値を変更できます。キーボード入力はできません。もちろん数字はツマミと同じ超縦長画像に描かれています。なんか…とんでもねー世界に来ちまったぞ…

webaudio-controlsを活用してHTML UI化する

GUI拡張と聞くとなんかものすごい技術努力によって実現しているかのように思えてしまいますが、ARIAはごく単純な仕組みとデータ形式でUIを表現していました。この程度であれば、HTML+JavaScript+CSS+SVGで表現できるでしょう。sforzandoはLinux版を提供していないので(もちろんAndroid版もありません)、ARIAGUIをロードできるようにするには自前で実装するしかありません。

aria2webのREADMEでも言及しているのですが、実はもう出来ていて、ここで試すことができます。

https://aria2web.firebaseapp.com/

今回これを作るにあたって活用したのは、この方面ですでに存在していたwebaudio-controlsという完成度の高いUIコントロールです。Web Componentsに基づいて作られていて、Javascriptから操作するのも簡単です。元コードの開発メンバーがだいたい日本のWeb Music Developers JPあたりにいそうです。

github.com

実のところ、ARIAがそこまでGUIの詳細な表示内容をデータ上に持っているとは考えていなかったので、ツマミやスライダーはこのwebaudio-controlsを使えば「それっぽいもの」が作れると思っていましたが、結果的には「ほぼ元通りのUIっぽいもの」を再現できるところまで出来ています。

ARIAXMLをHTMLに変換する処理は本当に簡単なもので、現状では100行も無いXSLTスタイルシートで実現しています。あの妙な画像も変換しなくていいの?と思うでしょうが、webaudio-controlsに含まれるwebaudio-knobに、完全に同じ機能が含まれています

これはどうやってARIA用にこういう画像を作るのかを調べていて割といろいろびっくりしたんですが、そもそもwebaudio-controlsを開発しているg200kgさんがKnobManというこのKnobコントロール画像を作成するツールの開発者で*3、これを使えばgimpとかでがんばって手作業でこういう画像を加工する必要はないわけです(最近はWebKnobManがメインらしい)。そもそもKnob Galleryに膨大な数のフリーKnob素材があるので、自分でゼロから作る必要はあんまり無さそうです。

もうちょっと調べてみると、KnobManの使い方はNative Instrumentsのフォーラムでも議論されていて、要するにこの画像フォーマットはもともとKontakt(たぶん)で決められていて、KnobManはそれを簡単に作るために作られたツールであって、ARIA GUIKontakt用音源のデータと同じ画像を再利用できるようにしたものっぽい、ということが把握できました。完全に謎のエコシステムが出来ている…!

ともあれ、当初予想していた以上にwebaudio-controlsを使ってUIを実現するというアイディアは的を射ていたことがわかりました。

ちなみにaria2webで使っているwebaudio-controlsのコードには少し手を加えてあります。具体的には、knobのサイズが不明な場合でも元画像のとおりによろしくやってくれるように変更しています*4XSLTで任意のARIAから変換してwebaudio-knobを生成している自分としては、画像のサイズがわからないので指定できないわけですが(これがXSLTでなければ画像をロードしてサイズを取得するだけなので瞬殺…)、webaudio-knobではサイズ指定がない場合はデフォルト値(120pxかなんか)で描画してしまうので、当初は常に変なサイズで描画されてしまう問題で悩みました。

なぜWeb AudioでもないのにWeb UIなのか

ここから先は現在も開発中なのですが、aria2webはそもそもオーディオプラグインのUIとしてARIAを流用するのが目的なので、活用するにあたってはこれをLV2やVSTのようなプラグインに統合することになります。最近ではどのプラットフォームにもWebViewがあるのでそれを使えば十分可能だろう、というのがわたしの理解でした。でした…とは!? この詳細は「Part IIに続く」(!)

…なのですが、もう少しこの設計について書きます。

先日も書いたのですが、LV2オーディオプラグインGUIの世界はバベルの塔が崩壊した状態で、各種のGUIフレームワークに分かれていて全部をサポートするのが難しい状態です。前回はsuilを紹介しましたが、suilでも解決できない課題があります(Part IIで書く)。

WebテクノロジーでUIを構築するというのは、この問題を解決するひとつの手段として考えたものです…というのは半分ウソで、そもそもWeb UIを使おうと思ったのはAndroidiOSのような特殊な世界でプラグインUIをどう構築するかという課題への解決策として思いついたものでした。つまりWeb UIならホスト側アプリでもロードできる、というわけです(詳しくはここで)。

いずれにしろ、ローカル環境を問わずにUIをホストする、一部で「ガワネイティブ」と呼ばれる技術が、オーディオプラグインUI開発の裾野を広げたり、あるいは一般的なアプリケーションのUI開発手法に近づけるために、有用だと思っています。個人的にはオーディオプラグインUIの開発手法は10年遅れている、juce_gui_basicsに技術投資する時代ではない、くらいの認識でいます。もっとも、ここは実のところ要件次第のところがあって、オーディオ処理ほどではないが演奏者が違和感を感じない程度に可能な限りレスポンシブに動くUIにはネイティブコードが最適でしょう。ただこれは設計次第なのと過渡的な問題だと理解しています。

ちなみに、この観点で紹介しておくべきものとして、JUCEのGUIをReactで開発できるBlueprintがあります。BlueprintはWebViewを使うのではなく、JUCE用のrendererを提供するreconcilerの実装になっている、すなわちもうひとつのReact Nativeのように振る舞っている、というところが特徴です。つまり、Reactのやり方で構築したUIが、JUCEのネイティブコンポーネントとして動作することになります。

これはこれでスマートなやり方なのですが、あくまでReact資産があったり、React開発でゼロから構築することを前提としないと難しいです。今回はwebaudio-controlsを使っていて、これは内部的にWeb ComponentsやSVGをDOMも含めて活用して実装されているため、React化するという選択肢はありませんでした。Blueprintはreconcilerを自前で実装していて、YogaやDuktapeを組み合わせて実装していてissuesを眺めてみてもそれなりに未実装の部分がいろいろあることがわかるので、あくまで出来る範囲のことをやる方針プロジェクトと理解しています。自分にはむしろCordovaやIonicのやり方が必要となったわけです。

今回は試験的にVue.jsを使っていますが、無くても良かったというレベルなので、今後依存関係として邪魔だな…となったらたぶん消します。(試験的なプロジェクトなので、ライブラリを開発している時みたいに「余計な依存関係はなるべく減らして…」とかは考えてはいません。)

ローカルプラグインからの利用については、Global空間に適当なWebViewからのフック用のJSオブジェクトを用意しておいて、webaudio-controlsからの操作通知はそこに送るように作ってあります。現在開発が進んでいるローカルのプラグインではこれでイベントを受け取っています。

ちなみに(言うまでもないかもしれませんが)Web上のデモはGUIのみです。音は出ません(デスクトップでも「まだ」音は出ませんが)。というのは、SFZサウンドフォントは大規模なものだと1GBにもなるような巨大なデータであるため、Web上環境には現状馴染まないためです。ダウンロードしてWeb Storage APIで格納するようなソリューションは実現可能かもしれませんが(あとはWeb Audio Pluginsと組み合わせたりとか)、個人的には現状あまり関心が無いです。

後半へ続く

本当は1回で全部まとめるつもりだったのですが、HTML化の部分だけでかなり長くなってしまったので、オーディオプラグインに組み込む部分はPart IIとして書きます。そもそもまだ出来ていないしな…!

*1:特定のキーのノートオンによって楽器のモードを切り替えることができる。ギターの奏法などで便利

*2:W3Cアクセシビリティ関連仕様であるWAI ARIAとは無関係です。

*3:webaudio-knob自体はもともとはagektmrさんが作ったもの

*4:変更が雑なのでgithubにPRを作れていない状態…

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:難しくて理解できないという話ではなくて、他にやるべきことがいくらでもある

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

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

目次

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

一般的なオーディオプラグインにはオーディオや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統合を実現する手法にも影響してくるので、まだ二転三転する可能性はあるのだけど、この辺は技術的に興味のある人が吸い寄せられてコメントしてくるくらい課題としてはけっこう面白いっぽいので、ちまちま実装を進めていこうと思っている。

PACEによるJUCEの買収とJUCE6開発計画が予告された件

4月になってからJUCE方面の動きが大きい。今日はついにPACE社がJUCEを買収したというニュースが出てきた。それなりにセンシティブな話題で、ある程度JUCEにコミットメントがある人には書きにくい話もあると思うので、完全に無責任な立ち位置で書いておこうと思う。

目次

PACEによる買収とjuce-framework/JUCEへの移行

JUCEは(ここでは何度も書いているので今さら感あるけど)クロスプラットフォームでクロスオーディオプラグイン規格の開発フレームワークで、今日まではROLI社が所有する製品だった。商用ライセンスとGPLv3で公開されている。

今回はROLI社が買収されるのではなく、JUCEの開発事業がPACEに買収されるらしい。JUCEはもともとJulian Storerという開発者がJule'sなんちゃら(忘れた)という自分の名前を冠して作ったライブラリで、彼はROLIに所属しているのだけど、最近はSOULというAPL(オーディオプログラミング言語)まわりの仕事をしていて、SOULは事業譲渡の対象ではないようだ。つまりJUCEはPACEのものになるけど、Juleは付いてこないということになる*1。ROLI社は楽器ハードウェアを販売している会社なので(Apple StoreなどでSeaboardやBLOCKSといったROLI製品が販売されているのを見たことがある人もいることだろう)、JUCE以外にもやるべきことはいっぱいある。

買収するほう、PACE社が分かる人がどれくらいいるか、多くはないんじゃないかと思うけど、iLokという製品を聞いたことがある/使っている人はDTMやってる人ならそれなりにいるかもしれない。USBドングルを売っているライセンシング・ソリューションを提供する会社だ。iLokは自分もADCで2年連続で見ているはずなのだけど、会社名までは全く覚えていなかった。最初 IP management company みたいな文言で書かれているのを見て「JUCE完全に身売りしないといけなくなったのか…」と暗い気持ちになったのだけど、製品開発にはノータッチノーサポートでIPだけ吸い取るような買収ではないようだ。

ROLIは実際のところかなり資金難に苦しんでいて、JUCE開発チームも2人か3人くらいで回している状態だった。JUCEにはいわゆる外部コントリビューターのようなものがいない。現時点ではROLIのポリシーなのかJUCEチームのポリシーなのかわからないが、当初はそもそもJuleの個人ユーティリティで彼は要望を受け付けてコードを取り込むのを嫌がって拒否していたようだ。つまりオープンソースではあるがオープン開発ではない。

ごく少人数で回しているJUCE開発は、実のところさまざまな方面で停滞していた。個人的に関心のある範囲だけでも、Projucerはビルドを全面的に面倒見るための機構なのに、痒いところに手が届かず、細々とした改善が求められていた。SteinbergがVST2 SDKを入手できなくしたので全面的なVST3サポートが必須だったのに、Linux版はVST3を全くサポートしなかった。

閑話休題、そこに現在のコロナ禍である。ROLI単独で状況が改善される見込みはほぼ無かった。資金難という話は昨年には出ていたので(昨年はNative Instrumentsなども大規模リストラを行っており、オーディオ開発ソフトウェアの世界は割としんどい年だったようだ)、PACE以外からもいくつか買収提案を受けていたようだ。PACEはJUCEからのシームレスなiLokサポートを実現できれば既成品の販売が強化できるので、それだけでもJUCEを手元に置いておく理由にはなるかもしれない。実のところiLok利用ベンダーとJUCEユーザーはある程度かぶっていたようだ。JUCEチームとしては資金の心配なく開発が継続できれば良いということだろう。JUCEユーザー(音楽ソフト開発者)としてはそれで得られる安心感は確かにある。

とはいえ…iLokである。もともとユーザーにとっては邪魔でしかないUSBドングルのライセンシングソリューションの会社でサポートの評判もいまいちらしく、全く歓迎されるべき事態には見えない。JUCE forumでこの話題が上がったのはこのスレッドだけど、リンク先のgearslutz.comの掲示板には残念がるコメントが散見される。まずタイトルがPace Ilok has bought the Juce Platform :-(である。散見ではあるけど、匿名掲示板でも大規模掲示板でもないことを考えると、十分「大きな」懸念が上がっていると思う。

PACEの買収はどんな影響をもたらすだろうか? 私見としては3つくらい今後の筋書きが考えられる。

  1. 何も変わらない。これが一番ありそうなシナリオだと思う。PACEはiLokの販路が拡大できれば十分なので、JUCEにiLokサポートを追加するような動きは見せるが、JUCEのライセンスモデルを変更したりすることはない。juce_product_unlockingモジュールはもしかしたらiLokサポート用に別モジュールが出来て消滅するかもしれない(が、そのためにはiLokがLinuxをサポートしなければならず、それ無しで移行するとは考えにくい)。
  2. 商用ライセンスに変更が加えられ、商用ライセンス版では特にソフトウェアプロテクションまわりでiLok以外の選択肢を排除するために独自の改良を加えられなくなる可能性が懸念される。こうなると独自にLV2サポートを追加するような変更が加えられなくなる。
  3. GPLv3ライセンスでの提供が廃止される。外部コントリビューションを受け付けていなかったので、PACEはJUCEのライセンスを恣に変更できる。もちろん、これまでにリリースされたバージョンは従前の商用ライセンスとGPLv3で利用できるが、誰かがOSSとしてforkしてメンテナンスする必要がある。幸い(?)本家の開発リソースは小さいのでそこまで劣勢にはならないだろう。ただLGPLで利用できないライブラリはライセンス的に使い勝手が悪いし、商用ユーザーにとってはそっちに協力してもメリットがないので、かなり閉じた世界になりそうではある。

(3)は一番問題のあるシナリオだけど、可能性は限りなく小さいと思う。競合製品を出しているわけでもないPACEにそんなことをするメリットは、悪意の深読みスペシャリストであるわたしにもちょっと思いつかない。JUCEの市場は小さくないけど、独占的とまで言えるほどではない。

…とまあ、懸念事項はあるのだけど、現時点で言えることとしては、少なくともJUCEチームにはいくつかの選択肢があったうえで一番よさそうなものを選んだのだということだ。PACEは少なくとも開発者にとっては親和的であるようだ。JUCEチームによる公式アナウンスには developer-focused company と書かれている。ただそれにしてはgithub organizationsも無いような会社なので、このコメントへはわたしは50%くらいは(良く言えば)リップサービスだなと思っている。

JUCEはROLIのorganizationからは外れて、新しくjuce-frameworkというorganizationの下で公開されることになったようだ。github teamsが価格改定されたこともあるし、タイミングとしては割とラッキーだったのかもしれない。

JUCE6の予告

ともあれ、資金的な困難が解消したこともあってか、JUCEチームは大規模リリースに向けて本格稼働を始めた(あるいはそれを公開するようになった)ようだ。今から4日前、JUCE6のリリースが告知された。個人的には嬉しい事だらけの内容だ。

forum.juce.com

JUCE6は現在すでにある程度コードがgithub上でjuce6というブランチで公開されている。以下の項目以外にもいくつか改善があるので、詳しくは上記アナウンスやgithub上のコミットを眺めてみてほしい。

CMakeサポート

JUCE6最大の変更はProjucerからCMakeへの全面的な移行だ。CMakeサポートはもともと「CLionサポート (beta)」というかたちで実装されていたわけだけど、あくまでビルドが可能というレベルで再利用性は低く、ファイル追加などはProjucer上で行うことが前提となっていた。JUCE6ではこれを全プラットフォームで全面的に使うということを意味している。Projucerは、なくなるわけではなく、CMakeLists.txtを生成するための補完的なツールとして生き残り続けるようだ(そもそも他にもいくつか機能がある)。

昨年12月にJUCE Advent Calendarでわたしは以下のように書いたのだけど、完全にその通りになってくれた。

わたしの個人的な感想としては「もうProjucerのビルドシステムはあきらめてCMakeを使おう」なのですが(パッケージ参照もちゃんと解決できるしVisual StudioでもAndroid StudioでもCLionでも開けるんですよコレ)

完全に余計な話だけど、CMakeサポートはJUCEのオリジナル開発者Julian Storerがやりたがらなかった仕事のひとつなので(「CMakeが嫌だからJUCEを作ったんだ」)、彼が組織上は居なくなるタイミングでCMakeサポートが追加されるというのは何とも象徴的ではある(タイミングの問題であって、CMakeが理由で決別ってことはまあないだろう)。PACEの買収のポジティブな側面として、CMakeサポートは新しくコミュニティからスカウトされてJUCEチームの一員となった開発者が書いている。

これを書いている時点では、examples/CMake/README.mdに詳しいドキュメントがまとめられている。「ドキュメントはわかりやすい場所に置いてほしい」というユーザーフィードバックが開発者に届いていたので、そのうち移動するかもしれない*2

Run Headless (Linux)

JUCEアプリケーションがLinux上ではheadlessでGUIなしで動作するようになった。GUIが無くても良いということは、CIサーバ上で非常に動かしやすくなったということだ。CircleCIを使う記事を去年書いたときにも言及したけど(CIまわりはわたしが自分で書かないと英語圏でも誰も書いていないような状況だった)、これまではX server相当の何かをセットアップしないと何も出来なかった。

atsushieno.hatenablog.com

GUIコードが一切存在しなくても、Projucerでファイルを生成するためにはJUCEアプリであるProjucerが実行できなければならなかったし、これがheadlessで動作しなかった。ビルドファイルをコミットする覚悟があるプロジェクトでも、プラグインのルックアップすらjuce_eventsなしにはできなかったし、juce_eventsだけ使ってjuce_gui_basicsを使わないコードを書くことはほぼ無かっただろう。headless化が具体的にどこまで基盤を変更することになったのかはわからないけど(ソースツリー差分で調べようと思えば調べられそうではある)、juce_events依存部分が整理されたことを期待している。

なおLinuxのみらしい。CIサーバ上でheadlessのメリットがあるのはLinuxくらいなので、これで十分だ(GUIなしのWindowsmacOSがあれば話は別だけど)。

Linux VST3サポート

ついにLinux上でもVST3がサポートされるようになった。だいぶ前にホスト側だけでもと思ってパッチを作ってpull requestを送っていて、最近も「VST3対応しなさそうだから自分で作って公開するか…?」となっていたのだけど、公式対応が出てきたのでこれでひと安心だ(まだリリースされたわけではないが)。

JUCEのアイデンティティクロスプラットフォームでオーディオプラグイン等をビルドできることであり、VST3サポートが無い問題はLinux系音楽開発クラスタにとっては致命的だった。VST2が利用できなくなった結果、VST3という出口のない状態で、JUCEのAPIを使って書いたコードがLinuxでは何の価値も無くなってしまったのだ。LADSPA v1サポートはホストだけだし、そもそもv1にはMIDIサポートが無い。そしてLV2サポートも無い。

これが理由でかつてJUCEを使っていたLinux系のオーディオプロジェクトのいくつかが本家JUCEを捨ててLV2サポートのあるforkやDISTRHOに切り替えたり(dexedやADLplugなど)、あるいはJUCEを捨ててLV2に移行する(sfizzなど)といったトレンドが発生していた*3し、わたしも別のものに移行するかJUCEでないものを作るしか無いと思っていた*4

なお、昨日の時点で関連コードがjuce6ブランチに追加されている。

DSPのビルディングブロック

DSPにはディレイ、コンプレッサー、フェイザーなどいくつかの定番フィルターがあって、それらはもちろん単体でもオーディオプラグインとして機能しうるものだけど、実際のオーディオプラグイン製品とくに楽器の類では、これらをオプションとして最初から組み込んでいるものが多い。そういったアプリケーションを簡単に作れるように、DSPのビルトイン部品が追加された…ということだろう。

WebViewまわりの刷新

最新のWebViewが各プラットフォームで利用できるようになるらしい(一度も使ったことがないのでわからない)。blueprintなど、JUCEでも最近のアプリケーションのGUI部分をjuce_gui_basicsではなく独自に作成するトレンドもあり、WebViewサポートによっていわゆるガワネイティブ的な使い方も出てくるようになる可能性がある。

総括

PACEによる買収は、まだ今後どうなるか分からないのである程度注意しながら様子を見ていきたいけど、とりあえず資金が確保できたことで開発が先に進むようになった側面があるので(OracleによるSun買収もそうだったとは思う)、短期的にはJUCE6のポジティブな計画をたのしみに待ちたい。

*1:まあそうは言っても、JUCEはもともとTracktion社から出てきたもので、彼はtracktion_engineにもcommitしていたりするので、今後もそんな関係かもしれない

*2:わたしが眺めているtheaudioprogrammer discordにbuild-systemというチャンネルがあって、そこで観測した

*3:これはJUCEをLinux方面で使っていないと滅多に目にすることはなかったと思う。iPlug2などLinuxサポートの無いライブラリに比べたらJUCEは比較的マシなほうなのだけど、それでは不十分だったということだ

*4:今でもちょっと思っている節はある

3月の開発記録

4月ももう1/3くらい過ぎているのに3月かよ?ってなりますが、PCを修理に出したり、そこに至るまでそもそもPCの不調にやられたりでいろいろ止められていた感じです。

3月からいよいよ世間がコロナ一色になって落ち着かないですね。わたしは世間一般でようやく自分の在職時代の日常だった在宅勤務が普及するようになったのを横目で眺めながら、家でゲームしたりしていました(半分くらいは↑のせい)。3月はどちらかといえば生産少なめです。

目次

aap-juce

先月も書いていたAndroid用オーディオプラグインのプロジェクトにJUCEを統合するやつ、あまりにも手作り作業が多かったので、これを誰でも手順通りにやれば作れるように作業フローをまとめてCIできるところまでもっていこう、というhigh意識でちょいちょい作業しました。

github.com

Android用に組み込むと言いながら、実際にはAndroid用とemscripten用の2つをビルドしていて、これはだいぶ面倒なことではあります。ひとつには、先月書いたjuce_emscripten app gallaryのページもCIでリリースしたいという希望があったのと、もうひとつは後で詳しく書くUI統合で利用する目論見があったためです。juce_emscriptenのデモは、2月にやりたいと書いていたtracktion_engine統合の実験も兼ねて移植したStepSequencerDemoも追加してあります(統合サンプル自体は未着手)。tracktion_engineの開発者もTLで見つけたらしく、質問されたりしていました。

juce-demos.atsushieno.dev

それはさておき、ビルド自動化にあたって一番問題だったのはやっぱりProjucerが生成するMakefileemscripten用)を手作業で加工していた部分なので、ここは諦めてProjucerに手を加えました。

その過程で気づいたのですが、juce_emscripten自体はソースツリーがJUCEからのforkになっていなかったものの、実際には本家からのforkとして公開するのも全然難しくはありませんでした。なので現在はそういう構成でJUCEの独自forkとして発展させています。(juce_emscriptenの本家Dreamtonicsは現在進行中の製品開発で忙しいようなので、この辺はあっちが落ち着いたら連携して統合する予定です。)

結果的に以下の5つを移植してビルド自動化して、半分くらいはまだ謎クラッシュしたり音が出なかったりといったところまではやってある感じです。

  • AudioPluginHost
  • andes
  • SARAH
  • dexed
  • Magical8bitPlug2

それぞれどんなものなのかは、(Androidではありませんが)juce_emscripten app gallaryで見ることができます。

ちなみにビルドはGitHub Actionsで行っています。これは3月になってようやくactionsのAndroid SDKコンポーネント不足が解消されることによって実現しました。Android Studio 4.0の想定するAndroid Gradle Plugin 4.0.x系列が使用するCMakeが3.6から3.10.2にバージョンアップしたのですが、去年12月に試したときは、GitHub ActionsのVM (ubuntu-18.04) ではAndroid SDKに含まれるcmake 3.10.2がインストールされていなかったので、actionsに直接プルリクを送りつけて対応を求めたのですが全然反応が無く、2月になって他のユーザーも問題に気づき始めてコメントされるようになってから、ようやく3月にマージされました。

また、GitHub Actionsで1つのサンプルをビルドするのにかかる時間は20〜30分、それが5本あるので、1ビルド150分くらいかかっています(!) 1つのアプリをビルドすると500MBくらい消費してしまうので、intermediatesを削除して回ることでようやく可能になっています… 500MBのfree tierでもビルドできるようにしたいのですが(今はProの1GBを使えている状態)、500MB削るのは困難そうです。

UI統合の再検討

aap-juceでjuce_emscriptenを使っている主な理由は、UI統合のスキームとして使えるんじゃないかという目論見があったためでした。ロジックとなるオーディオ処理はプラグイン側のアプリケーションで処理するが、UIはjuce_emscriptenで生成したHTMLをホスト側のアプリケーションでWebViewとしてロードする、という設計が最も合理的なのではないかと思ったためです。

ただ、実際にjuce_emscriptenのアプリをAndroid WebViewで実行してみると、途中でエラーが出て止まってしまいます。ChromiumのDevToolsにあるremote devicesの機能を使ってデバッグしてみると、emscriptenが内部的に使用しているAtomicsAndroidでサポートされていないことがわかりました。GUI部分にatomicを使う必要はないはずなのですが2020/6/1追記: GUIコードもオーディオプラグインパラメーターを直接いじる時にはatomicsを使う必要がある場面があります、これがJUCEの膨大なコードベースから呼び出されるlibc等のどこで使われているかは、emscriptenの標準ライブラリに踏み込まないとわからなそうなので(juce_emscripten自体はJSのAtomicsを参照していない)、ちょっとこれは無理ゲーの香りがして中断しています。

UIをHTMLで実現するというアイディアはjuce_emscriptenでなくても可能なので、このアプローチ自体は引き続き考慮しています。たとえば、JUCE方面ではUIをReactで記述するblueprintというプロジェクトがあります。

sfizzからARIAまわりを探る

2月にはtracktion_engineを移植して音楽を鳴らせるようにしたい、という話を書いていましたが、音楽を鳴らすためには再生エンジンの他に楽器として使えるプラグインが必要です。JUCE方面ではjuicysfpluginというFluidsynthを使ってサウンドフォントベースのinstrumentプラグインがあって、最新のFluidsynthがAndroidでも動くことはよく知っているのですが(何しろ自分が移植したコードが公式なので)、これ自体はgtkを使ったりしていて、移植するより自前で作ったほうが早いかな…でもfluidsynth相当ならMIDIプレイヤーでもいいんだよな…みたいなことを考えていました。

それでふとsfzならkey switchとかもサポートしているし、もう少し高機能なんじゃないかと思って、OSSの合成エンジンを探して(何しろLinux方面では一番ポピュラーなLinuxSamplerが非OSSなので)、まずはJUCE統合もあるSFZero-Xを見つけました(実際にはSFZeroを見つけてその中で最も新し目のforkを見つけたわけですが、細かいことはおいといて)。それで、sfzformat.comからリンクされているsfzを試してみたのですがサッパリ期待通りに鳴らない。どういうことだろうと調べてみて、どうやらSFZeroはkey switchサポートも含めていろいろ機能が足りないということがわかりました。それで他にもいろいろ試した結果、OSSではsfizzが良さそうだということになりました。

sfizzはLV2をサポートしていて、QTractorで試しに使っている範囲ではちゃんと音が出るようなので、Androidに移植するのも現実的であるように思えます。ただ、sfzによってはSforzandoの独自拡張として定義されているARIAというUIマークアップW3Cとは無関係、仕様も未公開)もサポートしているものがあって、これをまずsfizzでサポートしたい気持ちがちょっとあります(ほぼ未着手)。ARIAは単なるXMLマークアップで内容もシンプルそうなので、SVGあたりに変換すればWebViewで表示できそうです…というあたりでさっきのUI統合の話と繋がります(!)

ちなみに、sfzで移植したら面白いことになりそうだと思っているのは、sfzformat.comからもリンクされているこの辺のギター音源とかです。keyswitchやvelocityいかんで各種奏法を切り替えられるし音色も使い勝手が良さそうです。

unreal-instruments.wixsite.com

あと、関連してLV2をVSTとして使うlv2vstというプロジェクトがあって、コレがあるとTracktion WaveformのようなLV2をサポートしていないLinuxDAWでもLV2音源が使えるのですが、LV2 featureまわりの解決に問題があってsfizzがロードできなかったので、その辺を修正するパッチを書いて取り込んでもらいました。最新版ではsfizzもロード出来ますが、lv2vstはnon-X11 UIをサポートしておらず、sfizzがファイル名を受け取るためのAtomポートにファイル名をバイト列として渡せるルートが無いので、sfizz側に手を入れないと実質使えない状態です。わたしは諦めてsifzzを組み込んだJUCEプラグインをちょっとだけ作ってその後放置しています…

guitarixとそのUIを眺める

これはどちらかといえばデスクトップ側でsfizzとギター音源を組み合わせてからの話なのですが、ギター音源があってもそれ単体だとありがたみがありません。本格的なオーバードライブやアンプを組み込んで音を作りたくなるはずです。Collectiveとかで適当にプリセット音色をいじっているときは組み込みエフェクトだけでもそれなりの音が作れますが、Androidには移植できないのでどこかから持ってくる必要があります。

Native InstrumentsのKompleteで遊んでいた時はGuitar Rigっていう製品があったけどああいうものはLinuxには無いのか…と思って探したら、ありました。知ってました。Guitarix。今でもsourceforgeで(!)開発されていて、LV2もサポートしています。これなら移植しがいがありそうです。

しかもソースを覗いてみるとなにげにwebuiなんてディレクトリまであります。コレもHTML UIの仕組みに乗っけやすそうなやつなので、もう少し深入りしてみようと思っています。ただ、enyo.jsとかいうやや古そうなフレームワークなのと、今guitarix自体がUIをgtkなどからx11に移植しつつあって、これはもしかしたらemscriptenで直接サポートできるUIになるのではないかという期待もあり、とりあえず移植が終わるまで待っています。

Androidネイティブバイナリビルドの動向

sfizzにしろguitarixにしろ、LV2プラグインなので、LV2がプラグインフレームワーク開発の出発点であるわたしとしては着手しやすいものです…となるはずなのですが、実際にはLV2サポートの根幹となるネイティブライブラリのビルドを整備して、aap-juceのリポジトリと同じくらい手順フローを整備してプラグインを簡単に移植できるようにしたいところです。

そんな中、AndroidオーディオチームがOboeのパッケージを新しく"prefab"フォーマットのaarで配布するよ、という話が流れてきました。prefabというのはAndroid Studio 4.0(用のGradle Plugin)で新しくサポートされる形式です。

google.github.io

詳しくは今回は省きますが、これまでのaarとは、拡張子こそ同じだけど根本的に違う内容になっています。そしてこのprefabをビルドするための仕組みとして、Googleはndkportsという仕組みを併せて公開しました。

android.googlesource.com

1つ以上のネイティブライブラリのソースtarballとビルド記述ファイルとなるport.ktsがあれば、後はndkportsのビルドスクリプトがNDK各種ツールチェインを駆使してaarまでビルドしてくれるという代物です。という目標はいいのですが、現実にはまだまだ機能が全然足りない感じです。さいわいprefabはndkportsが無くてもビルドできるので(自前でzipアーカイブすれば!)、ある程度の省力化・ビルド自動化はできそうです。

もっともこれでは開発がスムーズになる気がしないので(フレームワークやLV2ビルドに手を入れたらアプリケーションをAndroid StudioからF9一発でデバッグできるようにはならない)、パッケージング以外の場面ではまだ取り込めないかもしれません。いずれにしろ、4月はこの辺に手を出すタイミングなのかなと思っています。