C++
AudioUnit
audio
VST
JUCE
JUCEDay 3

JUCEがプラグインをホストする仕組みについて

今回は、JUCEがVSTやAudio Unitなどのプラグインをホストする仕組みについて解説します。


プラグインフォーマットとJUCE

前提として、各プラグインフォーマット (VSTやAudio Unitなど) は、その設計や仕様にさまざまな違いがあります。

そのため、ホストとして動作するアプリケーションを開発する際、JUCEに頼らずそれぞれのフォーマットに個別に対応していこうとすると、かなり労力がかかります。1

JUCEはフレームワークとして、VSTやAudio Unitなどいくつかの主要なプラグインフォーマットに対応していて、それらのフォーマットでプラグインを作成する仕組みや、逆にプラグインをホストするための仕組みを提供しています。

現在JUCEがホスト側としてサポートしているプラグインフォーマットは、VST3/Audio Unit/LADSPAです。

以前にはVST2フォーマットもサポートしていましたが、SDKの開発元であるSteinberg社がVST2のサポートを停止したため、それに合わせてJUCE 5.4から、VST2がデフォルトでサポートされなくなりました。

https://juce.com/discover/stories/juce-5-4

現在最新のJUCE 5.4では、自分でSteinberg社からVST2 SDKを入手してJUCEで使えるようにセットアップすることで、これまで通りVST2を利用できるようにする回避策が用意されています。これを有効にすることで、VST2のホスト機能を有効にできます。

https://qiita.com/COx2/items/e52ea443c98d4e91efc9

ただ、この回避策もいつまで提供されるかは不明なので注意が必要です。(とはいえVST2の資産は多いので、特にホスト側の機能としてVST2サポートを完全に取り除くというのは、すぐにはなさそうな気もしますが・・・)


AudioProcessorクラスとホストの実装

JUCEはAudioProcessorというクラスによって、各フォーマットの違いを高度に抽象化しています。

そのため、プログラマはほとんどこのクラスの使い方だけを覚えれば、様々なプラグインフォーマットの違いを意識せずに、プラグイン/ホスト両方を開発できます。

ここでホスト側として、プラグインをロードしてAudioProcessorを利用するコードについて、見ていこうと思うのですが、JUCEに用意されている AudioPluginHost というサンプルは、ちょっと複雑で説明に向かないので、シンプルなプロジェクトを一つ用意しました。(現状ではMacでしか動作確認をしていません。)

https://github.com/hotwatermorning/JuceAdventCalendar2018

このプロジェクトをビルドして起動すると、以下のような画面が現れます。

app_init_view.png

この画面に、プラグインのモジュールファイル (拡張子が.vst3や.componentのファイル) をドラッグアンドドロップすると、そのプラグインをロードして、プラグインのエディターGUIを表示します。 (このときマイク入力が有効になるため、オーディオ出力がフィードバックする可能性があります。ご注意ください。)

editor_gui.png

このプロジェクトの中でプラグインをロードしている箇所は以下の部分です。


MainComponent.cpp

class MainComponent : Component

{
// ...
AudioPluginFormatManager apfm_;
std::unique_ptr<AudioProcessor> proc_;
};

MainComponent::MainComponent()
{
// AudioPluginFormatManagerに、ホストとして対応したいPluginFormatを設定する。
apfm_.addFormat(new VST3PluginFormat);
apfm_.addFormat(new AudioUnitPluginFormat);

// ↑ただし、既定で有効にするPluginFormatはProjucer側で事前に設定してあるので、
// 実際は個別に`addFormat()`を呼び出さなくても、`apfm_.addDefaultFormats()`を呼び出すだけで十分。
}

void MainComponent::LoadPlugin(PluginDescription const &desc)
{
UnloadPlugin();

auto const sample_rate = deviceManager.getAudioDeviceSetup().sampleRate;
auto const buffer_size = deviceManager.getAudioDeviceSetup().bufferSize;

String error;

AudioProcessor* p = apfm_.createPluginInstance(desc, sample_rate, buffer_size, error);

if(error.isEmpty() == false) {
std::cout << "Cannot load a plugin: " << error << std::endl;
return;
}

assert(p != nullptr);

p->prepareToPlay(sample_rate, buffer_size);

{
auto lock = make_lock();
proc_.reset(p);
}

// ...
}


https://github.com/hotwatermorning/JuceAdventCalendar2018/blob/0ee1feec300d09ba4a96cfa79df14ebb83f7c968/Source/MainComponent.cpp#L151

ここで登場している主要なクラスの説明は、以下のとおりです。

クラス
解説

PluginDescription
JUCEが探索した一つのプラグインの情報を保持する構造体で、
以下のような情報を保持している:


  • プラグイン名

  • 実体となるモジュール(.dllや.vst3など)のパス

  • プラグインフォーマットのタイプ

  • 入出力チャンネル数

VST3PluginFormat
AudioUnitPluginFormat
プラグインフォーマットを表すクラス。
そのフォーマットのプラグインを探索してPluginDescriptionを作成したり、
与えられたPluginDescriptionの情報からプラグインをロードしたりする

AudioPluginFormatManager
複数のプラグインフォーマットを登録しておいて、
まとめて扱えるようにしたクラス。

AudioProcessor
ロードしたプラグインの実体を保持し、
フレーム処理のためのAPIを提供するクラス。
パラメータやプログラム番号の管理/オーディオバスの管理などのAPIも提供している。

ここで定義しているLoadPlugin()メンバ関数は、PluginDescriptionという構造体を受け取り、それをAudioPluginFormatManagerのcreatePluginInstance()メンバ関数に渡して、AudioProcessorを構築しています。

AudioPluginFormatManagerには、VST3PluginFormatとAudioUnitPluginFormatの2種類のプラグインフォーマットを事前に登録してあるため、そのフォーマットのプラグインの情報を含むPluginDescriptionをLoadPlugin()メンバ関数に渡すと、プラグインをロードして、AudioProcessorを構築できます。

こうしてAudioProcessorさえ作成できたら、プログラマはAudioProcessorクラスに用意されたメンバ関数を呼び出すだけで、各プラグインフォーマットの違いを意識せずにプラグインを利用できます。2


MainComponent.cpp

void MainComponent::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)

{
// プラグインのフレーム処理を実行
proc_->processBlock(*bufferToFill.buffer, mb_);
}

https://github.com/hotwatermorning/JuceAdventCalendar2018/blob/0ee1feec300d09ba4a96cfa79df14ebb83f7c968/Source/MainComponent.cpp#L73


AudioProcessorの抽象化の仕組みついて

では、AudioProcessorは、どのように抽象化を実現しているのでしょうか?

AudioProcessorは多くのメンバ関数を仮想関数として定義しています。そしてAudioProcessorの派生クラスの方では、保持しているプラグインのAPIを利用する形で、仮想関数をオーバーライドすることで、AudioProcessorの抽象化を実現しています。 (後ほどVST3での例を見ます)

それを踏まえて、AudioProcessorの派生関係を見てみましょう。

AudioProcessorクラスはさらに、AudioProcessorGraphクラスとAudioPluginInstanceクラスに派生しています。

class_audio_processor.png

https://docs.juce.com/master/classAudioProcessor.html

AudioProcessorGraphは、AudioProcessorクラス同士を接続して、その接続状態が表すグラフ構造に従ってフレーム処理を行うクラスです。AudioProcessorGraphクラス自身がAudioProcessorクラスから派生しているため、あるAudioProcessorGraphを別のAudioProcessorGraphの中に追加して、階層的に接続状態を管理できる設計になっています。

一方、AudioPluginInstanceクラスのほうが、内部でプラグインを管理するクラスです。このクラスでは、PluginDescriptionを設定/取得するAPIが提供されています。ただし、あまりこのクラスを意識して使用することはありません。もっぱらAudioProcessorクラスの方を使用することになります。3

ここで、ドキュメント上には出てきませんが、AudioPluginInstanceからはさらに、それぞれのプラグインフォーマット用の VST3PluginInstanceAudioUnitPluginInstance などのクラスが派生しています。そしてロードしたプラグインの実体はそれら派生のクラスの中で管理されます。

これらの派生クラスのあたりの仕組みは、クラス定義からしてJUCEの内部に隠蔽されており、通常はプログラマがこれらの派生クラスを直接利用することはありません。

しかし、いまはその仕組みが実際にどうなっているかに興味があるので、その内部に立ち入ってみましょう。


派生クラスの定義と構築方法

このあとAudioPluginInstanceの派生クラスについて見ていくのですが、ここでは基本的にVST3に関するクラスを中心に見ていきます。

これは、筆者が親しんでいるプラグインフォーマットが主にVST2/VST3であり、さらにVST2がJUCE 5.4からデフォルトでは含まれなくなったためです。とはいえ、以下の話は他のプラグインフォーマット(VST2/Audio Unit/LADSPA)でも、大きな違いはありません。

まず、各プラグインフォーマット用のAudioPluginInstanceの派生クラスは、どこに定義されているのでしょうか。

これはJUCEの modules/juce_audio_processors/format_types/ ディレクトリ内にあるソースファイル(.cppファイル)の中にあります。たとえば、VST3用にAudioPluginInstanceクラスから派生したクラスは、juce_VST3PluginFormat.cpp ファイルに、VST3PluginInstanceクラスとして定義されています。

format_types_dir.png

ここで重要なのは、その派生クラスがソースファイルの中で定義されているということです。

つまり、プログラマはヘッダーファイルをインクルードしてVST3PluginInstanceクラスの定義を参照することはできず、VST3PluginInstanceクラスでなにかメンバ関数が定義されていても、それを呼び出したりはできません。

前述の通り、AudioProcessorの抽象化の仕組みとして、プログラマが利用するプラグインのインターフェースはAudioProcessorクラスの仮想関数として定義されていて、VST3PluginInstanceクラスがその仮想関数をオーバーライドして、プラグインとやりとりする設計になっています。

たとえば、AudioProcessorに仮想関数として定義された、プラグインから現在のプログラム番号を得る getCurrentProgram()メンバ関数は、VST3PluginInstanceクラスで、VST3 APIのgetParamNormalized()という関数を呼び出して、VST3プラグインから情報を取得するようにオーバーライドされています。


juce_VST3PluginFormat.cpp

class VST3PluginInstance : public AudioPluginInstance

{
public:
// ...

int getCurrentProgram() override {
return jmax (0, (int) editController->getParamNormalized (programParameterID) * (programNames.size() - 1));
}
};


https://github.com/WeAreROLI/JUCE/blob/5.4.1/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp#L2226

このように、AudioProcessorの(直接あるいは他のメンバ関数からの間接的な)仮想関数の呼び出しが、唯一VST3PluginInstanceクラスを操作する方法になります。

いささか隠蔽体質が強いようにも感じられますが、この方式には利点もあって、各プラグインフォーマットで独自に定義されたマクロ/クラス/定数などをソースファイル側に隠蔽しておくことで、ヘッダーでそれらの定義が広く参照されてコンパイルエラーやリンクエラーになることが防げる、ということがあります。

しかし、外からクラス定義が見えなくては、プログラマが自分でVST3プラグインをロードしてVST3PluginInstanceクラスを構築することもできません。これを行うのが、AudioPluginFormatクラスと、それをVST3用に派生したVST3PluginFormatクラスです。

audio_plugin_format.png

https://docs.juce.com/master/classAudioPluginFormat.html

まず、基底クラスであるAudioPluginFormatには、createInstanceFromDescription()メンバ関数が定義されています。

AudioPluginInstance * createInstanceFromDescription (const PluginDescription &, double initialSampleRate, int initialBufferSize);

この関数は、内部で間接的にcreatePluginInstance()という仮想関数を呼び出します。

VST3PluginFormatクラスの方では、createPluginInstance()をprivateなメンバ関数としてオーバーライドしています。その中でVST3 SDKのAPIを使用しながらVST3フォーマットのプラグインをロードし、ロードが成功したら、それをコンストラクタに渡してVST3PluginInstanceクラスを構築します。


juce_VST3PluginFormat.cpp

void VST3PluginFormat::createPluginInstance (const PluginDescription& description, double, int,

void* userData, PluginCreationCallback callback)
{
std::unique_ptr<VST3PluginInstance> result;

if (fileMightContainThisPluginType (description.fileOrIdentifier))
{
File file (description.fileOrIdentifier);

auto previousWorkingDirectory = File::getCurrentWorkingDirectory();
file.getParentDirectory().setAsCurrentWorkingDirectory();

if (const VST3ModuleHandle::Ptr module = VST3ModuleHandle::findOrCreateModule (file, description))
{
// (筆者注: VST3プラグインのロード)
std::unique_ptr<VST3ComponentHolder> holder (new VST3ComponentHolder (module));

if (holder->initialise())
{
// (筆者注: VST3PluginInstanceの作成)
result.reset (new VST3PluginInstance (holder.release()));

if (! result->initialise())
result.reset();
}
}

previousWorkingDirectory.setAsCurrentWorkingDirectory();
}

String errorMsg;

if (result == nullptr)
errorMsg = String (NEEDS_TRANS ("Unable to load XXX plug-in file")).replace ("XXX", "VST-3");

// (筆者注: 構築したVST3PluginInstanceを、引数に渡されたコールバックを通じてcreateInstanceFromDescription()に渡す)
callback (userData, result.release(), errorMsg);
}


https://github.com/WeAreROLI/JUCE/blob/5.4.1/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp#L3074

VST3PluginFormatクラスは、プログラマに公開されているクラスのため、プログラマはこのクラスのcreateInstanceFromDescription()関数を呼び出したり、このクラスを登録したAudioPluginFormatManagerクラスを利用することで、VST3プラグインをロードできます。そしてこのとき、内部で構築したVST3PluginInstanceはAudioPluginInstanceのポインタとして返されるため、その実体がVST3PluginInstanceであることは隠蔽されます。

この仕組みによって、プログラマが派生クラスの実体を知らなくても、VST3プラグインをロードできるというわけです。

juce_VST3PluginFormat.cppには、VST3PluginInstanceの定義だけではなく、VST3のホスト機能に必要となる以下のような仕組みも実装されています。


  • VST3プラグインを探索してPluginDescriptionを作成する仕組み(VST3PluginFormatクラスのメンバ関数として実装される)

  • VST3 SDKのIPluginFactoryクラスを生成して保持する仕組み


    • (VST3プラグインをホストする際には、VST3プラグインのモジュール(.vst3ファイル)からIPluginFactoryクラスのインスタンスを生成してそれを保持しておく必要がある)



  • プラグインのエディターGUIをJUCEのComponentクラスに紐づけて表示できるようにする仕組み

このように、VST3プラグインをホストするための機能がすべてこのソースファイルに含まれていて、その仕組みがプログラマから隠蔽されています。

そしてこれは、他のプラグインフォーマットでも同様です。

この隠蔽と抽象化の仕組みによって、プログラマは各プラグインフォーマットの詳細を気にする必要がなくなり、統一的なコードで開発できるようになります。

JUCE側としては、このソース側で仕組みの変更や修正を行っても、既存のプロジェクトのコードに影響を与えずに済むようになります。

また、先程書いたとおり、各プラグインフォーマット独自の定義をヘッダー側に公開しなくても良くなるという利点もあります。


隠蔽の問題点

とはいえ、この隠蔽の仕組みにも、若干問題となる点があります。

それは、各プラグインフォーマット向けのクラスがソースファイル側で実装されているため、プログラマがそのクラスを直接操作したり、継承によって拡張したりできない、ということです。

例えば、VST3というプラグインフォーマットは拡張性を重視して設計されているので、「ホスト側でこの機能を実装可能だからそれをプラグイン側に提供したい」あるいは「プラグイン側がこの新しい機能を持っていればそれを利用したい」というように、追加の機能を利用できる仕組みが用意されています。

しかし、JUCEの実装ではVST3PluginInstanceの仕組みが完全に隠蔽されているため、JUCEがVST3PluginInstanceで実装している以外の機能を自前で追加するのは困難です。4

加えて、ホスト側から扱える各プラグインフォーマットの機能は、AudioProcessorクラスのインターフェースを通じてできることに限られます。

これによって、各プラグインフォーマットに有用な機能が存在しても、JUCEを利用している場合はそれを扱えないケースが出てきます。

(たとえば、VST3にはプラグイン内のパラメータのセットをUnitという単位で管理する機能があるのですが、これはJUCE 5.4で "Plug-in parameter groups" という仕組みが導入されるまではJUCEで利用できませんでした。)

そしてこれと同様の問題は、プラグイン開発の際にも起こります。

とはいえ、実際JUCEはかなり広く使われていて、ホスト側もプラグイン側もJUCEを使って実装しているものが多いだろうということを考えると、JUCEが実装している以上の機能を自分で頑張って導入したとしても、それを扱える環境が限られてしまってあまり嬉しさはないのかもしれません。5

また、ホストやプラグインによって実装してる機能のレベルが異なると、それに対応するためのコストがかかってきて、結局各プラグインフォーマットを自力であつかっているのとあまり変わらない、みたいな状況にもなってしまうかもしれません。

それを考慮すると、「プログラマに拡張性を提供しないで、実装されている機能のレベルの違いもJUCEが吸収する」という現状の仕組みは、それはそれで納得できるものがあります。


まとめ

今回はJUCEがプラグインをロードするあたりの仕組みを解説しました。簡単にまとめると、以下のようになります。


  • AudioProcessorクラスが抽象化を担当している。

  • AudioProcessorからAudioPluginInstanceクラスが派生し、さらに各プラグインフォーマット向けのクラスが派生している。

  • 各プラグインフォーマット向けの派生クラスがプラグインの実体を保持していて、その定義はソースファイルに隠蔽されている。

  • AudioPluginFormatクラスの派生クラスが、PluginDescriptionの作成や、AudioPluginInstanceの構築を担当する。

JUCEは活発に開発が続いていて、各プラグインフォーマット用のソースファイルの実装や仕組み自体もけっこう大きく変わることがあるため、今回の解説した内容も将来的には別物になっている可能性があります。そうであっても、今回の内容がどこかで役に立つことがあれば嬉しいです。





  1. 各プラグインフォーマットの違いについては、Audio Developer Conference 2018のこの動画がとても参考になります。https://www.youtube.com/watch?v=swVqdbhfkkE&index=2&t=0s&list=LL6B6Vm3IlJcZBxyg0qXs3Hg 



  2. 関連して、AudioProcessorEditorというエディターGUIのためのクラスも存在するのですが、今回の記事では説明を省略します。 



  3. AudioPluginInstanceからはさらにAudioProcessorGraph::AudioGraphIOProcessorクラスが派生していますが、これはAudioProcessorGraph内で特殊な用途に使用されるクラスなので、プラグインとは特に関係しません。 



  4. JUCEのソースファイルが与えられているので、必要に応じて自分で拡張することも可能ではあります。ただし、JUCEのアップデートのたびにそこを対応し直す必要があるので、あまりやりたい方法ではないですね。 



  5. 実際、JUCEのシェアがどれくらいなのかは把握していないです。