「VSTe 操作するパラメータを管理するコードを覚える」の版間の差分
356行目: | 356行目: | ||
□■VST終了時の処理の流れ | □■VST終了時の処理の流れ | ||
1.CxxProc の getState(IBStream* state) | 1.CxxProc の getState(IBStream* state) | ||
: DAWが保持するべき値を更新。CxxProcのprotected:メンバ変数のパラメータ値を使う。DAWに覚えさせる。 | : DAWが保持するべき値を更新。CxxProcのprotected:メンバ変数のパラメータ値を使う。DAWに覚えさせる。 |
2023年6月4日 (日) 15:42時点における最新版
VSTeプログラミング初歩に戻る。
概要(プラグイン開発)
音声処理の核心に触れることなく、パラメータだけを追加して管理するコードを覚えます。音声処理はかなり難しいので、後回しでいいと思います。パラメータの追加方法と管理方法というか制御方法を覚えれば、音声処理をしやすくなるので、先に覚えると楽です。
ここでは、ユーザ定義の2つのクラスが登場します。好きな名前をつけられる部分です。いかにも勝手に名前つけやがったなという意味でCxxという接頭句をつけてみました。
- CxxProc(AudioEffectクラスから派生)
- CxxEdit(EditControllerクラスから派生)
ヘッダファイルではこんな風に定義されます。何も処理をしない基礎で既に習得済みの部分も多いですが、全部を記載します。追記した部分を後で示します。
#pragma once
//include headerfiles
#include "pluginterfaces\base\funknown.h"
#include "public.sdk/source/vst/vstaudioeffect.h"
#include "public.sdk/source/vst/vsteditcontroller.h"
#include "public.sdk/source/main/pluginfactory.h"
#include "pluginterfaces/vst/ivstparameterchanges.h"
#include "pluginterfaces/base/ibstream.h"
#include "vstgui/plugin-bindings/vst3editor.h"
namespace Steinberg{
namespace Vst {
#define xxVENDOR "VENDOR"
#define xxURL "https://wiki.yo-net.jp/"
#define xxEMAIL "mailto:__info@yo-net.jp"
#define xxVSTNAME "UnknownVST"
#define xxVERSION "1"
#define xxSUBCATEGORIES Vst::PlugType::kFx
static const FUID ProcessorUID(0x13E7D9FD, 0xE02B4003, 0xA2324A98, 0x45A53825);
//{13E7D9FD-E02B-4003-A232-4A9845A53825}
static const FUID ControllerUID(0xBFA33A3F, 0x06B34AEE, 0x83B53575, 0x0F947340);
//{BFA33A3F-06B3-4AEE-83B5-35750F947340}
enum ParaTag
{
INT32_OPTION_MENU01 = 1,
PARAMVALUE_KNOB01,
};
Class CxxProc : AudioEffect
{
public:
CxxProc();
tresult PLUGIN_API initialize(FUnknown* context) SMTG_OVERRIDE;
tresult PLUGIN_API process(ProcessData& data) SMTG_OVERRIDE;
tresult PLUGIN_API getState(IBStream* state) SMTG_OVERRIDE;
tresult PLUGIN_API setState(IBStream* state) SMTG_OVERRIDE;
static FUnknown* createInstance(void*) {
return (IAudioProcessor*)new CxxProc();
}
protected:
int32 int32OptionMenu01 = 0;
ParamValue ParamValue_Knob01 = 0.5;
};
Class CxxEdit : EditController
{
public:
tresult PLUGIN_API initialize(FUnknown* context) SMTG_OVERRIDE;
tresult PLUGIN_API getState(IBStream* state) SMTG_OVERRIDE;
tresult PLUGIN_API setState(IBStream* state) SMTG_OVERRIDE;
tresult PLUGIN_API setComponentState(IBStream* state) SMTG_OVERRIDE;
IPlugView* PLUGIN_API createView(const char* name) SMTG_OVERRIDE;
static FUnknown* createInstance(void*) { return (IEditController*)new CxxEdit(); }
};
}}//close namespace
ソースファイルはこんな感じ
#include "vst.h"
BEGIN_FACTORY_DEF(xxVENDOR, xxURL, xxEMAIL)
DEF_CLASS2(INLINE_UID_FROM_FUID(Steinberg::Vst::ProcessorUID),
PClassInfo::kManyInstances,
kVstAudioEffectClass,
xxVSTNAME,
Vst::kDistributable,
xxSUBCATEGORIES,
xxVERSION,
kVstVersionString,
Steinberg::Vst::CxxProc::createInstance)
DEF_CLASS2(INLINE_UID_FROM_FUID(Steinberg::Vst::ControllerUID),
PClassInfo::kManyInstances,
kVstComponentControllerClass,
xxVSTNAME " Controller",
0,
"",
xxVERSION,
kVstVersionString,
Steinberg::Vst::CxxEdit::createInstance)
END_FACTORY
namespace Steinberg{
namespace Vst {
CxxProc::CxxProc()
{
setControllerClass(ControllerUID);
}
tresult PLUGIN_API CxxProc::initialize(FUnknown* pFUnknown_Context)
{
tresult tresult_r = AudioEffect::initialize(pFUnknown_Context);
if (tresult_r == kResultTrue)
{
addAudioInput(STR16("AudioInput"), SpeakerArr::kStereo);
addAudioOutput(STR16("AudioOutput"), SpeakerArr::kStereo);
}
return tresult_r;
}
tresult PLUGIN_API CxxProc::process(ProcessData& refProcessDat_Data)
{
if (refProcessDat_Data.inputParameterChanges != nullptr)
{
int32 int32ParamChangeCount = refProcessDat_Data.inputParameterChanges->getParameterCount();
for (int32 int32i = 0; int32i < int32ParamChangeCount; int32i++)
{
IParamValueQueue* pIParamValueQueue_Q = refProcessDat_Data.inputParameterChanges->getParameterData(i);
if (pIParamValueQueue_Q != nullptr)
{
int32 int32Tag = pIParamValueQueue_Q->getParameterId();
int32 int32valueChangeCount = pIParamValueQueue_Q->getPointCount();
ParamValue ParamValue_value;
int32 int32SampleOffset;
if (pIParamValueQueue_Q->getPoint(int32valueChangeCount - 1, int32SampleOffset, ParamValue_value) == kResultTrue)
{
switch (int32Tag)
{
case INT32_OPTION_MENU01:
int32OptionMenu01 = (int32)(ParamValue_value * 2.0f);//3択は0, 0.5, 1 →x2.0→ 0, 1, 2 、2択は0, 1 →x1.0→ 0, 1 同様に4択ならx3
break;
case PARAMVALUE_KNOB01:
ParamValue_Knob01 = (29.5f * ParamValue_value) + 0.5f;
break;
}
}
}
}
}
if (refProcessDat_Data.numInputs != 1 || refProcessDat_Data.numOutputs != 1)
{
return kResultTrue;
}
if (refProcessDat_Data.inputs[0].numChannels != 2 || refProcessDat_Data.outputs[0].numChannels != 2)
{
return kResultTrue;
}
Sample32* pSample32_inL = refProcessDat_Data.inputs[0].channelBuffers32[0];
Sample32* pSample32_inR = refProcessDat_Data.inputs[0].channelBuffers32[1];
Sample32* pSample32_outL = refProcessDat_Data.outputs[0].channelBuffers32[0];
Sample32* pSample32_outR = refProcessDat_Data.outputs[0].channelBuffers32[1];
for (int32 int32i = 0; int32i < refProcessDat_Data.numSamples; int32i++)
{
pSample32_outL[int32i] = pSample32_inL[int32i];
pSample32_outR[int32i] = pSample32_inR[int32i];
}
return kResultTrue;
}
tresult PLUGIN_API CxxProc::getState(IBStream* pIBStream_State)
{
pIBStream_State->write(&int32OptionMenu01, sizeof(int32));
pIBStream_State->write(&ParamValue_Knob01, sizeof(ParamValue));
return kResultOk;
}
tresult PLUGIN_API CxxProc::setState(IBStream* pIBStream_State)
{
tresult tresult_r;
tresult_r = pIBStream_State->read(&int32OptionMenu01, sizeof(int32));
if (tresult_r != kResultOk) { return kResultFalse; }
tresult_r = pIBStream_State->read(&ParamValue_Knob01, sizeof(ParamValue));
if (tresult_r != kResultOk) { return kResultFalse; }
return kResultOk;
}
tresult PLUGIN_API CxxEdit::initialize(FUnknown* pFUnknown_Context)
{
tresult tresult_r = EditController::initialize(pFUnknown_Context);
if (tresult_r == kResultTrue)
{
StringListParameter* pStringListParameter_param1 = new StringListParameter(STR16("OptionMenu_Para1"), INT32_OPTION_MENU);
pStringListParameter_param1->appendString(STR16("TEXT1"));
pStringListParameter_param1->appendString(STR16("TEXT2"));
pStringListParameter_param1->appendString(STR16("TEXT3"));
parameters.addParameter(pStringListParameter_param1);//SteinBerg::Vst::parameters.addParameter()
RangeParameter* pRangeParameter_param2 = new RangeParameter(STR16("Knob_Para1"), PARAMVALUE_KNOB, STR16(""), 0.5, 30.0, 0.5);
parameters.addParameter(pRangeParameter_param2);//SteinBerg::Vst::parameters.addParameter()
}
return tresult_r;
}
tresult PLUGIN_API CxxEdit::getState(IBStream* pIBStream_State)
{
return kResultOk;
}
tresult PLUGIN_API CxxEdit::setState(IBStream* pIBStream_State)
{
return kResultOk;
}
tresult PLUGIN_API CxxEdit::setComponentState(IBStream* pIBStream_State)
{
tresult tresult_r;
int32 int32TmpOptionMenu01;
tresult_r = pIBStream_State->read(&int32TmpOptionMenu01, sizeof(int32));
if (tresult_r != kResultOk) { return kResultFalse; }
ParamValue ParamValue_TmpKnob01;
tresult_r = pIBStream_State->read(&ParamValue_TmpKnob01, sizeof(ParamValue));
if (tresult_r != kResultOk) { return kResultFalse; }
ParamValue ParamValue_Normalvalue;
ParamValue_Normalvalue = plainParamToNormalized(INT32_OPTION_MENU, (ParamValue)int32TmpOptionMenu01);
setParamNormalized(INT32_OPTION_MENU01, ParamValue_Normalvalue);
ParamValue_Normalvalue = plainParamToNormalized(PARAMVALUE_KNOB, ParamValue_TmpKnob01);
setParamNormalized(PARAMVALUE_KNOB01, ParamValue_Normalvalue);
return kResultOk;
}
IPlugView* PLUGIN_API CxxEdit::createView(FIDString FIDString_Name)
{
if (strcmp(FIDString_Name, ViewType::kEditor) == 0)
{
return new VSTGUI::VST3Editor(this, "view", "myEditor.uidesc");
}
return nullptr;
}
} } // namespace SteinbergとVstの終わり
■コードの解説
このサンプルはOption MenuコントロールとKnobコントロールを追加するコードです。CxxProcクラスが内部的な計算だけをする役割のクラスで、CxxEditクラスがインターフェイスの制御をする役割です。
ヘッダファイルの28行目、29行目で列挙型を使って以下のようにIDとして機能する変数を定義しています。
- INT32_OPTION_MENU01 = 1
- PARAMVALUE_KNOB01 = 2
この番号を使って67行目以降でIDによる操作されたコントロールの識別と168行目以降でコントロールの初期化のときに使われるIDとしています。ヘッダファイルで定義したCxxProc内のアクセス指定子protected:部で対応する内部変数を以下のようにしています。
- int32OptionMenu01
- PARAMVALUE_Knob01
このメンバ変数に値を保持させます。メンバ変数には実際に利用するときの値が格納されます。今回の例ではint32OptionMenu01には0,1,2の3種類。PARAMVALUE_Knob01には0.5~30.0です。これらの値はDAWのホストが認識する際に0~1の値に置き換えて管理する必要もあります。
置き換えを正規化とも言います。0,1,2を0~1で管理する場合は、0 = 0.0, 1 = 0.5, 2 = 1.0のように割り当てられます。0~1の範囲になった値の事を正規化された値と呼ぶことにします。
0.5~30.0の場合は0.5が0.0に対応、30が1.0の対応になります。0.5~30.0のように0.5から始まる値を扱う場合は0が基準になるより差分で考えます。つまり、30 - 0.5 = 29.5 ですから、あたかも0~29.5の値を管理するように考え、戻す時には+0.5をする。後で0.5加えればいいので、それぞれの保持値を29.5で割った値を管理していれば0~1の値に変換できることになります。
0.5~30の範囲を0.0~1.0で表現するための関数がありまして、170行目にも登場するplainParamToNormalized()という関数です。第一引数に正規化するコントロールのID、第二引数に正規化される前の値を付与します。戻り値に正規化された値が返ります。
インターフェースで扱う場合は最初に初期化が必要です。初めてVSTインターフェイスが描画されるとき、EditControllerクラスから派生したクラスCxxEditクラスの関数CxxEdit::Initialize(FUnknown* context)が呼び出されます。129、135行目のような関数を実行してコントロールにOption Nenuコントロールに対してはID、Tagsの名称を結び付ける処理が必要です。Knobコントロールに対してはID、Tags名称、値の範囲、初期値、単位を結び付ける処理が必要です。
Optiom Menuコントロールのように文字列を制御する場合は129行目のStringListParameter関数で結び付けます。Knobコントロールのようにノブの回転量を制御する場合は135行目のRangeParameter関数で結び付けます。
- StringListParameter* pStringListParameter_param1 = new StringListParameter(STR16("OptionMenu_Para1"), INT32_OPTION_MENU);
- 第一引数にTags名称となる文字列を設定します。inline UI EditorでControl Tagという項目に設定する項目名です。この名称でUIのコントロールと連動するようになります。inline UI EditorでTags名称とその値として、第二引数のID番号が自動取得できるようになります。今回の場合はIDは1です。INT32_OPTION_MENU が 1 という値を保持しています。生成したクラス変数pStringListParameter_param1でOption Menuコントロールが保持する文字列の登録、追加ができます。
- 文字列の登録にはpStringListParameter_param1->appendString(STR16("TEXT1"));のようなメンバ関数を使います。これで TEXT1 という文字列が登録されます。STR16はu"xxxx"のようなリテラルと同じことで、UTF-16を扱えます。英語版だけあれば国際化できているようなものなので、日本語を使わない方が良いと思います。大規模なものであれば、各言語のUIを作るのも良いと思います。
- 最後にparameters.addParameter(pStringListParameter_param1);として、inline UI Editorと連動できることが決定づけられます。parametersはParameterContainer型の変数で、EditControllerクラスで定義されてる変数です。EditControllerクラスを継承しているクラスCxxEditで使えるParameterContainerに含まれるメンバ関数です。今回は3つの文字列 TEXT1, TEXT2, TEXT3を登録しましたので、最小値が 0、最大値が 2 として扱われます。初期値は先頭の TEXT1 になります。
- RangeParameter* pRangeParameter_param2 = new RangeParameter(STR16("Knob_Para1"), PARAMVALUE_KNOB, STR16(""), 0.5, 30.0, 0.5);
- StringListParameterと同じように第一引数と第二引数を設定し、第三引数は単位です。DAWによっては、単位を採用して、パラメータ操作時に表示してくれたりする仕組みがあります。第四引数が最小値、第五引数が最大値、第六引数が規定値になります。
- parameters.addParameter(pRangeParameter_param2);のようにして、StringListParameterと同じく登録します。
これでUIの初期化ができました。以下に今後の処理の流れを示します。
□■VST起動時の処理の流れ
1.CxxEdit の initialize(FUnknown* context)
- 解説済み。起動時にまずはインターフェイス、コントロールの初期設定をする。
2.CxxProc の getState(IBStream* state)
- DAWが保持するべき値を更新。CxxProcのprotected:メンバ変数のパラメータ値を更新。初期値
3.CxxEdit の setComponentState(IBStream* state)
- DAWが保持しているVSTパラメータの値を取得して、1~0の値に正規化して、各コントロールを更新。初期値
4.CxxProc の setState(IBStream* state)
- DAWが保持しているVSTパラメータの値を取得する。このとき、DAW側が、前回の起動で保存していた値がVSTのパラメータに取得できる。
5.CxxEdit の setComponentState(IBStream* state)
- 保持されるべき値が更新された可能性もあるため、再度DAWが保持しているVSTパラメータの値を取得して、1~0の値に正規化して、各コントロールを更新。
6.CxxEdit の setState(IBStream* state)
- DAWが保持しているVSTパラメータの値を取得して、インターフェイスの都合で、コントローラに反映するべきことがあれば、処理できるが今回の場合は何もしない。
□■VST終了時の処理の流れ
1.CxxProc の getState(IBStream* state)
- DAWが保持するべき値を更新。CxxProcのprotected:メンバ変数のパラメータ値を使う。DAWに覚えさせる。
2.CxxEdit の getState(IBStream* state)
- インターフェイスの都合で、DAWが保持するべき値を更新できる。今回の場合は何もしない。
□■VST起動中の処理
- VST起動中は絶えず CxxProc::Process(ProcessData& data) 関数が繰り返し呼ばれ、インターフェイスで操作した値はDAWが感知し、引数である ProcessData& 型の変数 data の中に変更されたコントロールのIDとその値が送られてきます。認められた遅延時間内で処理できるように分割してデータが送られてきます。そのひとまとめの中に何回かコントロールを変更する可能性もあるので、そのひとまとめの最後だけをパラメータの変更とするだけでパラメータ変更の影響を十分に再現できるのが通常です。どうしても分解能をあげたパラメータである必要がある場合は全てを処理するとよいと思います。遅延してよい時間内におさまる処理であるかが重要です。他のVSTやDAW内の処理もありますので、全部のリソースを独り占めして良いわけではありません。ほどほどに。処理が認められた時間より多く遅延した場合は音が途切れます。
- 起動時の2.番目の処理CxxProc の getState(IBStream* state)
- DAW側がパラメータを取得しています。
tresult PLUGIN_API CxxProc::getState(IBStream* pIBStream_State)
{
pIBStream_State->write(&int32OptionMenu01, sizeof(int32));
pIBStream_State->write(&ParamValue_Knob01, sizeof(ParamValue));
return kResultOk;
}
int型、ParamValue型の順にDAW側にパラメータを取得させています。int32OptionMenu01は0~2、ParamValue_Knob01は0.5~30.0で、実際の値です。
- 起動時の3.番目の処理CxxEdit の setComponentState(IBStream* state)
tresult PLUGIN_API CxxEdit::setComponentState(IBStream* pIBStream_State)
{
tresult tresult_r;
int32 int32TmpOptionMenu01;
tresult_r = pIBStream_State->read(&int32TmpOptionMenu01, sizeof(int32));
if (tresult_r != kResultOk) { return kResultFalse; }
ParamValue ParamValue_TmpKnob01;
tresult_r = pIBStream_State->read(&ParamValue_TmpKnob01, sizeof(ParamValue));
if (tresult_r != kResultOk) { return kResultFalse; }
ParamValue ParamValue_Normalvalue;
ParamValue_Normalvalue = plainParamToNormalized(INT32_OPTION_MENU, (ParamValue)int32TmpOptionMenu01);
setParamNormalized(INT32_OPTION_MENU01, ParamValue_Normalvalue);
ParamValue_Normalvalue = plainParamToNormalized(PARAMVALUE_KNOB, ParamValue_TmpKnob01);
setParamNormalized(PARAMVALUE_KNOB01, ParamValue_Normalvalue);
return kResultOk;
}
int32TmpOptionMenu01のような仮の変数にDAWから読み取って、値を保持させる。その変数を使って正規化をします。
- ParamValue_Normalvalue = plainParamToNormalized(INT32_OPTION_MENU, (ParamValue)int32TmpOptionMenu01);
- 第一引数にコントロールのID、第二引数に実際の値を付与すると戻り値として、正規化された値が取得できます。その正規化された値を使って、
- setParamNormalized(INT32_OPTION_MENU01, ParamValue_Normalvalue);
- のようにすると、第一引数のセットしたいコントロールのID、第二引数に正規化された値を付与すると、インターフェイスにDAW側で保持していた値でエディタのコントロールを変更できます。
- 起動時の4.番目の処理 CxxProc の setState(IBStream* state)
tresult PLUGIN_API CxxProc::setState(IBStream* pIBStream_State)
{
tresult tresult_r;
tresult_r = pIBStream_State->read(&int32OptionMenu01, sizeof(int32));
if (tresult_r != kResultOk) { return kResultFalse; }
tresult_r = pIBStream_State->read(&ParamValue_Knob01, sizeof(ParamValue));
if (tresult_r != kResultOk) { return kResultFalse; }
return kResultOk;
}
CxxProcのメンバ変数int32OptionMenu01にDAWの状態を読み取ります。同じように、次の変数にも読み取ります。型の名前と順番は大事ですね。
- 起動時の5.番目の処理CxxEdit の setComponentState(IBStream* state)
- 3.番目の処理と同じことをします。
- 起動時の6.番目の処理CxxEdit の setState(IBStream* state)
- 何もしません。
- 終了時の1.番目の処理CxxProc の getState(IBStream* state)
- DAW側がパラメータを取得しています。
- 起動時の2.番目の処理と同じです。
- 終了時の2.番目の処理CxxEdit の getState(IBStream* state)
- 何もしません。
- 起動中の処理
tresult PLUGIN_API CxxProc::process(ProcessData& refProcessDat_Data)
{
if (refProcessDat_Data.inputParameterChanges != nullptr)
{
int32 int32ParamChangeCount = refProcessDat_Data.inputParameterChanges->getParameterCount();
for (int32 int32i = 0; int32i < int32ParamChangeCount; int32i++)
{
IParamValueQueue* pIParamValueQueue_Q = refProcessDat_Data.inputParameterChanges->getParameterData(i);
if (pIParamValueQueue_Q != nullptr)
{
int32 int32Tag = pIParamValueQueue_Q->getParameterId();
int32 int32valueChangeCount = pIParamValueQueue_Q->getPointCount();
ParamValue ParamValue_value;
int32 int32SampleOffset;
if (pIParamValueQueue_Q->getPoint(int32valueChangeCount - 1, int32SampleOffset, ParamValue_value) == kResultTrue)
{
switch (int32Tag)
{
case INT32_OPTION_MENU01:
int32OptionMenu01 = (int32)(ParamValue_value * 2.0f);
break;
case PARAMVALUE_KNOB01:
ParamValue_Knob01 = (29.5f * ParamValue_value) + 0.5f;
break;
}
}
}
}
}
…
}
refProcessDat_Dataの中からパラメータを変更した情報と、その変更したコントロールTagIDと値を取得するものです。まずは、refProcessDat_Data.inputParameterChangesがnullptrでなくなったらという分岐で、コントロールの値に変化があったかどうかを確かめれます。次にint32 int32ParamChangeCount = refProcessDat_Data.inputParameterChanges->getParameterCount();として、int32ParamChangeCountに変更された数が取得されます。なにもしないVSTeで最近のPCだとどうあがいても1という値になるようです。でも、万が一に備えて、最後の変更値だけを取得するプログラムにします。もっと処理をつめこんだときになにかご利益がでるかもしれません。IParamValueQueue* pIParamValueQueue_Q = refProcessDat_Data.inputParameterChanges->getParameterData(i);として、i番目の待ち行列全体を取得します。1番目しかないですけどね。IParamValueQueue型のポインタ変数 pIParamValueQueue_Qという変数で保持できます。更にpIParamValueQueue_Qがnullptrでなければ、int32 int32Tag = pIParamValueQueue_Q->getParameterId();で変更されたコントロールのIDを取得します。そしてint32 int32valueChangeCount = pIParamValueQueue_Q->getPointCount();で待ち行列に存在する変更情報の数を取得します。たいてい1です。そしてpIParamValueQueue_Q->getPoint(int32valueChangeCount - 1, int32SampleOffset, ParamValue_value)で最後尾の変更情報、int32SampleOffsetにはどれくらいのサンプル数をはさんで受け取った情報なのかを取得できます。変更数が1のときはint32SampleOffsetは0です。ParamValue_valueには値が取得できます。このとき取得できる値は、正規化された値です。なのでint32Tagで切り分けれるコントロールID毎に実際の数値に戻す処理をします。正規化についてだいぶ前で触れた時にもとに戻す計算のやり方を説明したので、ここでは説明を省略します。
VSTeプログラミング初歩に戻る。