juce_emscripten: 最新のJUCE on WebAssembly

English version

目次

2020年元旦からわれわれの界隈には刺激的なニュースが出てきました。

www.dtmstation.com

この記事で触れられているDreamtonics社がgithubで公開しているというのは、このjuce_emscriptenというJUCEのforkプロジェクトです。emscriptenを活用してwasmおよび周辺ファイルを生成します。

github.com

ちょっとだけ(本当にちょっとだけ)開発の手伝いをしていたので、詳しい解説をまとめようと思います。

Dreamtonics/juce_emscriptenは、JUCEオリジナルからのforkではなく、もともと5年くらい前にemscriptenでJUCEのWeb版を作ろうとしていた先人のプロジェクトのコードに基づくものです。これはかなり古いJUCEのバージョンに基づいているので、Dreamtonics/juce_emscriptenは最新版のJUCEにアップデートして、自社プロダクト(完全にJUCEで作られたアプリケーション)をビルド出来るようにした、ということになります。

(JUCEアプリケーションがWebブラウザで動作する先例としては、WebAudioModules (WAMs)を使用したWebDexedやWebOBXDといったアプリケーションがありました。WAMについては https://qiita.com/COx2/items/1f19d045936eebc3b82d でもう少し詳しく触れられているので、これを読むと良いと思いますが、WebAudioModules APIemscriptenを使用してJS上とネイティブコードの相互運用のごく基本的な部分だけを規定して、あとはC/C++のアプリケーションをそのまま移植できるようにしたものといえます。これらを実際にJUCEと統合した部分のソースコードWebOBXDのリポジトリで見られます。)

この最新のjuce_emscriptenがどれだけ実用的になっているか、まだ試していない人はここで試してみてください。

synthesizerv.com

juce_emscriptenを体験する

juce_emscriptenでは、JUCE本家のサンプルであるexamples/DemoRunnerがビルドできるように設定されています(後で言及しますが、juce_emscriptenはProjucerが対応していないので、Emscriptenでビルド出来るように調整されたMakefileが用意されています)。わたしが確認した限りではLinuxMacでビルドできます。WindowsならWSLでもできるでしょう。

emsdkがローカルで使える状態になっていない場合は、まずemmakeを使えるようにセットアップする必要があります。juce_emscriptenREADME.mdにあるビルドの説明が簡単です。

$ git clone https://github.com/emscripten-core/emsdk.git
$ cd emsdk
$ ./emsdk install latest
$ ./emsdk activate latest

emsdkがセットアップできたら、Emscriptenのツールチェインを呼び出せるように環境変数を設定します。インストールは1回で十分ですが、環境変数はその後いつでもemmakeを呼び出す前にシェル上で設定しておく必要があります。

$ source ./emsdk_env.sh

あとはemmakeでビルドするだけです。Emscripten用のビルドはexamples/DemoRunner/Builds/Emscriptenディレクトリ上にあります。生成済みのものがあるので、Projucer --resaveで生成する必要はありませんし、実のところ上書きするとビルドできなくなるので注意が必要です。

$ cd /path/to/juce_emscripten/examples/DemoRunner/Builds/Emscripten
$ emmake make

ビルドしたWebアプリケーションはローカルのChrome/Chromiumで実行できます。公式ドキュメントではcd build && python -m SimpleHTTPServerですが、わたしはnpx http-server buildで動かしています。HTTPサーバなら何でも良いです。

f:id:atsushieno:20200101003632p:plain
DemoRunner sshot

2021.12.30追記: このエントリーを書いてから、SPECTRE対策でSharedArrayBufferは無効化されるようになり、この手順では使えなくなりました。2021年現在、SharedArrayBufferはサイト側でCORS対策していないと利用できません。npx http-serverだと対応できない(--corsオプションだと足りない)ので、この辺のやり方を真似するとよいでしょう。

JUCEのプラットフォームバックエンドの基本

一体何をどうやったらデスクトップアプリケーション向けのフレームワークであるJUCEがWebブラウザで動作するようになるのでしょうか? 普段からクロスプラットフォームライブラリの実装を追いかけている人から見れば自明のことですが、どのクロスプラットフォームのライブラリも、プラットフォーム固有の部分を個別に実装して、(一般的には)共通のAPIのみを安定APIとして公開する、というアプローチが採られます。

JUCEの場合、本家がすでにWindows, Mac, Linux, iOS, Androidをサポートしており、そのプラットフォーム固有部分はJUCE本体の各モジュールで実装されています。JUCEはわれわれがよく目にするReact Native, Xamarin, Flutterといったクロスプラットフォーム開発フレームワークとは異なり、オーディオアプリケーション開発が主眼にあり、オーディオ/MIDI IO部分もクロスプラットフォームで実現する必要があります。オーディオ/MIDI IOはjuce_audio_devicesというモジュールで、GUIjuce_gui_basicsというモジュールで、主に実現しています。

また、GUIは主にメインスレッド上で動作するメッセージループのコールバックに基づいて動作する仕組みがどのOSにも存在しており、これはjuce_eventsというモジュールに独立して存在しています。メッセージループのコールバックの仕組みはオーディオI/Oでも用いられており、このjuce_eventsjuce_audio_basicsにとっても依存モジュールのひとつです。GNOMEデスクトップ開発に慣れている人であれば、gtkとglibが分かれていることを知っているかもしれませんが、juce_eventsはjuce_coreと合わせるとglibのような位置付けになります。

これらのモジュールのソースコードの中に、プラットフォーム固有の実装が含まれています。juce_audio_devicesの場合はjuce_audio_devices/nativeに、juce_gui_basicsの場合はjuce_gui_basics/nativeにあります。それぞれのディレクトリにAndroidだのiOSだのwin32だのといったファイルが含まれていることがわかるでしょう。

そして、プラットフォーム固有の実装は、自分で追加することもできるのです(そういえば今年のJUCE Advent Calendar1日目の記事もそういう内容でしたね)。juce_emscriptenは、ここに新しくemscripten / wasm用の実装を追加するものです。

自分のプロジェクトをjuce_emscriptenでビルドする際の注意点

juce_emscriptenは、条件が合えば自分のプロジェクトでも利用することが可能です。DemoRunnerとSynthesizer Vが動くというだけではもったいないので、他のアプリケーションも動くようにしたいですよね(!?)

ただし、現状ではjuce_emscriptenはSynthesizer Vが動作するために必要最低限のコードが実装されているものであって、まだいろいろ足りない部分があり、またビルドしたアプリケーションが動作するまではいくつかの手作業が必要になります。以下で少しずつ詳しく説明していきます。

missing modules

まず、README.mdのStatusセクションを見ると、どのモジュールがサポートされていないかがわかります(流動的に増えるかもしれないのでリンクだけ示しておきます)。

特によく引っかかりそうなのは、JUCEでよく使われていそうなオーディオプラグインサポートのためのモジュールjuce_audio_plugin_clientで、これはまだサポートされていません。juce_audio_plugin_clientを使っているアプリケーションはstandaloneでもビルドに失敗します。

もしサポートされていないものを使っている場合は、そのモジュールの機能を使わないように改造するか、ビルドエラーをもとに自分で実装する(!)しかありません。ゼロから自分でGUIもオーディオも実装するよりは、はるかに楽になっているはずですし、移植のための知見も本家との差分から調べやすくなっているとは思います。

missing Projucer builder type

また、現状ProjucerでプロジェクトファイルをEmscripten用に生成することはできません。Emscriptenの一般的な使い方と同様に、ProjucerでいったんLinuxMakefileを生成して、その内容に手を加えることになるでしょう。DemoRunner.jucerには、Emscripten用の出力ターゲットが追加されているので、これを再利用するのが一番手っ取り早いです。

注意すべきは、Projucerで出力したMakefileは、そのままでは使えないということです。DemoRunner.jucerには次のようなコメントがあります:

After resaving, do the following by hand:
Remove $(shell pkg-config ...) from the Makefile
Change JUCE library path to juce_emscripten
Add .html extension to target app
Optionally, add -s SAFE_HEAP=1 -s ASSERTIONS=1 to JUCE_CFLAGS for Debug build
Optionally, add -s WASM_OBJECT_FILES=0 to JUCE_CFLAGS and
 --llvm-lto 1 to JUCE_LDFLAGS for Release build.

重要なのは(1)JUCEモジュールのパスをProjucerの指定するmodulesのパスからjuce_emscriptenリポジトリmodulesに置き換え、(2)pkg-configを呼び出している部分を全部消すことと、(3)JUCE_TARGET_APPに拡張子.htmlを追加することです(こうすることでemmakeのビルド結果が.html、.js、.wasm等のセットになります)。これはProjucerで保存するたびに行う必要があります。

2020/02/02追記: これらに加え、JUCE_CPPFLAGSに -I/usr/include/freetype2 を追加してやる必要もありそうです。(本当は/usr/以下のパスを指定するのは良くないような気もしますが…)

LinuxMakefileのビルドファイルを手作業で編集するのが面倒になったら、Projucerに手を加えるような変更をcontributeすると良いかもしれません(!)

ちなみに、Projucerで新規GUIアプリケーションを作成して生成されたプロジェクトは、実のところビルドできません。利用モジュールの中にサポートされていないjuce_openglが含まれているためです。Projucer上でこのモジュールを外すと良いでしょう。

missing resources

もうひとつEmscriptenの制限でちょっと面倒なのは、フォントファイルをはじめとする各種リソースをemccの--preload-fileオプションで組み込まなければならないということです。実のところフォントを組み込まないとシンプルなASCII文字列すら描画してくれません。先のDemoRunnerでは、Linuxビルドが参照しているX11のフォントリソースを取り込んでビルドするようになっています。

DemoRunnerのビルダーを使い回す場合は(使いまわしたほうが楽だと思います)、DemoRunner固有の--preload-fileオプションは外しても大丈夫ですが、X11R6のフォントはコピーした上で--preload-fileを指定するのを忘れないようにしましょう。

足りない機能は自分で移植できる

いかがでしたか?(キュレーションサイト風) juce_emscriptenを使うと、既存のJUCEアプリケーションをWebブラウザ上にもってくることが、だいぶ現実的に可能になってきたことが見て取れたのではないでしょうか。

ただ、JUCE本体は幅広いツールチェインでサポートするプロジェクトの種類も多様です。おそらく"GUI Application" 以外の種類のプロジェクトを移植するには、juce_emscripten本体をちまちまとハックする必要があるかもしれません。とはいえ、おそらく最も面倒であろうGUIとオーディオI/Oのコア部分は、オリジナル開発者とDreamtonicsによって移植されているので、足りないところは問題にぶち当たった時にちまちまと実装してcontributeしていくと、完成度が上がっていくのではないでしょうか。

おまけ

2019年の仕事振り返り

仕事と言っても無職だけどな…!

2019年は、1年半前に始まった大無職時代をどのように生きるか、という問題に立ち向かった長い1年でした(こう書くと物は言いようっぽい)。音楽ソフトウェア開発者として、何もなかった頃の1年前と比べると、割といろいろ勉強できたと思います。

目次

1-2月 音楽制作環境(特にDAW)に慣れる

3/2に幻想音楽祭という音楽同人イベントがあり、第1回っぽいし初参加するにはちょうど良さそうな規模だったので、一度まじめに音楽制作というやつに取り組んでみてもいいかな〜と気軽に応募したのが通っていたため、昨年末から今年の最初の2ヶ月はそのために費やされました。

今でもそうといえばそうなのですが、特段「創作したい」という意識があるわけではなくて、自分の作っているMMLコンパイラ中心の制作環境がどこまでプロダクションで使えるのか試しておきたかったですし、自分の実用に耐えるレベルになる、というところまでは作り込んでおきたい、そうでないとこのアプローチを推奨することはできない、と考えていました(います)。

また完成品を作るにはどこかの時点でDAWを使わないといけないはずなので、DAWの使い方もちゃんと覚えよう、というhigh意識で進めていました。使っているDAWはこの時点でTracktion Waveformだったわけですが*1、この使い方は実際だいぶ勉強できていい経験になったと思います。

それまでほぼ創作活動は行っていなかったので、さすがにフルタイム無職とはいえ2ヶ月で完成度の高い作品を作り上げるのは無理がある、とわかっていたので、まずは過去作品を掘り出して使える部分を使いまわしながら2,3曲でっち上げたミニアルバムを出して、あとは技術書典4の時に書いたMMLコンパイラの本を展示・公開してブースの体裁を整えよう、と思いました。

「昔作っていた」を使い回すために、昔書いたMMLを自分のコンパイラMML文法に書き直す作業から始めたのですが(MMLはツールごとに文法が全然違う、インド・ヨーロッパ語族の言語みたいなもんです)、さすがにコンパイラ自身についてバグフィックスなどすることはあまり無く*2、どちらかといえばビジュアルMIDIプレイヤーでのコンパイルから再生を自動化するワークフローや、仮想MIDIキーボードへのMMLサポート組み込みといった、周辺ツールの改造がだいぶ進みました。

MMLでの打ち込みにはRoland SC-8820を使っていたのですが、これをやりこんでも最終的なプロダクションには使えないだろう、Waveform上でVST/AUに置き換えて完成させる必要があるだろう、とは思っていました。JUCEで作られているWaveformはLinux環境でもほぼ問題なく動きますが、VSTプラグインはほぼ全滅でWaveform付属のCollectiveくらいしか無かったので、最終的にはMacを調達してKontaktなどに置き換えました。

WaveformのMIDIインポート機能には拍子とテンポまわりにバグがあって、変拍子だらけの曲を作っていた自分としては致命傷だったのですが、対応が難しそうだったので、データ形式XMLだったのをいいことに、WaveformのデータモデルのライブラリをC#作って自前でMIDIインポートも自作して乗り切りました。

これが割と良い経験になったのでした。それまでWaveformにどんな機能があってどんな内容のデータが楽曲データとして保存されているのか、そもそもオーディオプラグインにどんな機能があるものなのか、全然わかっていなかったんですね。モデルの情報は特に無くて、Waveformの楽曲データから全て推測で作成したのですが(まあいざとなればtracktion_engineはOSSで公開されていますし)、十分にいろいろな情報が含まれていたので、Waveformのデータモデルを扱う機能は概ね把握できたと思っています。

3-5月 オーディオプラグインまわりをいじり始める

幻想音楽祭のための創作活動が終わって一息ついたのですが、2月にAndroid QのAPIが初登場して、人手不足だったXamarin.Androidチームから新APIバインディングを作る仕事を請負でやってくれないかみたいな話を内々に相談されていたのでそれを片付けたり(結局2日でほぼ終わってしまったので無償で)、去年公式リポジトリに投げてアップデート対応が必要だったFluidsynthのAndroidサポートにOboe(低レイテンシーオーディオ)対応なども追加したりと、いろいろ雑務(?)をこなして過ごしていました。

その一方で、DAWを使った制作プロセスの反省も含めて、もう少しオーディオプラグインをこちら側に近づけて、最初の打ち込み過程からMMLで直接いじりたいと思うようになりました。最初から作り込まずにベタ打ちで続けてもやる気出ないですし、2回作り込むの二度手間ですし(しかも最初に作り込んだのが邪魔になる)。今使われているDAWは作り込むのに向いていないんですよね。

楽曲再生エンジンだけならtracktion_engineが使えるわけですが、再生部分などはどうなっているのか相変わらずブラックボックスだったので、まずオーディオプラグインがどんなものでどこまでできるのか、今度は開発者としての視点で、VST3とJUCEを中心にいろいろ機能を調べるようになりました(特にJUCEはAUにも対応しているので)。Waveformのデータモデルから、オーディオプラグインでどんなことが実現できるのかはある程度見えていたので、良い動機づけになりました。

ただVST3はJUCEでサポートされておらず、最低限ホストを動かすためのパッチまで作ったりしましたが、進展が見られず、自分で進めたところでROLIの開発者と作業がかぶって無駄になるし、これじゃ良くても次のADC2019の頃まで出なさそうだなと踏んで、LV2をいじり始めるようになりました。LV2をいじったところで楽器が出てくるわけではないのですが、調べものとしては悪くなかったと思います。この辺の成果はひとつ10月に同人誌として結実しました。

6-8月 無職を失う

この頃は無職の意識高いツイートに腐心していたのですが(ツイッターはやってません)、

からの…

特段隠すつもりはあんまりなくてリアルではペラペラしゃべっているのですが、音楽系ソフトウェアのベンチャーで手伝いを始めて、最初3か月くらいしたら公知にするか…という感じでした。7月に始めて3か月くらい実験的に契約の予定だったのですが、ひと月でもうフルタイム社員にしちゃえという流れになりました。「ちょっと早くないかな…こっちは事前通告でいつでも辞められるけど会社側はそうもいかないし、3か月くらい試せば…?」などと言っていたのですが、実際3ヶ月で無職に戻ることにしたので(主に「自分で作りたいやつ」を作れる時間が全然とれなかったため)、結局謎のままで終わったのでした。

仕事は主にJUCEを使ったデスクトップアプリの開発で、C++開発者が求められていたのですが、5年くらいのキャリアが求められているところにC++キャリア0年での入社でしたが(!)、C++自体がわからなくてハマったのは最初の数週間くらいで、後はまあ言語で困ることは特に無かったです。JUCEは概ねモダンC++の世界なので、言語的なつまづきポイントは少なかったと思います(そういうものか…?)。まあもともと.NET開発者だった時もP/Invokeしまくってたし…

会社自体は面白いものを開発していてこの方面ではニュースバリューもあると思うので、気が向いたら年始に何か書くと思います。辞めてからもまだ手伝ったりしているし(謎)

JUCEをがっつり触れたのは良かったし本家にパッチを書いて送れる程度には分かってきたので、今後も機会があるたびに使える部分を使っていこうと思っています。

9-10月 M3に追われる

M3に初めてサークル参加しました。本当はこの時までにもう1枚CDとして新作を作るつもりだったのですが、わたしの仕事を始めるムーブが本当に良くなくて、当選してから創作するための時間が全く取れず(好きな時に創作できることはもちろん無く)、結局5月頃に少し書き出していたDAWのエンジン部分に関するテキストに手を加えてシーケンサーエンジンを支える技術として同人誌頒布と春先の新譜(!)の2本でサークルとしての体裁を整えることになりました(MML本も見本としては置きました)。

次回は技術書典8と日程まるかぶりなのと、開発しておきたいものがあってそっちに注力していたら当日までに創作する時間をとれる気がしないのとあって、申し込んでいません。ただM3で何も出来なかったというダメージが大きく、それが仕事を辞めた大きな要因のひとつです。別にただのフルタイム労働であって長時間労働させられていたということは全然無いのですが、それで時間が取られすぎるということは労働すること自体が問題だったということでしょう。もちろんいずれ労働しないと食っていけなくなるわけですが、自由なうちは自由にします。

11月 ADC2019〜原点回帰

11月半ばまではフルタイムで仕事していたので特段なにもしていなかったのですが、下旬にADC2019に行ってきました。2018の時は、無職になってまだ3ヶ月くらいの新人の頃だったので、どんなカンファレンスなのかもあまりわかっていなかったのですが、今回はそれなりに勝手を知っていたこともあって、かなりエンジョイ勢でした。今年はオーディオプラグインまわりでいろいろ遊んでいたので、SteinbergのブースでVST3の話を質問したり、NIのブースでKontaktの質問したり、AppleのブースでAUの仕組みを教えてもらったり…といった感じでした。あとGoogleのオーディオチームと1on1で話せるというので、Androidで自分が作っているものを見せてじっくり相談してきました。ROLIの人ともLinuxサポートまわりの話ができたし、収穫はいろいろありました。ADCの話は来年何かしらの発表の場を設ける予定です。

12月になってもまだ実は仕事が続いていて、週2日だけ手伝っていたのですが、それもひと区切りついたので、最近ようやく5月頃の状態に戻って自分のソフトウェアの開発に本腰を入れられる状態です。11月下旬から12月はAndroid同人誌の原稿やらアドベント カレンダー用の調査執筆が多発して、あんまり進められなかったので、これからだ…!という気持ちで年末を迎えています。

2020年は…?

2020年をどう過ごすのか、自分でもまだわかっていません。今個人で作っているソフトウェア次第ですが、どこかしらに所属して開発するかもしれないし、そうでなければ自分で何とかするかもしれません。ある程度は自由な時間を確保したいので、他によほど面白いものでも無い限り、フルタイムで無関係な仕事をすることは無いと思います。まずは有職中にほとんど進展が無かったプロジェクト(こんな感じ)を進めるところからですね…

f:id:atsushieno:20191230004611p:plain

とりあえずはかなり柔軟に時間を使える予定で、自分のプロジェクトもフルタイムで進める必要があるわけではないので、適当に面白そうな仕事の話とかあったら相談してください〜

*1:ちなみにライセンスだけはFL StudioだのCubaseだのも無駄にいろいろ持っていたりして…

*2:ただlanguage serverまわりはvscode自体の更新に伴う問題が多く、創作上のメリットが皆無だったのでほぼ放棄することになりました

libinstpatchとは

音楽技術etc. Advent Calendar 2019の…うーん…とりあえず21日目くらいにしようかな…のエントリーです。残りは上手く埋まったら埋めていきます。とりあえず今回はlibinstpatchについてです。短めに。

github.com

libinstpatchはswami projectの一環として公開されているインストゥルメンタルパッチの抽象表現ライブラリです。インストゥルメンタルパッチとは何かというと、具体的には次の3つの実装があるので、これを見ると何となくでもすぐに理解できると思います:

要するにサウンドフォントやDLSはどちらも性格が似ているので、両方ともまとめて扱えるようにしてしまおう、というものです(GigaSamplerはどの辺のコミュニティで使われているのか正直よくわかりません)。どちらも懐かしいといえば懐かしい存在ではありますが、現役で使われていなくもないものですね。

どういうところで使われるかというと、サウンドフォントベースのサンプラーや仮想MIDIバイスのソフトウェアで使うことができます…そうです、Fluidsynthの最新版2.1でこのlibinstpatchがサポートされた結果、FluidsynthでもDLSが使えるようになりました。実のところ作者がFluidsynthのメンテナなので、推して知るべしではあります。libinstpatchをどうやって使うかも、ここから使われ方を見てみれば分かるかと思います。

SFもDLSも(たぶんGigaSamplerも)単なるインストゥルメンタルパッチのデータストレージにすぎないので、このライブラリ自体がやっていることは単純です。サウンドフォントを読み書きするライブラリのAPIと機能的には大して変わらないはずです。

データロードの部分だけはIpatchConvert_から始まる名前のフォーマット別実装が含まれています。特に難しいことはないでしょう。

Xamarin/binding-tools-for-swiftについて

Xamarin Advent Calendar 2019 24日目のエントリーです。えっオマエ生きてたの?って感じですが、軽めのネタ・調べ物で参戦です。

目次

what is it?

2019年はXamarin方面では落ち着いた1年になっていたかと思います。ネイティブ・プラットフォーム側はJetpack ComposeやSwiftUIが発表され、これから新しい時代が始まろうとしていますが、Xamarinは独自のクラシックなエコシステムに立脚するだけで続いていくのか、新しい波にきちんと乗っていけるのか、来年は動きを見せていくべき1年なのだろうなと思います。

Xamarin.Android方面ではbinding generatorからKotlin artifactを取り除いたり、AndroidSupportComponentsからAndroidXサポートを切り出したリポジトリが出現するなどの進展は見られましたが、それ以上に大きな動きは無いようです。まあ退屈と言えば退屈ですね。

一方で、今年公開された中でわたしがダントツに面白そうだと思ったのは、xamarin-macios方面で今年の秋頃に公開されたらしいbinding-tools-for-swiftです。

github.com

これまでもtom-swiftyやらSwiftNetifierやら、いろんな名前で開発されてきたSwiftとC#の相互運用のためのツールが、ついに公開されたということですね。tom-swiftyの名前はツール名としても残っているみたいですね(Tom Swiftyという単語はWikipediaにも項目があるくらい一般的であるようです)。

binding-tools-for-swiftに関する公式リポジトリ以外の情報は現状ほぼ皆無ですが、Xamarinで開発を担当してきたメンバー(らしい。わたしがいなくなってから入ってきた人みたい)が導入的なブログポストをまとめています。

plinth.org

現在製品化されているXamarin.iOSとXamarin.MacObjective-CAPIに対応するバインディングバインディング生成機構を、ObjCRuntimeを経由して実現するものですが、Swiftの場合はランタイムも含めさまざまな独自要素が含まれてくるので、新しいバインディング生成ツールが必要とされてきました。

Swift自体はもう何年も前から存在する言語なので、今さらと思われるかもしれませんが、一般的な評価として、Swiftの言語仕様が安定してきたのはSwift3〜4あたりの比較的最近のことであり、言語機能はObjective-Cよりも複雑で、さらにABI安定性もSwift4でようやく目標となったような段階なので*1バインディングツールに関しては、ようやく本腰を入れて取りかかれる状態になったとも言えます。(ABI不安定なときにがっつり実装しても新バージョンのSwiftで台無しになったら残念なことになるので…)

わたしはAppleユーザーとは言いがたいですしこれまで試したことも無かったのですが、今回はこのbinding-tools-for-swiftをどうやって使うのか、何が面白いのかといった話を書きます。

binding-tools-for-swiftという名前は長いので、以降公式ドキュメントの慣行に準じてBTFSと表記します。

今使えるの?

BTFSはまだ著しく開発中で、おそらく大概のライブラリはバインドに失敗するのではないかと思います。仕様としてバインディング作成が難しい(Xamarin.Androidのように、言語間の違いを吸収して完全自動でバインドするのが、チャック・ノリスにしか出来ない)とかいう以前に、そもそもツールチェインが整っていないとか、エラーログがまともに出力されない、といったレベルの状態なので、われわれ外部の開発者はもう少し整備されるのを待ったほうがよさそうです。

BTFSのビルド手順はトップディレクトリでmakeを実行するだけなのですが、現状では、xamarin-maciosをビルドするのと同様に、特定のバージョンのXcode、monoやXamarin.iOS、Xamarin.Macのダウンロードが必要になります(Xamarinプラットフォームはmasterでも可能そうですが、おそらくBTFS開発者向けです)。Xcodeだけでも15GBくらいは必要になるのでストレージに余裕の無い人は試せないと思います。不足コンポーネントをダウンロードするためのjenkinsスクリプトのコマンドなどを示してくれるので、それに従っていればいずれビルドできるでしょう(適当)。

いったんビルドできたら、mono tom-swifty/bin/Debug/tom-swifty.exe --helpを実行して確認してみるとよいでしょう。

実際にライブラリをバインドするときは、quickstart guideにもある以下のコマンドを実行することになります。

mono /path/to/tom-swifty.exe --swift-bin-path SWIFT_BIN_PATH --swift-lib-path SWIFT_LIB_PATH -o /path/to/output_directory -C /path/to/YOURLIBRARY.framework -C /path/to/binding-tools-for-swift/swiftglue/bin/Debug/PLATFORM/XamGlue.framework -module-name YOURLIBRARY

SWIFT_BIN_PATHSWIFT_LIB_PATHには以下のようなパスを指定することになります。BTFS用に手を加えられた独自のswiftコンパイラが必要になります。

/path/to/binding-tools-for-swift/SwiftToolchain-v3/GITHASH/build/Ninja-ReleaseAssert/swift-macosx-x86_64/bin
/path/to/binding-tools-for-swift/SwiftToolchain-v3/GITHASH/build/Ninja-ReleaseAssert/swift-macosx-x86_64/lib

わたしはBTFSをビルドしてコマンドラインで実行できるところまでは進めましたが、バインド出来るライブラリは特に見つけられませんでした。AudioKitあたりはバインディングがあってもいいんじゃないかなーと思ったのですが、framework以下の実行ファイルへのシンボリックリンクを読めないレベルの完成度でした。

一般ユーザーのところに降ってくるまでには、MSBuildタスクなどが完成してMSBuildやVS{Mac, Win}から呼び出せたりNuGetで取ってこられるようになったりしていると思います。

とりあえず、今まだ使えないとしても、BTFSがどんな工程を経てユーザーのライブラリに対するバインディングを生成するのか、という部分は実装のステータスとは別に面白い話になると思うので、このエントリの以降の内容はFunctional Outlineのドキュメントで説明されているビルドプロセスについて、他のドキュメントにも言及しつつ説明していきます。

どんなバインディングが出来るの?

BTFSでは、以下の言語要素がサポートされています。

サポートされていないのは↓のような要素です。

ObjectModelingのドキュメントにおおよその構造が書かれていますが(ちょっと長いのでここでは引用しません)、ISwiftObjectというインターフェースを実装するオブジェクトとなるようです。Xamarin.AndroidにおけるIJavaObjectみたいなものでしょう。(XAの場合は、IJavaObject自体は単なるJavaオブジェクトのマーカーでしかなく、必ずJava.Lang.Objectクラスから派生することになりますが。)

Objective-CのときもプロトコルはストレートにC#マッピング出来ない存在でしたが、Swiftのプロトコルへのバインディングもやや複雑な説明を要する存在になっています。Protocol Handlingのドキュメントで詳しくまとめられていますが、EveryProtocolという何やら神っぽいクラス(!?)を定義して、それを渡して廻すようです。

swiftライブラリのメタデータ構築

Swiftのライブラリをソースからビルドすると、swiftmoduleというライブラリのメタデータを格納したファイルや、SILと呼ばれる中間表現のバイナリファイルになります。BTFSではこれらを対象にバインディングの元になるSwiftTypeという型情報を構築していきます。最終的にSwiftTypeを集めたものはInventoryと呼ばれるメタデータの集合体になり、次のビルドプロセスであるところの「ラッピング」の入力になります。

もっとも、このアプローチでは十分な型情報を得ることができないので、次にXML relectionという解析プロセスを経て型情報を完成させます。

ちなみに、これらの過程ではswiftのライブラリのバイナリから識別子を取得する必要があるのですが、swiftコンパイラはswiftの識別子をバイナリコードとしてプラットフォーム準拠の形式にするためにmanglingという加工処理を行います(C++コンパイラなどでも行われています)。BTFSにはこれを解析するDemanglerというコンポーネントが含まれています。

独自swiftc拡張

BTFSのドキュメントによると、swiftcが生成するswiftmoduleやライブラリファイルには、バインディングの生成に必要となる型情報が十分には含まれていないため、Xamarinで独自にswiftcに手を加えた(?)コンパイラをビルドするようです。XML reflectionの過程で使用されるのはこのスクリプトでビルドされる…かもしれない…コンパイラです。(通常はビルド済バイナリのダウンロードが走るだけになりそう)

https://github.com/xamarin/binding-tools-for-swift/blob/master/jenkins/build-swift.sh

XmlReflectionのドキュメントを眺めてみると、既存のswiftcに手を加えるというよりはそもそも自前でswiftコンパイラを作るみたいな壮大な(?)話が書かれています。まあパーサを作ってXMLメタデータを生成するだけなのでそれほど非現実的でもないかも…? わたしもXamarin.Androidのビルド用でJavaスタブコードのパーサー書いたりしましたし(たぶん今でも使われているはず…?)

ともあれ、このXml reflectionのアウトプットはModule Declarationと呼ばれ、これも次のビルドプロセスであるラッパー生成処理への入力となります。

ラッパーswiftライブラリの生成

InventoryとModule Definitionという2つの類似するメタデータ集合が出来上がったら、次はこれらをもとにいよいよバインディングを生成…というわけにはいきません。 ユーザーのswift APIを、いったんC#のP/Invokeで呼び出せるスタイルのAPIでラップしたswiftコードを生成します。この過程はWrappingBuilderという部品が実現しています。

BTFSの中で、メタデータ情報を解析して型ツリー情報を構築するのはDynamoと呼ばれています。このBTFSから、swiftコードを生成したり、後でC#コードを生成したりすることになります。

ラッパー生成処理は、実際には2つのユーティリティで行われています。WrappingBuilderが基本的なクラス構造を定義して、もうひとつOverrideBuilderという部品が、C#でopenクラスを派生できるように必要になる派生クラスを生成します。Xamarin.Androidにもgen-java-stubs.exeとかGenerateJavaStubsというMSBuildタスクがあるのですが、それに相当するものと考えてよさそうです。

C#バインディング生成

ラッパーを準備してAPIメタデータを整地したところで、いよいよC#バインディングの生成に入ることが出来ます。まず、ラッパーのライブラリから、再びSwiftTypeのInventoryとreflectorによるModule Declarationの2つのメタデータ集合を生成します(処理内容は最初と同じです。対象がラッパーになっただけです)。ここでまたDemanglerが必要になります。

C#のコードを生成するのは、ラッパーを生成する時に登場したDynamoの役割です。これでようやく完成です!

BTFSの制限

BTFSがサポートしているのはSwift 5までの文法で、Swift 5.1はサポートしていません。SwiftUIが多大に依存しているOpaque Result Typesなどはまだ使えないということですね。

まとめ

今回はXamarin/binding-tools-for-swiftで行われているバインディングのビルド過程を追ってみました。途中で独自のswiftコンパイラハックを加えていたりと、非常にいかがわしい萌え技術が使われているのではないでしょうか。

*1:たとえばこのswiftのname manglingに関するドキュメントを見ると4.0と4.2で違っていることがわかります

LV2の概要

音楽技術Advent Calendar 2019の11日目のエントリーです。(まあだいぶ穴が開いているのですが、マイペースに埋めていきます。)

What is LV2?

LV2は主にLinux環境で利用できる、クロスプラットフォームのオーディオプラグインの仕様です。

オーディオプラグインとは、主にDAW (digital audio workstation)環境でDTM(desktop music)の作業を行う場面で、楽器やエフェクターとして利用できるオーディオデータの生成・加工ソフトウェアとして使われるものです。オーディオプラグインは、さまざまな「ホスト」となるDAWなどのソフトウェア*1の上で、さまざまなものを繋ぎ合わせて使用することが多いです。よくある使い方としては、サンプラープラグインでサンプリング音源データをノート(キー)に合わせて波形を調整して、それをロータリーエンジンやリバーブプラグインで加工して、ミキサープラグインでゲイン調整してオーディオデータとして出力する、といった流れになります。

オーディオプラグインは、複数のホスト、複数のプラグインベンダーの間で互換性が求められるもので、デファクトスタンダートとなる仕様がいくつか存在しています。有名どころではSteinberg VSTApple AU (AudioUnit)が挙げられます。オーディオ処理がもともとプラットフォーム固有の実装になりがちだったこともあってか、ここには業界標準となるような仕様が存在しないのが現状です。また、DAW開発社でも、自分たちのDAWでしか使用できない独自のプラグイン形式を規定して公開しているものがあります。Avid Audio eXtensions (AAX)などがその典型です。

本稿で取り上げるLV2とはLADSPA v2の略であり、LADSPAとはLinux Audio Developer's Simple Plugin APIの略です。Linux環境ではVSTAUのようなデファクトスタンダートのように使われている仕様のオーディオプラグイン機構が動作しなかったため、独自にオーディオプラグイン機構を開発して発展させる必要がありました。それでもっともポピュラーだったと言える仕様がLADSPA (v1)です。

オーディオI/Oそのものはプラットフォーム固有の実装とならざるを得ない部分があるのですが、一旦そこを抽象化すると、実際のオーディオ処理の部分は、実のところプラットフォーム固有の実装になることはあまり無かったため、LADSPA v2は最早L (Linux)固有のものではなく、MacでもWindowsでも動作可能なものになっています。LV2と同様、VSTクロスプラットフォームMacでもLinuxでも使用できる存在となっていきました。

LV2は2019年現在、ardourやqtractor、museなど主要なLinuxDAWでサポートされていますが、それ以外の製品、特にWindowsMacでは未採用の製品が多いです。

LV2プラグインの概要

LV2は(VSTなどと同様)現在進行形で拡張されている仕様です。後方互換性を維持するために、LV2仕様はモジュラーアーキテクチャになっており、コア部分と拡張部分を切り分けてあります。そのため、使用可能な最低バージョンがモジュールによって異なります。これはVST3のMA(モジュールアーキテクチャ)と似ている側面があります。

LV2仕様のモジュールリスト どのプラグインフォーマットでも、プラグインにはメタ情報が含まれているものですが、LV2の場合はこれをRDFによって提供することになっています。実際には、XMLとしてのRDFではなく、RDFを「シンプルな」テキスト形式で記述するTurtle (Terse RDF Triple Language)と呼ばれる独自記法に変換した.ttlというファイルに記述することになります。

RDFの記述内容は、プラグインAPIについても当てはまり、VST3やAUの場合は、単にCやObjective-CAPIを実装するだけで済むのですが、LV2の場合は、実際にコードとして実行するために必要になる最小限の部分のみをコードとして実装し、残りの部分はこのメタデータとして作成する作業が必要になってきます。すなわち、(1)プラグイン利用者の視点でいえば、そのプラグインにどんなポートやパラメータがあるか、どんな入力を受け付けるかは、RDFの情報だけでも把握できますし、(2)プラグイン作成者の視点でいえば、そのプラグインにどんなポートやパラメータがあるか、どんな入力を受け付けるかは、コードだけで完結せず、メタデータに記載しなければならないことになります。

プラグインでは独自の型を規定して公開することも可能です。MIDIメッセージやプリミティブ型(Atom型)などLV2標準に含まれる型の多くが、このルールに基づいて規定されています。もっとも、一般的なプラグインにおいて、複雑な独自型を公開型として定義する必要は滅多に無いでしょう。

モダンなプラグインフォーマットでは、プラグインが単一のライブラリファイルとして提供されていることはなく、関連ファイルとまとめて1つのフォルダなどにまとめて配置するものが多いです。LV2も同様で、「LV2パス」のいずれかに.lv2という名前で終わる「プラグインバンドル」のディレクト*2に関連ファイルをまとめます。この中に入るのは典型的にはこのようなファイル群です:

LV2フォルダ構成 ファイル名は任意に決定できますし、1つのプラグインバンドルに複数のプラグインを含めることもできます(manifest.ttlに記載する必要があります)。マニフェストの内容は、このようなテキストにまで圧縮できます(Turtle syntaxとはこのようなものです):

<http://example.com/arbitrary-paths/foobar>
    a lv2:Plugin ;
    lv2:binary <foobar.so>  ;
    rdfs:seeAlso <foobar.ttl> .

.soLinux環境における共有ライブラリなので、実際のプラットフォームに合わせて変わります。

LV2の拡張性

LV2の標準仕様は、バージョン1.0時点で既知であった最小限の機能のみを提供し、それ以降のバージョンで追加された機能については、ホストから渡されない限り、プラグインが利用することはできない仕組みになっています。

拡張機能の中には「URIと一位識別子URID(uint32)のマッピング」「プラグインのステートの保存と復元」「UIの呼び出し」など、かなり本質的なものが含まれているため、LV2をプログラムで扱うのであれば、LV2拡張に対する基本的な理解が不可欠です。

LV2拡張のインスタンスは、LV2_Featureという構造体で表されます。

struct LV2_Feature
{
    const char *URI;
    void *data;
}

LV2拡張はURIで種別が識別され、それぞれの拡張に応じて必要になるデータがdataに渡されます。多くの拡張機能はコードによる実装を必要とし、それらは(直接ないし間接的に)関数ポインタとしてホストからクライアントに渡されることになるので、dataが空であることは通常はありません。たとえばURILV2_URID_MAP_URIhttp://lv2plug.in/ns/ext/urid#map)のとき、dataに渡されるべきものはLV2_URID_Map構造体です。

// typedef void* LV2_URID_Map_Handle
// typedef uint32_t LV2_URID
struct LV2_URID_Map
{
    LV2_URID_Map_Handle handle;
    LV2_URID(* map)(LV2_URID_Map_Handle handle, const char *uri);
}

LV2プラグインは、lv2.hをインクルードしてLV2_descriptor()およびLV2_lib_descriptor()という関数を定義することになります。いずれもLV2ホストがプラグインdlopen()でロードした後、dlsym()で動的に呼び出すことになります。LV2_lib_descriptor()ではLV2_Feature*型のfeatures が定義されており、ここにはLV2ホストから「ホストがサポートするLV2拡張」のデータを含むリストが渡されます。

先のLV2_URID_Mapの例でいえば、mapがこの拡張機能の実体であり、この関数ポインタをプラグイン側はLV2_URID_mapの実装として呼び出せる、という仕組みです。

LV2プラグイン後方互換

LV2プラグイン機構は、LV2 Coreと数多くのプラグインによって成り立っていますが、それぞれのプラグインにはバージョンがあり、これらは必ずしも後方互換性があるとは限らないようです。筆者が試した範囲では、Atom型として定義されている型がXML Schema Datatypesに由来するxsd:*型であるとしてRDF上で定義されているものがいくつかあり、これらのプラグインをlv2_validateツールで検証するとwarningが報告されました。LV2の仕様自体が後方互換性を意識していても、実際に後方互換性が達成できるかどうかは、拡張となるプラグインのバージョン間互換性・設計次第であるようです。

もしかしたらこれはLV2_Atomをサポートした時点で生じた非互換問題で、2012年に出たこの記事でQTractorの開発者に言及されている問題かもしれませんが、2012年の問題が2019年になっても問題になっているとしたら、7年も前の非互換変更が今でも尾を引いているということであり、後方互換性の問題は無視できないかもしれません。

LV2ホスティング

オーディオプラグインを使うためには、オーディオプラグインMIDIなどの制御命令や音声データを渡して処理させるホストが必要です。一般的にはDAWと呼ばれるソフトウェアがこれを担いますが、プログラムとしてはLV2プラグインをロードして処理を実行できるものであれば何でもかまいません。たとえば、プラグインのテストには、テストのみを目的とするローダーが使われるでしょう。

LV2をサポートするDAWとしては、Ardour、Audacity、Carla、QTractorなどが有力です。

LV2ホスティングを実現するためのリファレンス的な実装として、lilvというライブラリがあります。ローカルにインストールされているLV2プラグインプラグインクラスをリストアップして、プラグインインスタンスを生成して、ポートをデータバッファに接続して、オーディオ処理を走らせることができます。

lilv

lilvはLV2ホスティングのためにあるCライブラリです。RDFの詳細の多くを隠蔽しつつ、プラグインクラスを検索し、プラグインインスタンスを生成して、音声データ処理を行えます。

RDFの処理は、同じ作者が開発しているserd(RDFの解析)、sord(Turtle syntaxの解析)、sratom(LV2 atom型とRDFの相互変換)、そしてlv2の4つのソースコード・パッケージにのみ依存しています。実際にはlv2などがさらにlibsoundioやcairoなどをオプションとしてサンプルプラグインのビルド時に参照することになるので、依存関係はもう少し膨れ上がります。

LV2のRDFを書けるようになるためには、RDFについてそれなりに勉強しなければならないことになりますが、率直に言えば、オーディオプラグインを開発するためにRDFを勉強するというのは、全く本質的な作業ではない、と言わざるを得ないでしょう。これは、lilvのAPIを調べていくことである程度緩和されます。RDFの面倒な部分はとりあえず置いておいて、lilvのAPIでworld, plugin class, plugin, port, instanceといった概念を理解したほうが早いです。

*1:有名どころではSteinberg社のCubaseApple社のLogic Proなど

*2:単なる一般則なのか仕様で厳格に決められているのかは不明ですが、筆者はこの例外を見たことがありません

JUCEにおけるz-orderの扱い

また音楽技術Advent Calendar 2019JUCE Advent Calendar 2019のcross postingです。(現状どっちもとても埋まる気がしない)

JUCE GUIのComponentには、その内容としてchild componentsをaddAndMakeVisible()関数を使って載せることができます。これはやや設計として失敗の雰囲気があるものの(全てのComponentが子をもつ設計は200x年代前半の臭いがありますね)、古典的には一般的なGUIコンポーネントの設計です。そして、コンポーネントの描画はpaint()関数で行われます。JUCEの子要素は自分でレイアウトして、自身の描画もpaint()で行うのが基本です。あれ? ちょっと雲行きが怪しくなってきましたね…? 大丈夫、子要素のpaint()関数も呼び出されるので問題ありません。

子要素はデフォルトではabsolute layoutのようにpaint()で渡されたGraphicsで渡される領域の範囲内で描画します。描画する領域は親から渡され、子の描画はその子のpaint()関数が行う…ということは、複数の子がある場合、描画順序によっては、ある子の描画内容を後から別の子が上書きしてしまう可能性があります。

これでは困りますね。少なくとも描画順序を制御できなければ困ります。こういう時にわれわれが使うのは、そう、「前面に移動」「背面に移動」です。MSOfficeですら制御できるアレです。もう少し真面目に言うとz-orderですね。

このz-orderですが、JUCEのComponentには対応するプロパティがありません。z-order無いのかよ!となりそうですが、z-orderを指定できる場所がひとつあります。Componentに子要素を追加する時に使うaddAndMakeVisible()関数です。

void Component::addAndMakeVisible (Component * child, int zOrder = -1)

zOrder : The index in the child-list at which this component should be inserted. A value of -1 will insert it in front of the others, 0 is the back.

これで順番を指定できそうですね。さっそく具体例を見てみましょう。簡単な重ね合わせの生じるデモを作ったので、これを土台に検証します。

f:id:atsushieno:20191210024147p:plain

#pragma once  
#include "../JuceLibraryCode/JuceHeader.h"  
  
class Component1 : public Component {  
public:  
  void paint(Graphics& g) override {  
    g.fillAll(Colours::green);  
  }  
};  
  
class Component2 : public Component {  
public:  
  void paint(Graphics& g) override {  
    g.fillAll(Colours::blue);  
  }  
};  
  
class MainComponent : public Component  
{  
  Component1 c1;  
  Component2 c2;  
  Label l1{"label1", "Label1"};  
  Label l2{"label2", "Label2"};  
public:  
  //==============================================================================  
  MainComponent() {  
  c1.setBounds(0, 0, 200, 200);  
  c2.setBounds(100, 100, 200, 200);  
  l1.setBounds(0, 0, 200, 200);  
  l2.setBounds(100, 100, 200, 200);  
  addAndMakeVisible(c1);  
  addAndMakeVisible(c2);  
  addAndMakeVisible(l1);  
  addAndMakeVisible(l2);  
  
  setSize (300, 300);  
  }  
  
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)  
};

JUCEのソースファイルなのでPIPにしてもよかったのですが、とりあえずデフォルトテンプレートのMainComponent.hを書き換えてMainComponent.cppの中身を空っぽにしただけです。多分ビルドできるでしょう。

緑の矩形と青の矩形が重なり合っていますね。この画面で、緑を青の上に出したいと思ったら、zOrderを指定してaddAndMakeVisible()を呼び出すと良いということですね。

  addAndMakeVisible(c1, 1);  
  addAndMakeVisible(c2, 0);  
  addAndMakeVisible(l1);  
  addAndMakeVisible(l2);  

f:id:atsushieno:20191210024254p:plain

なるほど順番が変わりましたね! 実際にレイアウトを調整するときは間に他の要素が入るかもしれないので、zOrderの差を大きめに設定しておきましょう。

addAndMakeVisible(c1, 100);  
addAndMakeVisible(c2, 1);  
addAndMakeVisible(l1, 110);  
addAndMakeVisible(l2, 10);

f:id:atsushieno:20191210024147p:plain

( ゚д゚) ・・・
 
(つд⊂)ゴシゴシ
 
(;゚д゚) ・・・

あ…れ…? なんか想定外の挙動ですね…?

zOrderってなんか値域とかあるんだっけ…?と思いながら情報を探してみると、こんなforumの議論が…

forum.juce.com

In particular, if a component has N children, then all z-indices >= N will be considered equivalent, because of this code in Component::addChildComponent:

「子要素の数がNだったらzOrderの値がN以上だったら全部Nに揃えられる」…そマ?

まじすか…どうもzOrderの設計はやっつけっぽい…。その後のスレッドでは「Componentのメモリ使用量は可能な限り最小限にしてあって云々…」とあるのですが、ホントか〜? ホントにメモリ使用量を気にするんならComponentがMouseListenerを実装しているのおかしくね〜?とか思ったりしちゃったり何だったりしますね…

ともあれ、zOrderはこういう感じで「期待通りに動作させるには慎重に使わないといけない」ものなので、使うときは十分に注意しましょう。たぶんaddChildComponent()がひと通り済んだ時点で適切な順序でソートしたほうが良いです。

以上juce_gui_basicsの小ネタでした。

オーディオプラグインの理想的なGUIフレームワークを模索する

音楽ツール・ライブラリ・技術 Advent Calendar 20197日目のエントリーです。今日はポエムに近いです。

オーディオプラグインGUIの要求事項

オーディオプラグインフレームワークというソフトウェアはやや特殊な世界で、歴史的な経緯を脇に置いて2019年現在に求められている要件を列挙するなら、

  • WindowsおよびMac、可能なら*1それ以外(Linux, Web, iOS, Androidなど)をサポートすること
  • オーディオ処理とGUI機構を提供すること
  • DAW上から起動できてGUIイベントを部分的に共有すること(キーボードイベントなど)

などが挙げられます。今回はGUIフレームワークについて少し掘り下げて検討します。先日M3で頒布した同人誌では言及しなかった部分ですね。

VST SDKのように独自ブランドのGUIフレームワークを提供するものや、JUCEのように複数オーディオプラグイン機構・複数プラットフォーム向けに独自のGUIフレームワークを提供するものは、いくつか存在します。オーディオプラグインフレームワークにおいては、GUIフレームワークの提供は必須ではありません。しかし、どのようなGUI機構であってもオーディオプラグインフレームワーク側と接続可能になるような、オーディオ/MIDIストリームの出入り口を用意しておく必要はあります。

オーディオプラグインに求められるGUIアプリケーションとしての要件は、一般的なデスクトップGUIアプリケーションのものと比べると、だいぶクロスプラットフォームで開発しやすいものであるといえます。その理由のひとつは、起動および操作がほぼ必ずDAWを経由したものになるためです。メインメニューは無く、キーボードイベントを「DAWから奪い去る」ことがあまり歓迎されず、DAWからダイアログのようにポップアップされているときだけ操作することになります。

さまざまな制約を受けながら作成できるGUIの利便性は当然ながら一般的なGUIアプリケーションよりだいぶ低く、そのようなGUIであれば「ネイティブアプリケーションのポテンシャルを全然引き出せない」クロスプラットフォームGUIフレームワークの欠点が目立たなくなります。

VSTGUIやJUCE (juce_gui_basics) は、そのような特殊な環境で発展してきたといえます。もちろん、Carbon/CocoaWindows APIを直接使うGUIをもつオーディオプラグインもあり、これらも一般的なGUIアプリケーションに求められる要求事項とはだいぶ無縁に作られてきたはずです。

また、オーディオ処理がリアルタイムで厳格なフレームの中での完結を求められることもあり、これに対応するかたちでGUIを構築するのであれば、ウィジェット/コントロールを操作するGUIフレームワークを使うよりは、コールバックベースの低レベルの描画APIに基づいて実装するものが多くなるのも、それなりにわかりみがあります。

もちろんGUIの描画そのものはオーディオスレッドで行われるべきものではないので、ユーザーコードでオーディオコールバックから呼び出される描画呼び出しはpostにとどまり、実際の描画処理はUIスレッドで行われることになります。一方で、そもそもGUIはオーディオ処理に100%追従することが前提となっていないので、ある程度の遅延は容認できるし、何ならオーディオ処理とは異なりGCJITで世界が止まっても致命的な問題ではない、ということがいえます。実際これはAndroidにおけるオーディオAPIの開発方針でもあります(UIはART上でKotlin/Javaでも可能、オーディオはNDK + OpenSLES/AAudio)。

どのようなx-plat GUI Fxが向いているのか

さて、こうなってくると、オーディオプラグインGUIフレームワークは、クロスプラットフォームGUIフレームワークが利用できる分野であるといえそうです。

もし「低レベルの描画APIだけ提供していれば良い」というのであれば、CairoなりSkiaなりを使えば解決ということになりますが、実際にはマウスやキーボード入力のサポートも必要になるので、きちんとアプリケーションループをもったGUIツールキットであることは必要でしょう。

ここで「クロスプラットフォームフレームワークでは実現できない、プラットフォームの入力デバイスを完全にサポートする必要がある」という立場であれば、ネイティブのGUIツールキットで個別に開発するのが妥当でしょう(Surface DialがLinuxでも使えてLeap Motionのようなデバイスがどのプラットフォームでも動作する現在、筆者としてはそのような状況はあまり一般的には想定できないところですが)。

クロスプラットフォームGUIフレームワークを使う路線でいくとしても、このカテゴリにまとめられるGUIフレームワークにはいくつかの種類があります。

(1) JavaScriptとブラウザ環境が前提であるもの。CordovaやIonicなどが挙げられます。JUCEコミュニティの一部ではReactを使ってGUIを構築するアプローチが話題となっているようです。このやり方であればWeb開発の手法でオーディオプラグインGUIを開発できることになるので、開発は楽になりますがブラウザを組み込むことになるのはアプリケーションとしてはだいぶ重量感が増します。(オーディオプラグインにはGB単位でサンプリングを含むものが多く存在するので、それに比べたら誤差のようなものではあります。) またWebViewの組み込みはどんな環境でも問題を引き起こすことが多く、プラットフォームやブラウザコントロールのバージョンなど、環境による挙動の違いをもたらす変数が大きいこともマイナス点でしょう。

(2) プラットフォームごとにネイティブのUIコントロールを呼び出すように実装されたもの。XamarinやReact Native、WxWidgetsはここに分類されます。開発が楽になるかどうかはそれぞれの開発者のバックグラウンド次第、アプリケーションとしての配布が楽かどうかもフレームワーク次第と、十把ひとからげに評価するには変数の多いところです。プラットフォームのネイティブコントロールがUX満足度を高める要因になりますが、オーディオプラグインGUIでは、伝統的に標準コントロールがあまり使われていないので、伝統的なGUIではメリットが小さそうです。一方でプラットフォームごとに挙動がバラバラになるデメリットはそのままです。React NativeはWindows用のバックエンドはあるものの、Gtkなどはサポートされていないでしょう。XamarinのGtkサポートも実は貧弱です。Gtk2なので(!) もともとはGNOMEの開発を主導していた会社なのに(!)

(3) UIコントロールを独自にレンダリングするもの。Gtk, Qt, Flutter, JUCE, Unity UiWidgetsはこのカテゴリに属するといえます。これらは、フレームワークの完成度が重要なファクターになってきます。特にどのプラットフォームでも満足に動作するものは皆無といってもよいでしょう。QtやGtkLinux以外ではあまり歓迎されず、オーディオプラグインGUIとしても実績値は高くありません。またQtもGtkもデスクトップが「どちらであるか」によってエイリアンになる可能性が高く、まだX11が直接使われている可能性が高いといえるでしょう。しかしX11もまたWaylandなどで置き換えられつつある存在です。また、オーディオプラグインの主戦場はデスクトップ環境であり、Androidで快適、iOSでもまあまあ許容されているFlutterは、デスクトップではまだまだ発展途上(そもそも開発中)なので、現状で適切な選択肢であるとは言いがたいところです。不十分な多言語入力もこの種のフレームワークにありがちな問題です。

opinion

JUCEは古典的なオーディオプラグイン用のGUIフレームワークとしては必要最低限の機能を提供していて、しかもC++なので、現在ではよく採用されています。ウィジェット・ツールキットとしては他のフレームワークと比べると描画APIに毛が生えた程度であり、少しでも高度な機能を使おうとすると自前で実装することになります。基本中の基本のようなHBox/VBoxのようなレイアウトが"Advanced GUI layout techniques"と言われるのがJUCEのレベル感です。レイアウトエンジンがウィジェットとして組み込まれておらず、単独で存在するFlexBoxやGridのレイアウトの実装などを自前でコントロールの描画に適用することになります。JUCEはユーザー数の割には開発者層が薄く、特にjuce_gui_basicsは絶望的なので、筆者としては全然将来に期待していません。

(レイアウトエンジンは必ずしもGUIフレームワークと密接に結びついているわけではありません。具体的な例を挙げると、Facebookが開発したCSS Flexbox相当のレイアウトエンジンであるYogaはWeb以外でも使うことができましたし、AppleのAutoLayoutやAndroidのConstraintLayoutはCassowaryと呼ばれる制約付きレイアウトを実装したもので、Cassowary自体はGUIフレームワークから独立して存在しうるものです。なのでたとえばAbletonがQt用にCassowaryを実装することもできたりします。独自のGridレイアウトも同様といえます。)

筆者がこの分野で一番将来に期待しているのはFlutterです。FlutterはMaterial Design Componentをフル実装しており、十分にモダンなGUIを構築できる一方で、描画の低レベルレイヤーはSkiaが使われており、すなわちVulkanやANGLE、Metalのポテンシャルを引き出せる可能性が十分にあります(SkiaのMetalバックエンドはiOS 11以降のみですが)。Flutterでなくても、モバイル環境で育ったGUIフレームワークであれば、たとえばマルチタッチに対応した豊富な機能を持つGUIを構築することが可能でしょう。juce_gui_basicsで満足していたら逆立ちしても実現できません。

ただし、Flutterがオーディオプラグインの世界に適用されるためには、C/C++と相互運用できるだけの十分なFFIサポートが不可欠です。この部分はDartでまだまだ発展途上であり、残念ながら筆者がlibclangで相互運用を試してみた範囲では、とても実用段階にあるとは言いがたいところです。プラットフォーム独立の動的ライブラリのロードや、Stringの変換くらいは、ファーストクラスで実装されている必要があるでしょう。

あと、JUCEで最近さかんに試されているReact NativeというかJavaScriptにもいえることですが、float型が無いのはホントに大丈夫なのか?っていう問題はあるかもしれません。まあWebAudioで実証済みの世界とはいえるかもしれません。

*1:わたしはLinuxをサポートしないフレームワークには価値も未来も無いと考えています