読者です 読者をやめる 読者になる 読者になる

VSTホストを作ろう!

C++ VST DAW

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さんです。よろしくお願いします。