VST3ホストを作ろう!

VST3ホストを作ろう!

@hotwatermoningです。

この記事はC++ Advent Calender 2014の参加記事です。

昨年の記事では、VSTという音楽制作プラグインの規格に対応したVSTホストアプリケーションを作成しました。

しかし、前回使用したVST2.x系列のSDKは開発元であるSteinberg社からの配布が終了してしまいました。 (実は今回取り上げるVST3のSDKの中にVST2の開発に必要なソースファイルは同梱されていますが、VST2のドキュメントは含まれていません)

そのような背景から、今回はVST3に対応したホストアプリケーションを作成します。
VST3の仕様について筆者が誤って理解しているところがあるかもしれません。ご了承下さい。(指摘いただけるとありがたいです)

VST3とは

VSTという音楽制作アプリケーション用のプラグイン規格の概要ついては、前述の記事ですでに紹介してありますので、VSTについてご存じない方はそちらをご参照下さい。

VST3はこれまで標準的に使われていたVST2を更に進化させたバージョンです。
公式サイトでは次のように紹介されています。

"Steinberg は12年に渡って、VST により世界のネイティブプラグインフォーマットを牽引してきました。
最新版の VST3 は、完全に書き換えられたコードにより数々の新機能を搭載すると共に、VST 史上最高の安定性を備えています。
VST3 はバーチャルエフェクト、バーチャルインストゥルメント (VSTi) の可能性をより高め、音楽創造を更に豊かにするテクノロジーです。

VST3 :  | https://www.steinberg.net/

VST3ではVST2にはなかった多くの機能が機能が追加されています。

  • Audioバスの構成を柔軟に変更できる仕組み
  • パラメータやそのプリセットをツリー構造で管理できる仕組み
  • バスのバイパス機能
  • オートメーションの精度の向上
  • などなど

これまでのVSTプラグインのタイプはVST EffectVSTiかという分類でしたが、VST3では少し状況が変わりました。
プラグインに指定するsubcategoriesという|文字区切りの文字列にFxが含まれていればエフェクトプラグインとして、Instrumentが含まれていればバーチャル・インストゥルメントとして見なされます。 そして両方含まれていれば、どちらのタイプとしても使用できるプラグインと見なされるようになります。 詳しくは、VST3 API DocumentationのFAQのWhy do Plug-ins need subcategories?の項をご参照下さい。

さらに、VST2.4のドキュメントにはVST MIDIエフェクトのドキュメントが付属していましたが、VST3ではこれは廃止されたようです。VST3の仕組みでVST MIDIエフェクトが実装できるのかもしれません。

VST3はVST2から全く新しく設計された規格のため、VST2との互換性はありません。
モジュールの拡張子もこれまではWindows用バイナリは.dll, Mac用のバンドルは.vstでしたが、VST3規格では統一されて.vst3となります。

このため、VST3として作成されたプラグインはVST3対応のホストでなければ使用できません。

こちらのサイトによれば、最近のCubase, Nuendo, Sonar, Studio One2などではVST3がサポートされていますが、Ableton Live, Bitwig Studioなどではまだサポートされていないそうです。
Supported Hosts | Support | Waves

また、VST3では、Windows環境でのプラグインのインストール先が固定されました。

VST2の時は、Windows環境では、HKEY_LOCAL_MACHINE\SOFTWARE\VST\VSTPluginsPathに記載されたパスが標準ということになっていました。 *1

しかし、このパスは最初にインストールされるDAWによって勝手な場所が指定されたり、VSTPluginsPathにパスが設定されていたとしても問答無用でC:/Program Files/VstPluginsへインストールされるプラグインがあったため、特定場所がVSTプラグインを配置する標準パスとは言えませんでした。
VST3からは以下のパスが標準となります。

-------------------------------------------------------------------------------------------------------------------
Priority   Location             Path                                       Comment
-------------------------------------------------------------------------------------------------------------------
1          Global               /Program Files/Common Files/VST3/          native bitdepth: 32bit Plug-in on 32bit OS, 64bit on 64bit OS)
1          Global               /Program Files (x86)/Common Files/VST3/    32bit Plug-ins on 64bit Windows
2          Application          $APPFOLDER/VST3/ 
-------------------------------------------------------------------------------------------------------------------

Macではこれまでより、/Library/Audio/Plug-ins/VST3//Users/$USERNAME/Library/Audio/Plug-ins/VST3/VSTプラグインのインストール先としてデファクトスタンダードになっていましたが、これもVST3 SDKでは明記されるようになりました。

-------------------------------------------------------------------------------------------------------------------
Priority   Location             Path 
-------------------------------------------------------------------------------------------------------------------
!          User                 /Users/$USERNAME/Library/Audio/Plug-ins/VST3/ 
2          Global               /Library/Audio/Plug-ins/VST3/
3          Global               /Network/Library/Audio/Plug-ins/VST3/
4          Application          $APPFOLDER/Contents/VST3/ 
-------------------------------------------------------------------------------------------------------------------

詳しくは、VST3 API DocumentationのVST 3 Locationsのページをご参照下さい。

音楽制作のためにプラグインを使う側からしても変更点の多いVST3ですが、プラグインやホストの製作者側から見た時には更に変更点が多く、内部の実装はほとんどVST2と互換性がありません。

このようにVST2から大きく様変わりしたVST3ですが、個人的にはやり過ぎ感なあるような気がしています。

同じくVSTという名前が付けられていますが、実質別物と言えるほどの仕様の違いがあり、開発の複雑さもVST2の頃より増しているように思います。
VST2よりもフリーのプラグインが少ないのもおそらくVSTプラグイン開発のハードルが上がったためだろうと思います。

Sonarで有名なcakewalkのブログにもそのような発言が見られました。
The Cakewalk Blog » Blog Archive » Developer Notes: SONAR X3 VST Enhancements

とはいえ、VST3のプラグインも、VST3対応のDAWも徐々に充実してきているため、今後はVST3がもう少し活発になっていくかもしれません。

VST3プログラム開発の情報

VST3プラグイン、及びVST3プラグインに対応したホストを開発するには、VST3 SDKを使用します。
SDKは開発元であるSteinbergから入手可能です。(Steinbergの開発者登録が必須となります。)
Developers :  | http://www.steinberg.net/

現在最新のSDKvstsdk360_22_11_2013_build_100.zipですので、今回の開発でもこのバージョンを使用しています。

VST3のSDKに付属しているドキュメントの内容はあまり親切ではありません。
そのため開発に際しては、ドキュメントとともにSDKのヘッダーファイルも参照したり、付属しているサンプルプラグインのソースも参考にすることになるでしょう。

SDKのドキュメントは大きく3つのセクションに分かれています。 * VST3 API: VST3に特有のクラスや仕様のドキュメント * Base Module: Steinberg社で使用されている様々なクラスやテンプレートやコンテナ。VST3のクラスもこれらのクラスを使用している。 * VST-MA: VST Module ArchitechtureというCOMのような技術に関する情報。VST3プラグインの仕組みはこの技術をベースにして構築されている。

VST3プラグイン/ホスト作成には、VST3 APIのセクションだけを見ても書かれていない情報があるため、必要に応じて他のセクションを横断的に参照する必要があるでしょう。

ネットで見つかるVST3の開発に関する情報は、VST2のものと比べ非常に少ないです。

[12/02追記]
こちらのサイトでVST3プラグインの作成方法が紹介されていました。解説が丁寧で分かりやすそうです。
C++でVST作り
[12/02追記終わり]

他のプラグイン規格(AAX, RTAS, Audio Unitなど)と合わせてVST3にも対応したクロスプラットフォームのライブラリが公開されていますので、その実装を参考にすると良いかもしれません。

昨年の記事で紹介したVstHostですが、現在最新のバイナリはVST3にも対応しています。
しかし公開されているソースはVST2.4を対象にしていた頃のものなので、VST3部分の情報はありません。

今回作成したプログラム

今回作成したサンプルプログラムのソースはこちらです。

今回作成したプログラムはこちらにあります。(無保証、自己責任でお願いします。)

32bit版バイナリ

64bit版バイナリ

このサンプルプログラムを試すには、VST3プラグインが必要です。 VST3プラグインを持っていない方は、u-heのzebraletteなんかが無料で公開されているのでオススメです。

ソースコード解説

ここから、今回作成したプログラムのソースコードを解説していきます。また、VST3の仕様に絡む解説もありますので、その部分では見出しに[VST3]を設定しています。

おおまかな構造

プラグインを扱う部分以外の大きな構造は以前のVST2対応ホストと変わっていません。
鍵盤を押すとノート情報が生成され、それがプラグインに送られて、プラグイン内部で合成された波形をオーディオデバイスに送信します。

追加機能としては、VSTiではない、エフェクトプラグインもロード可能になりました。プラグイン内部で加工した波形を確認するために、ダミーの波形をプラグインに入力しています。

また、VST3は一つのモジュール(.vst3ファイル)のなかに複数プラグインをふくめる機能があるので、そのようなモジュールが指定された場合はどれか一つ選ぶためのダイアログを出す機能が追加されました。
VST2でも一つのモジュール(.dllファイル)のなかに複数プラグインをふくめる機能はあったようですが、これを利用しているプラグインがほとんどなく、筆者自身その機能に詳しくなかったのでVST2対応Hostではその機能を入れていませんでした。

プラグインを扱う部分は、VST2の頃と大きく変わっています。

以下では、VST3プラグインを扱うための3つのクラス

  • Vst3PluginFactory
  • Vst3Plugin
  • Vst3HostCallback

について、順に解説してきます。

しかし、VST3の仕組みをそのまま扱うとプログラムがややこしくなってしまうため、今回はユーティリティクラスを用意しました。

上記のクラスの解説に移る前に、VST-MAの仕組みとそれを扱うユーティリティクラスについて紹介します。

[VST3] VST3-MAの仕組みについて

前述のとおり、VST3のシステムはVST-MAというCOMのような仕組みをベースにしています。
VST-MAについてVST Module ArchitectureのドキュメントIntroductionには次のように記載されています。

VST-MA is a component model system which is used in any
 Steinberg host application as the basic layer for Plug-in 
support as well as for internal application components.
It is object-oriented, cross-platform and (almost) compiler-independent. 
The basics are very much like Microsoft(R) COM, so if you are familiar with this technology, understanding VST-MA should be quite easy.

VST3のプラグインやホストの開発では、VST-MAおよびVST3 APIで定義された抽象的なインターフェースを、VST-MAの仕組みに則って実装していくことで、独自のプラグインやホストを作成します。

VST3で定義されたインターフェースが、プラグイン側/ホスト側のどちらで実装されるべきかについては、VST3 API Documentationの

  • VST 3 Interfaces to be implemented by Plug-in
  • VST 3 Interfaces to be implemented by Host

に定義されています。

VST-MAでもっとも重要になるインターフェースがFUnknownです。
これはVST-MAで最もベースとなるインターフェースであり、他の全てのVST-MAおよびVST3のインターフェースはすべてFUnknownを継承しています。 FUnknownは、参照カウントの仕組みと、自分の実体であるコンポーネントが実装している他のインターフェースを取得する機能を提供しています。

また他に重要な要素として、VST-MAではIIDとCIDという2種類のIDの仕組みがあります。

  • IID: インターフェースIDと呼ばれ、VST-MAの特定のインターフェースを取得するために使用する。
  • CID: コンポーネントIDあるいはクラスIDと呼ばれ、何らかのインターフェースを実装した特定のクラスを指定する時に使用する。

IIDとCIDを使用するのは、FUnknown::queryInterface()の呼び出しと、ファクトリークラスのcreateInstance()の呼び出しです。

あるコンポーネントがなんらかのインターフェースを実装していて、そのインターフェース型のポインタを得るとき、C++ではキャストによって型変換を行いますが、VST-MAではFUnknown::queryInterface()という関数を使用します。
目的のインターフェースを表すIIDを渡してqueryInterface()を呼び出すと、引数に渡したポインタにそのインターフェースのポインタがセットされ、ステータスを表す戻り値が返ります。
queryInterfaceの例

// なんらかのインターフェース
void doSomethingWithISomeAnotherFeature(ISomeFeature *p1)
{
     ISomeAnotherFeature *p2;

     // p1からISomeFeature2のインターフェースを取得。
     Steinberg::tresult const result = p1->queryInterface(ISomeAnotherFeature::iid, (void **)&p2);
     if(result == kResultTrue) {
         p2->doSomething();
         p2->release(); // インターフェースを使い終わる時はreleaseで参照カウントを下げる
     }

ファクトリークラスから、特定のコンポーネントを構築する際には、ファクトリークラスのcreateInstance()に、目的のクラスのCIDと、どのインターフェースのポインタとして取得するかを指定するIIDを渡します。
構築に成功すると、指定したインターフェースのポインタとしてそのコンポーネントを取得できます。

createInstanceの例

void createComponent(IAwesomeFactory *factory, FUID target_cid)
{
    IAwesomeComponent *component;
    Steinberg::tresult = factory->createInstance(target_cid, IAwesomeComponent::iid, (void **)&component);
    if(tresult == kResultTrue) {
        component->doSomething();
        // インターフェースを使い終わったらreleaseで参照カウントを下げる。
        // この時はこのコンポーネントを参照してるポインタが存在しないため、ここでdeleteが行われる
        component->release();
    }
}

上記のコード片では、使い終わったポインタに対してrelease()を呼び出し、参照カウントを下げています。このような参照カウントの操作は、複数のポインタで一つのメモリ領域を管理するために必要となります。

以下はVST-MAではなくCOMの資料ですが、参照カウントについての解説で参考になります。
COM オブジェクトの有効期間の管理

ユーティリティクラス

VST-MAのqueryInterface()createInstance``はどちらも関数呼び出しの結果を受け取るための変数をvoid**型の引数で渡し、ステータスをSteinberg::tresult`型の値で返します。
これらの関数はよく使われるのに対して、関数のインターフェースは使いにくいものになっています。

また、取得したポインタの参照カウントをプログラマ自身で管理する仕組みはバグの原因となります。

そのため今回のプログラムでは、VST-MAのインターフェースを表すスマートポインタと、queryInterface(), createInstance()を使いやすくするラッパー関数を用意して使っています。

struct SelfReleaser
{
        template< class T>
        void operator() ( T * p) {
               if( p) {
                      p->release();
              }
       }
};

template <class T >
std:: unique_ptr< T, SelfReleaser >  to_unique(T *p )
{
        return std:: unique_ptr <T , SelfReleaser >(p);
}

//! 失敗か成功かどちらかの状況を返すクラス
//! is_right() == trueの時は成功の状況
template<class Left, class Right>
struct Either
{
    //...略
};

//! pに対してqueryInterfaceを呼び出し、その結果を返す。
/*!
   @return queryInterfaceが正常に完了し、有効なポインタが返ってきた場合は、
   Rightのオブジェクトが設定されたEitherが返る。
   queryInterfaceがkResultTrue以外を返して失敗した場合は、そのエラーコードをLeftに設定する。
   queryInterfaceによって取得されたポインタがnullptrだった場合は、kNoInterfaceをLeftに設定する。。
   @note   失敗した時に、そのエラーコードが必要になることを考えて、Boost.Optionalではなく、Eitherを返すようにした
*/
template<class To, class T>
Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> queryInterface_impl(T *p, Steinberg::FIDString iid)
{
    typedef Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> either_t;
    To *obtained = nullptr;
    Steinberg::tresult const res = p->queryInterface(iid, (void **)&obtained);
    if(res == kResultTrue && obtained) {
        return either_t(to_unique(obtained));
    } else {
        if(res != kResultTrue) {
            return either_t(res);
        } else {
            return kNoInterface;
        }
    }
}

namespace prevent_adl {

    template<class T>
    auto get_raw_pointer(T *p) -> T * { return p; }

    template<class T>
    auto get_raw_pointer(T const &p) -> decltype(p.get()) { return p.get(); }

}   // prevent_adl

template<class To, class Pointer>
Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> queryInterface(Pointer const &p, Steinberg::FIDString iid)
{
    return queryInterface_impl<To>(prevent_adl::get_raw_pointer(p), iid);
}

template<class To, class Pointer>
Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> queryInterface(Pointer const &p)
{
    return queryInterface_impl<To>(prevent_adl::get_raw_pointer(p), To::iid);
}

template<class To, class Factory>
Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> createInstance_impl(Factory *factory, Steinberg::FUID class_id, Steinberg::FIDString iid)
{
    typedef Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> either_t;
    To *obtained = nullptr;

    Steinberg::tresult const res = factory->createInstance(class_id, iid, (void **)&obtained);
    if(res == kResultTrue && obtained) {
        return either_t(to_unique(obtained));
    } else {
        return either_t(res);
    }
}

//! なんらかのファクトリクラスからあるコンポーネントを取得する。
template<class To, class FactoryPointer>
Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> createInstance(FactoryPointer const &factory, Steinberg::FUID class_id, Steinberg::FIDString iid)
{
    return createInstance_impl<To>(prevent_adl::get_raw_pointer(factory), class_id, iid);
}

//! なんらかのファクトリクラスからあるコンポーネントを取得する。
template<class To, class FactoryPointer>
Either<Steinberg::tresult, std::unique_ptr<To, SelfReleaser>> createInstance(FactoryPointer const &factory, Steinberg::FUID class_id)
{
    return createInstance_impl<To>(prevent_adl::get_raw_pointer(factory), class_id, To::iid);
}

以下の各クラスの解説にもこれらのユーティリティクラスを使用したコードが含まれていますが、やっていることはqueryInterfaceの生の呼び出しや、自分で適切にaddRef/releaseを呼び出すような処理と大差ありません。

[VST3] VST3の名前空間について

VST3 SDKで定義された型や変数は、Steinberg名前空間に含まれています。 その中で特にVST3 APIのドキュメントに含まれるようなVST3特有の型はSteinberg::Vst名前空間に定義されています。

Vst3PluginFactory クラス

ところどころにVST3の仕様の解説を挟みながら、個々のクラスの解説をしていきます。

Vst3PluginFactoryクラスは、プラグインのモジュール(.vst3ファイル)をロードして保持し、VST3プラグインを表すVst3Pluginクラスを構築するためのクラスです。

Steinberg::IPluginFactoryというクラスのラッパークラスになっていて、 * コンポーネント情報へのアクセスを簡単にする。 * 指定されたコンポーネント情報からVst3Pluginクラスを構築する仕組みをカプセル化する。 という役割を持っています。

VST3プラグインのファクトリーとVST3プラグインコンポーネントについて少し補足します。

[VST3] VST3プラグインのファクトリー

VST-MAでは、モジュール(.vst3ファイル)はこのような関数をエクスポートしている必要があります。

IPluginFactory* PLUGIN_API GetPluginFactory ();

モジュールをメモリ上にロードし、エクスポートされたこの関数を呼び出すと、IPluginFactoryインターフェースが得られます。 このインターフェースのgetFactoryInfo()を呼び出すとベンダー名や連絡先の情報が取得できたり、getClassInfo()を呼び出すと、このモジュールが保持しているプラグインの情報(正確にはプラグインコンポーネントの情報)が取得できます。

getClassInfo()で取得したSteinberg::ClassInfoにセットされているCIDを元にIPluginFactory::createInstance()を呼び出すと、目的のプラグインを構築できます。

[VST3] VST3プラグインコンポーネント

VST3のプラグインは、基本的にProcessorControllerという2つのコンポーネントから構成されます。

  • Processor : 実際にMidi情報や音を受け取ったり加工したりする役割をもったコンポーネント。オーディオを扱う機能を提供するIAudioProcessorと、より汎用的な機能を提供するIComponentからなる。
  • Controller : GUI向けの機能やパラメータを扱う仕組み提供するコンポーネント

プラグインが2つのコンポーネントに分かれていることによって、例えばホストアプリケーションでProcessorのみを使用し、Controllerの機能は使用しないというような運用が可能になっています。 詳しくはVST3 API DocumentationのBasic Conceptionをご参照下さい。

このように一つのプラグインを表すコンポーネントの情報が2つ存在するため、IPluginFactoryから取得できるコンポーネント情報のリストにはProcessorの情報とControllerの情報がそれぞれ含まれています。

ところが、Processor側でControllerのインターフェースも一緒に実装してしまっているようなプラグインでは、コンポーネント情報がProccessor側だけしか存在しません。

そのため、VST3プラグインを構築する際には、IPluginFactory::createInstance()ProcessorのCIDを渡すようにします。

Vst3Pluginクラス

Vst3Pluginクラスは、ProcessorControllerという2つのコンポーネントをまとめたり、その他さまざまなインターフェースをプラグインから取得して、VST3プラグインを扱いやすい形にしたクラスです。

Vst3PluginクラスはコンパイラファイアウォールとしてImplクラスを持っています。
これはVst3Pluginのヘッダー側に不必要に多くのVST3 SDKの実装を公開したくなかったためです。
ですので、実質的にはVst3Pluginが持っているImplというクラスがVST3プラグインを管理する実体となっています。

VST3プラグインの構築

Vst3PluginFactory::CreateByID()Vst3PluginFactory::CreateByIndex()を呼び出すと、その過程でVst3Plugin::Implクラスのコンストラクタが呼び出されます。
VST3プラグインはこのImplコンストラクタ、更にその中のLoadPlugin()の中で構築され、続いて初期化処理が行われます。

 void LoadPlugin(IPluginFactory *factory, ClassInfo const &info, host_context_type host_context)
    {
        LoadInterfaces(factory, info, host_context.get());
        Initialize(
            std::move(queryInterface<Vst::IComponentHandler>(host_context.get()).right())
            );
    
        //! End of use of host context, delete it here.
    }

ここで、factoryVst3PluginFactoryから渡されたファクトリーのインターフェース、infoは構築したいプラグインコンポーネント情報です。host_contextは後述するVst3HostCallback::Implクラスです。

LoadPlugin()は内部で2つの関数を呼び出しています。

最初のLoadInterface()では、VST3プラグインを実際にファクトリーから構築し、構築したプラグインからVst3Pluginクラスで必要となるインターフェースを順次取得し、保持していきます。

その次のInitialize()では、取得したインターフェースを元に、プラグインの出入力バスの設定やパラメータの情報を構築したり初期化処理を行っています。

LoadPlugin()で呼び出しているそれぞれの関数を詳しく見て行きましょう。

LoadInterfaces()関数

void LoadInterfaces(IPluginFactory *factory, ClassInfo const &info, Steinberg::FUnknown *host_context)
{
    Steinberg::int8 cid[16];
    for(size_t i = 0; i < 16; ++i) {
        cid[i] = info.cid()[i];
    }

    auto maybe_component = createInstance<Vst::IComponent>(factory, cid);
    if(!maybe_component.is_right()) {
        throw Error(ErrorContext::kFactoryError, maybe_component.left());
    }
    auto component = std::move(maybe_component.right());
    tresult res;
    res = component->setIoMode(Vst::IoModes::kAdvanced);
    if(res != kResultOk && res != kNotImplemented) {
        throw Error(ErrorContext::kComponentError, res);
    }
    status_ = Status::kCreated;

    res = component->initialize(host_context);
    if(res != kResultOk) {
        throw Error(ErrorContext::kComponentError, res);
    }
    status_ = Status::kInitialized;

    BOOST_SCOPE_EXIT_ALL(&component) {
        if(component) {
            component->terminate();
            component.reset();
        }
    };

はじめに、Vst3PluginFactoryから渡されたIPluginFactoryのcreateInstance()に対象のプラグインのCIDを渡して、VST3プラグインを構築しています。
この時、構築したプラグインProcessor側のコンポーネントの一つであるIComponentインターフェースとして取得しています。

次いで、setIoMode()プラグインのIoモードを指定します。これはCubaseの'simple instrument tracks'という機能をサポートするための仕組みのようです。詳しくは、VST3 API DocumentationのComplex Plug-in Structures / Multi-timbral InstrumentsのページにあるThe Simple Modeの項をご参照下さい。

その次にIComponent::initialize()を呼び出し、IComponentを初期化します。 ここで渡しているhost_contextとは、VST2におけるAudioMasterCallbackのようなもので、プラグインからホスト側へ要求や情報を伝えるためのコールバック先です。

次に、Processor側のもう一つのコンポーネントであるIAudioProcessorインターフェースを取得します。

    auto maybe_audio_processor = queryInterface<Vst::IAudioProcessor>(component);
    if(!maybe_audio_processor.is_right()) {
        throw Error(ErrorContext::kComponentError, maybe_audio_processor.left());
    }
    auto audio_processor = std::move(maybe_audio_processor.right());

    res = audio_processor->canProcessSampleSize(Vst::SymbolicSampleSizes::kSample32);
    if(res != kResultOk) {
        throw Error(ErrorContext::kAudioProcessorError, res);
    }

インターフェースが取得できたら、オーディオデータを処理する精度を問い合わせています。今回はfloat精度でオーディオデータを扱うので、floatでの処理ができないプラグインの場合はエラーにしています。

その次は、ControllerコンポーネントのインターフェースであるIEditControllerを取得しています。

    // $(DOCUMENT_ROOT)/vstsdk360_22_11_2013_build_100/VST3%20SDK/doc/vstinterfaces/index.html
    // Although it is not recommended, it is possible to implement both, 
    // the processing part and the controller part in one component class.
    // The host tries to query the Steinberg::Vst::IEditController 
    // interface after creating an Steinberg::Vst::IAudioProcessor 
    // and on success uses it as controller. 
    auto maybe_edit_controller = queryInterface<Vst::IEditController>(component);
    bool edit_controller_is_created_new = false;
    if(!maybe_edit_controller.is_right()) {
        FUID controller_id;
        res = component->getControllerClassId(controller_id);
        if(res == kResultOk) {
            maybe_edit_controller = createInstance<Vst::IEditController>(factory, controller_id);
            if(maybe_edit_controller.is_right()) {
                edit_controller_is_created_new = true;
            } else {
                //! this plugin has no edit controller.
            }
        } else {
            if(res != kNoInterface && res != kNotImplemented) {
                throw Error(ErrorContext::kComponentError, res);
            }
        }

    }

    auto edit_controller = std::move(maybe_edit_controller.right());
    if(edit_controller_is_created_new) {
        res = edit_controller->initialize(host_context);
        if(res != kResultOk) {
            throw Error(ErrorContext::kEditControlError, res);
        }
    }
    BOOST_SCOPE_EXIT_ALL(&edit_controller, edit_controller_is_created_new) {
        if(edit_controller_is_created_new && edit_controller) {
            edit_controller->terminate();
            edit_controller.reset();
        }
    };

前述のとおり、VST3のプラグインは基本的にProcessorControllerコンポーネントに分かれています。
構築したProcessorに対応するControllerコンポーネントIDを取得するにはIComponent::getControllerClassId()を使用します。 ただし、Processor側でIEditControllerインターフェースを実装してしまっているようなプラグインはすでに取得したIComponentからIEditControllerのインターフェースを取得できるはずなので、先にそれを試行しています。 どちらの方法でもIEditControllerが取得できないようなプラグインがあるかどうかは不明ですが、その場合はIEditControllerがないプラグインとしています。。

    plugin_info_ = info;
    component_ = std::move(component);
    audio_processor_ = std::move(audio_processor);
    edit_controller_ = std::move(edit_controller);
    edit_controller2_ = std::move(edit_controller2);
    edit_controller_is_created_new_ = edit_controller_is_created_new;
}

必要なインターフェースが揃ったらそれをメンバー変数に保持して、LoadInterfaces()は終了します。

Initialize()関数

LoadInterface()に続くInitialize()では、プラグインの初期化処理を進め、パラメータのリストを構築したりします。

void Vst3Plugin::Impl::Initialize(std::unique_ptr<Vst::IComponentHandler, SelfReleaser> component_handler)
{
    tresult res;

    if(edit_controller_) {  
        input_changes_.setMaxParameters(edit_controller_->getParameterCount());
        output_changes_.setMaxParameters(edit_controller_->getParameterCount());

        if(component_handler) {
            res = edit_controller_->setComponentHandler(component_handler.get());
            if(res != kResultOk) {
                OutputDebugStringW(L"Can't set component handler");
            }
        }

        auto maybe_cpoint_component = queryInterface<Vst::IConnectionPoint>(component_);
        auto maybe_cpoint_edit_controller = queryInterface<Vst::IConnectionPoint>(edit_controller_);

        if (maybe_cpoint_component.is_right() && maybe_cpoint_edit_controller.is_right())
        {
            maybe_cpoint_component.right()->connect(maybe_cpoint_edit_controller.right().get());
            maybe_cpoint_edit_controller.right()->connect(maybe_cpoint_component.right().get());
        }

Initializeの引数に渡しているcomponent_handlerは、先述のhost_contextから取得したIComponentHandlerインターフェースです。

初めに、edit_controller_が有効なら、edit_controller_->setComponentHandler()を呼び出して、Controllerの操作をホスト側に通知するコールバックを登録しています。

続いて、component_edit_controller_それぞれからIConnectionPointというインターフェースを取得し、2つのコンポーネントを接続しています。これによって、ProcessorControllerという2つのコンポーネントが接続されたことになります。

このあたりは筆者の理解が弱い部分ですが、やっている処理は、VST3 API DocumentationのInitialization of communication from Host point of viewを参考にしています。

次に、プラグインの出入力バスの設定を行っています。 component_からバス情報を取得し、それぞれのバスを有効にして、Vst3Plugin::Implのバスのバッファinput_buses_output_buses_を初期化しています。

        OutputBusInfo(component_.get(), edit_controller_.get());

        input_buses_.SetBusCount(component_->getBusCount(Vst::MediaTypes::kAudio, Vst::BusDirections::kInput));
        for(size_t i = 0; i < input_buses_.GetBusCount(); ++i) {
            Vst::BusInfo info;
            Vst::SpeakerArrangement arr;
            component_->getBusInfo(Vst::MediaTypes::kAudio, Vst::BusDirections::kInput, i, info);
            audio_processor_->getBusArrangement(Vst::BusDirections::kInput, i, arr);
            input_buses_.GetBus(i).SetChannels(info.channelCount, arr);
            component_->activateBus(Vst::MediaTypes::kAudio, Vst::BusDirections::kInput, i, true);
        }

        output_buses_.SetBusCount(component_->getBusCount(Vst::MediaTypes::kAudio, Vst::BusDirections::kOutput));
        for(size_t i = 0; i < output_buses_.GetBusCount(); ++i) {
            Vst::BusInfo info;
            Vst::SpeakerArrangement arr;
            component_->getBusInfo(Vst::MediaTypes::kAudio, Vst::BusDirections::kOutput, i, info);
            audio_processor_->getBusArrangement(Vst::BusDirections::kOutput, i, arr);
            output_buses_.GetBus(i).SetChannels(info.channelCount, arr);
            component_->activateBus(Vst::MediaTypes::kAudio, Vst::BusDirections::kOutput, i, true);
        }

        for(size_t i = 0; i < component_->getBusCount(Vst::MediaTypes::kEvent, Vst::BusDirections::kInput); ++i) {
            component_->activateBus(Vst::MediaTypes::kEvent, Vst::BusDirections::kInput, i, true);
        }
        for(size_t i = 0; i < component_->getBusCount(Vst::MediaTypes::kEvent, Vst::BusDirections::kOutput); ++i) {
            component_->activateBus(Vst::MediaTypes::kEvent, Vst::BusDirections::kOutput, i, true);
        }

        input_buses_.UpdateBufferHeads();
        output_buses_.UpdateBufferHeads();

その後、edit_controller_からパラメータを一覧を取得したり、プログラムリストを構築したり、再生時にIAudioProcessorとパラメータの変更情報をやりとりするバッファの初期化を行ったりして、Initialize()は終了します。

        //! 可能であればこのあたりでIPlugViewを取得して、このプラグインがエディターを持っているかどうかを
        //! チェックしたかったが、いくつかのプラグイン(e.g., TyrellN6, Podolski)では
        //! IComponentがactivatedされる前にIPlugViewを取得するとクラッシュした。
        //! そのため、この段階ではIPlugViewは取得しない

        PrepareParameters();
        PrepareProgramList();

        input_changes_.setMaxParameters(parameters_.size());
        output_changes_.setMaxParameters(parameters_.size());
    }

コメントにある通り、当初はここでIPlugViewというプラグインのエディターGUIを提供するインターフェースを取得して、プラグインのエディターGUIが存在するかどうかを判定したかったのですが、それによってクラッシュするプラグインがあったため、ここではIPlugViewを作成していません。

Vst3Pluginのセットアップ

LoadPlugin()の呼び出しが正常に終了すると、Vst3PluginFactoryからVst3Pluginが取得できます。 続いて、このプラグインを使用してオーディオ処理をするために、セットアップを行います。

VstHostDemo.cpp(290)

    vst3_plugin_->SetBlockSize(block_size_);
    vst3_plugin_->SetSamplingRate(sampling_rate_);
    vst3_plugin_->Resume();

SetBlockSize()はVST2と同じように、一度の再生フレームで合成可能な最大のサンプル数を設定する関数です。

Vst3Plugin::SetBlockSize()Vst3Plugin::SetSamplingRate()は値をセットするだけで、実際にVST3プラグインにこれらのパラメータをセットするのは、次のResume()になります。

Resume()の定義は以下のようになっています。

void Vst3Plugin::Impl::Resume()
{
    BOOST_ASSERT(status_ == Status::kInitialized || status_ == Status::kSetupDone);

    tresult res;
    if(status_ != Status::kSetupDone) {
        Vst::ProcessSetup setup = {};
        setup.maxSamplesPerBlock = block_size_;
        setup.sampleRate = sampling_rate_;
        setup.symbolicSampleSize = Vst::SymbolicSampleSizes::kSample32;
        setup.processMode = Vst::ProcessModes::kRealtime;

        res = GetAudioProcessor()->setupProcessing(setup);
        if(res != kResultOk && res != kNotImplemented) { throw std::runtime_error("setupProcessing failed"); }
        status_ = Status::kSetupDone;
    }

    res = GetComponent()->setActive(true);
    if(res != kResultOk && res != kNotImplemented) { throw std::runtime_error("setActive failed"); }
    status_ = Status::kActivated;

    is_resumed_ = true;

    //! Some plugin (e.g., TyrellN6.vst3, podolski.vst3) need to create its plug view after the components is active.
    tresult const ret = CreatePlugView();
    has_editor_ = (ret == kResultOk);

    hwm::dout << "Latency samples : " << GetAudioProcessor()->getLatencySamples() << std::endl;

事前に設定したブロックサイズとサンプリングレートを渡してIAudioProcessor::setupProcessing()を呼び出し、VST3プラグインに、これらのパラメータを設定しています。

setupProcessing()が成功すると、次にIComonent::setActive()を呼び出しています。

この段階で、IComponentがアクティブになったため、ここでIPlugViewインターフェースを取得し、プラグインがエディターGUIを持っているかどうかをチェックしています。

次にIAudioProcessor::setProcessing()を呼び出しています。 ただし、プラグインによっては、setProcessing()kNotImplementedを返す場合があります。 ここでは、kNotImplementedが返されても正常にResume()は成功したという状態にしています。

    //! 略
    /*!

    */
    res = GetAudioProcessor()->setProcessing(true);
    if(res == kResultOk || res == kNotImplemented) {
        status_ = Status::kProcessing;
        is_processing_started_ = true;
    } else {
        hwm::dout << "Start processing failed : " << res << std::endl;
    }

このセットアップの流れと、また、逆の停止処理の流れはVST3 API DocumentationのWorkflow Diagramに記載がありますので、詳しくはそちらをご参照下さい。

Vst3Pluginのオーディオ処理

ここまででプラグインのセットアップは完了したため、プラグインを使用できるようになりました。 次にVst3Pluginのオーディオ処理の部分を見ていきます。

VST3プラグインのオーディオ処理は、VST2と同じくフレーム単位で行います。

今回のプログラムでは、VstHostDemo.cpp(419)から再生スレッド内で呼び出している関数がオーディオ処理を行う部分です。

    float **synthesized = vst3_plugin_->ProcessAudio(pos, num_samples);

この呼び出しはVst3Plugin::ImplProcessAudio()へ転送されます。

float ** Vst3Plugin::Impl::ProcessAudio(size_t frame_pos, size_t duration)
{
    ClassInfo &cinfo = *plugin_info_;
    double const tempo = 120.0;
    double beat_per_second = tempo / 60.0;
    Vst::ProcessContext process_context = {};
    process_context.sampleRate = sampling_rate_;
    process_context.projectTimeSamples = frame_pos;
    process_context.projectTimeMusic = frame_pos / 44100.0 * beat_per_second;
    process_context.tempo = tempo;
    process_context.timeSigDenominator = 4;
    process_context.timeSigNumerator = 4;

    process_context.state =
        Vst::ProcessContext::StatesAndFlags::kPlaying |
        Vst::ProcessContext::StatesAndFlags::kProjectTimeMusicValid |
        Vst::ProcessContext::StatesAndFlags::kTempoValid |
        Vst::ProcessContext::StatesAndFlags::kTimeSigValid;

最初に、IAudioProcessorに渡すための今回の再生フレームのコンテキスト(再生位置やテンポやループ再生状態の情報)を設定してします。 今回はサンプルプログラムなので、テンポ120.0, 4/4拍子, 再生位置は再生フレームごとに前進しつづける、というような設定にしています。実際のDAWでは、DAW内部の再生状態に合わせてここを設定することになるでしょう。

続いて、IAudioProcessorMIDI情報を送るEventListを設定しています

    Vst::EventList input_event_list;
    Vst::EventList output_event_list;
    {
        auto lock = boost::make_unique_lock(note_mutex_);
        for(auto &note: notes_) {
            Vst::Event e;
            e.busIndex = 0;
            e.sampleOffset = 0;
            e.ppqPosition = process_context.projectTimeMusic;
            e.flags = Vst::Event::kIsLive;
            if(note.note_state_ == Note::kNoteOn) {
                e.type = Vst::Event::kNoteOnEvent;
                e.noteOn.channel = 0;
                e.noteOn.length = 0;
                e.noteOn.pitch = note.note_number_;
                e.noteOn.tuning = 0;
                e.noteOn.noteId = -1;
                e.noteOn.velocity = 100 / 127.0;
            } else if(note.note_state_ == Note::kNoteOff) {
                e.type = Vst::Event::kNoteOffEvent;
                e.noteOff.channel = 0;
                e.noteOff.pitch = note.note_number_;
                e.noteOff.tuning = 0;
                e.noteOff.noteId = -1;
                e.noteOff.velocity = 100 / 127.0;
            } else {
                continue;
            }
            input_event_list.addEvent(e);
        }
        notes_.clear();
    }

IAudioProcessorは、再生時にMIDIイベント情報をIEventListインターフェースとして受け取ります。

VST3 SDKには、IEventListインターフェースを実装したEventListクラスが用意されていますので、ホスト側ではこのクラスを利用して、MIDIイベントの情報をIAudioProcessorへ渡せます。 また、IAudioProcessorからホスト側へMIDIイベントを返す仕組みもあり、ここではoutput_event_listという変数がそのイベントを受け取るバッファーになります。

今回はサンプルプログラムなので、鍵盤画面から送られてきたノートのオン/オフの情報のみをEventList型の変数であるinput_event_listにセットして合成処理を行います。

続いて、プラグインに渡す/プラグインから受け取るオーディオバッファの設定です

    std::vector<Vst::AudioBusBuffers> inputs(input_buses_.GetBusCount());
    for(size_t i = 0; i < inputs.size(); ++i) {
        inputs[i].channelBuffers32 = input_buses_.GetBus(i).data();
        inputs[i].numChannels = input_buses_.GetBus(i).channels();
        inputs[i].silenceFlags = false;

        if(inputs[i].numChannels != 0) {
            for(int ch = 0; ch < inputs[i].numChannels; ++ch) {
                for(int smp = 0; smp < duration; ++smp) {
                    inputs[i].channelBuffers32[ch][smp] = 
                        wave_data_[(wave_data_index_ + smp) % (int)process_context.sampleRate];
                }
            }
            wave_data_index_ = (wave_data_index_ + duration) % (int)process_context.sampleRate;
        }
    }

    std::vector<Vst::AudioBusBuffers> outputs(output_buses_.GetBusCount());
    for(size_t i = 0; i < outputs.size(); ++i) {
        outputs[i].channelBuffers32 = output_buses_.GetBus(i).data();
        outputs[i].numChannels = output_buses_.GetBus(i).channels();
        outputs[i].silenceFlags = false;
    }

今回作成したプログラムは、バーチャル・インストゥルメントではないオーディオエフェクトプラグインもロードできる仕組みになっていますので、プラグインで加工した音を確かめるために、ダミーの波形入力バッファに書き込んでいます。

ここで使用しているAudioBusBuffersクラスは、先ほどのIEventListインターフェースとEventListクラスの関係と同様に、IAudioProcessorが受けとるIAudioBusBuffersインターフェースに対応するクラスです。

次は、プラグインへ送るパラメータ変更の通知です。

    input_changes_.clearQueue();
    output_changes_.clearQueue();

    TakeParameterChanges(input_changes_);

ホスト側のオートメーションの操作やプラグインのエディターGUIの操作によるパラメータの変更は、任意の場所から直接IAudioProcessorに適用することはできません。 必ずプラグインの再生フレーム処理の中で、IAudioProcessorに送信される必要があります。

Vst3Pluginは、パラメータの変更を一時的に保持しておくparam_changes_queue_というキューを持っています。
プラグインのエディターGUIから送られてきたパラメータの変更情報は、一度このキューに溜められた後、次回の再生処理の時にTakeParameterChanges()によってinput_changes_に取り出され、IAudioProcessorに送信されます。

また、EventListの場合と同様に、パラメータの変更をIAudioProcessorの側からをホスト側へ返す仕組みもあり、それを受け取るためにoutput_changes_という変数を用意しています。

ここで使用しているinput_changes_, output_changes_の型はParameterChangesというクラスであり、これは、IEventListEventListの関係と同様に、IAudioProcessorが受け取るIParameterChangesインターフェースに対応するクラスです。

ここまでで、1フレームの再生に必要なデータが揃ったので、Vst::ProcessDataというクラスに情報をまとめて、 IAudioProcessor::process()を呼び出します。

    Vst::ProcessData process_data;
    process_data.processContext = &process_context;
    process_data.processMode = Vst::ProcessModes::kRealtime;
    process_data.symbolicSampleSize = Vst::SymbolicSampleSizes::kSample32;
    process_data.numSamples = duration;
    process_data.numInputs = inputs.size();
    process_data.numOutputs = outputs.size();
    process_data.inputs = inputs.data();
    process_data.outputs = outputs.data();
    process_data.inputEvents = &input_event_list;
    process_data.outputEvents = &output_event_list;
    process_data.inputParameterChanges = &input_changes_;
    process_data.outputParameterChanges = &output_changes_;

    GetAudioProcessor()->process(process_data);

    for(int i = 0; i < output_changes_.getParameterCount(); ++i) {
        auto *queue = output_changes_.getParameterData(i);
        if(queue && queue->getPointCount() > 0) {
            hwm::dout << "Output parameter count [" << i << "] : " << queue->getPointCount() << std::endl;
        }
    }

    return output_buses_.data();
}

呼び出しが成功して正常に音の合成や加工が行われていれば、渡したオーディオバッファにデータが書き込まれているはずなので、そのポインタを返して、Vst3Plugin::ImplProcessAudio()は終了します。

Vst3PluginのエディターGUI

読み込んだVST3プラグインにエディターGUIが用意されている場合は、Vst3Plugin::OpenEditor()を呼び出してエディターGUIを作成できます。 内部的には、Vst3PluginIEditControllerから取得したIPlugViewというクラスを使用します。

bool Vst3Plugin::Impl::OpenEditor(HWND parent, IPlugFrame *frame)
{
    BOOST_ASSERT(HasEditor());

    tresult res;
        
    res = plug_view_->isPlatformTypeSupported(kPlatformTypeHWND);
    if(res != kResultOk) {
        throw std::runtime_error("HWND is not supported.");
    }

    plug_view_->setFrame(frame);
    res = plug_view_->attached((void *)parent, kPlatformTypeHWND);
    bool const success = (res == kResultOk);
    if(success) {
        is_editor_opened_ = true;
    } else {
        MessageBox(NULL, L"Failed to attach", NULL, NULL);
        is_editor_opened_ = false;
    }

    return success;
}

isPlatformTypeSupported()で、現在のプラットフォームでのエディターGUIが開けるかどうかを確認します。

その後setFrame()で、IPlugFrameインターフェースを設定しています。
VST3ではエディターGUIのサイズが変更できるようになりました。IPlugFrameインターフェースはエディターGUIのリサイズ情報をホストのウィンドウ側に通知するためのコールバックの機能を提供します。

setFrame()の呼び出し後、attached()でエディターGUIの親ウィンドウとなるハンドルを渡すと、エディターGUIが開かれます。

今回のプログラムでは、EditorFrame.hppで定義しているEditorFrameクラスがエディターGUIの親ウインドウ兼IPlugFrameになっています。

エディターGUIを破棄するには、removedを呼び出します。

void Vst3Plugin::Impl::CloseEditor()
{
    if(is_editor_opened_) {
        plug_view_->removed();
        is_editor_opened_ = false;
    }
}

[VST3] VST3プラグインのパラメータ変更

Vst3Pluginのオーディオ処理の項で紹介したとおり、VST3では、エディターGUIで操作したパラメータの変更はそのままプラグインIAudioProcessorには適用されません。 エディターGUIからのパラメータの変更はまず、IComponentHandlerインターフェースを通じてホスト側へ通知されてきます。 このインターフェースは、プラグイン構築時にhost_contextとして渡したものです。

そしてホスト側では、受け取ったパラメータの変更をキューに貯め、次回の再生フレームでIAudioProcessorへパラメータの変更を通知します。

Processor側は大抵の場合専用の再生スレッドで動作し、エディターGUIの操作はGUIスレッドで操作されますが、このようにController側のパラメータの変更を非同期にProcessor側へ通知する仕組みによって、ProcessorControllerの側で同期処理を考慮する必要がなくなります。

逆にホストからプラグインへパラメータの変更を指示する場合は、IEditController::setParameterNormalized()を呼び出して、Controller側に変更を通知し、 IAudioProcessorへ送るキューにもパラメータ変更の情報を追加して、次回再生フレームでProcessor側に変更が適用されるようにします。

[VST3] VST3プラグインのプログラム(パラメータのプリセット)

VST3では、プログラム(パラメータのプリセット)の仕組みがVST2から変わりました。

VST3では、プラグイン複数のプログラムのリストを持つことができるようになりました。また、それぞれのリストは、Unitという構造に紐付いています。

Unitとは、プラグインのパラメータをより細かく管理するために新たに導入された仕組みです。
階層構造で管理され、一つのUnitの下に複数Unitを持つことが可能です。また、ID0をもつグローバルのUnitが存在します。

それぞれのUnitprogramListIdという値を持っており、この値によってUnitとプログラムのリストが紐付いています。

今回の実装ではUnitを検索し、programListIdに有効な値が設定された最初のUnitから、使用するプログラムのリストを決定しています。

またプログラムをプラグインに適用する方法もVST2から変わりました。 これまでのようなプログラムを変更するための専用のAPIは用意されず、プラグインのパラメータのリストに、現在のプログラムのインデックスをあらわすパラメータが追加されました。 そのようなパラメータはParameterInfo::flagskIsProgramChangeが設定されています

Vst3PluginImpl.cpp(739)
void Vst3Plugin::Impl::PrepareProgramList()
{
    parameter_for_program_ = -1;
    for(auto const &param_info: parameters_) {
        if((param_info.flags & Vst::ParameterInfo::ParameterFlags::kIsProgramChange) != 0) {
            parameter_for_program_ = param_info.id;
        }
    }

ホストがプラグインのプログラムを変更するときは、このパラメータに対して、新たなプログラムのインデックスを0.0 - 1.0の範囲に収まるように正規化して、通常のパラメータの変更処理と同じようにControllerProcessorに設定します。

Vst3PluginImpl.cpp(293)

void    Vst3Plugin::Impl::SetProgramIndex(size_t index)
{
    if(parameter_for_program_ == -1) {
        //nothing to do
    } else {
        current_program_index_ = 
            DiscretizeProgramIndex(GetEditController()->getParamNormalized(parameter_for_program_));
    }

    if(index == current_program_index_) {
        return;
    }

    current_program_index_ = index;

    auto const &program = programs_[current_program_index_];
    auto const normalized = NormalizeProgramIndex(current_program_index_);

    param_value_changes_was_specified_ = false;

    //! `Controller`側、`Processor`側それぞれのコンポーネントにプログラム変更を通知
    if(parameter_for_program_ == -1) {
        GetEditController()->setParamNormalized(program.list_id_, normalized);
        EnqueueParameterChange(program.list_id_, normalized);
    } else {
        GetEditController()->setParamNormalized(parameter_for_program_, normalized);
        EnqueueParameterChange(parameter_for_program_, normalized);
    }
}

上の関数は、setParamNormalized()で、Controller側へのプログラム変更を通知し、EnqueueParameterChange()によって次回の再生フレームの処理でProcessor側にプログラムの変更を通知しています。

Vst3HostCallbackクラス

Vst3HostCallbackクラスは、プラグインからホスト側へ処理や要求をコールバックするためのクラスです。

このクラスもImplクラスを持っており、Implクラスの側でIHostApplicationIComponentHandlerなどのインターフェースを実装しています。

IHostApplicationインターフェースはBasic Host Callback Interfaceと定義されており、ホストアプリケーションの名前をプラグインに返したり、プラグインで内部的にコンポーネント間の通信を行うためのIMessageインターフェースをプラグイン側に渡す機能を提供します。

IComponentHandlerは、プラグインControllerで行われたパラメータの変更をホスト側に通知するコールバックです。

Vst3HostCallback::Implでは、以下のようにIComponentHandlerインターフェースの関数をオーバーライドしています。

    /** To be called before calling a performEdit (e.g. on mouse-click-down event). */
    tresult PLUGIN_API beginEdit (Vst::ParamID id) override
    {
        hwm::dout << "Begin edit   [" << id << "]" << std::endl;
        return kResultOk;
    }

    /** Called between beginEdit and endEdit to inform the handler that a given parameter has a new value. */
    tresult PLUGIN_API performEdit (Vst::ParamID id, Vst::ParamValue valueNormalized) override
    {
        hwm::dout << "Perform edit [" << id << "]\t[" << valueNormalized << "]" << std::endl;
        parameter_change_notification_handler_(id, valueNormalized);
        return kResultOk;
    }

    /** To be called after calling a performEdit (e.g. on mouse-click-up event). */
    tresult PLUGIN_API endEdit (Vst::ParamID id) override
    {
        hwm::dout << "End edit     [" << id << "]" << std::endl;
        return kResultOk;
    }

    /** Instructs host to restart the component. This should be called in the UI-Thread context!
    \param flags is a combination of RestartFlags */
    tresult PLUGIN_API restartComponent (int32 flags) override
    {
        hwm::dout << "Restart request has come [" << flags << "]" << std::endl;
        request_to_restart_handler_(flags);
        return kResultOk;
    }

プラグインController側で操作が行われると、これらの関数呼び出されます。

beginEdit()は、エディターGUIつまみが動かされるなどして、あるパラメータの変更が行われる直前、endEdit()はそのパラメータの一連の変更が終了した直後に呼び出されます。
performEdit()beginEditendEdit()の呼び出しの間で、パラメータが変更がされ続ける度に呼び出されます。

今回のプログラムではperformEdit()が呼ばれた時に、事前に設定した関数parameter_change_notification_handler_を呼び出しています。

ここには、VstHostDemo.cpp(93)で定義されたラムダ式が設定されており、performEditの呼び出しに渡された引数をVst3Plugin::EnqueueParameterChange()に渡して、次回再生フレームが来た時にパラメータの変更がProcessor側に通知されるようにしています。

また、Vst3HostCallback::Implクラスでは、IComponentHandler2クラスも実装しています。
IComponentHandler2複数のパラメータを同時に変更するための機能や、プラグイン側のDirtyフラグの状態を受け取る機能を提供しています。 今回のプログラムでは、これらの関数を利用する機能はありません。単にデバッグ用に呼び出しのログを取るだけになっています。

まとめ

このようにして、VST3プラグインをロードして、エディターGUIの情報を受け取ったり、実際にノートを送って合成できるようになりました。

VST3に関しては本当に情報が少なく、自分でも正しく理解できているかどうか怪しいところが多々あるのですが、それでもなんとか形になって動くものができて良かったです。

最近はVST3の上に構築されたARAというプラグインの規格も登場しているので、今後はVST3と合わせてこの規格も調べてみたいです。

明日は、oniprogさんです。よろしくお願いします。

*1:VST2.4のドキュメント、IntroductionのWhat is a VST Plug-In?を参照