そろそろ忘れそうなので備忘録的に書いておく。
自分で独自のオーディオプラグインフレームワークを開発していて課題になるのは、自分でフレームワークからアプリケーションまで全てを開発するのは不可能だということだ。そういうわけで可能な限り既存のリソースを使い回すためにJUCEのバックエンドとしてサポートを追加しようと考えるのは割と自然なことだろう*1。
JUCEではオーディオプラグインをホストする機能はjuce_audio_processorsというモジュールでコア機能とVST, AU, LADSPAのホスティングをサポートしている。幸いJUCEに独自のオーディオプラグインを追加するのは難しくない。今回はこれをざっくり解説する。
念のため明記しておくが、今回の主題はホスト側のみであって、プラグインを作る側ではない。プラグイン側は後編としてこちらにまとめた。
目次
- 自分のAudioPluginFormat派生クラスを作る
- PluginDescriptionにプラグインのメタデータを格納する
- 自分のAudioPluginInstance派生クラスを作る
- 自分のAudioPluginParameter派生クラスを作る
- 自分のAudioPluginFormatをアプリケーションから使う
自分のAudioPluginFormat派生クラスを作る
JUCEでは、ホストする対象となるオーディオプラグイン仕様は、juce::AudioPluginFormatから派生するクラスとして定義する。VST3PluginFormatとかAudioUnitPluginFormatといったクラスがこのモジュールの中でも具体的に定義されている。juce_audio_processors内部に自分のクラスを追加する必要はないので、自分のモジュールを作ってメンテナンスするのが一番楽だろう*2。モジュールの作り方は以前に解説してある。
AudioPluginFormatクラスにはいくつかオーバーライドすべきメンバーがあるが、このクラスの基本的な役割は2つだ:
前者はより具体的に書くと
getDefaultLocationsToSearch()でデフォルト検索パスを設定し、searchPathsForPlugins()でプラグインを含む可能性のあるファイル群をリストアップして、fileMightContainThisPluginType()で引数ファイルにプラグインが含まれている可能性があるか判断し、findAllTypesForFile()で指定されたファイルに含まれるプラグイン(群)を取得する
といった流れで実装する。ファイルベースのOSでない等の事情でこの一連の流れが面倒な場合は、その環境でどういう流れになるべきか、少し挙動を調査・検討する必要があるだろう。
後者はcreatePluginInstance()で実装する。この関数は非同期で戻り、生成したインスタンスは引数にあるPluginCreationCallbackに渡して呼び出すことになる。
PluginDescriptionにプラグインのメタデータを格納する
AudioPluginFormat::findAllTypesForFile()を実装する時点で、プラグインのメタデータをPluginDescriptionというクラスのインスタンスとして返す必要がある。OwnedArrayの引数に結果を追加するので、メモリ解放を心配する必要はない(解放できるメモリのポインタを渡す必要がある)。PluginDescriptionは派生して定義するものではない。プラグインフレームワークでメモリ管理すべきものをここに含めるのは適切ではないからだ。
個々のプラグインはPluginDescriptionのメンバーの値で識別できる必要がある。fileOrIdentifierやuidが有用だろう。またKnownPluginListクラスなどで検索結果をローカルにキャッシュする仕組みを活用することから、識別できる値はプロセスごとに変わってもいけない。
自分のAudioPluginInstance派生クラスを作る
メタデータ処理が終わったらいよいよjuce::AudioPluginInstanceクラスを派生させてプラグインのインスタンスを生成する。このクラスはjuce::AudioProcessorの派生クラスで、実際にはオーディオ処理の大半はこちらで行われている。
生成されたインスタンスは次のような流れでオーディオ処理を走らせることになる。いずれもアプリケーションからコールバックされる前提で実装する。
prepareToPlay()でプラグインをオーディオを処理できる状態にするprocessBlock()でオーディオバッファとMIDIバッファを処理するreleaseResource()で最初に準備したリソースを解放する
またエディタUIを表示したりUIで編集したデータを読み書きすることもある
createEditor()でエディタUIを表示するgetStateInformation()でstateをプラグインから取得するsetStateInformation()でstateをプラグインに反映するgetNumPrograms(),getCurrentProgram(),getProgramName()などでプログラム(プリセットなど)を取得する(set...()で設定もできる)
最低限のプラグインフォーマットのサポートは、このクラスの純粋仮想関数を全部実装するだけでいける。ただ純粋でない仮想関数の中にも絶対に実装すべき重要なものが含まれている可能性は多分にある。Busの設定やチャンネルレイアウトの変更通知などが純粋仮想関数になっていない。いったん定義してしまった抽象クラスに後から純粋仮想関数を追加するとAPI互換性を破壊することになるので、後からメンバーが追加される時は必須であるべきものであっても純粋仮想関数にならない。
ちなみにprocessBlockでMIDIメッセージを処理するにはタイムスタンプを付加し考慮する必要がある。一般的な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行追加するだけで足りる。