そろそろ忘れそうなので備忘録的に書いておく。
自分で独自のオーディオプラグインフレームワークを開発していて課題になるのは、自分でフレームワークからアプリケーションまで全てを開発するのは不可能だということだ。そういうわけで可能な限り既存のリソースを使い回すために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行追加するだけで足りる。