VSTi開発概要2002.10.18

はじめに

このページでは、VSTi開発(特にプログラミング)の概要を自分用の メモ的なことも含めてまとめておきます。実はあまりドキュメントを読んで ないので、解釈の間違ってるものもあるかもしれません。もし決定的な 間違いがあれば、連絡いただけると幸いです。 今のところGUI部分をのぞいた部分のVSTiの概要をのみになっています。 。GUI部分に関しては、次の機会に示したいと思います。 また、シンセの信号処理部分そのものについても触れていません。こちらも 別の機会にということで。 なおVST SDK2.0で、プラットフォームはWindowsを前提にしています。

VSTiの概要

簡単に言えば、VSTiそのものはAudioEffectXクラスを継承した独自の クラスを定義して、実装すべきメソッドを実装するだけ。 それとホストアプリがVSTiを起動(=生成)する為のインタフェースとして、 main()という名前のエキスポート関数を書く必要がある。 以上をC++で書いてDLLとしてコンパイルリンクすれば、VSTiの出来上がり。

Synth1で実装したVSTi関連メソッド
以下VSTのSDK2.0付属のサンプル(VSTSYNTHxxx.cpp)と照らすと理解しやすいと思います。

0.コンストラクタ/デストラクタ
まず、VSTiのきまりにのっとった初期化処理、たとえば「入出力チャネル数はいくつか?」 などの指定や、ユニークIDの設定、GUIを持つ場合はその登録(実際の登録処理はGUIオブジェクトの コンストラクタから行われているようだ。)を行う。 それと、シンセ固有の初期化もここで行えばいい。 あと重要なこととして、プリセット音色の数と、シンセパラメタの数の指定をここで行う。 (親クラスのコンストラクタ呼び出し時に指定する。) このあたりは、VST2.0 SDKのサンプル参照。


1.簡単なメソッドから。。。
(1)bool getEffectName (char* name)
→VSTiの名前を教えてください要求。

素直に名前をおしえてあげればOK。 synth1では、
strcpy (name, "Synth1 V1.01");
return true;
で終わり

(2)bool getVendorString (char* text)
→ベンダー名を教えてください要求。

素直にベンダー名をおしえてあげればOK。 synth1では、
strcpy (text, "Daichi");
return true;
で終わり

(3)bool getProductString (char* text)
→製品名を教えてください要求。

これも、名前と一緒で、(1)と同じにしました。

(4)bool getOutputProperties(long index, VstPinProperties* properties)
→出力プロパティを教えてください要求。

実はよくわかってないけど、サンプルとまったく同じで、文字列の部分だけ、
sprintf (properties->label, "Synth1 %1d", index + 1);
として終わり。

(5)long canDo (char* text)
→コレができるかどうか示してください。

これも実はよくわかってないけど、サンプルとまったく同じ。
receiveVstEvents→できます!
receiveVstMidiEvent→できます!
と答えています。


2.音色プログラム管理関連メソッド
VSTiでは、音色のカレントプログラムの概念があり、各プログラムは、 名前(24文字だったかな?)とともにそのパラメタを管理する必要がある。

(6)void setProgram(long programNo)
→プリセットをチェンジしろ指示。

シンセ固有のプリセット値を読み込む。 なおVSTiでは、カレントプログラムの概念があるので、同時にcurProgramメンバ変数を programNoに更新する必要あり。(これをしないと、CUBASEではプリセットチェックの表示が 更新されなかった。curProgramなんてプレフィックスついてない名前なので、 最初はメンバ変数だとはぜんぜん思わなかった。)

(7)bool getProgramNameIndexed (long category, long index, char* text)
→プリセット名教えてください要求

index番目(0ベース)の音色名(Synth Bass1とか)をtextにコピーして終わり。 カテゴリはaeffect.hで定義されてるが無視して問題ないと思う。
※Synth1ではプリセットの選択は、シンセ本体で行わせたかったので、
lstrcpy(text,"no vst preset");
return true;
としてるだけ。 知らないindexなら、falseを返す。

(8)bool copyProgram (long destination)
→カレントプログラムの内容を指示されたプリセット番号にコピーしろ指示。

カレントプログラムの全パラメタをdestination番のプログラムにコピーする。 Synth1では、そんなのいやなので何もしていない。 なんか、余計なお世話だ。というような感じのするメソッド。。。 解釈を間違えているのかも。 コピーしなかったらfalseを返す。

(9)void setProgramName(char *name)/getProgramName(char *name)
→カレントプログラムの名前を変更しろ指示/名前を教えてください要求

Synth1ではsetProgramNameに対しては何もしていない。 getProgramNameに対しても
lstrcpy(text,"no vst preset");
としているだけ


3.音色パラメータ、オートメーション関連メソッド
(10)void getParameterLabel (long index, char *label)
→パラメタの単位名を教えてください要求

index番のパラメタに対応する単位を教えてあげる。独自のGUI表示を もたないVSTiとかで使われるのだろう。 Synth1では、
*label=0;
で終わり。

(11)void getParameterDisplay (long index, char *text)
→パラメタの値を表示するテキストを教えてください要求

カレントプログラムのindex番のパラメタを表示するときに 実際どういうテキストで表示するかを教えてあげる。 これも独自のGUI表示をもたないVSTiとかで使われると思う。 なお、VSTでは各パラメタを0〜1の範囲の小数点で扱うのが流儀のよう なので注意が必要。 詳細はサンプル参照。 Synth1では、これに苦労しました。このことを最初から知っていれば。。。

(12)void getParameterName (long index, char *label)
→パラメタ名を教えてください要求

index番のパラメタ名を"VCF Cutoff Frequency"などと教えてあげればよい。 ただし、ここで*label='\0'などとしてあげると、そのパラメタをエディット できないようにすることができる。(たとえばFruityの場合、パラメタエディットの 一覧表示にでてこなくなる。) Synth1では、ポリフォニック数や、バンク番号などはこれを使ってエディット できないようにしている。

(13)float getParameter (long index)
→カレントプログラムのパラメタ値を教えてください要求

カレントプログラムのindex番のパラメタ値をfloat型で返せばよい。 VSTは各パラメタを0〜1の範囲の小数点で扱うのが流儀のようです。 しかし、Synth1内部ではそんな風に扱ってないので、パラメタ毎に 変換して返しています。 ここで返してあげたものが、ホストによって曲データとともに保存され る。

(14)void setParameter (long index, float value)
→カレントプログラムのパラメタ値を変更しろ指示

カレントプログラムのindex番のパラメタ値をvalueに変更する。 これはオートメーションの再生時や、曲データをロードした時に 保存していたパラメタ値を再現するために呼ばれる。 これも、valueは0.0〜1.0です。 ここで値が変更された場合、個別GUIの表示(つまみ位置等)も変更する 必要がある。そのための処理もここで行う。 が、直接この関数からつまみ表示等を書き換えてはいけない。 どうするのかというと、 具体的にには、
editor->postUpdate();
っていうのを使う。 editorとは個別GUIのオブジェクトを指している。 この意味は、GUI君に「パラメタが変更されたから後で再描画してね」っていう 事を伝えているだけです。 こうしておくと、この関数から戻ったあとに、しかるべきタイミングで ホストがGUI君に再描画指示(updateメソッドコール)を行ってくれる。 このしくみによって無用な再描画を省くことができ、パフォーマンスが向上する。
ただし、Synth1ではもうちょっと凝った事をしてます。上の仕組みだけだとGUI君は再描画するとき(updateメソッド)に、「パラメタが変化」したって 事はわかるが、「どのパラメタが変化したの?」ってがわからないので、全部 のパラメタ(つまみなど)を再描画しなくちゃいけない。 これでは非効率なので、Synth1では、独自にパラメタ番号も通知して、 それを管理するようなしくみをいれてます。


4.ここが要。実際の信号処理関連メソッド
(15)void setSampleRate(float sampleRate)
→サンプリング周波数はコレにきまりました通知

サンプリング周波数が通知されてくるだけです。 本来はこれにあわせてシンセの各種係数を再計算などするのだと 思います。 Synth1では44.1K固定なので、係数計算などは何もしてません。
サンプルと同じように、
AudioEffectX::setSampleRate (sampleRate);
を呼んでるだけです。

(16)void setBlockSize (long blockSize)
→ブロックサイズはコレにきまりました通知

ブロックサイズなるものが通知されてくるだけです。 これもサンプルと同じように、
AudioEffectX::setBlockSize (blockSize);
を呼んでるだけです。
ブロックサイズとなにか。 これは、イベント処理(processEvent)をする際に必要になる 基準サイズだと思ってもらえばいいと思います。 midiイベントの時間情報は、このサイズの範囲内に収められています。 (のはずです。) 詳細はprocessEvent()で説明します。

(17)void resume ()
→レジューム??

サンプルと同じです。
wantEvents();
これは、イベントループ関連だと思いますがここは、いじくる必要ないと思います。

(18)void process (float **inputs, float **outputs, long sampleFrames)
→信号処理をして、出力バッファに入れなさい指示その1

ここです。ここが実際の音声信号処理を行うところです。 シンセなので、入力inputsは無視です。 指示されたsampleFramesだけ、信号を生成します。 ステレオなら出力バッファは、outputs[0],outputs[1]がそれぞれLRです。 ただし、出力信号をそのままoutputs[0],[1]に書き出すのではなく、 もともと入っている信号に加算する必要があります。 なお、信号レンジは-1.0〜+1.0が適正値です。
ここでは、sampleFrames回の信号処理ループを行うことになりますが、 ループ中に、適宜midiイベントを取り出して処理を進めていく必要が あります。この辺の処理が厄介なのですが、サンプルではこの辺の処理は かなり省略されています。
たとえば、ドの音がnoteオンの状態ではドの音を出力すればよいですが、 noteOffになった時からは、出力を切るかまたは、リリース用の音に切り替えて 信号を出力しないといけません。これは、noteOn/Offに限らず、ピッチベンドなど あらゆるmidiメッセージでもいえることです。 要するにMIDIの時間情報と同期を取る必要があります。 そのために、信号処理ループの中でmidiメッセージの取り出しを行う必要があるのです。 では、この同期に必要な情報はなにかというと、
a)midiイベント内の、deltaFrames
b)現在、信号処理を行っているカレントサンプルFrame
c)一連のmidiメッセージの中でのカレントmidiイベント番号

になります。b)とc)は、VSTiのフレームワークにはどうも取り入れられていないような ので、自前でメンバ変数として用意する必要があります。といっても、とても簡単なものです。 これらは、曲の先頭を0として延々続くものではなく、processEvent()が呼ばれた時に リセット=0として(基点として)、あとは、process()/processReplacing()で処理する度に、 進めればいいのです。 また、a)のdeltaFramesも、そのようにprocessEvent()が呼ばれた瞬間を基点した値になっています。 c)の一連のmidiメッセージは、processEvents()で引き渡ってきます。 上記を前提に擬似コードをかくと、
// メンバ変数
VstMidiEvent *m_events;	// 一連のmidiイベント
int m_eventNum;		// 一連のmidiイベントの数
int m_currentEventNo;	// c)カレントのmidiイベント番号
int m_currentSample;	// b)カレントサンプルFrame

void MySynth::process(float **inputs, float **outputs, long sampleframes)
{
	int nRestSample=sampleframes;
	VstMidiEvent *pE;
	int nProcessSample;

	// 信号処理ループ
	while(nRestSample > 0){
		// 次のイベントまでの処理できる時間を求める
		if(m_currentEventNo<m_eventNum){
			// 次のイベントあるので処理時間求める
			pE = (VstMidiEvent*)&m_events[m_currentEventNo];
			nProcessSample=pE->deltaFrames-m_currentSample;
			nProcessSample=MAX(0,MIN(nRestSample,nProcessSample));
		}else{
			// もうイベントはないので残り全部いっきに処理
			pE=NULL;
			nProcessSample=nRestSample;
		}

		// 信号処理
		int nRestSample2;
		nRestSample2=nProcessSample;
		while(nRestSample2>0){
			// 信号処理
			xxxxx;
			xxxxx;
			xxxxx;
			nRestSample2 --;
		}
		m_currentSample += nProcessSample;	// カレントサンプル進める

		// MIDIイベント処理
		if(pE){
			MyProcessMIDIShortData(pE->midiData[0],pE->midiData[1],pE->midiData[2]);
			m_currentEventNo++;		// カレントイベント進める
		}

		nRestSample -= nProcessSample;
	}

}
という感じです。
(19)void processReplacing (float **inputs, float **outputs, long sampleFrames)
→信号処理をして、出力バッファに入れなさい指示その2

基本的にprocess()と同じです。違いは、process()では、outputsの値に加算したのに対し、 ここでは、そのまま上書きすることです。

(20)long processEvents (VstEvents* ev)
→イベント処理

ホストから、midiメッセージが送られてきます。 正確には、VSTイベントと言われるひとつ抽象度か上のものなので、 ここから、midiイベントだけをバッファリングしておきます。 同時に、カレントサンプルなどをリセットします。 Synth1では、こんな感じです。
m_eventMaxNum;	// バッファ(=m_events)のサイズ(単位はイベント数)です。

long Synth1VST::processEvents (VstEvents* ev)
{
	m_currentEventNo=0;	// カレントサンプルFramesリセット
	m_currentSample=0;	// カレンロ
	m_eventNum=0;		// イベント数0リセット
	if(ev->numEvents>0){
		if(m_eventMaxNum < ev->numEvents){
			// 今のバッファサイズで小さすぎるので、サイズ拡張
			if(m_events) delete m_events;
			m_eventMaxNum = ev->numEvents;
			m_events = new VstEvent[m_eventMaxNum];
		}
		// MIDIイベントをコピー
		for(int i=0;i<ev->numEvents;i++){
			if(ev->events[i]->type==kVstMidiType){
				m_events[m_eventNum]=*(ev->events[i]);
				m_eventNum++;
			}
		}
	}
	return 1;	// want more
}



Copyright © 2002 by Daichi. All rights reserved.
return home