CI環境でJUCEを使ってオーディオプラグインを適用する

JUCE Advent Calendar 2021、7日目のエントリーです。今年2回目。

最近MMLからTracktion Engineを使って再生できる楽曲をCI環境でMP3レンダリングするというややぶっ飛んだ試みで数日費やしていたのですが、どうやらうまくいきそうなので、何をやったのか共有します。

自分が理解している限りでは、CI環境でJUCEを使ってオーディオプラグインをホストして適用できた開発者は誰もいません。CIみたいな環境で利用するためにはJUCE本体にパッチを当てる必要があります。

今回はGitHub Actionsを使用していますが、本エントリーの趣旨としてはサービスはCircleCIでもいいしBitriseでもいいし何でもいいです。JUCEにどう手を加えるかがポイントです。

Why CI?

いま自分がやっている取り組みは、かつてMIDIで音楽データを打ち込んだものがそのままネットで流通して誰もが打ち込み技法を学び合い教え合えたような時代が、モダンなオーディオプラグインの世界でも実現できることを実証するという試みです。そのためには、楽曲データの生成をどこでも再現可能にする必要があります。打ち込んだ音楽がその作業環境になったPCでしかレンダリングできないようでは、これは実現できていないのです。

CI環境で音楽ファイルをWAVまでレンダリングできれば、その楽曲はおよそポータブルであるということができるでしょう。そしてこれは単なるMIDIファイルやチップチューンの表現力の範囲を超えて、一般的なDAWのワークフローでオーディオプラグインを使用した楽曲で、少なくとも同じ構造で再現できるところまで実現したいのです。

必要になるソフトウェア構成要素

(1) tracktion_engineのWAVレンダリング

12/9追記: これは「自分の」ソフトでCIレンダリングするために必要だったものを列挙しているだけなので、表題のような「JUCEでオーディオプラグインレンダリングをCIで動かす」ことだけが目的(たとえば自分のプラグインやホストをテストするとか)でレンダリングは自前で出来るというのであればTracktion Engineはいらないです。

Tracktion Engine、今さら言及するのは何ですがTracktion WaveformのシーケンサーエンジンとなるJUCEモジュールです。

github.com

今回やっているのはTracktion Engineに含まれるtracktion_engine::Renderer::renderToFile()というstatic関数を使ってtracktion_engine::EditオブジェクトをWAVファイルにエクスポートして、それをffmpegでWAVからMP3にコンバートするという作業です。Tracktion EngineではMP3などフォーマットを指定してエクスポートする関数も用意されているのですが、オーバーロードの提供方法が中途半端で(まあ誰も使わないから仕方ない)、必要になる引数をTracktion Engineのどこかから全部取得するか自前で全部生成するかみたいな二択になって面倒だったので、今回は使いませんでした。

(2) オーディオプラグインのスキャン

tracktion_engine::Editインスタンスは自分の楽曲ファイル(*.tracktionedit)をロードして生成していて、これはVST3インストゥルメントをフルに使った楽曲なので、オーディオプラグインがCI環境でロードできる必要があります。

Tracktion EngineはJUCEの上に成り立っていて、自身が保持しているjuce::AudioPluginFormatManagerに少なくとも一度はプラグインをスキャンさせないといけないのですが、これを実現できるのはjuce::PluginListComponentというGUIコンポーネントしかありません(あるいは「全て自前で」実装する必要がある)。

(3) オーディオプラグインのセットアップ

CI環境上に、juce_audio_proessorsjuce_audio_plugin_clientのモジュールがオーディオプラグインインスタンスを生成できるようなオーディオプラグインをインストールします。この時点でだいたい想像がつくかと思いますが、オープンソースでないオーディオプラグインはだいたい利用できなくなります。プラグインのインストールと利用にはstudiorackなどのソリューションが活用できるでしょう。

ここでひとつ重要なのは、自分のPC上で利用しているものと同じパスにあるプラグインを(現時点では)使わないといけないということです。同じバージョン・同じ規格のプラグインであればパスを調整する余地がありますが、今回はそこまで面倒を見ていられないので、パスを調整する仕事はしないことにします。そしておそらくstateバイナリを他のプラットフォームでも利用できるのは現状VST3とLV2くらいでしょう。今回はTracktion Waveformでもロードできる楽曲にしているので、(忸怩たるものがありますが)LV2は使わずにVST3で統一してあります。

(4) オーディオプラグインが使用する外部ファイルのパス調整

プラグインによっては、外部のファイルをロードしないと使えないものがあります。今回自分が楽曲で使用しているのはsfizzサンプラーですが、最新のsfizzバージョン1.1.1にはsfzファイルをユーザー指定のパスから探索できる機能が実装されています。楽曲で利用しているプラグインのstateには/home/atsushi/...みたいなパスが(不可避的に)保存されているのですが、実行環境に合わせてこのパスをrelocateできるわけです。sfizzのGUIにはsfzディレクトリ指定に対応するボタンがあって、この設定値は~/.config/SFZTools/sfizz/settings.xmlというファイルに保存されるので、CIではこれを事前に自動生成します。

プラグイン側にこのような機能が用意されていない場合、CI環境に制作環境と同じパスを生成してファイルを配置しなければならず、ひいては他人のPC環境で再生する場合にも同様の不都合がもたらされることになってしまいます。プラグイン側にこういったオプションを指定する機能が用意されていない場合は機能をリクエストしたほうがよいでしょう。

juce::PluginListComponentにheadlessサポート(もどき)を追加する

今回の一番の課題はjuce::PluginListComponentでした。このクラス、GUIとモデルの分離が全く行われていないのです。さらに、(ここが問題なのですが)このPluginListComponentで通常GUIから指示できるスキャン処理を外部から完全自動化する方法がありません。というのは、スキャンするプラグインがあらかじめリストで与えられている場合を除いては、まずスキャンするパスを選択するようなダイアログを出すような振る舞いになっているためです。関連コードはこの辺です。

GUIとモデルの分離が全く行われていないという観点では、そもそもjuce_audio_processorsモジュールがGUI部分を分離できていないという問題があります。この辺はJUCEの設計が現代から20年くらい遅れている(VST2の時代の設計に引っ張られている)感じがあります。VST3のモジュールアーキテクチャが前提だったらもっと違ったものになったんだろうなあ…

ともあれ、仕方ないので、今回はこのjuce::PluginListComponentを無理やりheadlessもどきにして、事前にheadlessもどきモードだったらこのダイアログをすっ飛ばして現在取得できるパスリスト(一般的にはプラグインフォーマット固有のデフォルトリスト)でスキャンを実行するようにしてみました。パッチはこんな感じです。

https://github.com/atsushieno/augene-ng-production/blob/main/juce-plugin-scanner-headless.patch#L1

使っているコードも(自分のプロジェクトに対するパッチですが)こんな感じです。

https://github.com/atsushieno/augene-ng-production/blob/d6fa60bd27833eb247d4c6a8b56f4ad02411be8e/augene-headless-plugin-scan.patch#L1

これはあくまでGUIコンポーネントでありこのインスタンスを生成しないと使えないのは正確にはheadlessとは言えないのですが、そもそもjuce_audio_processorsがheadlessではないので、CI環境では従来どおりX11をセットアップして実行する必要があります。これについては2年前に書いたやつを見てください。

atsushieno.hatenablog.com

できたもの

ここにあります。

github.com

GitHub Actions、ローカルで確認できないので完全に大変だった…ちなみに31トラック7分のMP3をレンダリングするのに60分かかります。レンダリング以外のタスクは15分もかからないので(15分も短くはないけどいざとなればバイナリパッケージを用意できる)、あとはTracktion Engine本体を最適化できるかどうかです。

余談

ちなみに現時点でJUCEのdevelopブランチにはこんな変更履歴があって(といっても見る日によって内容が変わるURLですが)、

f:id:atsushieno:20211207215602p:plain
history for juce_PluginListComponent.cpp

どうやら開発チームでも一度バックグラウンドでプラグインスキャンを実行できる仕組みを作ろうとして、VST3だとどうやらうまくいかないので「canScanOnBackgroundThread()でfalseを返す」ようにして、それじゃ全然使い物にならないと考えてか関数自体を削除する、みたいな動きを見せているので、やりたかったことなのかもしれません(逆にわたしのパッチでも回避できない類の問題なのかもしれない)。まあ使えるプラグインなら使う、というスタンスでいきたいですね。