git rebase -iの編集画面で、コミットごとのdiffを表示するvimプラグイン書いた

git rebase -iの編集画面で、コミットごとのdiffを表示するvimプラグイン書いた。

動機

僕はコードを書いている時に、gitのコミットを割りと雑に作成して、あとでgit rebase -iを実行して履歴を修正することが多い。

コミット履歴を編集するためのgit-rebase-todoファイルにはコミットハッシュとコミットログが表示される。 しかし、コミット履歴を変更しようとするとき、ほとんどの場合コミットログだけでは不十分で、具体的にソースにどんな変更を加えたかを確認したくなる。

gitのエディターにvimを指定しているので、git rebase -i時にvim上でgit-rebase-todoファイルが開かれるが、コミットごとのdiffはGitXのような別のアプリケーションに移動して確認していた。

これはいちいちウィンドウを移動することになって面倒なので、どうせならvim上でdiffも確認できるようになれば嬉しいと思って、そのような機能を持つプラグインを作成した。

作成したvimプラグイン

hotwatermorning/auto-git-diff

このプラグインは、rebase時のコミット履歴編集画面で、テキストカーソルがある行のコミットハッシュを認識して、Previewウィンドウ内にそのコミットとそれのひとつ前のコミットのdiffを表示する。

テキストカーソルが動くと、その移動を検知して、新しい行のコミットハッシュを認識して、自動的にdiffを更新する。

これでコミットごとの変更が簡単に確認できるようになったので、rebase作業がだいぶ快適になった。

感想

初めてちゃんとしたvimプラグイン作って公開した。いままでvimrcを自分用にカスタマイズする程度だったが、自分でプラグインを作ってみて、vimスクリプトプラグインの仕様などいろいろ勉強になった。

謝辞

いろんなvimプラグイン作者のブログとか参考にさせてもらいました。 特にid:rhysdさんのcommittia.vimは、ソースとかReadmeの書き方とかかなり参考にさせてもらいました。多謝〜〜〜。 (committia.vimがrebaseのrewordコマンドの時も使えるようになったら、rebaseの編集時に加えてrebase適用時にもdiffが見れるようになってrebase作業がさらに幸せになれそうな気がしてるので、対応したらいいなー。もしくは僕もなにかコントリビュートできたらいいなー。とか思ってます。)

git-guiのタブ幅を変更する

git-guiのタブ幅を変更する

git-guiのタブ幅はデフォルトで空白8文字分になっているが、これを空白4文字分にする。

C:\Program Files (x86)\Git\share\git-gui\lib\diff.tclclear_diff{} プロシージャに次の2行を追加する。

    set w [font measure font_diff "0"]
    $ui_diff configure -tabs [expr {4 * $w}]

ちなみに、まだ導入されていないが、この機能をgit-guiの設定項目に加えるという話も出ている。 http://git.661346.n2.nabble.com/PATCH-git-gui-add-configurable-tab-size-to-the-diff-view-git-gui-why-not-added-to-git-gui-td7618876.html

2014年総括

2014年総括

2014年お疲れさまでした。

今年は、例年よりも多くの人に関わる一年でした。 勉強会やイベントに例年より多く参加したり、知らない地へ旅行することもありつつ、過去からのつながりも感じたりして、盛りだくさんの年になりました。

主催/共催した勉強会やイベントは、

発表や出展した勉強会やイベント

その他参加した勉強会やイベントは

Sapporo.cppのコミュニティ活動以外では、大学のDTMサークルのOBで集まって、M3という同人音楽の即売会に参加しました。

抜けがあるかもしれませんがこんな感じで。

今年はこれらのイベントに参加して、そこからのつながりがまた別のイベントへの参加につながったりして、とても多くのことを体験できました。

最初のGlobal Game Jamでは、当初はプログラマとして登録しつつも実はサウンド担当にも興味があったところで、チームで作成するゲームの方向性がちょうど自分の作れる音楽の雰囲気に合っていそうだったので幾つかプロトタイプを作ってみたところ、最終的にサウンドを担当することになりました。 なんらかのプロダクトのための使われる音楽を作った経験は初めてだったので、新鮮で面白かったです。

C++テンプレートテクニック 2nd打ち上げにも参加できてよかったです。いつもお世話になっているアキラさん(id:faith_and_brave)とεπιστημηさんに久しぶりにお会いして、他のレビューアーさんや推薦文の寄稿者ともお話できてよかったです。

Boost.勉強会は2年半ぶりの開催でしたが、多くに人に(道外からもたくさん)参加していただいて、とても嬉しかったです。 会場のネットワークがうまく準備できなかったりして、残念なところもありましたが、久しぶりにお会いできた人や、この勉強会で初めて会えた人もいて、開催できてよかったです。

オープンソースカンファレンス 2014 Hokkaidoでは例年通りチロルチョコを配布しました。勉強会で知り合った人に協力をお願いして一緒に出展できたり、普段から仲良くさせてもらってるほかのコミュニティに挨拶できてよかったです。 ここの懇親会でOhotechの人からお話をいただいたことがきっかけで、8月にOhotech 特盛へ参加することになりました。

プログラミング生放送+CLR/H+Sapporo.cpp 勉強会@札幌は、 CLR/Hとプロ生勉強会との共催でした。以前にも3つのコミュニティと合同で勉強したことがあって、今回もプロ生勉強会からお話をいただいて、再び合同勉強会が実現しました。CLR/Hさんはいつもお世話になってるコミュニティなので、この勉強会でもいろいろお話ができてたのしかったです。プロ生勉強会のブログではプロ生ちゃんにSapporo.cppのコミュニティを紹介していただきました。

Code 2014にも参加しました。Codeというイベントは以前から参加の興味はあったのですが、これまではタイミングが悪くて参加できていませんでした。今年はちゃんと参加して、セッションを聞いたり、自分でもCode Golfを書いたり、他の参加者と麻雀したりいろいろと遊べてとても楽しかったです。

8月にはid:USAGI-WRPさんが札幌に引っ越してきたのでその歓迎会を行いました。USAGIさんは今、Sapporo.cppや大人の放課後シリーズの勉強会で一緒に活動しています。

Boost.Beerでは、道外からid:redboltzさんが遊びに来て、札幌のC++erと一緒にビアガーデンで飲んで話して盛り上がりました。翌日からはredboltzさんと、@ さんと稚内、利尻、礼文へ遊びに行きました。

札幌に住んでいながら、これまでの人生では北海道は全然見てまわったことがなかったため、とてもとても楽しかったです。特に礼文島の景色は絶景でした。8月なのに上着を羽織ってもすこし肌寒く、さらに曇りがちでしたが、その天候すら、高い木のない荒涼とした景色を引き立てているようでした。

Ohotech 特盛 #10の参加はオープンソースカンファレンス 2014の懇親会でOhotechの人にお話をいただいたことがきっかけで実現しました。ちょうどSapporo.cppの他のメンバーでうまくタイミングを調整できて、みんなで北見まで勉強会参加ができました。僕はゲームプログラミングと絡めてマルチスレッドプログラムの発表を行いました。北見工大のゲーム制作サークルが作ったプログラムで遊べたり、ゲームプログラムの話が聞けたりして面白かったです。

Ohotech 特盛のあとは、観光をして帰りました。屈斜路湖摩周湖帯広豚丼。どれも素晴らしいものでした。特に屈斜路湖の景色は感動でした。遥か彼方まで見渡せる丘から見下ろしたカルデラに湖中島が映る姿は、この景色を見れてよかったと感じさせるものがありました。

Sapporo.cppでも勉強会の #6, #7, #8を開催できて、一年の締めくくりとして忘年会も開催できました。
僕自身は他に、大人の放課後シリーズやHokkaido.pmやSapporo.vimにも参加できました。サークルの友達で曲を作りあってM3にも参加できました。

今年一年のコミュニティ活動で多くの人と仲良くさせてもらったり、また遠くへ繰り出したりできて、Sapporo.cppの@さん、@さん、@さんを始め、多くの人にお世話になりました。ありがとうございます。

来年もよろしくお願いいたします。

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?を参照

Boost.勉強会 #15 札幌を開催しました

Boost.勉強会 #15 札幌を開催しました

Boost.勉強会 #15 札幌について

Boost.勉強会 #15 札幌を開催しました。 多くのご参加ありがとうございました。 運営に携わっていただいた方々もありがとうございました、お疲れさまでした。

前回のBoost.勉強会 #6 札幌から2年半ぶりの開催となりまして、札幌市内のみならず、市外、道外からも多くの方に参加していただけてとても嬉しかったです。
登壇者の発表内容もさまざまで面白かったです。

当日は、利用した北大施設のネットワーク環境が不調で、ちゃんとしたネットワークを提供できなかったのが残念です。
ちなみに、動画配信に際してUstreamではなくYoutube Liveというサービスを利用したのはこれが関係していたりします。

今回の勉強会で初めてお会いした人や、久しぶりに会えた人もいて、いろいろと話ができて良かったです。
とはいえ、いつも勉強会と懇親会の後に「あの人とももっと話しておきたかった」とか思うので、また勉強会とかイベントを主催したり参加したりして、お話する機会ができるといいなと思います。

f:id:heisseswasser:20140524210558j:plain

C++のポーズ!」

自分の発表の補足と反省点

今回僕はbalorというライブラリを紹介しました。 発表資料はこちら

balor(読み方がわからないので、僕はバロールと読んでいます)は、syanjiという人が作ったC++ Windows GUIライブラリです。

サイト紹介されている通り、

  • WIN32 APISTL と連携しやすい。
  • 無名関数(ラムダ式)によるイベント記述。
  • 右辺値参照サポートによってインスタンス作成にヒープメモリ割り当てを強制しない。

などの特徴があります。

これらよって、MFCよりも簡単にコードを書くことができ、SIGNAL/SLOTの仕組みをもつQtよりも純粋にC++の世界でコードを書けるものになっています。

balorは32bitプラットフォームでのビルドをサポートしていますが、残念ながら64bit版をサポートしないまま開発が停止してしまっています。
そのためこのライブラリを広く使っていくことはためらわれますが、簡単にWindows 32bit GUIアプリケーションを書くには非常に便利だと感じています。

そして、これが一番伝えたかったことなのですが、
balorは右辺値参照やラムダ式などモダンなC++の書き方でGUIプログラミングができるものであり、MFCやQtにはない特徴を持っていて面白いライブラリです。 (ただしラムダ式によるイベントハンドラの設定はQtでもQt5から可能)
ここらへんもうちょっと時間を割いて紹介できればよかったとちょっと後悔しています。

僕はいままでC++GUIライブラリとしてMFCを使って来て、Qtは少し触ってみた程度でしたが、初めてbalorに触った時、C++のモダンな考え方が取り入れられたこのライブラリによるGUIプログラミングを非常に楽しく感じました。

balorは開発が停止してしまっていたり32bitしかサポートしていないなどの欠点のため、利用できるシーンは限られますが、 直接ライブラリを利用しないとしても、他のGUIライブラリにはない上記の特徴は自分でGUIライブラリを書いたりするときに参考にできる部分もあると思います。 江添さんのブログでは使うべきではないとまで断言されてしまってますが、僕としては使う使わないはともかく面白いライブラリですよという点を伝えたかったので、そこがうまくできなかったのが残念です。

そんなわけで勉強会で紹介してみた次第でした。

(ところで、balorの64bit対応はそんなに難しいことではないかもしれなくて、balorのVisual Studioプロジェクト構成でプラットフォーム設定を64bit版にしてビルドして、出てきたコンパイルエラー数個を修正するだけで、今回用意したサンプルアプリケーションを64bit版でビルドできてしまいました。この辺りは今後別の記事で書くかも)

第2回 Sapporo.vim(の懇親会に)参加しました

第2回 Sapporo.vimを寝坊しました。

Boost.勉強会のあとカラオケオールして、目覚ましかけて仮眠したらバッチリ寝過ごしてしまいました。

起きて準備して会場のインフィニットループ社に着いたらもう終了の時間だったという・・・。
でも懇親会へは参加して、Vimの話とか、例外安全性の話とかして、数学の話とか聞かせてもらって、みんなで@thincaさんにプレッシャーかけたりして、めっちゃ楽しかったので大満足でした!

quickrunめっちょ便利

C++テンプレートテクニック第2版をいただきました!

C++テンプレートテクニック第2版をいただきました!

f:id:heisseswasser:20140415004104p:plain

id:faith_and_braveさんとεπιστημηさんが執筆されたC++テンプレートテクニックの第2版をいただきました。ありがとうございます!

この第2版では、微力ながら僕も原稿のレビューと本書推薦文という形で執筆のお手伝いをさせていただきました。

本書の紹介

テンプレートという仕組みは、C++の言語機能の中でも特に難解だと思われる方も多いかもしれませんが、現にC++標準ライブラリや、C++の準標準ライブラリとも呼ばれるBoostでも広く使われている実践的な機能です。

本書ではテンプレートの仕組みや、テンプレートによって可能になるさまざまなプログラミングの手法が、わかりやすい解説とサンプルコードで紹介されています。

本書について、第1版のときから、テンプレートを学ぶ人達にぜひこの本を手にとって欲しいと思うほどの良書と感じていましたが、以前にもまして経験と研鑽を積んだ著者の力でさらに充実した第2版は、ますます面白さと実用性を兼ね備えた一冊へと進化しているように感じます。

また第2版に向けて、C++11から可能になったテクニックやコンセプトに基づく設計手法の紹介などが追加されたため、すでに第1版を読んだ方でも新たな知見が得られるものと思います。

発売は4/17の木曜日から!C++を書くすべての人にオススメです!!

VSTホストを作ろう!

VSTホストを作ろう!

@hotwatermorningです。

この記事は、C++ Advent Calendar 2013の5日目の参加記事です。

はじめに

今回は、VSTiという種類のプラグインを読み込んで音を鳴らす、「VSTホスト」の作り方について紹介します。

VSTホストの画面を作るにあたり、「balor」というC++Windows GUIライブラリを使用しました。balorについてはリンク先をご参照ください。

この記事の内容について、筆者がVST規格について勘違いしている点があるかもしれません。
ご了承ください。

VSTとは

VSTとは簡単に言うと音楽製作ソフトのプラグイン周りの規格です。Wikipediaでは、以下のように解説されています。

Steinberg's Virtual Studio Technology(一般的にはVST)とは、ソフトウェア・シンセサイザーエフェクタープラグインと波形編集ソフトウェアやデジタル・オーディオ・ワークステーション(DAW)間のリアルタイムなデータ受け渡しを担い各種の加工などを施すプログラムを、プラグインとして提供するための標準的な規格の一つである。

http://ja.wikipedia.org/wiki/Virtual_Studio_Technology

余談ですが、Steinbergというのは、この規格を開発したドイツのSteinberg社のことで、音楽製作ソフト「Cubase」の開発で有名です。Steinbergプラグインの規格であるVSTの他に、ASIOというオーディオドライバの規格も開発しています。

このVSTという規格に則って作成されたプラグインは、「VSTプラグイン」と呼ばれ、同じくVSTの規格に則って作成されたホストアプリケーション「VSTホスト」上にロードされ、音楽製作に利用されます。

VSTプラグインは業界のデファクト・スタンダードとも言える規格ですので、自分でVSTプラグインを開発すれば、多くの音楽製作ソフト上で自分のVSTプラグインを使用できます。

VSTには3つのプラグインのタイプがあります。

  • VSTエフェクト(狭義のVST) : オーディオ信号を加工する
  • VSTi : MIDI信号を受けてオーディオ信号を生成する
  • VST MIDIエフェクト: MIDI信号を加工する

今回はこの内、VSTiを読み込むVSTホストを作成します。

現在VSTには2.x系列と3.x系列があり、先日、2.x系列の開発終了が発表されました。 http://www.steinberg.net/en/newsandevents/news/newsdetail/archive/2013/09/03/article/vst-2-development-ends-2504.html 今年中にもVST 2.x系列のSDKSteinbergのサイトから消えてしまうそうです。

VST 3.x系列は現在も開発が続いており、つい先日新たなバージョン VST 3.6が発表されました。 http://steinberg.net/en/newsandevents/news/newsdetail/article/cubasis-17-vst-36-and-free-nanologue-2607.html

VST 3.x系列が旬なネタですが、まだまだ筆者の知識が足りないため、今回作成するホストはVST 2.4対応のものとなります。

VST開発の情報

VSTの開発には、開発元であるSteinberg社が公開しているSDKを使用します。SDKのダウンロードには、Steinbergの開発者登録が必須となります。 http://www.steinberg.net/en/company/developer.html

今回はVST 2.4に対応したホストを作成するので、VST 2.4のSDKを使用します。

ネットに転がっているVSTの開発の情報は、VSTプラグイン側のものが大半です。日本語でも、VSTプラグインの作成方法についてまとめているサイトがいくつかありますので、自分でVSTプラグインを作成したいという時に参考になるでしょう。

一方、VSTのホスト側の実装にはあまりまとまった資料がありませんが、一つ有名なものとして「VSTHost」があります。 http://www.hermannseib.com/english/vsthost.htm

VSTHostは以前はオープンソースで公開されていましたが、作者いわく、そのコードが他人のプログラム内に勝手に使われているのを発見したため、オープンソース版の開発は凍結したとのことです。

とはいえ、公開されている最終版V1.16qでもVST 2.4をサポートしているので、VST 2.4までのプラグインをホストするアプリケーションを開発したい場合には非常に有用な情報といえます。

他に、VSTの仕様について有用な情報があるサイトとして、 http://www.asseca.org/index.htmlがあります。こちらは、VSTホストを実装する上で重要になるopcodeという定数について、ドキュメントに明記されていない捕捉が書いてあることがあります。

作成したVSTホストアプリケーションについて

実行バイナリ

今回作成したVSTホストのデモアプリケーションは以下に公開しています。
https://www.dropbox.com/s/cvdgwnsxa8ol3f5/VstHostDemo.exe

このアプリケーションは、Windows 7以降で動作します。Visual StudioのビルドターゲットをXP対応にしてビルドすればXPでも動くバイナリが作成できると思われます。 また32bitアプリケーションですので、64bitバイナリのVSTiは読み込めません。

このアプリケーションの使用は自己責任でお願いいたします。

ソースコード

ソースコードはこちらにあります。Visual Studio 2012のソリューションとなっています。
https://github.com/hotwatermorning/VstHostDemo

ビルドに必要な外部ライブラリは次の3つです。

  • Boost 1.53.0 (試してはいないですが、1.54.0以降でも問題ないかもしれません。)
  • Balor 1.0.1
  • VST SDK 2.4

プロジェクトの設定でBoostおよびbalorのインクルードディレクトリとライブラリディレクトリを設定します。Boostもbalorもオートリンク的な仕組みがありますので、それぞれでどのライブラリとリンクしなければならないかを設定する必要はありません。

VSTSDKの配置については、SDKを解凍した時に現れるvstsdk2.4というディレクトリの中身を、そのままソリューションのディレクトリのvstsdk2.4の中に移動していただければ大丈夫です。

他のコンパイラであってもC++11に対応していれば、上記の設定を行うことで、ビルドができるかもしれません。

プログラムの動作

プログラムのデモ動画を作成しました。


VstHostDemo

このアプリケーションを起動すると、ファイルダイアログが開くので、ロードしたいVST 2.x系列のVSTiを選択して開きます。(あるVSTプラグインが2.x系と3.x系のどちらの系列のものかは、ファイルの拡張子で判別可能です。VST 2.x系列のプラグインの場合、Windows環境ならば拡張子.dllMac環境ならば.vstです。VST 3.x系列からは拡張子.vst3に統一されました。)

VSTiを持っていないという人は、SUPERWAVEのP8辺りが試してみるのに良いと思われます。 VSTiがロードできると、鍵盤の付いたメインウィンドウと、プラグインのエディターウィンドウが開きます。(プラグインがエディターウィンドウを持っていない場合はプラグインウィンドウは開きません。)

メインウィンドウの鍵盤をクリックすると、その鍵盤を押した情報がVSTiへ送られ、VSTiで生成された音がPCのWaveformオーディオデバイスから出力されます。ドロップダウンリストで、VSTiのプログラム(パラメータのプリセットのこと)を変更しながら、いろいろな音色を楽しんでみましょう。

ソースコード解説

プログラムはSource.cppの下部のWinMainから開始しますが、主要な処理は、そこから呼んでいるhwm::main_impl関数になります。 (この後に紹介するコード片はすべてhwm名前空間内に定義してあります。)

ソースを追いながら解説していくほうが分かりやすいと思うので、ソースとコメントを中心に解説していきます。

0. プログラムの構造について

このプログラムの構造は以下のようになります。

f:id:heisseswasser:20131205023149p:plain

1. メインの処理

それではソースを見ていきましょう。Source.cppの50行目からメインの処理が始まります。

変数定義などを飛ばして少し進むと、DLLのパスを選択するダイアログを表示している部分があります。

    //! ロードするVSTi選択
    gui::OpenFileDialog  file_dialog;
    file_dialog.pathMustExist(true);
    file_dialog.filter(_T("VSTi DLL(*.dll)\n*.dll\nAll Files(*.*)\n*.*\n\n"));
    file_dialog.title(_T("Select a VSTi DLL"));
    bool selected = file_dialog.show(frame);
    if(!selected) { return 0; }

これはbalorのライブラリのクラスで、WindowsGetOpenFileName関数をラップしたものです。

ここでDLLが選択されると次はHostApplicationクラスとVstPluginクラスの初期化です。

    //! VSTプラグインと、ロードしているVSTホストの間でデータをやりとりするクラス
    HostApplication     hostapp(SAMPLING_RATE, BLOCK_SIZE);

    //! VstPluginクラス
    //! VSTプラグインのCインターフェースであるAEffectを保持して、ラップしている
    VstPlugin           vsti(file_dialog.filePath(), SAMPLING_RATE, BLOCK_SIZE, &hostapp);

    if(!vsti.IsSynth()) {
        gui::MessageBox::show(
            frame.handle(),
            _T("This plugin [") + 
            io::File(file_dialog.filePath()).name() +
            _T("] is an Audio Effect. VST Instrument is expected.")
            );
        return 0;
    }

このアプリケーションでは、VSTiであるVSTプラグインのみをターゲットにしているので、VSTiであることを表すIsSynth()falseの場合は関数を抜けています。

次はデバイスです。
WaveOutProcessorというクラスでWaveformオーディオデバイスをオープンしています。このクラスはソースファイルのWaveOutProcessor.hppで定義されています。

OpenDevice関数の引数に渡したラムダ式で、WaveOutProcessorの再生バッファに空きがあるときに呼ばれるコールバック関数の処理を記述しています。

    //! Wave出力クラス
    //! WindowsのWaveオーディオデバイスをオープンして、オーディオの再生を行う。
    WaveOutProcessor    wave_out_;

    //! デバイスオープン
    bool const open_device =
        wave_out_.OpenDevice(
            SAMPLING_RATE, 
            2,  //2ch
            BLOCK_SIZE,             // バッファサイズ。再生が途切れる時はこの値を増やす。ただしレイテンシは大きくなる。
            BUFFER_MULTIPLICITY,    // バッファ多重度。再生が途切れる時はこの値を増やす。ただしレイテンシは大きくなる。

            //! WaveOutProcessorの再生バッファに空きがあるときに呼ばれるコールバック関数。
            //! このアプリケーションでは、一つのVstPluginに対して合成処理を行い、合成したオーディオデータを再生バッファへ書き込んでいる。
            [&] (short *data, size_t device_channel, size_t sample) {

                auto lock = get_process_lock();

                //! VstPluginに追加したノートイベントを
                //! 再生用データとして実際のプラグイン内部に渡す
                vsti.ProcessEvents();
                
                //! sample分の時間のオーディオデータ合成
                float **syntheized = vsti.ProcessAudio(sample);

                size_t const channels_to_be_played = 
                    std::min<size_t>(device_channel, vsti.GetEffect()->numOutputs);

                //! 合成したデータをオーディオデバイスのチャンネル数以内のデータ領域に書き出し。
                //! デバイスのサンプルタイプを16bit整数で開いているので、
                //! VST側の-1.0 .. 1.0のオーディオデータを-32768 .. 32767に変換している。
                //! また、VST側で合成したデータはチャンネルごとに列が分かれているので、
                //! Waveformオーディオデバイスに流す前にインターリーブする。
                for(size_t ch = 0; ch < channels_to_be_played; ++ch) {
                    for(size_t fr = 0; fr < sample; ++fr) {
                        double const sample = syntheized[ch][fr] * 32768.0;
                        data[fr * device_channel + ch] =
                            static_cast<short>(
                                std::max<double>(-32768.0, std::min<double>(sample, 32767.0))
                                );
                    }
                }
            }
        );

    if(open_device == false) {
        return -1;
    }

デバイスのオープン処理あとは、メインウィンドウであるframeに対して、描画やマウス系のイベントハンドラを設定しています。

    //! 以下のマウスイベント系のハンドラは、鍵盤部分をクリックした時の処理など。
    //! 
    //! MIDI規格では、"鍵盤が押された"という演奏情報を「ノートオン」
    //! "鍵盤が離された"という演奏情報を「ノートオフ」というMIDIメッセージとして定義している。
    //! これらのほか、音程を変化させる「ピッチベンド」や、楽器の設定を変え、音色を変化さたりする「コントロールチェンジ」など、
    //! さまざまなMIDIメッセージを適切なタイミングでVSTプラグインに送ることで、VSTプラグインを自由に演奏できる。
    //! 
    //! このアプリケーションでは、画面上に描画した鍵盤がクリックされた時に
    //! VstPluginにノートオンを送り、鍵盤上からクリックが離れた時に、ノートオフを送っている。

    ((中略))

    frame.onMouseDown() = [&] (gui::Frame::MouseDown &e) {
        BOOST_ASSERT(!sent_note);

        if(!e.lButton() || e.ctrl() || e.shift()) {
            return;
        }

        auto note_number = getNoteNumber(e.position());
        if(!note_number) {
            return;
        }

        e.sender().captured(true);
        
        //! プラグインにノートオンを設定
        vsti.AddNoteOn(note_number.get());
        sent_note = note_number;
    };

ハンドラの登録が終わったら frameに対して、プラグイン名やプログラムリストを表示するコントロールを追加しています。

    //! プラグイン名の描画
    gui::Panel plugin_name(frame, 10, 10, 125, 27);
    plugin_name.onPaint() = [&font, eff_name] (gui::Panel::Paint &e) {
        e.graphics().font(font);
        e.graphics().backTransparent(true);
        e.graphics().drawText(eff_name, e.sender().clientRectangle());
    };

    //! プログラムリストの設置
    gui::Panel program_list_label(frame, 10, 80, 75, 18);
    program_list_label.onPaint() = [&font_small] (gui::Panel::Paint &e) {
        e.graphics().font(font_small);
        e.graphics().backTransparent(true);
        e.graphics().drawText(_T("Program List"), e.sender().clientRectangle());
    };

    std::vector<std::wstring> program_names(vsti.GetNumPrograms());
    for(size_t i = 0; i < vsti.GetNumPrograms(); ++i) {
        program_names[i] = balor::locale::Charset(932, true).decode(vsti.GetProgramName(i));
    }

    gui::ComboBox program_list(frame, 10, 100, 200, 20, program_names, gui::ComboBox::Style::dropDownList);
    program_list.list().font(font_small);
    program_list.onSelect() = [&] (gui::ComboBox::Select &e) {
        int const selected = e.sender().selectedIndex();
        if(selected != -1) {
            auto lock = get_process_lock();
            vsti.SetProgram(selected);
        }
    };

その後、プラグインが専用のエディタウィンドウを持っている場合はそれを表示しています。

    //! エディタウィンドウ
    gui::Frame editor;
    {
        //! ロードしているVSTプラグイン自身がエディタウィンドウを持っている場合のみ。
        if(vsti.HasEditor()) {
            editor = gui::Frame(eff_name, 400, 300, gui::Frame::Style::singleLine);
            editor.icon(gpx::Icon::windowsLogo());
            
            //メインウィンドウの下に表示
            editor.position(frame.position() + balor::Point(0, frame.size().height));
            editor.owner(&frame);
            editor.maximizeButton(false);   //! エディタウィンドウのサイズ変更不可
            //! エディタウィンドウは消さないで最小化するのみ
            editor.onClosing() = [] (gui::Frame::Closing &e) {
                e.cancel(true);
                e.sender().minimized(true);
            };
            vsti.OpenEditor(editor);
        }
    }

ここまででメインの処理は完了したので、メッセージループを回してGUIの処理を開始します。

    //! メッセージループ
    //! frameを閉じると抜ける
    frame.runMessageLoop();

frameが閉じられるとメッセージループを抜けてくるので、終了処理を行い、main_implから脱出します。

    //! 終了処理
    vsti.CloseEditor();
    wave_out_.CloseDevice();

    return 0;

ここまでで、メインの処理は終わりです。

2. VSTプラグインの取り扱い

続いて、VSTプラグインをホストする際にもっとも重要となるVstPluginクラスの実装を見てみます。

VstPluginは、VSTプラグインのCインターフェースであるAEffectオブジェクトをラップし、VSTプラグインの初期化/終了処理を行ったり、VSTホストとなるアプリケーションからVSTプラグイン本体へのアクセスを簡単にするクラスです。

以下がコンストラクタ定義です。

    VstPlugin(
        balor::String module_path,
        size_t sampling_rate,
        size_t block_size,
        HostApplication *hostapp )
        :   module_(module_path.c_str())
        ,   hostapp_(hostapp)
        ,   is_editor_opened_(false)
        ,   events_(0)
    {
        if(!module_) { throw std::runtime_error("module not found"); }
        initialize(sampling_rate, block_size);
        directory_ = balor::locale::Charset(932, true).encode(module_.directory());
    }

DLLのパスを引数で受け取り、balor::system::Moduleクラスのオブジェクトmodule_にてDLLをロードします。ロードが成功したらVSTプラグインの初期化を行います。

VstPlugin内部で、DLLを保持しているのは次の理由からです。構築されたVSTプラグインは、最終的にeffCloseという通知コードをVSTホストから受けて自身を破棄するのですが、それより先にDLLがアンロードされてしまうと予期せぬエラーの原因となります。そのため、VstPluginクラスの中にbalor::system::Moduleクラスを保持して、VstPluginクラスのオブジェクトが生存している間はDLLがアンロードされないようにしています。

続きまして、VSTプラグインの初期化処理です。

初期化処理はまず、ロードしたモジュールからVSTプラグイン本体を構築するためのエントリポイントとなる関数を取得します。そして、VSTプラグインからの通知を受け取るためのコールバック関数を引数に渡して、そのエントリポイントとなる関数を呼び出します。

VSTプラグインの構築が完了したら、AEffect::userメンバ変数にこのVstPluginクラスのオブジェクトのアドレスを渡しています。このメンバ変数はVSTホストの側で自由に扱っていい場所なので、VSTホストの設計によってこの領域の使用法は変わりうるでしょう。

  //! プラグインの初期化処理
    void initialize(size_t sampling_rate, size_t block_size)
    {
        //! エントリポイント取得
        VstPluginEntryProc * proc = module_.getFunction<VstPluginEntryProc>("VSTPluginMain");
        if(!proc) {
            //! 古いタイプのVSTプラグインでは、
            //! エントリポイント名が"main"の場合がある。
            proc = module_.getFunction<VstPluginEntryProc>("main");
            if(!proc) { throw std::runtime_error("entry point not found"); }
        }

        AEffect *test = proc(&hwm::VstHostCallback);
        if(!test || test->magic != kEffectMagic) { throw std::runtime_error("not a vst plugin"); }

        effect_ = test;
        //! このアプリケーションでAEffect *を扱いやすくするため
        //! AEffectのユーザーデータ領域にこのクラスのオブジェクトのアドレスを
        //! 格納しておく。
        effect_->user = this;

VSTプラグインの構築と、VSTホスト側でも構築直後の初期化処理(このアプリケーションではAEffect::userの設定)が完了したので、プラグインにその通知をします。

        //! プラグインオープン
        dispatcher(effOpen, 0, 0, 0, 0);

dispatcherとは、VSTホストからVSTプラグインへ通知を送るためのAPIです。

VSTホストからVSTプラグインへ通知を送るためには、effXXXという名前で定義された定数を渡してdispatcherを呼び出します。 (AEffectがもともとメンバとして持っているdispatcherは引数が冗長なため、このプログラムではVstPluginクラスのメンバ関数としてラッパー関数を定義しています。)

VSTプラグインeffOpenを送ったので、次は初期設定系の関数を呼び出しています。

        //! 設定系
        dispatcher(effSetSampleRate, 0, 0, 0, static_cast<float>(sampling_rate));
        dispatcher(effSetBlockSize, 0, block_size, 0, 0.0);
        dispatcher(effSetProcessPrecision, 0, kVstProcessPrecision32, 0, 0);
        //! プラグインの電源オン
        dispatcher(effMainsChanged, 0, true, 0, 0);
        //! processReplacingが呼び出せる状態に
        dispatcher(effStartProcess, 0, 0, 0, 0);

block_sizeとは、VSTプラグインの合成処理関数の呼び出しで一度に扱える最大のサンプル数のことです。つまり、sampling_rateが44100Hzの時、block_sizeが256であれば、

256[sample] / 44100[sample/sec] = 0.0058049886621315[sec]

となり、一度の合成処理の呼び出しで、およそ5.8ミリ秒のオーディオを合成できることになります。通常は、オーディオデバイスに指定したバッファサイズと同じ値を設定します。

続くeffMainsChangedeffStartProcess、そしてeffStartProcessと対応するeffStopProcessというopcodeプラグインの電源のようなものです。DAWと呼ばれる高機能な音楽製作ソフトウェアでは、VSTプラグインの電源オン/オフ機能がある場合がありますが、内部的には、これらのopcodeVSTプラグインに通知が送られ、VSTプラグインの合成可能状態/休止状態を切り替える仕様になっています。

このプログラムでは簡単のため、VstPluginクラスの初期化時に合成可能状態にし、合成可能状態/休止状態の切り替え機能は省いています。

VSTプラグインへの初期設定系の処理が終わり、続く部分では、バッファの準備と、VSTプラグイン情報の取得を行っています。このバッファは、VSTプラグインの合成処理において、プラグイン外部とオーディオデータをやりとりするために使用します。

        //! プラグインの入力バッファ準備
        input_buffers_.resize(effect_->numInputs);
        input_buffer_heads_.resize(effect_->numInputs);
        for(int i = 0; i < effect_->numInputs; ++i) {
            input_buffers_[i].resize(block_size);
            input_buffer_heads_[i] = input_buffers_[i].data();
        }

        //! プラグインの出力バッファ準備
        output_buffers_.resize(effect_->numOutputs);
        output_buffer_heads_.resize(effect_->numOutputs);
        for(int i = 0; i < effect_->numOutputs; ++i) {
            output_buffers_[i].resize(block_size);
            output_buffer_heads_[i] = output_buffers_[i].data();
        }

        //! プラグイン名の取得
        std::array<char, kVstMaxEffectNameLen+1> namebuf = {};
        dispatcher(effGetEffectName, 0, 0, namebuf.data(), 0);
        namebuf[namebuf.size()-1] = '\0';
        effect_name_ = namebuf.data();

        //! プログラム(プラグインのパラメータのプリセット)リスト作成
        program_names_.resize(effect_->numPrograms);
        std::array<char, kVstMaxProgNameLen+1> prognamebuf = {};
        for(int i = 0; i < effect_->numPrograms; ++i) {
            VstIntPtr result = 
                dispatcher(effGetProgramNameIndexed, i, 0, prognamebuf.data(), 0);
            if(result) {
                prognamebuf[prognamebuf.size()-1] = '\0';
                program_names_[i] = std::string(prognamebuf.data());
            } else {
                program_names_[i] = "unknown";
            }
        }

これでプラグインの初期化処理は完了です。

続いて、VstPluginのノートイベント処理の部分です。

このアプリケーションでは、メインウィンドウの鍵盤部分をクリックした時に以下の関数が呼び出されます。

    //! ノートオンを受け取る
    //! 実際のリアルタイムオーディオアプリケーションでは、
    //! ここでノート情報だけはなくさまざまなMIDI情報を
    //! 正確なタイミングで記録するようにする。
    //! 簡単のため、このアプリケーションでは、
    //! ノート情報を随時コンテナに追加し、
    //! 次の合成処理のタイミングで内部VSTプラグイン
    //! にデータが送られることを期待する実装になっている。
    void AddNoteOn(size_t note_number)
    {
        VstMidiEvent event;
        event.type = kVstMidiType;
        event.byteSize = sizeof(VstMidiEvent);
        event.flags = kVstMidiEventIsRealtime;
        event.midiData[0] = static_cast<char>(0x90u);           // note on for 1st channel
        event.midiData[1] = static_cast<char>(note_number); // note number
        event.midiData[2] = static_cast<char>(0x64u);           // velocity
        event.midiData[3] = 0;              // unused
        event.noteLength = 0;
        event.noteOffset = 0;
        event.detune = 0;
        event.deltaFrames = 0;
        event.noteOffVelocity = 100;
        event.reserved1 = 0;
        event.reserved2 = 0;

        auto lock = get_event_buffer_lock();
        midi_events_.push_back(event);
    }

    //! ノートオンと同じ。
    void AddNoteOff(size_t note_number)
    {
        VstMidiEvent event;
        event.type = kVstMidiType;
        event.byteSize = sizeof(VstMidiEvent);
        event.flags = kVstMidiEventIsRealtime;
        event.midiData[0] = static_cast<char>(0x80u);   // note on for 1st channel
        event.midiData[1] = static_cast<char>(note_number); // note number
        event.midiData[2] = static_cast<char>(0x64u);           // velocity
        event.midiData[3] = 0;              // unused
        event.noteLength = 0;
        event.noteOffset = 0;
        event.detune = 0;
        event.deltaFrames = 0;
        event.noteOffVelocity = 100;
        event.reserved1 = 0;
        event.reserved2 = 0;

        auto lock = get_event_buffer_lock();
        midi_events_.push_back(event);
    }

簡単のためノートオン/ノートオフのみ対応、それもノート番号のみが指定できるものとなっていますが、コメントにある通り、実際のリアルタイムオーディオアプリケーションでは、MIDI情報を送る正確なタイミングを制御したり、ピッチベンドやコントロールチェンジといった、他のMIDI情報を扱ったりすることになります。

ここでノートイベントを保持するバッファに排他処理を仕掛けているのは、ノート情報追加の処理はGUIスレッドから呼び出され、次に解説するノートイベントのVSTプラグインへの送信処理は再生スレッドから呼び出されるためです。

ノートイベントを追加する部分の解説が終わったので、次はそのデータを使って合成処理を呼び出す機能について解説します。

これは2つの関数から成り立っており、ひとつはMIDI情報をVSTプラグインに送る関数。もうひとつは、そのMIDI情報を元にVSTプラグイン内部で合成処理を行う関数です。

これら2つの関数は、このアプリケーションではWaveformオーディオデバイスをオープンするときに渡したラムダ式の中から呼び出されます。

まずは前者の、MIDI情報をVSTプラグインに送る関数からです。

    //! オーディオの合成処理に先立ち、
    //! MIDI情報をVSTプラグイン本体に送る。
    //! この処理は下のProcesAudioと同期的に行われるべき。
    //! つまり、送信するべきイベントがある場合は、
    //! ProcessAudioの直前に一度だけこの関数が呼ばれるようにする。
    void ProcessEvents()
    {
        {
            auto lock = get_event_buffer_lock();
            //! 送信用データをVstPlugin内部のバッファに移し替え。
            std::swap(tmp_, midi_events_);
        }

        //! 送信データがなにも無ければ返る。
        if(tmp_.empty()) { return; }

        //! VstEvents型は、内部の配列を可変長配列として扱うので、
        //! 送信したいMIDIイベントの数に合わせてメモリを確保
        //! VstEvents型に、もともとVstEventのポインタ二つ分の領域が含まれているので、
        //! 実際に確保するメモリ量は送信するイベント数から二つ引いたもので計算している。
        //!
        //! ここで確保したメモリは
        //! processReplacingが呼び出された後で解放する。
        size_t const bytes = sizeof(VstEvents) + sizeof(VstEvent *) * std::max<size_t>(tmp_.size(), 2) - 2;
        events_ = (VstEvents *)malloc(bytes);
        for(size_t i = 0; i < tmp_.size(); ++i) {
            events_->events[i] = reinterpret_cast<VstEvent *>(&tmp_[i]);
        }
        events_->numEvents = tmp_.size();
        events_->reserved = 0;

        //! イベントを送信。
        dispatcher(effProcessEvents, 0, 0, events_, 0);
    }

その後、オーディオデータを合成する関数が呼び出されます。

    //! オーディオ合成処理
    float ** ProcessAudio(size_t frame)
    {
        BOOST_ASSERT(frame <= output_buffers_[0].size());

        //! 入力バッファ、出力バッファ、合成するべきサンプル時間を渡して
        //! processReplacingを呼び出す。
        //! もしプラグインがdouble精度処理に対応しているならば、
        //! 初期化の段階でeffProcessPrecisionでkVstProcessPrecision64を指定し、
        //! 扱うデータ型もfloatではなくdoubleとし、
        //! ここでprocessReplacingの代わりにprocessReplacingDoubleを呼び出す。
        effect_->processReplacing(effect_, input_buffer_heads_.data(), output_buffer_heads_.data(), frame);

        //! 合成終了
        //! effProcessEventsで送信したデータを解放する。
        tmp_.clear();
        free(events_);
        events_ = nullptr;

        return output_buffer_heads_.data();
    }

注意点などは、コメントに書いてあるとおりです。

合成用の処理の解説が終わったので、続いてプラグインのエディターウィンドウについてです。

VSTプラグインは、自分専用のエディターウィンドウを持っている場合があり、そのエディターウィンドウ上のつまみやボタンをいじることで、ユーザーが自由に音色を変化させられます。このエディターウィンドウは、VSTホストからはプラグインウィンドウと呼ばれることもあります。

エディターウィンドウを表示するには、VSTホスト側でエディターウィンドウを表示するためのウィンドウを生成し、そのウィンドウハンドルを親ウィンドウとして、effEditOpenを指定したdispatcherに渡します。DAWのような高機能なVSTホストでは、この親ウィンドウにプログラム(VSTプラグインのパラメータのプリセット)やバンク(プログラムを集めたもの)を読み書きするためのメニュー持っていることがあります。

    void    OpenEditor(balor::gui::Control &parent)
    {
        parent_ = &parent;
        dispatcher(effEditOpen, 0, 0, parent_->handle(), 0);

        //! プラグインのエディターウィンドウを開く際に指定した親ウィンドウのサイズを
        //! エディターウィンドウに合わせるために、エディターウィンドウのサイズを取得する。
        ERect *rect;
        dispatcher(effEditGetRect, 0, 0, &rect, 0);

        SetWindowSize(rect->right - rect->left, rect->bottom - rect->top);

        parent.visible(true);
        is_editor_opened_ = true;
    }

エディターウィンドウを閉じるには、 effEditCloseを指定してdispatcherを呼び出します。

    void    CloseEditor()
    {
        dispatcher(effEditClose, 0, 0, 0, 0);
        is_editor_opened_ = false;
        parent_ = nullptr;
    }

VstPluginクラスの解説の最後に、終了処理です。 デストラクタで終了処理関数を呼び出しています。

    ~VstPlugin()
    {
        terminate();
    }
    //! 終了処理
    void terminate()
    {
        if(IsEditorOpened()) {
            CloseEditor();
        }

        dispatcher(effStopProcess, 0, 0, 0, 0);
        dispatcher(effMainsChanged, 0, false, 0, 0);
        dispatcher(effClose, 0, 0, 0, 0);
    }

終了処理関数は最後にeffCloseを指定してdispatcherを呼び出しています。 この呼び出しによって、VSTプラグイン内部で、VSTプラグイン本体が破棄されます。

この関数を抜け、さらにデストラクタを抜けた段階で、balor::system::Moduleが破棄され、DLLもアンロードされ、全ての終了処理が完了します。

3. VSTホスト側のコールバック処理

VSTプラグインのロードと並んで、VSTホストの実装で重要になる要素がVSTホスト側のコールバック関数です。 このコールバック関数をVSTプラグインの構築の際に渡してやることで、VSTプラグインVSTホストへ要求や通知を投げられるようになっています。 このアプリケーションでは、このコールバック関数の処理をHostApplicationクラスに実装しています。

VstPlugin内部の、VSTプラグイン構築時に渡しているコールバック関数はhostapp.hppに定義された以下のstatic非メンバ関数です。

//! プラグイン内部からの要求などを受けて呼び出される
//! Host側のコールバック関数
VstIntPtr VSTCALLBACK VstHostCallback(AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void *ptr, float opt);

この関数の実装は次のようになっています。

VstIntPtr VSTCALLBACK VstHostCallback(AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void *ptr, float opt)
{
    //! VstPluginの初期化が完了するまではこちらの処理が呼ばれる
    if( !effect || !effect->user) {
        switch(opcode) {
            case audioMasterVersion:
                return kVstVersion;
            default:
                return 0;
        }
    } else {
        //! VstPluginの初期化が完了すると、effect->userに、effectを保持しているVstPluginのアドレスが入っているはず。
        VstPlugin *vst = static_cast<VstPlugin *>(effect->user);
        return vst->GetHost().Callback(vst, opcode, index, value, ptr, opt);
    }
}

AEffectが初期化完了している時は、AEffectのオブジェクトからVstPluginクラスのオブジェクトを取り出し、そこからさらにVstPluginに設定されたHostApplicationクラスのオブジェクトを取り出して、HostApplicationクラスのcallback()メンバ関数へ処理を渡しています。

VSTプラグインからの通知や要求は、audioMasterXXXという名前のopcode付きでVSTホスト側のコールバック関数に渡されます。そのため、コールバック関数内では、opcodeの値ごとに処理をしていきます。

SDKのドキュメントに記載された全てのopcodeに対応する必要はありません。作成するVSTホストがどの機能をサポートするかによって、その機能に対応するopcodeの処理を実装します。

VstIntPtr HostApplication::Callback(VstPlugin* vst, VstInt32 opcode, VstInt32 index, VstIntPtr value, void *ptr, float opt)
{
    int result = false;
    opt; //未使用の変数の警告を抑制

    switch( opcode )
    {

VSTホスト側コールバック関数でもっとも重要になるのは、 audioMasterGetTimeというopcodeが渡された時です。

    case audioMasterGetTime:
        //! VSTホストの現在の時刻情報を返す
        //! このアプリケーションでは簡単のために
        //! ホストは0位置で停止中という情報を返している。
        timeinfo_.samplePos = 0;
        timeinfo_.sampleRate = sampling_rate_;
        timeinfo_.nanoSeconds = GetTickCount() * 1000.0 * 1000.0;
        timeinfo_.ppqPos = 0;
        timeinfo_.tempo = 120.0;
        timeinfo_.barStartPos = 0;
        timeinfo_.cycleStartPos = 0;
        timeinfo_.cycleEndPos = 0;
        timeinfo_.timeSigNumerator = 4;
        timeinfo_.timeSigDenominator = 4;
        timeinfo_.smpteOffset = 0;
        timeinfo_.smpteFrameRate = kVstSmpte24fps ;
        timeinfo_.samplesToNextClock = 0;
        timeinfo_.flags = (kVstNanosValid | kVstPpqPosValid | kVstTempoValid | kVstTimeSigValid);

        return reinterpret_cast<VstIntPtr>(&timeinfo_);

これは、VSTプラグインVSTホストの現在再生位置やテンポなどの情報を必要としている時に呼び出されます。 VSTホスト側では、VstTimeInfoという構造体のメンバを適切に埋め、そのアドレスをプラグインに返します。 VSTプラグインVSTホストのどのような情報を要求しているかは、このコールバック関数の引数valueに、VstTimeInfoFlags列挙子の組み合わせで表されています。詳しくはSDKのドキュメントを参照してください。

VSTプラグインによっては合成処理を行うprocessReplacingの処理の中でVSTホストの時刻情報を必要とすることがあります。その場合に、processReplacingの呼び出しのたびにこのコールバックが呼び出されるため、この処理が重たいと非常に計算負荷が高くなってしまいます。 VSTホストを実装する際には、なるべくaudioMasterGetTimeの時刻情報の計算処理が負荷にならないように工夫する必要があります。

HostApplicationクラスとVSTホストのコールバック処理についての解説は以上となります。

4. WaveOutProcessorについて

WaveOutProcessorクラスは、WindowsのWaveformオーディオデバイスを扱うクラスです。 デバイスをオープンし、デバイスにオーディオデータを書き込み、デバイスバッファに空きがあるときには随時再生データを要求します。 実際に音を鳴らすための処理と、その中で行っているオーディオデバイスの取り扱いについては、ソースコード中のコメントを参照してください。

今回はWindowsのマルチメディアAPIを使用してWaveformオーディオデバイスから再生を行いましたが、これはレイテンシや音質の面で難があるため、実際のリアルタイムオーディオアプリケーションではASIOやWASAPIなどのリアルタイム性に優れたオーディオドライバを使用します。

まとめ

今回はVST 2.4のSDKを使用し、VST 2.4までのVSTiをホストするVSTホストアプリケーションを作成しました。

このアプリケーションでは鍵盤をクリックすると音が鳴るだけでしたが、例えば

  • PCのキーボードを鍵盤に見立ててKeyDown/KeyUpイベントに合わせてVSTプラグインMIDIのノートオン/オフを送る機能を加えてPCのキーボードを楽器にする。
  • MIDIファイルを読み込み、タイミングよくVSTプラグインMIDI情報を送る機能を加えてVSTi対応のMIDIプレーヤーにする。
  • MIDI情報の編集機能を加えてVSTi対応のMIDIシーケンサーにする。

など、ここからさらにプログラムを強化していくこともできます。

みなさんも、VSTプラグインをホストして、自分ならではの音楽製作ソフトを作ってみませんか?

補足-1

最後に、今回ウィンドウ周りを扱うのに、balorというライブラリを使用しましたが、GUIのみならずDLLの扱いなどもサポートしており、非常に使い勝手が良く、製作に大変重宝しました。作者様に感謝です。

補足-2

今回はVSTホストについての記事でしたが、12/7開催のSapporo.cpp 札幌C++勉強会 #5 : ATNDでは、Piapro StudioというVSTプラグイン形式のボーカロイドエディターの紹介があります! 興味がありましたら是非ご参加ください。お待ちしております。

明日は

@hgodaiさんです。よろしくお願いします。