LoginSignup
5
2

More than 3 years have passed since last update.

VST3ホストを作ろう(11) 〜VST-MA の作法〜

Last updated at Posted at 2020-05-11

目次

はじめに

 以前に解説したとおり VST-MA のもとになった COM1 は言語によらない規格として作られています。

 COM はオブジェクト指向プログラミング的にインターフェースやコンポーネントを構造化して扱う仕組みをサポートしていますが、クラス同士の継承関係を表現する方法やクラスのオブジェクトを構築/破棄する仕組みは、言語によってどのように実現されるかが異なります。
 特定の言語によらずに COM を利用できるようにするために、 COM はそれらの言語による違いを吸収する独自の仕組みを用意しています。

 COM を利用する場合はその独自の仕組みに沿ってプログラムを書く必要があるため、 COM を利用しないでプログラムを書く場合に比べて多少面倒な書き方をする必要があります。

 VST-MA も COM の仕組みを引き継いでいるため、 VST-MA を利用する場合も、 COM を利用する場合と同様に多少面倒な書き方をする必要があります。これに慣れていないうちは違和感を覚えるかもしれませんが、これは作法のようなものなので、使い方を理解すれば特に難しいものではありません。

 この記事では、 VST-MA を利用する場合の作法について解説します。また記事の後半では、 VST-MA の仕様に関する話としてインターフェースのバージョニングと継承に関する仕様について解説します。2

VST-MA の作法

 VST-MA を利用する場合は、VST-MA の作法に沿ってプログラムを書く必要があります。その作法とは以下のとおりです。

  • コンポーネントは隠蔽し、インターフェースを受け渡す
  • コンポーネントの寿命は参照カウントで管理する
  • インターフェースのキャストは queryInterface() で行う
  • コンポーネントを指定して構築するには createInstance() を使う

 これらの作法について、以下で順に解説します。

コンポーネントは隠蔽し、インターフェースを受け渡す

 VST-MA は、ホストアプリケーション側からそのプラグイン側で定義されたクラスを利用したり、あるいは逆に、プラグイン側からホストアプリケーション側で定義されたクラスを利用するための技術です。
 ただし、ホストアプリケーションとプラグインは別のバイナリファイルとして作られているため、バイナリ境界を超えてコンポーネント自体を受け渡すことはできません。前回解説したように、インターフェースはバイナリ境界を超えられるため、コンポーネントを直接受け渡すのではなく、必ずインターフェースの形で受け渡しを行う必要があります。
 そしてインターフェースを受け取った側は、そのインターフェースに定義された仮想関数を呼び出すことで、コンポーネントの実体を知らないままインターフェースが定義する機能を使えるようになっています。

 以下のサンプルコードは、ホストアプリケーション側からプラグイン側へ1フレーム分の再生処理に必要なデータを送って、再生処理を行うコードを抜粋したものです。

 VST3 SDK ではノートオンやピッチベンドチェンジの情報は Event という構造体で表現されます。ホストアプリケーション側からプラグイン側へ Event を送るには IEventList という Event 構造体の コンテナ機能を持ったインターフェースを用意して、それをプラグインへ渡します。

 ホストアプリケーション側では、 IEventList を継承した MyEventList というコンポーネントを用意してそこに Event を追加しますが、プラグイン側へはそのコンポーネントを IEventList として渡します。

インターフェースを受け渡して処理を行うサンプルコード
//=====================================
// ホストアプリケーション側

// IEventList インターフェースを最低限満たす、シンプルなコンポーネントの定義
class MyEventList : public IEventList
{
public:
  MyEventList() { FUNKNOWN_CTOR; }
  ~MyEventList() { FUNKNOWN_DTOR; }

  int32 PLUGIN_API getEventCount () SMTG_OVERRIDE {
    return events_.size();
  }

  tresult PLUGIN_API getEvent (int32 index, Event& e) SMTG_OVERRIDE {
    e = events_[index];
    return kResultOk;
  }

  tresult PLUGIN_API addEvent (Event& e) SMTG_OVERRIDE {
    events_.push_back(e);
    return kResultOk;
  }

  DECLARE_FUNKNOWN_METHODS
private:
  std::vector<Event> events_;
};

// イベントリストを準備する。
MyEventList *el = new MyEventList();

// ノートオンの情報を構築して、イベントリストに追加する。
Event e;
e.type = Vst::Event::kNoteOnEvent;
e.noteOn.channel = 0;
e.noteOn.pitch = 60;
e.noteOn.velocity = 0.5;
e.noteOn.length = 0;
e.noteOn.tuning = 0;
e.noteOn.noteId = -1;
el->addEvent(e);

// ProcessData は、1フレーム分の再生処理に必要なデータをまとめて
// プラグインへ渡すための構造体
Vst::ProcessData process_data;

// process_data の inputEvents メンバ変数の型は IEventList * になっている。
// これを、今準備した MyEventList のポインタの値で初期化する。
process_data.inputEvents = el;
// (process_data の他のメンバ変数の初期化処理は省略)

// 準備した process_data をプラグインへ渡して、再生処理を行う。
// ここで audio_processor は IAudioProcessor インターフェースのポインタとする。
// IAudioProcessor はプラグインのオーディオ処理を行うための機能を定義したインターフェースで、
// process() 関数は、 ProcessData の参照を受け取り、1フレーム分の再生処理を行う。
audio_processor->process(process_data);

// 使い終わった IEventList は破棄する。
// (実際には、毎フレームの再生処理で MyEventList を作り直すのはコストが掛かるので、
// 事前に構築したものを使い回すようにしたほうが良い)
el->release();


//=====================================
// プラグイン側

// プラグインのコンポーネント定義。
// ここで基底クラスに指定している AudioEffect は
// プラグインのコンポーネントを実装するために VST3 SDK が用意しているヘルパークラスで、
// IComponent インターフェースや IAudioProcessor インターフェースを継承している。
class MyPlugin : public AudioEffect
{
public:
  // ホストアプリケーションから呼ばれる、1フレーム分の再生処理を行う関数。
  // IAudioProcessor インターフェースで定義された仮想関数をオーバーライドしている。
  tresult PLUGIN_API process(ProcessData &data) SMTG_OVERRIDE {

    // ホストアプリケーションから送られてきたイベントリストを、
    // IEventList インターフェースの形で受け取る。
    IEventList *el = data.inputEvents;

    Event e;
    for(int i = 0, end = el->getEventCount(); i < end; ++i) {

      // Event の参照を渡して、指定したインデックスのイベントを取得する。
      el->getEvent(i, e);

      // 取得したイベントのデータをもとに再生処理を行う。
    }
  }
};

 このように、VST-MA を使うプログラムでは、バイナリ境界を超えてクラスをやり取りする際にはインターフェースを受け渡すようにして、コンポーネントはその下に隠蔽して扱うことになります。

コンポーネントの寿命は参照カウントで管理する

 VST-MA では、コンポーネントの寿命を参照カウントで管理します。参照カウントは FUnknown インターフェースに定義された addRef()release() 関数でそれぞれインクリメント/デクリメントできます。

 コンポーネントの構築直後には、参照カウントは 1 に初期化されます。
 release() 関数の呼び出しによって参照カウントが 0 になると、コンポーネントが自分自身の delete 演算子を呼び出して自身を破棄します。

 以下のサンプルコードは、 addRef() 関数と release() 関数によってコンポーネントの寿命を管理する例です。前回の記事で紹介した IBStream インターフェースとそれを継承した MemoryStream コンポーネントを使用します。

IBStream * createMemoryStream()
{
  // IBStream インターフェースを継承した MemoryStream コンポーネントを構築する。
  // コンポーネントは、構築直後、参照カウントが 1 に初期化される。
  MemoryStream *p = new MemoryStream();

  p->setSize(256);
  return p;
}

IBStream *s = createMemoryStream();

// この時点で s の参照カウントは 1

s->write("abcd", 4); // s が生きているので、 s のメンバ関数を呼び出し可能。

s->addRef(); 

// この時点で s の参照カウントは 2

s->release();

// この時点で s の参照カウントは 1

s->write("efgh", 4); // この時点でも s が生きているので、 s のメンバ関数を呼び出し可能。

s->release(); // ここで s の参照カウントが 0 になり、s が自分自身を delete する。

// 以降は s を使用してはいけない。
// さもなければアクセス違反や予期せぬエラーを引き起こす。

// release() 呼び出したあとのポインタには nullptr を代入しておき、
// もうそのポインタは使用できないことを明示しておくと良い。
s = nullptr;

 インターフェースのポインタをクラスのメンバやグローバル変数などに保持して長期に渡って使用するとき、そのインターフェースの実体であるコンポーネントが別の箇所で勝手に破棄されては困ります。
 そのため、インターフェースのポインタをどこかに保持して利用するときは必ず addRef() 関数を呼び出して参照カウントを増やしておき、勝手にコンポーネントが破棄されないようにします。そして自分の利用が終わったときに必ず release() 関数を呼び出すようにして、自身の利用が終わったことをコンポーネントに伝えます。

 例えば、あるインターフェースをメンバ変数に持つようなクラスは、以下のようにコンストラクタやデストラクタを実装して、必ず addRef() 関数と release() 関数の呼び出しが対になるようにします。

// IBStream を受け取ってなにか処理を行うクラス
class MyClass
{
public:
  // コンストラクタ
  // 引数で受け取ったインターフェースのポインタを複製し、
  // このクラスで IBStream インターフェースを利用する間、
  // 勝手にインターフェースが破棄されないように参照カウントを増やす。
  explicit MyClass(IBStream *s) {
    assert(s != nullptr);
    stream = s;
    stream->addRef();
  }

  // コピーコンストラクタ
  // コピー元である rhs からインターフェースのポインタを複製し、
  // 参照カウントを増やす
  MyClass(MyClass const &rhs) {
    stream = rhs.stream;
    stream->addRef();
  }

  // コピー代入演算子
  // コピー元である rhs からインターフェースのポインタを複製し、
  // 参照カウントを増やす
  MyClass & operator=(MyClass const &rhs) {
    stream = rhs.stream;
    stream->addRef();
    return *this;
  }

  // 簡単のために、ムーブコンストラクタはサポートしない。
  MyClass(MyClass &&) = delete;
  // 簡単のために、ムーブ代入演算子はサポートしない。
  MyClass & operator=(MyClass &&) = delete;

  // デストラクタ
  // インターフェースの利用が完了したので、参照カウントを減らす。
  ~MyClass() { stream->release(); }

  // 複製したインターフェースを使ってなにか処理を行う関数
  void doSomethingWithIBStream() { /*...*/ }

private:
  IBStream *stream;
};

このクラスは以下のように利用できます。

// IBStream インターフェースを継承したコンポーネントを構築して、
// そのインターフェースを取得する。
IBStream *s = createMemoryStream();

{ 
  // このコンストラクタの中でインターフェースのポインタが複製され、
  // 参照カウントがインクリメントされる。
  MyClass c1(s);

  c1->doSomethingWithIBStream();

  // c1 にインターフェースのポインタを複製したことで、
  // このあとは s は不要になったものとする。
  // なので release() を呼び出したあとで s を nullptr にリセットし、
  // s を利用できない状態にする。
  // c1 の中で参照カウントをインクリメントしてあるので、
  // ここで release() を呼び出しても、
  // インターフェースの実体であるコンポーネントは破棄されない。
  s->release();
  s = nullptr;

  {
    // ここで MyClass のコピーコンストラクタによって
    // インターフェースのポインタが複製され、
    // 参照カウントがインクリメントされる。
    MyClass c2 = c1;

    c2->doSomethingWithIBStream();

  } // ここで c2 のデストラクタが呼ばれ、
    // c2 が保持していたインターフェースの参照カウントがデクリメントされる

} // ここで c1 のデストラクタが呼ばれ、
  // c1 が保持していたインターフェースの参照カウントがデクリメントされる。
  // このタイミングで、このインターフェースの参照カウントが 0 になって、
  // createStreamFromSomewhere() で構築したコンポーネントが破棄される。

 参照カウントの呼び出しをプログラマが自身で管理するのはかなり煩雑で、バグの温床になります。この問題に対して VST-MA では、参照カウントを自動で管理するために FUnknownPtr というスマートポインタクラスを用意しています。通常はこのようなスマートポインタを使ってインターフェースを扱うと良いでしょう。

インターフェースのキャストは queryInterface() で行う

 先に書いたとおり、コンポーネントは複数のインターフェースを継承できます。

 通常の C++ プログラムでは、以下のように、多重継承しているクラスの基底クラス同士のポインタを dynamic_cast で変換できます。

通常の C++ でのキャスト
class IExampleBase { /*...*/ };
class IExampleA: public IExampleBase { /*...*/ };
class IExampleB: public IExampleBase { /*...*/ };

// 2つの抽象基底クラスを多重継承しているクラス
class ComponentX : public IExampleA, public IExampleB { /*...*/ };

// ここで getComponentX() 関数は、
// ComponentX を構築して、それを IExampleA として返す関数とする。
IExampleA *p1 =  getComponentX();

// IExampleA のポインタを、別の IExampleB のポインタに変換する
IExampleB *p2 = dynamic_cast<IExampleB*>(p1);

 このような変換が可能なのは、このような継承関係を持ったクラスのオブジェクトがメモリ上に存在するとき、そのオブジェクトの構造がメモリ上でどのようにレイアウトされるかがコンパイラ自身によって分かっているためです。

 一方で VST-MA の元になった COM では、このような方法でインターフェース同士をキャストすることはできません。これは以下のような理由のためです。

  • C++ 以外の言語ではクラスの継承に相当する仕組みを C++ とは全く違う方法で実現しているかもしれない。
  • ホストアプリケーションとプラグインがどちらも C++ で実装されていたといても、それぞれをビルドしたコンパイラの種類やバージョン、あるいはビルド設定が違えば、オブジェクトのメモリ上でのレイアウトが異なる可能性がある。

 COM ではこの問題に対して独自の方法3でインターフェースをキャストできるようにしています。そして VST-MA でもそれを参考にした、 queryInterface() という関数を使う方法を提供しています。

 queryInterface() 関数は、以下のサンプルコードのように使用します。

VST-MA でのキャスト
class IExampleA : public FUnknown { /*...*/ };
class IExampleB : public FUnknown { /*...*/ };

// 2つのインターフェースを多重継承しているコンポーネント
class ComponentX : public IExampleA, public IExampleB { /*...*/ };

// ここで getComponentX() 関数は、
// ComponentX を構築して、それを IExampleA として返す関数とする。
IExampleA *p1 = getComponentX();

IExampleB *p2 = nullptr;

// IExampleA インターフェースから IExampleB のインターフェースのポインタを取得する。
// 処理が成功すると、 p2 に IExampleB インターフェースのポインタがセットされる。
// このとき p2 は一度 addRef() が呼ばれた状態になるので、
// 利用が完了したタイミングで release() を呼び出す必要がある。
tresult res = p1->queryInterface(
    IExampleB::iid, // どのインターフェースのポインタを受け取るかを表す IID
    (void**)(&p2)   // インターフェースのポインタを受け取るためのポインタ変数
    );
if(res != kResultOk) {
  // 失敗した場合は、エラーコードとして kResultOk 以外の値が返る。
  // この場合は p2 を利用してはいけない。

  // TODO: エラーハンドリング
  return;
}

// queryInterface() 関数が成功した場合は
// p2 に有効なポインタがセットされ、 kResultOk が返る。

// IExampleB インターフェースのポインタが取得できたので、それを利用する。
p2->doSomething();

// 利用が終わったら参照カウントを減らす。
p2->release();

 queryInterface() はすべてのインターフェースが継承している FUnknown に定義された関数であり、すべてのインターフェースからこの関数を利用できます。(またそのため、すべてのコンポーネントは、queryInterface() 関数を正しく実装する必要があります)

queryInterface() の関数宣言は以下のようになっています。

queryInterface() 関数の定義
virtual tresult queryInterface  (const TUID _iid, void ** obj) = 0; 

https://steinbergmedia.github.io/vst3_doc/base/classSteinberg_1_1FUnknown.html#a4199134d0669bfa92b7419dac14c01a7

 第一引数の _iid には、この関数を呼び出したインターフェースに対して、どのインターフェースのポインタを取得したいかを表す IID を渡します。

 第二引数の obj には、 _iid で指定したインターフェースのポインタ型の変数に対するポインタを渡します。関数が成功すると、このポインタ変数に、インターフェースのポインタがセットされます。

 queryInterface() 関数を呼び出したとき、もしインターフェースの実体であるコンポーネントが _iid で指定したインターフェースも実装している場合は、この関数は成功し、 obj にそのインターフェースのポインタをセットし、そのインターフェースに対して一度 addRef() 関数を呼び出します。そして、関数の成功を表す Steinberg::kResultOk を返します。
 もし _iid で指定したインタフェースを実装していない場合は、 この関数は失敗し、kNoInterface を返します。

コンポーネントを指定して構築するには createInstance() を使う

 ホストアプリケーション側からプラグイン側のコンポーネントを構築しようと思ったとき、ホストアプリケーションとプラグインはそれぞれは別のバイナリファイルになっているため、プラグイン内のコンポーネントをホストアプリケーション側から new して構築するようなことはできません。これは、ホストアプリケーションは、プラグイン側にあるコンポーネントのクラス定義を知らないためです。
 VST-MA ではこの問題に対して、コンポーネントの ID である CID を指定してコンポーネントを構築する方法を提供しています。

 以下のサンプルコードは、ホストアプリケーションが、プラグインに含まれる先頭のコンポーネントの情報を取得して、その情報に含まれる CID をもとにコンポーネントを構築する例です。

createInstance() の使い方
using namespace Steinberg;

// IPluginFactory をプラグインのモジュールから取得する。
IPluginFactory *factory = /*...*/;

PClassInfo ci;

// ここでは例として、 IPluginFactory から
// 先頭(0番目)のコンポーネントの情報を取得している。
// getClassInfo() 関数にコンポーネントのインデックスと PClassInfo のポインタを渡すことで
// そのポインタが指す PClassInfo オブジェクトに
// コンポーネントの名前や種別、 CID の情報などが書き込まれる。
factory->getClassInfo(0, &ci);

// IComponent は、プラグインの最も基本的な機能を定義しているインターフェース。
// 通常、ホストアプリケーションが VST3 プラグインを構築するときは、
// プラグインのコンポーネントを最初にこのインターフェースとして取得することになる。
IComponent* c = nullptr;

// getClassInfo() 関数で取得した情報をもとに、コンポーネントを構築する。
// その際、構築したコンポーネントは IComponent インターフェースとして受け取る。
// 関数が成功すると、 c に IComponent インターフェースのポインタがセットされる。
// このとき c は一度 addRef() が呼ばれた状態になるので、
// 利用が完了したタイミングで release() を呼び出す必要がある。
tresult ret = factory->createInstance(
    ci.cid,          // 構築するコンポーネントの CID
    IComponent::iid, // 構築したコンポーネントをどのインターフェースの型で受け取るかを表す IID
    (void**)&c       // インターフェースのポインタを受け取るためのポインタ変数
    );

if(ret != kResultOk) {  
  // もし指定した CID のコンポーネントがモジュールに含まれていなかったり、
  // 指定した IID のインターフェースをコンポーネントが実装していなかったり、
  // その他なんらかのエラーが発生したりしたときは、関数が失敗し、エラーコードが返る。
  // このときは、 c を利用してはいけない。

  // TODO: エラーハンドリング
  return;
}

// createInstance() 関数が成功した場合は
// c に有効なポインタがセットされ、 kResultOk が返る。

// IComponent インターフェースのポインタが取得できたので、それを利用する。
c->setIoMode(Vst::IoModes::kAdvanced);

// 使い終わったら参照カウントを減らして、構築したコンポーネントを破棄する。
c->release();

 createInstance() の関数宣言は以下のようになっています。

virtual tresult createInstance(FIDString cid, FIDString _iid, void **obj) = 0;

https://steinbergmedia.github.io/vst3_doc/base/classSteinberg_1_1IPluginFactory.html#a26a0c88f3703edf621060667eab3eb1f

 この関数の見た目は queryInterface() と似ていて、実際に使い方も似ています。大きな違いとして、引数列の先頭に cid という引数が追加されています。

 第一引数の cid には、構築したいコンポーネントを表す CID を渡します。
 第二引数の _iid には、構築したコンポーネントを、どのインターフェースとして受け取りたいかを表す IID を渡します。
 先に解説したとおり、ホストアプリケーションとプラグインの間でコンポーネント自体を受け渡すことはできないため、構築したコンポーネントは必ずインターフェースの形で受け渡しを行うことになります。コンポーネントは複数のインターフェースを継承していることがあるため、createInstance() の呼び出し元は、コンポーネントが継承しているであろう適当なインターフェースをここで指定することになります。
 第三引数の obj には、_iid で指定したインターフェースのポインタ型の変数に対するポインタを渡します。関数が成功すると、このポインタ変数に、インターフェースのポインタがセットされます。

 コンポーネントの構築が成功して、かつコンポーネントが _iid で指定したインターフェースを継承していた場合は、 createInstance() 関数は、そのインターフェースのポインタを obj にセットして、 kResultOk を返します。
 指定した CID のコンポーネントが見つからなかったり、コンポーネントの構築に失敗したり、コンポーネントが _iid で指定したインターフェースを継承していなかったりする場合は、それらのエラーに対応するエラーコードが返ります。

インターフェースのバージョニングと継承について

 ここからは、インターフェースに新しい機能を追加したくなった場合のバージョニングの仕組みや、それに伴うインターフェース同士の継承に関する仕様について解説します。

 この項目は、 VST-MA 利用者の作法というよりは、 VST-MA の仕様として VST3 SDK 開発者側が注意するべき内容になります。VST-MA の仕様としてそれなりに重要な部分ではあるのでここで解説しますが、興味がなければ読み飛ばしても特には問題になりません。

 ここで解説する内容は、VST3 SDK のドキュメント中の "Versioning and inheritance" をもとにしています。ただし、このドキュメントに記載されているバージョニングと継承に関する仕様と、実際に VST3 SDK が行っているインターフェースの定義の仕方には、若干の齟齬や説明不足な点があります。この点はこのセクションの最後で補足します。

バージョニングについて

 VST-MA のインターフェースの定義は、ホストアプリケーション/プラグインのどちらからも参照されます。もしこの定義が変更されることがあると、ホストアプリケーションとプラグインの両方が一斉に新しい定義に対応しなければ、それぞれが想定しているインターフェースの仕様が異なってしまい、両者の間でインターフェースを受け渡したときに予期せぬエラーが発生する可能性があります。

 そのため VST-MA では、一度公開されたインターフェースの定義は決して変更してはならないと定めています。
 もしインターフェースに機能を追加したい場合は、その機能を含んだ新しいバージョンのインターフェースを作成することになります。このとき、新しいインターフェースの名前は通常、元になるインターフェースの名前の末尾に順番に数字を増やしたものになります。 VST3 SDK の中に IComponentHandler2 や IPluginFactory3 のように名前の末尾に数字が付いたインターフェースが用意されているのは、このためです。

 新しいバージョンのインターフェースを定義するときには、以下の二種類の方法があります。

  1. 新しいインターフェースがもとのインターフェースを継承する方法
  2. 新しいインターフェースがもとのインターフェースを継承しない方法
    • 例: IComponentHandler2
    • この場合は、ソースコード上には明示的な継承関係が現れない。そのため、ドキュメントで [extends <もとのインターフェース名>] のように、それがどのインターフェースを拡張したものなのかを明示することになっている。

 どちらの方法でも実質的にはあまり違いはありませんが、コンポーネント側で新しいインターフェースに対応するために行う修正内容が少し異なります。

 サンプルコードで見ていきます。まず最初に IExampleA というインターフェースが定義されていて、そのインターフェースを実装した ComponentX というコンポーネントが存在したとします。(この話に関係のないメンバ関数の定義などは省略しています)

最初の状態
class IExampleA : public FUnknown
{
  virtual void doSomething() = 0;
};

class ComponentX : public IExampleA
{
  void doSomething() SMTG_OVERRIDE { /*...*/ }
};

 このインターフェースに対して、新しい機能を追加した IExampleA2 というインターフェースを定義することになったとします。

 このとき、 1. の IExampleA2IExampleA を継承する方法によって IExampleA2 を定義した場合は、コンポーネント側では以下のコードのように対応を行います。

新しいインターフェースがもとのインターフェースを継承する場合
class IExampleA : public FUnknown
{
  virtual void doSomething() = 0;
};

// IExampleA インターフェースの新しいバージョンを定義する。
// このとき、もとのインターフェースを継承するようにする。
class IExampleA2 : public IExampleA
{
  virtual void doAnotherThing() = 0;
};

// このとき、コンポーネント側で IExampleA2 に対応するには、
// 継承するインターフェースを IExampleA2 に変え、
// 新しく追加された仮想関数をオーバーライドする。
class ComponentX : public IExampleA2 // IExampleA に代わって IExampleA2 を継承する。
{
  void doSomething() SMTG_OVERRIDE { /*...*/ };
  void doAnotherThing() SMTG_OVERRIDE { /*...*/ };
};

 これに対して、 2. の IExampleA2IExampleA を継承しない方法によって IExampleA2 が定義された場合は、コンポーネント側では以下のコードのように対応を行います。

新しいインターフェースがもとのインターフェースを継承しない場合
class IExampleA : public FUnknown
{
  virtual void doSomething() = 0;
};

// IExampleA インターフェースの新しいバージョンを定義する。
// このとき、もとのインターフェースは継承せず、
// ドキュメント上で IExampleA2 は IExampleA の新しいバージョンだと記載するのみ。
class IExampleA2
{
  virtual void doAnotherThing() = 0;
};

// このとき、コンポーネント側で IExampleA2 に対応するには、
// 基底クラスに IExampleA2 を追加し、
// 新しく追加された仮想関数をオーバーライドする。
class ComponentX : public IExampleA
                 , public IExampleA2 // IExampleA に加えて IExampleA2 を継承する。
{
  void doSomething() SMTG_OVERRIDE { /*...*/ };
  void doAnotherThing() SMTG_OVERRIDE { /*...*/ };
};

 注意すべき点として、 2. の方法で IExampleA2 が定義されている場合は、 IExampleA を継承せずに IExampleA2 のみを継承したコンポーネントも定義できてしまいます。しかし、 IExampleA2 はあくまで IExampleA の機能を拡張したものという扱いのため、当然 IExampleA2 の利用者はそのインターフェースが IExampleA としても使えることを想定しています。
 そのため、 IExampleA2 のみを継承して IExampleA を継承しないようなコンポーネントは定義するべきではありません。

 上記でインターフェースの新しいバージョンを定義するための二種類の方法を紹介しましたが、この違いはインターフェースの利用者にとってはほとんど影響ありません。どちらの方法が使われている場合でも、 queryInterface() 関数を使って、 IExampleA のポインタと IExampleA2 のポインタを相互に取得できます。

インターフェース同士の継承について

 上のセクションで、インターフェースは新しいバージョンを定義するときに、もとのインターフェースを継承することがあると書きました。(継承しないこともあります)

 VST-MA では、それ以外の目的、例えばあるインターフェースを特殊化する目的で、インターフェースの継承をするべきではないとしています。

 例えば、図形を表すインターフェース IShape とそれを特殊化した直方体を表すインターフェース IRect を定義する場合、以下のように継承を用いるのではなく、

推奨されない方法
class IShape : public FUnknown
{
  virtual void setPosition(long x, long y) = 0;
};

class IRect : public IShape
{
  virtual void setDimension(long width, long height) = 0;
};

以下のように、独立したインターフェースとして定義するべきとしています。4

推奨される方法
class IShape : public FUnknown
{
  virtual void setPosition(long x, long y) = 0;
};

class IRect : public FUnknown
{
  virtual void setDimension(long width, long height) = 0;
};

 これは、インターフェースの新しいバージョンを定義するときに、インターフェースの継承関係があると都合悪いケース5があったり、インターフェースの継承関係が複雑になってしまうことがあるためです。6

補足

 さて、このセクションの冒頭で、VST3 SDK のドキュメントに記載してあるバージョニングと継承に関する仕様と、実際に VST3 SDK 内でのインターフェースの定義のされ方には、若干の齟齬や説明不足な点があると書きました。ここではそれについて補足します。

バージョニング目的以外で継承が使用されている例について

ドキュメントでは、インターフェースの継承はバージョニング目的でのみ行うべきだと書いてありますが、 VST3 SDK の中で重要な役割を果たす IComponentインターフェースと IEditController インターフェースはこの仕様を満たしていません。これらのインターフェースはともに IPluginBase インターフェースを継承していて、そのどちらも IPluginBase の新しいバージョンというものではないため、ドキュメント中の仕様と齟齬がある状態になっています。
 これについては、おそらく歴史的な事情があるのだろうと思うので、そういうものなのだと割り切って考えたほうが良さそうです。

バージョニングするときに必ずしも継承をしないことについて

 VST3 SDK の "Versioning and inheritance" では、インターフェースの継承を行うのはバージョニング目的に限ると書いていますが、逆にバージョニングの際に必ずもとのインターフェースを継承をするかどうかについては触れていません。

 実際、上に例を出した IComponentHandler2 というインターフェースは、 IComponentHandler というインターフェースの新しいバージョンとして導入されましたが、 IComponentHandler は継承せずに、独立したインターフェースとして定義されています。

 もちろんそのように、新しいバージョンのインターフェースをもとのインターフェースと独立した形で定義してもなにも問題はないのですが、ドキュメント中には新しいバージョンのインターフェースをそのように定義することについて何も触れられていないため、説明不足な感があります。

おわりに

 今回は、 VST-MA を利用するプログラムを書く際の作法について紹介しました。これだけ理解していれば、あとは自力でも VST3 SDK のドキュメントとソースコードを読んでいくことができると思います。7

 今回までで VST3 プラグインの基本的な仕組みや、設計のベースにある VST-MA の解説が終わったので、次回は実際に VST3 プラグインのモジュールをホストアプリケーションからロードしてプラグインを構築する流れを解説する予定です。


  1. https://ja.wikipedia.org/wiki/Component_Object_Model 

  2. この記事では、VST-MA によってすでに定義されたインターフェースやコンポーネントを使う方法を解説します。 VST-MA に用意された DEFINE_INTERFACEIMPLEMENT_REFCOUNT などのマクロを使いながら独自のコンポーネントを定義するための方法については触れません。これについて知りたい場合は VST-MA のドキュメントや、 VST3 SDK 中のコンポーネントのクラス定義などを参考にしてください。 

  3. https://docs.microsoft.com/ja-jp/cpp/atl/queryinterface?view=vs-2019 

  4. このサンプルコードは、 https://steinbergmedia.github.io/vst3_doc/base/versionInheritance.html を改変したものです。 

  5. あるインターフェースの新しいバージョンを定義するとき、もとのインターフェースに継承関係があると、それらのインターフェースを継承したコンポーネントを実装する際に、同じインターフェースが複数個、基底クラスとして現れてしまうことがあります。このとき、うまく queryInterface() 関数を実装しなければ、複数個あるうちのどのインターフェースのポインタを返せばいいのかが曖昧になって、コンパイルエラーが発生します。 

  6. VST-MA のもとになった COM でも、インターフェース同士の継承は推奨されていません。参考: https://docs.microsoft.com/en-us/windows/win32/com/iunknown-and-interface-inheritance 

  7. VST3 SDK のドキュメントは必要な解説が足りていないことが多々あって、さまざまな箇所を自分で推測しながら読む必要があるので大変なことも多いですが・・・。 

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2