Back to Index

Update: 2024-09-28

Table of Contents

VST 3 の開発についてまとめました。何かあれば GitHub のリポジトリに issue を作ってもらえれば対応します。

初版を書いたときは VST 3 SDK 3.6.13 と VSTGUI 4.8 を使っていました。最後に動作を確認したバージョンは VST 3 SDK 3.6.14 と VSTGUI 4.9 です。

最後に編集したときのバージョンは VST 3 SDK 3.7.5 と VSTGUI 4.11 です。ところどころ廃止 (depricated) になったメソッドなどが紛れているかもしれないので注意してください。そのままコピペすることは推奨しません。

最新版の SDK は Steinberg のページからダウンロードできます。

GitHub のリポジトリからクローンすることもできます。

この文章では Steinberg のページからダウンロードできる SDK を使います。 GitHub のリポジトリには helloworld や VST3PluginTestHost が含まれていないからです。 my_plugins から始まるパスは開発中のプラグイン内のパスを表しています。それ以外のパスは特に断りがないときは VST3_SDK 以下の部分だけを書いています。

VST3_SDK 以下の各ディレクトリのライセンスです。

ディレクトリ ライセンス
base BSD 3-Clause License
cmake Proprietary Steinberg VST 3 License あるいは GPLv3
doc Proprietary Steinberg VST 3 License あるいは GPLv3
pluginterfaces Proprietary Steinberg VST 3 License あるいは GPLv3
public.sdk BSD 3-Clause License
vstgui4 BSD 3-Clause License

Steinberg にメールを送信して確認したところ、 Proprietary Steinberg VST 3 License あるいは GPLv3 のどちらを選択するかに関わらず VST3_License_Agreement.pdf に署名して Steinberg に送信する必要があります。

選択したライセンスに関わらず VST compatible logo の表示が義務付けられています。 VST compatible logo は doc/artwork に入っています。また VST compatible logo の正しい表示方法が VST3_Usage_Guidelines.pdf に書いてあります。

ソースコードを公開するときは GPLv3 で配布することができます。利用したライブラリのライセンスはバイナリの配布時に同梱する必要があります。ユーザマニュアルや GUI のクレジット画面でも利用したライセンスを参照できるようにすることが推奨されています。

ソースコードを公開しないときは Proprietary Steinberg VST 3 License と BSD 3-Clause License に従って配布することになります。 Proprietary Steinberg VST 3 License の詳細は VST3_License_Agreement.pdf に書いてあります。

VST 3 SDK の主な学習リソースは同梱されているサンプルプラグインのコードになります。サンプルプラグインは public.sdk/samples/vst に入っています。とりあえず againnote_expression_synth を押さえておけば何とかなります。

VST 3 SDK に付属してくるドキュメンテーションはダウンロードしたディレクトリの doc/index.html から開けます。次のリンクは GitHub でホストされているドキュメンテーションです。

VSTGUI4 のドキュメンテーションが 404 ですが、次のリンクが使えます。

ドキュメンテーション内をまとめて検索する方法が提供されていません。検索窓からクラスなどを探すときはトップページからのリンク先ごとに検索結果が異なるので注意してください。

開発者フォーラムもあります。

外部のリソースとしてはうつぼかずらさんによるC++でVST作りがとても参考になります。

VST 3 はホストによっては実装が中途半端なようです。ここではホストごとの特徴的な挙動や不具合をまとめています。

不具合というより仕様ですが、プラグインの GUI ウィンドウの大きさを変更できないので VST3Editor の UI エディタで編集をするときにワークアラウンドが必要です。

VST 3 SDK を編集してビルドできるように開発環境を構築します。

ここでは CMake + VS Code + PowerShell を使っています。 Visual Studio や Qt Creater などの IDE は使っていません。

次のソフトウェアをインストールします。

Visual Studio Community 2019 のインストール時には「C++ によるデスクトップ開発」にチェックが入っていることを確認して、あとはデフォルトにしておきました。

次のリンクに VST 3 SDK のビルドに必要なパッケージが書いてあります。

Ubuntu か Mint なら上のドキュメンテーションのコマンドをコピペで動きます。

バグレポートをしていただいた方からの情報によると Debian buster でもドキュメンテーションのコマンドでビルド環境が整うようです。ただし libxcb-util のバージョンが Ubuntu とは異なるため、バイナリの互換性はないようです。

Fedora 31 でもビルドできることを確認しました。パッケージの名前が一部異なります。パッケージを探すときは rpmfind.net が使えます。

次の VS Code のエクステンションをインストールしました。

XML と Bookmarks は無くても何とかなります。

XML のプラグインは GUI が記述された uidesc というファイルが XML だったのでフォーマットするためにインストールしました。 Red Hat のプラグインにしたのは要素の属性ごとに改行するフォーマットオプションがあったからです。 OpenJDK が必要なのでインストールには手間がかかります。

Bookmarks はパラメータ追加で複数のファイルを行ったり来たりするときに便利です。

ドキュメンテーションなどの検索ツールとして ripgrep をインストールしました。まず Rust をインストールしてから PowerShell で次のコマンドを実行します。

cargo install ripgrep

検索コマンドの例です。

rg "How to use cmake"

ファイル名だけを表示するときは -l オプションが使えます。

rg -l "ParamID"

特定のファイルを除外したいときは glob オプションの -g が使えます。

rg -g "!*.html" -g "!*.js" "EditController"

今回はこれだけでなんとかなりました。他のオプションは -h で確認できます。

rg -h

ビルド方法はドキュメンテーションと vst3sdk リポジトリの README.md に書いてあります。

新規プラグインの作り方は変わったようです (2022-12-28) 。以下のドキュメンテーションについては検証していません。

my_plugins/helloworld にテンプレートが入っているのでコピーして適当に名前を付けることでプロジェクトを作成します。 my_pluginsVST3_SDK の1階層上のディレクトリに入っています。

プロジェクト作成後のディレクトリ構造の例です。関係ないディレクトリは省略しています。

VST_SDK
+---my_plugins
|   +---helloworld
|   +---helloworld_with_VSTGUI
|   \---SomeFX # 追加したプロジェクト。 my_plugins/helloworld のコピー。
\---VST3_SDK
    +---base
    +---bin
    +---cmake
    +---doc
    +---pluginterfaces
    +---public.sdk
    \---vstgui4

my_plugins/CMakeLists.txtSomeFX を追加します。

add_subdirectory(helloworld)
add_subdirectory(helloworld_with_VSTGUI)
add_subdirectory(SomeFX)

PowerShell を開いて VST3_SDK で次のコマンドを実行します。 cmake のオプションで SMTG_MYPLUGINS_SRC_PATH を指定することでプロジェクトのパスを追加できます。

mkdir build
cd build
cmake -G"Visual Studio 16 2019" -DSMTG_MYPLUGINS_SRC_PATH="../../my_plugins" ..
cmake --build .

これでデバッグビルドが始まります。コードを変更したときは build ディレクトリに移動して cmake --build . でリビルドできます。

ビルドのコンフィグを変更するときは cmake-gui が使えます。

cmake-gui # build ディレクトリ内で実行する。

--config Release を追加します。

cmake --build . --config Release

32bit (x86) でビルドするときは一つ目の cmake のコマンドに -A Win32 を追加します。

mkdir build32 # 64bit 版と同じディレクトリを使わない。
cd build32
cmake -G"Visual Studio 16 2019" -A Win32 -DSMTG_MYPLUGINS_SRC_PATH="../../my_plugins" ..
cmake --build .

ビルドターゲットを 64bit から 32bit に変更するとビルドに失敗するときがあります。ビルドが失敗するときは build ディレクトリを消して作り直してみてください。

VST 3 プラグインのデバッグビルドは build\VST3\Debug 、リリースビルドは build\VST3\Release に配置されます。ビルドされたプラグインは *.vst3 という名前のディレクトリにパッケージされています。

VST 3 プラグインは OS ごとにインストールする場所が決まっています。詳細は Plug-in Locations - VST 3 Developer Portal に書いてあります。 Windows 10 では C:\Program Files\Common Files\VST3 に配置すれば使えるようになります。

PowerShell で関数を作って任意のプラグインをコマンド一つでデプロイできるようにします。 $PROFILE を開きます。 $PROFILE は bash の .bashrc に相当します。

code $PROFILE

次のコードを $PROFILE に追加します。

function cpvst($name, [switch] $release) {
  $target_dir = if ($release) { "Release" } else { "Debug" }
  $vst_path = ".\VST3\$target_dir\$name.vst3"
  Copy-Item $vst_path -Destination "C:\Program Files\Common Files\VST3" -Force -Recurse
}

$PROFILE を読み込んで更新します。 bash の source と同じような操作です。

.$PROFILE

C:\Program Files\Common Files\VST3 にファイルをコピーするには管理者権限が必要ですが、手間を省くために開発を行っているユーザに書き込み権限を与えます。エクスプローラーで VST3 を右クリックしてプロパティを開きます。タブからセキュリティを選んで「アクセス許可を変更するには[編集]をクリックします。」というテキストの横にある編集ボタンをクリックします。上のほうにあるグループ名またはユーザ名のリストから開発を行っているユーザを選択します。私の環境では Users (DESKTOP-****) のような名前がついていました。ユーザを選択したら下のほうにあるアクセス許可のリストの書き込みにチェックを入れて OK で VST3 への書き込みができるようになります。

これで cpvst が使えるようになりました。

cpvst helloworld      # build/VST3/Debug/helloworld.vst3 をデプロイ。
cpvst SomeFX -release # build/VST3/Release/SomeFX.vst3 をデプロイ。

Windows 10 の手順とほとんど同じですが cmake に渡す引数が変わります。

mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DSMTG_MYPLUGINS_SRC_PATH="../../my_plugins" ..
cmake --build .

リリースビルドのときは -DCMAKE_BUILD_TYPE=Debug-DCMAKE_BUILD_TYPE=Release に変えます。

cmake でビルドするとデプロイを自動的に行ってくれます。 VST3_SDK/build/VST3/Debug/ あるいは VST3_SDK/build/VST3/Release/ 内のディレクトリが ~/.vst3 にリンクされます。

サンプルプロジェクトのビルドを無効にするときは SMTG_ADD_VST3_HOSTING_SAMPLESSMTG_ADD_VST3_PLUGINS_SAMPLESFALSE を指定します。

# ` (backtick) は powershell で次の行に続くという意味。 bash の \ と同じ。
cmake -G"Visual Studio 16 2019" `
  -DSMTG_MYPLUGINS_SRC_PATH="../../Plugins" `
  -DSMTG_ADD_VST3_HOSTING_SAMPLES=FALSE `
  -DSMTG_ADD_VST3_PLUGINS_SAMPLES=FALSE `
  ..

プラグインのコンパイラオプションを指定するときは my_plugins/SomePlugin/CMakeLists.txttarget_compile_options を追加します。次の例では AVX2 を有効にするオプションを指定しています。

# C++17 を有効化。
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# ...

smtg_add_vst3plugin(${target} ${plug_sources})
if(MSVC) # Windows, MSVC
  target_compile_options(${target} PRIVATE /O2 /fp:fast /arch:AVX2)
elseif(UNIX)
  if(APPLE) # macOS, clang
    target_compile_options(${target} PRIVATE -O3 -fPIC -fno-aligned-allocation -Wall -mfma -mavx2)
  else() # Linux
    target_compile_options(${target} PRIVATE -O3 -fPIC -mfma -mavx2)
  endif()
endif()

ビルドを終えたらプラグインの動作をテストします。

VST 3 SDK はプラグインのビルドのたびに自動でいくつかのテストを行ってくれます。 API の使い方を間違えているなどの明らかなミスは、このテストで捉えられます。

GUI とプラグイン情報のチェックには VST3PluginTestHost が使えます。

VST3PluginTestHost のインストーラは VST_SDK/VST3_SDK/bin 以下のディレクトリに入っています。 Linux 版は無いようです。 GitHub のリポジトリには VST3PluginTestHost が含まれていないようなので注意してください。

簡易なシーケンサがついていますが MIDI キーボードがあれば演奏することができます。 wav を読み込んでエフェクタに音を送ることができますが、モノラルになってしまいます。音のレンダリングができないので、チューニングなどの細かい確認をするには向いていません。

音をレンダリングして確認したいときは REAPER が使えます。 Linux でも動きます。

VST3 プラグインは大きく次の3つの部品に分けることができます。

コントローラはパラメータとユーザインタフェースの橋渡し役です。

プロセッサでは信号処理を行います。

コントローラからプロセッサ、あるいはプロセッサからコントローラへとパラメータを直接を伝えることはありません。コントローラとプロセッサの間でのパラメータの伝達は、DAWなどの VST3 ホストの仕事です。

エディタでは GUI に関する処理を行います。エディタはコントローラの下にぶら下がるような関係になっています。

コントローラとプロセッサをファクトリに登録することで VST3 プラグインが使えるようになります。ファクトリのコードはテンプレートが用意されているのでほとんど書くことはありません。

ここに書いてある内容は動作確認していないので間違いがあるかもしれません。 noteexpressionsynth のコードから辿って見つけました。

FDebugBreak が使えます。

FDebugPrint の引数は printf と同じです。

#include "base/source/fdebug.h"

// ...
void somewhere(float value)
{
#if DEVELOPMENT // 必ず DEVELOPMENT で囲むこと。
  FDebugBreak("Start debugging at somewhere.\n");
#endif
}
// ...

FDebugPrint が使えます。

出力を見るには DAW がデバッグ出力をサポートしている必要があります。 FL Studio 20.5 では左上のメニューから OPTIONS -> Debugging log でデバッグ出力を表示できます。

FDebugPrint の引数は printf と同じです。

#include "base/source/fdebug.h"

// ...
void somewhere(float value)
{
#if DEVELOPMENT // 必ず DEVELOPMENT で囲むこと。
  FDebugPrint("somewhere, value: %g\n", value);
#endif
}
// ...

base/source/fdebug.h のコメントによると FDebugPrint を直接使わずに、 FDebugPrint をラップしたマクロを使うことが推奨されています。

// base/source/fdebug.h

/** Send "comment" string to the debugger for display. */
#define SMTG_WARNING(comment) FDebugPrint ("%s(%d) : %s\n", __FILE__, __LINE__, comment);

/** @name Shortcut macros for sending strings to the debugger for display.
    First parameter is always the format string (printf like).
*/

///@{
#define SMTG_DBPRT0(a) FDebugPrint (a);
#define SMTG_DBPRT1(a, b) FDebugPrint (a, b);
#define SMTG_DBPRT2(a, b, c) FDebugPrint (a, b, c);
#define SMTG_DBPRT3(a, b, c, d) FDebugPrint (a, b, c, d);
#define SMTG_DBPRT4(a, b, c, d, e) FDebugPrint (a, b, c, d, e);
#define SMTG_DBPRT5(a, b, c, d, e, f) FDebugPrint (a, b, c, d, e, f);
///@}

テンプレートとして用意されている my_plugins/helloworld をリファクタリングしつつ、パラメータ追加のワークフローを作っていきます。

ビルドの節で my_plugins/helloworld をコピーして作成した my_plugins/SomeFX に変更を加えていきます。 my_plugins/SomeFX/** に該当するパスは my_plugins を省略して SomeFX/** のように書いています。

コードが書いてあるファイルが7個しかないので、ディレクトリ移動の煩雑さを減らすために一つのディレクトリにまとめてしまいます。

+---CMakeLists.txt
+---resource
|   +---Info.plist
|   \---plug.rc
\---source
    +---plugcontroller.cpp
    +---plugcontroller.hpp
    +---plugfactory.cpp
    +---plugids.hpp
    +---plugprocessor.cpp
    +---plugprocessor.hpp
    \---version.hpp

ここではヘッダの拡張子に “.hpp” を使っています。

SomeFX/CMakeList.txt を変更します。

set(plug_sources
    source/plugcontroller.hpp
    source/plugids.hpp
    source/plugprocessor.hpp
    source/version.hpp
    source/plugfactory.cpp
    source/plugcontroller.cpp
    source/plugprocessor.cpp
)

SomeFX/resource/plug.rcversion.h のインクルードパスを version.hpp に変更します。

#include "../source/version.hpp"

この項の内容は doc/vstinterfaces/addownplugs.html に書いてあることと同じなので、あわせて参照することをお勧めします。

SomeFX/CMakeLists.txt を変更します。

set(target SomeFX)

SomeFX/source/plugids.hpp にある 2 つの FUID を変更します。コメントにあるように Online GUID Generator を使って生成できます。

次のコードの ID をコピーして使わないでください。 ID が競合すると DAW で予期しないプラグインがロードされる不具合につながります。

static const FUID MyProcessorUID(0x00000000, 0x00000000, 0x00000000, 0x00000000);
static const FUID MyControllerUID(0x11111111, 0x11111111, 0x11111111, 0x11111111);

プラグインの名前などは SomeFX/source/version.hpp で変更できます。

#define stringPluginName "Hello World"

#define stringOriginalFilename "SomeFX.vst3"
#if SMTG_PLATFORM_64
#define stringFileDescription stringPluginName " VST3-SDK (64Bit)"
#else
#define stringFileDescription stringPluginName " VST3-SDK"
#endif
#define stringCompanyName "Some Company\0"
#define stringCompanyWeb "https://example.com"
#define stringCompanyEmail "mailto:someaddress@example.com"

#define stringLegalCopyright "(c) 2019 Someone"
#define stringLegalTrademarks "VST is a trademark of Steinberg Media Technologies GmbH"

プラグインの名前を変えるときは Info.plistSomeFX の部分を変更します。

あとは各ファイルで定義されている名前空間 HelloWorld を適当に変更します。

SomeFX を変更して音が出るようにします。

SomeFX/source/plugprocessor.cppPlugProcessor::process で音のデータをバッファに書き込みます。

//--- Process Audio---------------------
//--- ----------------------------------
if (data.numInputs == 0) return kResultOk;
if (data.numOutputs == 0) return kResultOk;
if (data.numSamples <= 0) return kResultOk;

// double は処理しない。
if (data.symbolicSampleSize == Vst::kSample64) return kResultOk;

// この条件を追加しないと out of bounds へのアクセスでクラッシュする。
if (data.inputs[0].numChannels < 2) return kResultOk;
if (data.outputs[0].numChannels < 2) return kResultOk;

float *in0 = data.inputs[0].channelBuffers32[0];
float *in1 = data.inputs[0].channelBuffers32[1];
float *out0 = data.outputs[0].channelBuffers32[0];
float *out1 = data.outputs[0].channelBuffers32[1];

// バイパス。
for (int32_t i = 0; i < data.numSamples; ++i) {
  out0[i] = in0[i];
  out1[i] = in1[i];
}

このコードは入力と出力のチャンネル数が同じであることを仮定しています。入出力のチャンネル数は PlugProcessor::initialize で指定できます。

kResultOk は 0 で kResultFalse は 1 なので if の条件で出てくるときは注意してください。 VST3 SDK では if (isIt() == kResultFalse) doIt(); のように == でチェックしている箇所があります。

このままだとパラメータの管理が手間になるのでリファクタリングします。

VST3 では1つのパラメータをコントローラとプロセッサの2か所で保持させることになります。

TODO 変更

VSTGUI 4.8 ではコードで直接 GUI を書く方法と、 VST3 inline UI editor を使って GUI を作る方法があります。ここでは VST3 inline UI editor という名前を短くして UI エディタと呼ぶことにします。

本格的な開発をするならコードで直接 GUI を書く方法をお勧めします。私が試した限りでは FL Studio 20.5 と Reaper 5.980 では UI エディタを使う方法に不具合がありました。不具合については GUI -> VST3 inline UI editor を使った GUI の作成 -> ワークアラウンド にまとめています。

ここでは例として手軽でコードが少ない UI エディタを使う方法で GUI を作ります。

SomeFX/source/plugcontroller.hpp を変更します。

#include "vstgui/plugin-bindings/vst3editor.h"

class PlugController : public Vst::EditController {
public:

  // ...

  IPlugView *PLUGIN_API createView(const char *name) SMTG_OVERRIDE
  {
    if (name && strcmp(name, "editor") == 0)
      return new VSTGUI::VST3Editor(this, "view", "plug.uidesc");
    return 0;
  }
};

SomeFX/CMakeLists.txt を変更します。

# ...

smtg_add_vst3plugin(${target} ${plug_sources})
set_target_properties(${target} PROPERTIES ${SDK_IDE_MYPLUGINS_FOLDER})
target_include_directories(${target} PUBLIC ${VSTGUI_ROOT}/vstgui4) # 追加。
target_link_libraries(${target} PRIVATE base sdk vstgui_support)    # vstgui_support を追加。

# ...

ビルドしたプラグインを開くと真っ黒の画面が表示されます。黒い部分を右クリックするとコンテクストメニューが開いて UI エディタでの編集が選択できます。

UI エディタの使い方は vstgui4/vstgui/Documentation/html/page_uidescription_editor.html に書いてあります。

UI エディタでの編集が終わったらの左上にあるメニューから File -> Save As を選んで plug.uidesc を保存します。保存した plug.uidesc を適当なディレクトリにおいて SomeFX/CMakeLists.txtsmtg_add_vst3_resource でパスを指定することでリソースに追加できます。

# ...
target_link_libraries(${target} PRIVATE base sdk vstgui_support) # 上で変更した行。

# リソースの追加。
# 初回作成時など UI エディタで作った uidesc ファイルがないときはコメントアウトしておく。
smtg_add_vst3_resource(${target} "resource/plug.uidesc")

# ...

画像ファイルを使うときも smtg_add_vst3_resource でリソースに追加できます。

vst3sdk では生ポインタを扱う場面が出てきますが、参照カウンタが使えるので use after free を防ぐことができます。

new したインスタンスを生ポインタに格納した後は remember() で参照カウントを増やします。また、メモリリークを防ぐためにクラスのデストラクタで必ず forget() を呼んで参照カウントを減らします。以下は私が実際に書いて動いた適当な例です。

class PlugController : public Vst::EditController, public Vst::IMidiMapping {
public:
  Steinberg::Vst::PlugEditor *editor = nullptr;

  ~PlugController()
  {
    if (editor) editor->forget();
  }

  // ...
};

IPlugView *PLUGIN_API PlugController::createView(const char *name)
{
  // オートメーションなどでホストから変更されたパラメータを GUI に伝えるためにエディタクラスのポインタを持っておく。
  if (name && strcmp(name, "editor") == 0) {
    if (editor != nullptr) editor->forget();
    editor = new Vst::PlugEditor(this);
    editor->remember();
    return editor;
  }
  return 0;
}

CMake では C++14 のフラグを立てていながらスマートポインタを使っていないのが疑問でしたが、 Wikipedia によると VST 3 がリリースされたのは 2008 年なので C++11 が使えなかったのだろうと思います。

主にカスタムビューの作成についてまとめています。

オプションメニューのアイテムは StringListParameter を使うことで設定できます。

オプションメニューとリンクするパラメータを SomeFX/source/plugcontroller.cppinitialize に追加します。

#include "public.sdk/source/vst/vstparameters.h"

// ...

tresult PLUGIN_API PlugController::initialize(FUnknown *context)
{
  // ...

  auto parameterType = new Vst::StringListParameter(USTRING("Type"), ParameterID::type);
  parameterType->appendString(USTRING("uhhyou"));
  parameterType->appendString(USTRING("yey"));
  parameters.addParameter(parameterType);
}

あとは UI エディタで StringListParameter として追加したパラメータのIDをオプションメニューの control-tag として指定すれば設定完了です。

FL Studio 20.5 ではプラグインのウィンドウの大きさを変更できないので編集が困難です。ワークアラウンドとして plug.uidesc を直接編集して編集画面の大きさを変更することができます。

いったん UI エディタを開いて何も編集せずにメニューの Save as で plug.uidesc を保存します。 plug.uidesc をテキストエディタで開いて custom -> attributes -> EditorSize を変更することでGUI エディタのウィンドウの大きさを設定できます。 EditorSize のフォーマットは "x, y, width, height" となっています。変更した plug.uidesc をリソースとしてプラグインに追加すると次回から UI エディタのウィンドウが設定した大きさになります。

Reaper v5.980 では UI エディタを開こうとするとクラッシュするので使えません。

カスタムビューの概要については コードでGUI -> カスタムビューの実装と関連するコントロールの表示の変更 を参照してください。

サイン波を表示するカスタムビューを作ります。

次の3つのファイルを作成します。

SomeFX/source/gui/waveview.hpp の内容です。

#pragma once

#include "vstgui4/vstgui/vstgui.h"

namespace VSTGUI {

class WaveView : public CControl {
public:
  WaveView(const CRect &size);

  void draw(CDrawContext *pContext) override;

  CLASS_METHODS(WaveView, CControl);

  double lfo(double phase);

  double gain = 0.9;
  double shape = 0.0;
  double phase = 0.0;

protected:
  CLineStyle lineStyle{CLineStyle::kLineCapRound, CLineStyle::kLineJoinRound};
  CDrawContext::PointList points;
};

} // namespace VSTGUI

SomeFX/source/gui/waveview.cpp の内容です。

#include "waveview.hpp"
#include <cmath>

namespace VSTGUI {

WaveView::WaveView(const CRect &size) : CControl(size) {}

void WaveView::draw(CDrawContext *pContext)
{
  pContext->setDrawMode(CDrawMode(CDrawModeFlags::kAntiAliasing));
  CDrawContext::Transform t(
    *pContext, CGraphicsTransform().translate(getViewSize().getTopLeft()));

  const auto width = getWidth();
  const auto height = getHeight();
  const double borderWidth = 2.0;
  const double halfBorderWidth = borderWidth / 2.0;

  // Background.
  pContext->setLineWidth(borderWidth);
  pContext->setFillColor(pal.background());
  pContext->drawRect(CRect(0.0, 0.0, width, height), kDrawFilled);

  // Waveform.
  pContext->setLineWidth(2.0);
  pContext->setLineStyle(lineStyle);
  pContext->setFrameColor(CColor(19, 193, 54, 255));
  const size_t size = (size_t)(width + 1.0);
  if (points.size() != size) points.resize(size);
  for (size_t x = 0; x < points.size(); ++x)
    points[x] = CPoint((CCoord)x, height * lfo(x / width));
  pContext->drawPolygon(points);

  // Always draw border at last. Otherwise, inner object will be drawn over border.
  pContext->setFrameColor(isMouseEntered ? pal.highlightMain() : pal.border());
  pContext->drawRect(
    CRect(halfBorderWidth, halfBorderWidth, width, height), kDrawStroked);

  setDirty(false);
}

double WaveView::lfo(double phase)
{
  const double twopi = 6.283185307179586;

  phase = this->phase + phase * twopi;
  if (phase > twopi) phase -= twopi;
  auto sign = (pi < phase) - (phase < pi);
  auto wave = gain * sign * pow(fabs(sin(phase)), shape);
  return (wave + 1.0) * 0.5;
}

} // namespace VSTGUI

SomeFX/source/gui/waveviewcreator.cpp の内容です。

#pragma once

#include "waveview.hpp"

#include "vstgui4/vstgui/uidescription/detail/uiviewcreatorattributes.h"
#include "vstgui4/vstgui/uidescription/iviewcreator.h"
#include "vstgui4/vstgui/uidescription/uiattributes.h"
#include "vstgui4/vstgui/uidescription/uiviewfactory.h"
#include "vstgui4/vstgui/vstgui.h"

namespace VSTGUI {
namespace UIViewCreator {

class WaveViewCreator : public ViewCreatorAdapter {
public:
  WaveViewCreator()
  {
    UIViewFactory::registerViewCreator(*this);
  }

  IdStringPtr getViewName() const
  {
    return "WaveView"; // カスタムビューの名前。
  }

  IdStringPtr getBaseViewName() const
  {
    return "CControl"; // カスタムビューの親クラスの名前。
  }

  CView *create(const UIAttributes &attributes, const IUIDescription *description) const
  {
    // UI エディタの右下からドラッグ & ドロップしたときに作成されるインスタンスを設定。
    return new WaveView(CRect(0, 0, 50, 50));
  }

  bool apply(
    CView *view, const UIAttributes &attributes, const IUIDescription *description) const
  {
    // uidesc から読み込んだパラメータを適用。
    // ここでは位置、大きさ、 sub-controller だけを設定。
    // vstgui4/vstgui/uidescription/uiviewcreator.cpp の CViewCreator からコピーしてきた。

    auto waveView = dynamic_cast<WaveView *>(view);
    if (waveView == 0) return false;

    CPoint point;
    CRect size;
    if (attributes.getPointAttribute(kAttrOrigin, point)) {
      auto viewSize = waveView->getViewSize();
      size.setTopLeft(point);
      size.setWidth(viewSize.getWidth());
      size.setHeight(viewSize.getHeight());
      waveView->setViewSize(size, false);
      waveView->setMouseableArea(size);
    }
    if (attributes.getPointAttribute(kAttrSize, point)) {
      size = waveView->getViewSize();
      size.setSize(point);
      waveView->setViewSize(size, false);
      waveView->setMouseableArea(size);
    }

    const auto subControllerAttr = attributes.getAttributeValue(kAttrSubController);
    if (subControllerAttr) {
      view->setAttribute(
        'uisc',
        static_cast<uint32_t>(subControllerAttr->size() + 1),
        subControllerAttr->c_str());
    }

    return true;
  }

  bool getAttributeNames(std::list<std::string> &attributeNames) const
  {
    // uidesc に書き込まれる属性の名前を設定。
    // kAttr* は vstgui4\vstgui\uidescription\detail\uiviewcreatorattributes.h を参照。

    attributeNames.emplace_back(kAttrOrigin);
    attributeNames.emplace_back(kAttrSize);
    attributeNames.emplace_back(kAttrSubController);
    return true;
  }

  IViewCreator::AttrType getAttributeType(const std::string &attributeName) const
  {
    // uidesc の属性を C++ で受け取るときの型を設定。

    if (attributeName == kAttrOrigin)
      return kPointType;
    else if (attributeName == kAttrSize)
      return kPointType;
    else if (attributeName == kAttrSubController)
      return kStringType;
    return kUnknownType;
  }

  bool getAttributeValue(
    CView *view,
    const std::string &attributeName,
    std::string &stringValue,
    const IUIDescription *desc) const
  {
    // UI エディタで設定された属性を uidesc に書き込む文字列に変換。

    if (attributeName == kAttrOrigin) {
      stringValue = UIAttributes::pointToString(view->getViewSize().getTopLeft());
      return true;
    } else if (attributeName == kAttrSize) {
      stringValue = UIAttributes::pointToString(view->getViewSize().getSize());
      return true;
    } else if (attributeName == kAttrSubController) {
      return getViewAttributeString(view, 'uisc', stringValue);
    }
    return false;
  }

protected:
  static bool
  getViewAttributeString(CView *view, const CViewAttributeID attrID, std::string &value)
  {
    uint32_t attrSize = 0;
    if (view->getAttributeSize(attrID, attrSize)) {
      char *cstr = new char[attrSize + 1];
      if (view->getAttribute(attrID, attrSize, cstr, attrSize))
        value = cstr;
      else
        value = "";
      delete[] cstr;
      return true;
    }
    return false;
  }
};

WaveViewCreator __gWaveViewCreator;

} // namespace UIViewCreator
} // namespace VSTGUI

`SomeFX/CMakeLists.txt` にファイルを追加します。

```cmake
set(plug_sources
    source/plugcontroller.hpp
    source/plugids.hpp
    source/plugprocessor.hpp
    source/version.hpp
    source/plugfactory.cpp
    source/plugcontroller.cpp
    source/plugprocessor.cpp
    source/gui/waveview.hpp        # 追加
    source/gui/waveview.cpp        # 追加
    source/gui/waveviewcreator.cpp # 追加
)

ここに書いてある内容は手持ちの DAW では動かなかったので検証できていません。同じ仕組みを使っている noteexpressionsynth のキーボード UI もうまく動いていなかったので、 DAW 側で実装されていない可能性が高いと判断して掲載しています。

UI エディタで、パラメータを受け取る側のビューの sub-controller に適当な文字列を設定します。ここでは WaveViewController としています。

コントロールクラスに VST3EditorDelegate を継承させて createSubController を実装します。 SomeFX/source/plugcontroller.hpp でメソッドを追加します。

class PlugController : public Vst::EditController, public VSTGUI::VST3EditorDelegate {
public:
  // ...

  VSTGUI::IController *createSubController(
    VSTGUI::UTF8StringPtr name,
    const VSTGUI::IUIDescription *description,
    VSTGUI::VST3Editor *editor) SMTG_OVERRIDE;
};

SomeFX/source/plugcontroller.cpp に実装を追加します。

class WaveViewController : public DelegationController {
public:
  WaveViewController(IController *controller) : DelegationController(controller) {}

  ~WaveViewController()
  {
    if (waveview != nullptr) waveview->forget();
  }

  CView *
  verifyView(CView *view, const UIAttributes &attributes, IUIDescription *description)
  {
    auto control = dynamic_cast<WaveView *>(view);
    if (control != nullptr) { // 必要に応じてタグのチェックを追加する。
      waveview = control;
      waveview->remember();
    }
    return controller->verifyView(view, attributes, description);
  }

  void valueChanged(CControl *pControl) override
  {
    // 多分 DAW がこのメソッドの呼び出しを実装していない。
    // Reaper 5.980 と FL Studio 20.5 では動かなかった。

    Vst::ParamID tag = pControl->getTag();

    if (waveview == nullptr) return;

    if (tag == ParameterID::volume) {
      waveview->gain = pControl->getValueNormalized();
      waveview->setDirty(true);
    }
  }

protected:
  WaveView *waveview = nullptr;
};

IController *PlugController::createSubController(
  UTF8StringPtr name, const IUIDescription *description, VST3Editor *editor)
{
  if (name == "WaveViewController") {
    return new WaveViewController(editor);
  }
  return nullptr;
}

これで動くはずですが、コメントで書いたように DelegationControllervalueChanged がホストから呼び出されていないようです。ドキュメンテーションの vstgui4/vstgui/Documentation/html/page_uidescription_editor.html では DelegationControllernotify を使っていますが、 CControl::kMessageValueChanged は VSTGUI 4.8 で廃止されたので動きません。

VSTGUIEditor をベースにしてコードでGUIを作ります。

VSTGUIEditor を使った GUI の作成についてはうつぼかずらさんによるC++でVST作りが参考になります。ここではC++でVST作りで紹介されていない内容について扱っています。

VSTGUI でカスタムフォントを使うにはプラグインバンドルの Contents/Resources/Fonts*.ttf ファイルを配置します。

プラグインの CMakeLists.txt に以下の行を追加します。 ${target} はプラグイン名あるいはプロジェクト名です。ライセンスファイルもまとめて同梱しています。

smtg_add_plugin_resource(${target} "../common/resource/Fonts/Tinos-BoldItalic.ttf" "Fonts")
smtg_add_plugin_resource(${target} "../common/resource/Fonts/LICENSE.txt" "Fonts")

コード側では CFontDesc でフォントファミリの文字列とスタイルを指定すれば使えます。

SharedPointer<CFontDesc> font = new CFontDesc(
  "Tinos",                                    // フォントファミリ名。
  10.0,                                       // サイズ。
  CTxtFace::kBoldFace | CTxtFace::kItalicFace // スタイル。
);

太字とイタリックは使えました。 Noto フォントなどが提供している demi light などのより細かい文字の太さが使えるかどうかは確認していません。

フォントファミリ名あるいはスタイルの指定が正しくないときは VSTGUI が設定した kSystemFont がデフォルトとして使われます。 macOS では 12 ポイントの Helvetica 、 それ以外の OS では 12 ポイントの Arial がデフォルトです。詳細は vst3sdk/vstgui4/vstgui/lib/cfont.cpp を参照してみてください。

ホストがオートメーションによってパラメータを変更したときは EditController.setParamNormalized() に変更された値が渡されます。 setParamNormalized() をオーバーライドして GUI に値を渡すことができます。

  1. VSTGUIEditor を継承したエディタクラスに ID と値を受け取って CControl などの要素を更新するメソッドを作る。
  2. EditController を継承したコントローラクラスにエディタクラスのポインタを持たせる。
  3. setParamNormalized() をオーバーライドしてエディタクラスのポインタから GUI に値を渡す。

VSTGUIEditorsetIdleRate でリフレッシュレートを変更できます。指定する値の単位は ms (ミリ秒) で、デフォルト値は 100ms (10Hz) です。カスタムビューの更新が遅く感じるときなどに変更してみてください。

bool PlugEditor::open(void *parent, const PlatformType &platformType)
{
  setIdleRate(1000 / 60);

  // ...
}

キーボードイベントは CView::onKeyboardEvent() をオーバーライドして受け取ります。 onKeyboardEvent が呼ばれるためにはビューがフォーカスを取る必要があります。

CControl を継承したクラスでは setWantsFocus(true) をコンストラクタで呼ぶだけで、マウスクリックなどでフォーカスが取れるようになります。

struct SomeControl : public CControl {
public:
  explicit SomeControl(const CRect &size)
    : CControl(size)
  {
    setWantsFocus(true); // 継承元が CControl ならこれで十分。
  }

  void onKeyboardEvent(KeyboardEvent &event) override
  {
    // ここでキーボードイベントをハンドル。
  }
};

CControl を継承したクラスは Ctrl + 左クリック のイベントが奪われてしまうという問題があります。この問題を避けるためには CView が使えます。

CView を継承したクラスではフォーカスを明示的に取る必要があります。フォーカスを取るには CFrame::setFocusView() が使えます。 CFrameVSTGUIEditor::getFrame() から取得できます。

struct SomeView : public CView {
public:
  // VSTGUIEditor::getFrame() を使うので editor としてコンストラクタで受け取る。
  Steinberg::Vst::VSTGUIEditor *editor = nullptr;

  explicit SomeView(
    Steinberg::Vst::VSTGUIEditor *editor,
    const CRect &size)
    : CView(size), editor(editor)
  {
    setTransparency(false); // CControl のコンストラクタからコピーした。なくてもいい。
    setMouseEnabled(true);  // CControl のコンストラクタからコピーした。必要。
    setWantsFocus(true);    // 継承元が CControl ならこれで十分。 CView ではこれだけでは不足。

    if (editor != nullptr) editor->addRef();
  }

  virtual ~SomeView()
  {
    if (editor != nullptr) editor->release();
  }

  void grabFocus()
  {
    if (!editor) return;
    if (editor->getFrame()) editor->getFrame()->setFocusView(this);
  }

  void releaseFocus()
  {
    if (!editor) return;
    if (editor->getFrame()) editor->getFrame()->setFocusView(nullptr);
  }

  void onMouseEnterEvent(MouseEnterEvent &event) override
  {
    grabFocus(); // マウスが SomeView の領域に入るとフォーカスを取る。
    event.consumed = true;
  }

  void onMouseExitEvent(MouseExitEvent &event) override
  {
    releaseFocus(); // マウスが SomeView の領域から出るとフォーカスを外す。
    event.consumed = true;
  }

  void onKeyboardEvent(KeyboardEvent &event) override
  {
    // ここでキーボードイベントをハンドル。
  }
};

以下は公式ドキュメンテーションへのリンクです。キーボードイベントを受け取る条件については書いてありますが、どうすればその条件を満たせるのか (どう実装すればいいのか) について書いていないので、あまり役には立ちません。

あるコントロールの値を変更したときに、連動して別のコントロールの値や表示を変更したい時があります。ここでは例としてコントロールの値に応じてカスタムビューで波形を表示するようにします。

カスタムビューは CView を継承したクラスならどれを継承しても作ることができます。例えばスライダーの見た目を変更したいなら CSlider を継承する、テキストを表示するカスタムビューを作りたいときは CParamDisplay を継承するといった形で実装の手間を減らせるときがあります。継承して使えるクラスを探すときは vstgui4/vstgui/libvstgui4/vstgui/lib/controls を覗いてみてください。

ここでは波形を表示するだけなので CControl を継承して draw をオーバーライドしただけの WaveView というカスタムビューを作ります。 waveview.hpp の内容です。

#pragma once

#include "vstgui/vstgui.h"

namespace VSTGUI {

class WaveView : public CControl {
public:
  WaveView(const CRect &size);

  void draw(CDrawContext *pContext) override;

  CLASS_METHODS(WaveView, CControl);

  double lfo(double phase);

  double amount = 0.9;
  double shape = 0.0;
  double phase = 0.0;

protected:
  CLineStyle lineStyle{CLineStyle::kLineCapRound, CLineStyle::kLineJoinRound};
  CDrawContext::PointList points;
};

} // namespace VSTGUI

waveview.cpp の内容です。ここでは描画する波形が簡単なので WaveView::lfo として GUI 側で実装しています。コードがより複雑になるときは GUI 側に波形を生成するコードを書かずに、プロセッサクラスから信号処理の部分をヘッダにまとめてインクルードすることをお勧めします。

#include "waveview.hpp"

#include <cmath>

namespace VSTGUI {

WaveView::WaveView(const CRect &size) : CControl(size) {}

void WaveView::draw(CDrawContext *pContext)
{
  // アンチエイリアスを有効にする。
  pContext->setDrawMode(CDrawMode(CDrawModeFlags::kAntiAliasing));

  // 座標変換して (x, y) = (0, 0) がビューの左上となるようにする。
  // 座標変換しないときの (0, 0) はプラグインウィンドウの左上になる。
  CDrawContext::Transform t(
    *pContext, CGraphicsTransform().translate(getViewSize().getTopLeft()));

  const auto width = getWidth();
  const auto height = getHeight();
  const double borderWidth = 2.0;
  const double halfBorderWidth = borderWidth / 2.0;

  // Background.
  pContext->setLineWidth(borderWidth);
  pContext->setFillColor(pal.background());
  pContext->drawRect(CRect(0.0, 0.0, width, height), kDrawFilled);

  // Waveform.
  pContext->setLineWidth(2.0);
  pContext->setLineStyle(lineStyle);
  pContext->setFrameColor(CColor(19, 193, 54, 255));
  const size_t size = (size_t)(width + 1.0);
  if (points.size() != size) points.resize(size);
  for (size_t x = 0; x < points.size(); ++x)
    points[x] = CPoint((CCoord)x, height * lfo(x / width));
  pContext->drawPolygon(points);

  // Always draw border at last. Otherwise, inner object will be drawn over border.
  pContext->setFrameColor(isMouseEntered ? pal.highlightMain() : pal.border());
  pContext->drawRect(
    CRect(halfBorderWidth, halfBorderWidth, width, height), kDrawStroked);

  setDirty(false);
}

double WaveView::lfo(double phase)
{
  const double pi = 3.14159265358979323846;

  phase = this->phase + phase * 2 * pi;
  if (phase > 2 * pi) phase -= 2 * pi;
  auto sign = (pi < phase) - (phase < pi);
  auto wave = amount * sign * pow(fabs(sin(phase)), shape);
  return (wave + 1.0) * 0.5;
}

} // namespace VSTGUI

エディタクラスに関数を追加します。

class PlugEditor : public VSTGUIEditor, public IControlListener {
// ...

  void addWaveView(const CRect &size);

protected:
  ParamValue getPlainValue(ParamID tag);

  const CRect WaveViewSize{100.0, 100.0, 200.0, 200.0};

  // getViewAt が反応するように 1 ピクセル内側を指定する。
  const CPoint WaveViewPos{WaveViewSize.left + 1.0, WaveViewSize.top + 1.0};

// ...
};

addWaveView の実装です。 parameter.hpp で定義したパラメータIDを使ってコントローラから初期値を取得しています。

#include "parameter.hpp"

void PlugEditor::addWaveView(const CRect &size)
{
  auto view = new WaveView(size);
  view->shape = getPlainValue(SomePlugin::ParameterID::lfoShape);
  view->phase = getPlainValue(SomePlugin::ParameterID::lfoInitialPhase);
  frame->addView(view);
}

ParamValue PlugEditor::getPlainValue(ParamID tag)
{
  auto normalized = controller->getParamNormalized(tag);
  return controller->normalizedParamToPlain(tag, normalized);
};

openWaveView を作成します。

bool PlugEditor::open(void *parent, const PlatformType &platformType)
{
  // ...

  addWaveView(WaveViewSize);

  // ...
}

エディタクラスの valueChanged で関連するコントロールの変更を検知して WaveView を更新します。ここでは VSTGUIEditor::open で作成したコントロールを取得するために getViewAt を使っています。 getViewAt に渡している WaveViewPos はエディタクラスのメンバとして定義した WaveView の位置です。

void PlugEditor::valueChanged(CControl *pControl)
{
  ParamID tag = pControl->getTag();
  ParamValue value = pControl->getValueNormalized();
  controller->setParamNormalized(tag, value);
  controller->performEdit(tag, value);

  if (tag == SevenDelay::ParameterID::lfoShape) {
    auto some = dynamic_cast<WaveView *>(frame->getViewAt(WaveViewPos));
    if (some == nullptr) return;
    some->shape = getPlainValue(tag);
    some->setDirty(true);
  } else if (tag == SevenDelay::ParameterID::lfoInitialPhase) {
    auto some = dynamic_cast<WaveView *>(frame->getViewAt(WaveViewPos));
    if (some == nullptr) return;
    some->phase = getPlainValue(tag);
    some->setDirty(true);
  }
}

コントロールを取得する別の方法としては frame->getView があります。引数の index がどういう値なのかドキュメンテーションに書いていなかったのでここでは使っていません。興味のある方は vstgui4/vstgui/lib/cviewcontainer.* を読んでみてください。

CSplashScreen でスプラッシュスクリーンを作成できます。

エディタクラスにスプラッシュスクリーンを追加する関数を作ります。

class PlugEditor : public VSTGUIEditor, public IControlListener {
  // ...

  void addSplashScreen(CRect &buttonRect, CRect splashRect);

  // ...
};

addSplashScreen の実装です。ここではスプラッシュスクリーンとしてカスタムビューの CreditView が表示されるようにしています。開いたスプラッシュスクリーンをクリックしたときに閉じるようにするにはカスタムビュー側の setListenerCSplashScreen を渡しておく必要があります。 setListenerCControl で実装されています。

void PlugEditor::addSplashScreen(CRect &buttonRect, CRect splashRect)
{
  auto credit = new CreditView(splashRect, nullptr);
  auto splash = new CSplashScreen(buttonRect, this, -666, credit);
  credit->setListener(splash);
  frame->addView(splash);
}

open でスプラッシュスクリーンを追加します。

bool PlugEditor::open(void *parent, const PlatformType &platformType)
{
  // ...

  // viewRect はウィンドウの大きさ。
  addSplashScreen(
    CRect(30.0, 340.0, 190.0, 380.0),
    CRect(
      viewRect.left + 200.0,
      viewRect.top + 20.0,
      viewRect.right - 200.0,
      viewRect.bottom - 20.0));

  // ...
}

source/gui/splash.hpp を作って CreditView を実装します。

#pragma once

#include "vstgui/vstgui.h"

namespace Steinberg {
namespace Vst {

using namespace VSTGUI;

class CreditView : public CControl {
public:
  CreditView(const CRect &size, IControlListener *listener) : CControl(size, listener) {}

  void draw(CDrawContext *pContext) override;

  void onMouseDownEvent (MouseDownEvent& event) override
  {
    if (event.buttonState.isLeft()) return;
    valueChanged();
    event.consumed = true;
    event.ignoreFollowUpMoveAndUpEvents(true);
  }

  CLASS_METHODS(CreditView, CControl)

private:
  UTF8String fontName{"DejaVu Sans Mono"};
  CCoord fontSize = 18.0;
  CCoord fontSizeTitle = 24.0;
};

} // namespace Vst
} // namespace Steinberg

source/gui/splash.cpp を作ります。

#include "splash.hpp"

namespace Steinberg {
namespace Vst {

using namespace VSTGUI;

void CreditView::draw(CDrawContext *pContext)
{
  pContext->setDrawMode(CDrawMode(CDrawModeFlags::kAntiAliasing));
  CDrawContext::Transform t(
    *pContext, CGraphicsTransform().translate(getViewSize().getTopLeft()));

  const auto width = getWidth();
  const auto height = getHeight();
  const double borderWidth = 8.0;
  const double halfBorderWidth = borderWidth / 2.0;

  // Background.
  pContext->setLineWidth(borderWidth);
  pContext->setFillColor(pal.background());
  pContext->drawRect(CRect(0.0, 0.0, width, height), kDrawFilled);

  pContext->setFont(
    new CFontDesc(fontName, fontSizeTitle, CTxtFace::kBoldFace | CTxtFace::kItalicFace));
  pContext->setFontColor(pal.foreground());
  pContext->drawString(
    UTF8String("🐸🍻🐨🍻🐰").getPlatformString(), CPoint(20.0, 50.0));

  // Border.
  pContext->setFrameColor(isMouseEntered ? pal.highlightMain() : pal.border());
  pContext->drawRect(
    CRect(
      halfBorderWidth,
      halfBorderWidth,
      width - halfBorderWidth,
      height - halfBorderWidth),
    kDrawStroked);

  setDirty(false);
}

} // namespace Vst
} // namespace Steinberg

SomeFX/CMakeLists.txt にファイルを追加します。

set(plug_sources
    //...
    source/gui/splash.hpp # 追加
    source/gui/splash.cpp # 追加
)

エディタクラスに IMouseObserver を継承させます。

class PlugEditor : public VSTGUIEditor, public IControlListener, public IMouseObserver {
// ...

  // IMouseObserver の実装。
  void onMouseEntered(CView *view, CFrame *frame) override {}
  void onMouseExited(CView *view, CFrame *frame) override {}
  CMouseEventResult
  onMouseMoved(CFrame *frame, const CPoint &where, const CButtonState &buttons) override
  {
    return kMouseEventNotHandled;
  }
  CMouseEventResult
  onMouseDown(CFrame *frame, const CPoint &where, const CButtonState &buttons) override;

// ...
};

エディタクラスの open メソッド内で frame からマウスイベントを伝えるように指定します。

bool PlugEditor::open(void *parent, const PlatformType &platformType)
{
  // ...

  frame->registerMouseObserver(this);

  // ...
}

onMouseDown を実装します。

void PlugEditor::onMouseEvent(MouseEvent &event, CFrame *frame) override
  {
    if (!event.buttonState.isRight()) return;

    auto componentHandler = controller->getComponentHandler();
    if (componentHandler == nullptr) return;

    FUnknownPtr<IComponentHandler3> handler(componentHandler);
    if (handler == nullptr) return;

    auto control = dynamic_cast<CControl *>(frame->getViewAt(event.mousePosition));
    if (control == nullptr) return;

    // Context menu will not popup when the control has negative tag.
    ParamID id = control->getTag();
    if (id < 1 || id >= LONG_MAX) return;

    IContextMenu *menu = handler->createContextMenu(this, &id);
    if (menu == nullptr) return;

    menu->popup(event.mousePosition.x, event.mousePosition.y);
    menu->release();

    event.consumed = true;
  };

0 あるいは負の tag が指定されたコントロールではコンテキストメニューを表示したくないので、 id の条件を id < 1 || id >= LONG_MAX としています。 LONG_MAX を使っているのは ParamIDunsigned long の typedef だからです。

コンテキストメニューが開かないときは event.consumed を変えずにリターンすることで、他のコントロールにマウスイベントが伝わるようにしています。

RunLoop クラスを VST3Editor のコードからコピーして x11runloop.hpp というヘッダにします。

#pragma once

#if LINUX

#include "base/source/fobject.h"
#include "vstgui/lib/vstguibase.h"

#include "pluginterfaces/gui/iplugview.h"
#include "vstgui/lib/platform/linux/x11frame.h"

namespace Steinberg {
namespace Vst {

using namespace VSTGUI;

// Map Steinberg Vst Interface to VSTGUI Interface
class RunLoop : public X11::IRunLoop, public AtomicReferenceCounted {
public:
  struct EventHandler : Steinberg::Linux::IEventHandler, public Steinberg::FObject {
    X11::IEventHandler *handler{nullptr};

    void PLUGIN_API onFDIsSet(Steinberg::Linux::FileDescriptor) override
    {
      if (handler) handler->onEvent();
    }
    DELEGATE_REFCOUNT(Steinberg::FObject)
    DEFINE_INTERFACES
    DEF_INTERFACE(Steinberg::Linux::IEventHandler)
    END_DEFINE_INTERFACES(Steinberg::FObject)
  };
  struct TimerHandler : Steinberg::Linux::ITimerHandler, public Steinberg::FObject {
    X11::ITimerHandler *handler{nullptr};

    void PLUGIN_API onTimer() final
    {
      if (handler) handler->onTimer();
    }
    DELEGATE_REFCOUNT(Steinberg::FObject)
    DEFINE_INTERFACES
    DEF_INTERFACE(Steinberg::Linux::ITimerHandler)
    END_DEFINE_INTERFACES(Steinberg::FObject)
  };

  bool registerEventHandler(int fd, X11::IEventHandler *handler) final
  {
    if (!runLoop) return false;

    auto smtgHandler = Steinberg::owned(new EventHandler());
    smtgHandler->handler = handler;
    if (runLoop->registerEventHandler(smtgHandler, fd) == Steinberg::kResultTrue) {
      eventHandlers.push_back(smtgHandler);
      return true;
    }
    return false;
  }
  bool unregisterEventHandler(X11::IEventHandler *handler) final
  {
    if (!runLoop) return false;

    for (auto it = eventHandlers.begin(), end = eventHandlers.end(); it != end; ++it) {
      if ((*it)->handler == handler) {
        runLoop->unregisterEventHandler((*it));
        eventHandlers.erase(it);
        return true;
      }
    }
    return false;
  }
  bool registerTimer(uint64_t interval, X11::ITimerHandler *handler) final
  {
    if (!runLoop) return false;

    auto smtgHandler = Steinberg::owned(new TimerHandler());
    smtgHandler->handler = handler;
    if (runLoop->registerTimer(smtgHandler, interval) == Steinberg::kResultTrue) {
      timerHandlers.push_back(smtgHandler);
      return true;
    }
    return false;
  }
  bool unregisterTimer(X11::ITimerHandler *handler) final
  {
    if (!runLoop) return false;

    for (auto it = timerHandlers.begin(), end = timerHandlers.end(); it != end; ++it) {
      if ((*it)->handler == handler) {
        runLoop->unregisterTimer((*it));
        timerHandlers.erase(it);
        return true;
      }
    }
    return false;
  }

  RunLoop(Steinberg::FUnknown *runLoop) : runLoop(runLoop) {}

private:
  using EventHandlers = std::vector<Steinberg::IPtr<EventHandler>>;
  using TimerHandlers = std::vector<Steinberg::IPtr<TimerHandler>>;
  EventHandlers eventHandlers;
  TimerHandlers timerHandlers;
  Steinberg::FUnknownPtr<Steinberg::Linux::IRunLoop> runLoop;
};

} // namespace Vst
} // namespace Steinberg

#endif

PlugEditor のコードに x11runloop.hpp をインクルードします。また CFrame の初期化が変わります。

//...

#include "x11runloop.hpp"

// ...

bool PlugEditor::open(void *parent, const PlatformType &platformType)
{
  if (frame) return false;

  frame = new CFrame(
    CRect(viewRect.left, viewRect.top, viewRect.right, viewRect.bottom), this);
  if (frame == nullptr) return false;
  frame->setBackgroundColor(CColor(0xff, 0x00, 0x00, 0xff));
  frame->registerMouseObserver(this);

  // Linux で VSTGUI を使うには frame::open に platformType と config を渡す。
  IPlatformFrameConfig *config = nullptr;
#if LINUX
  X11::FrameConfig x11config;
  x11config.runLoop = VSTGUI::owned(new RunLoop(plugFrame));
  config = &x11config;
#endif
  frame->open(parent, platformType, config);

  // ...
}

この項の内容は実装をあきらめたので中途半端です。次の問題が解決されていないので注意してください。

FL Studio 20.5 でズーム後にプラグインのウィンドウがリサイズされなかったことが、あきらめた理由です。


ズーム機能は GUI を任意の倍率で拡大する機能です。 VSTGUI4 で高解像度ディスプレイへ対応するときに使えます。

まずはエディタクラスに CommandMenuItemTargetAdapter を継承させて、ユーザがコンテキストメニューのアイテムを選択したときに通知されるようにします。

class PlugEditor : public VSTGUIEditor,
                   public IControlListener,
                   public IMouseObserver,
                   public CommandMenuItemTargetAdapter {
// ...

  // CommandMenuItemTargetAdapter の実装。
  bool validateCommandMenuItem(CCommandMenuItem *item) override
  {
    return false;
  };
  bool onCommandMenuItemSelected(CCommandMenuItem *item) override;

// ...
};

onCommandMenuItemSelected の実装です。 2 つのメニューアイテムを用意して、 1.0 倍と 2.0 倍を切り替えられるようにしています。倍率の切り替えには CFramesetZoom を使います。

bool PlugEditor::onCommandMenuItemSelected(CCommandMenuItem *item)
{
  if (item->getCommandCategory() != "Scale") return false;
  if (frame == nullptr) return true;

  switch (item->getTag()) {
    case 0: {
      frame->setZoom(1.0);
    } break;

    case 1: {
      frame->setZoom(2.0);
    } break;
  }

  return true;
}

onMouseDown を変更してマウスが右クリックされたときに表示されるコンテキストメニューにズームを行うアイテムを追加します。

アイテムが選択されると ContextMenuTargetexecuteMenuItem が呼び出されます。さらに item->execute() から CommandMenuItemTargetAdapteronCommandMenuItemSelected が呼び出されます。

class ContextMenuTarget : public IContextMenuTarget, public FObject {
public:
  ContextMenuTarget(CCommandMenuItem *item) : item(item)
  {
    item->remember();
  }

  ~ContextMenuTarget() override
  {
    item->forget();
  }

  Steinberg::tresult PLUGIN_API executeMenuItem(Steinberg::int32 tag) override
  {
    item->execute();
    return Steinberg::kResultTrue;
  }

  OBJ_METHODS(ContextMenuTarget, Steinberg::FObject)
  FUNKNOWN_METHODS(Steinberg::Vst::IContextMenuTarget, Steinberg::FObject)
protected:
  CCommandMenuItem *item;
};

CMouseEventResult
PlugEditor::onMouseDown(CFrame *frame, const CPoint &where, const CButtonState &buttons)
{
  if (!buttons.isRightButton()) return kMouseEventNotHandled;

  auto componentHandler = controller->getComponentHandler();
  if (componentHandler == nullptr) return kMouseEventNotHandled;

  FUnknownPtr<IComponentHandler3> handler(componentHandler);
  if (handler == nullptr) return kMouseEventNotHandled;

  auto control = dynamic_cast<CControl *>(frame->getViewAt(where));
  if (control == nullptr) return kMouseEventNotHandled;

  ParamID id = control->getTag();
  if (id < 1 || id >= LONG_MAX) return kMouseEventNotHandled;

  auto contextMenu = handler->createContextMenu(this, &id);
  if (contextMenu == nullptr) return kMouseEventNotHandled;

  // ここからメニューの追加。

  // kIsGroupStart を設定してもメニューが入れ子にならない。
  IContextMenu::Item groupItem{};
  UString128("Scale").copyTo(groupItem.name, 128);
  groupItem.flags = IContextMenu::Item::kIsGroupStart;
  contextMenu->addItem(groupItem, nullptr);

  std::array<double, 2> scales = {1.0, 2.0};
  for (size_t index = 0; index < scales.size(); ++index) {
    IContextMenu::Item item{};
    UString128(std::to_string(scales[index]).c_str()).copyTo(item.name, 128);
    item.flags = 0;
    item.tag = (int32_t)index;

    // 注意: VSTGUI 4.8 では CCommandMenuItem::Desc に渡した tag が
    // CCommandMenuItem に渡されずに無視される。
    auto command = new CCommandMenuItem({nullptr, item.tag, this, "Scale", nullptr});
    command->setTag(item.tag);
    contextMenu->addItem(item, new ContextMenuTarget(command));
  }

  // kIsGroupEnd を設定してもメニューが入れ子にならない。
  groupItem.flags = IContextMenu::Item::kIsGroupEnd;
  contextMenu->addItem(groupItem, nullptr);

  // ここまでメニューの追加。

  contextMenu->popup(where.x, where.y);
  contextMenu->release();
  return kMouseEventHandled;
}

コメントにも書いていますが、 kIsGroupStartkIsGroupEnd を設定してもメニューが入れ子になりません。

どこを右クリックしてもメニューが開くようにするには COptionMenu でメニューを作っておいて、コントロール上で右クリックされているときは IContextMenu に変換する、そうでなければ COptionMenu をそのままポップアップする、という処理が考えられます。 vstgui4/vstgui/plugin-bindings/vst3editor.* に実装例があります。