JUCEでカスタムAudioPluginFormatをホストする

そろそろ忘れそうなので備忘録的に書いておく。

自分で独自のオーディオプラグインフレームワークを開発していて課題になるのは、自分でフレームワークからアプリケーションまで全てを開発するのは不可能だということだ。そういうわけで可能な限り既存のリソースを使い回すためにJUCEのバックエンドとしてサポートを追加しようと考えるのは割と自然なことだろう*1

JUCEではオーディオプラグインをホストする機能はjuce_audio_processorsというモジュールでコア機能とVST, AU, LADSPAのホスティングをサポートしている。幸いJUCEに独自のオーディオプラグインを追加するのは難しくない。今回はこれをざっくり解説する。

念のため明記しておくが、今回の主題はホスト側のみであって、プラグインを作る側ではない。プラグイン側は後編としてこちらにまとめた。

atsushieno.hatenablog.com

目次

自分のAudioPluginFormat派生クラスを作る

JUCEでは、ホストする対象となるオーディオプラグイン仕様は、juce::AudioPluginFormatから派生するクラスとして定義する。VST3PluginFormatとかAudioUnitPluginFormatといったクラスがこのモジュールの中でも具体的に定義されている。juce_audio_processors内部に自分のクラスを追加する必要はないので、自分のモジュールを作ってメンテナンスするのが一番楽だろう*2。モジュールの作り方は以前に解説してある。

atsushieno.hatenablog.com

AudioPluginFormatクラスにはいくつかオーバーライドすべきメンバーがあるが、このクラスの基本的な役割は2つだ:

  • (主に)システムにインストールされているオーディオプラグインを検索する
    • そのために必要な検索パスのリストを指定する
    • 対象パスからプラグインを含むファイルを絞り込む
    • 対象ファイルから含まれているプラグイン(群)の情報をPluginDescriptionクラスのインスタンスとして格納する
  • 指定されたPluginInstanceオブジェクトからAudioPluginInstanceインスタンスを生成する

前者はより具体的に書くと

  • getDefaultLocationsToSearch()でデフォルト検索パスを設定し、
  • searchPathsForPlugins()プラグインを含む可能性のあるファイル群をリストアップして、
  • fileMightContainThisPluginType()で引数ファイルにプラグインが含まれている可能性があるか判断し、
  • findAllTypesForFile()で指定されたファイルに含まれるプラグイン(群)を取得する

といった流れで実装する。ファイルベースのOSでない等の事情でこの一連の流れが面倒な場合は、その環境でどういう流れになるべきか、少し挙動を調査・検討する必要があるだろう。

後者はcreatePluginInstance()で実装する。この関数は非同期で戻り、生成したインスタンスは引数にあるPluginCreationCallbackに渡して呼び出すことになる。

PluginDescriptionにプラグインメタデータを格納する

AudioPluginFormat::findAllTypesForFile()を実装する時点で、プラグインメタデータPluginDescriptionというクラスのインスタンスとして返す必要がある。OwnedArrayの引数に結果を追加するので、メモリ解放を心配する必要はない(解放できるメモリのポインタを渡す必要がある)。PluginDescriptionは派生して定義するものではない。プラグインフレームワークでメモリ管理すべきものをここに含めるのは適切ではないからだ。

個々のプラグインPluginDescriptionのメンバーの値で識別できる必要がある。fileOrIdentifieruidが有用だろう。またKnownPluginListクラスなどで検索結果をローカルにキャッシュする仕組みを活用することから、識別できる値はプロセスごとに変わってもいけない。

自分のAudioPluginInstance派生クラスを作る

メタデータ処理が終わったらいよいよjuce::AudioPluginInstanceクラスを派生させてプラグインインスタンスを生成する。このクラスはjuce::AudioProcessorの派生クラスで、実際にはオーディオ処理の大半はこちらで行われている。

生成されたインスタンスは次のような流れでオーディオ処理を走らせることになる。いずれもアプリケーションからコールバックされる前提で実装する。

  • prepareToPlay()プラグインをオーディオを処理できる状態にする
  • processBlock()でオーディオバッファとMIDIバッファを処理する
  • releaseResource()で最初に準備したリソースを解放する

またエディタUIを表示したりUIで編集したデータを読み書きすることもある

  • createEditor()でエディタUIを表示する
  • getStateInformation()でstateをプラグインから取得する
  • setStateInformation()でstateをプラグインに反映する
  • getNumPrograms(), getCurrentProgram(), getProgramName()などでプログラム(プリセットなど)を取得する(set...()で設定もできる)

最低限のプラグインフォーマットのサポートは、このクラスの純粋仮想関数を全部実装するだけでいける。ただ純粋でない仮想関数の中にも絶対に実装すべき重要なものが含まれている可能性は多分にある。Busの設定やチャンネルレイアウトの変更通知などが純粋仮想関数になっていない。いったん定義してしまった抽象クラスに後から純粋仮想関数を追加するとAPI互換性を破壊することになるので、後からメンバーが追加される時は必須であるべきものであっても純粋仮想関数にならない。

ちなみにprocessBlockMIDIメッセージを処理するにはタイムスタンプを付加し考慮する必要がある。一般的なMIDIアプリケーションであればあまり問題にならないかもしれないが、オーディオプラグインではオーディオ処理のためのと合わせてMIDIメッセージも流れてくるのが一般的だ。なぜなら関数呼び出しというのはコストが比較的高いので、何回も頻繁に呼び出せるものではないからだ。しかしオーディオバッファはそれなりの大きいチャンクに分けられて呼び出されるので、MIDIメッセージにタイムスタンプがないと、長い場合は数百ミリ秒のレベルでしかイベントが処理されないことになる。これでは音楽にならないため、MIDIイベントにはタイムスタンプが付加されて、プラグインがこれをよろしく処理する、というのが一般的だ。

自分のAudioPluginParameter派生クラスを作る

ここまでのタスクをこなすだけでも、JUCEアプリケーションからオーディオ処理を呼び出して実行できるようにはなっている。ただしプラグインのパラメーターを調整しようと思って、たとえばプラグインパラメーターを調整するUI*3を表示しても何も表示されない。パラメーターの定義はAudioPluginInstanceに追加してやる必要があるのだ。

言い換えれば、パラメーター情報のクエリはメタデータのレベルでは不可能で、インスタンス化しないと出来ない、ということでもある。これはVSTのような仕様でサポートされていないということだろう。

いずれにせよ、AudioPluginInstanceインスタンスを生成するたびに、AudioPluginParameterというパラメーター情報と操作の両方を実装するクラスのインスタンスを生成して追加してやらなければならない。一般的には自分のAudioPLuginInstance派生クラスのコンストラクターで実装することになるだろう。

このクラスではgetValue()setValue()getName()などを実装するだけで、難しいことは特に無い。もっとも、「パラメーター」のセマンティクスは必ずしもオーディオプラグイン仕様によっては存在しないので(たとえばLV2にはportの概念しかなく、portはデータを送信するためにあるのであって値を取得することは前提となっていない)、仕様次第では何らかの調整が必要になる可能性はある。

自分のAudioPluginFormatをアプリケーションから使う

JUCEはアプリケーションを全てソースからビルドするような仕組みになっている。JUCEのフレームワークを直接参照しているプロジェクトに後付けで自分のプラグインフォーマットをサポートさせることはできない。既存のアプリケーションで自分のプラグインフォーマットをサポートしようと思ったら、アプリケーションのコードにサポート追加のためのコードを書かなければならない。

幸い、独自プラグインをサポートするためのAPIは用意されている。AudioPluginFormatManager::addFormat()を呼び出すだけだ。AudioPluginFormatManagerは一般的にはオーディオプラグインを扱うアプリケーションごとに生成されているはずなので、その部分に1行追加するだけで足りる。

*1:JUCE以外のプラグイン開発フレームワークでもアプリケーション資産があれば対応を考えるところだ

*2:一旦内部に作ってしまうと自分のforkを延々とメンテし続けることになってしまって維持するのがしんどくなるだろう

*3:自分で実装する必要はない。JUCEにはパラメーター定義から設定コントロールをシンプルにリストアップできるクラスがある