VST3のテストについて その２

[VST3制作の過去記事一覧はこちら](https://qiita.com/aike@github/items/05a14911bb89db8b6c2c)

# はじめに
音楽制作用DAWのプラグイン仕様VST3のテストついてインターネットのヤホーで調べました。今回はテストツールvalidatorから呼ばれる独自のテストを自作プラグインに組み込む方法の説明です。
この記事はVST3プラグインを書く人向けの内容です。

前回記事は[こちら](https://qiita.com/aike@github/items/a17234584efc690139d6)。

# １．テストファクトリー
まず、テストを生成するためのITestFactoryインタフェースを実装したクラスを用意します。

```TestFactory.h
#pragma once
#include "base/source/fobject.h"
#include "pluginterfaces/test/itest.h"

namespace Steinberg {

class PluginTestFactory : public ITestFactory, public FObject
{
public:
	tresult PLUGIN_API createTests(FUnknown* context, ITestSuite* parentSuite) SMTG_OVERRIDE;
	static FUnknown* createInstance(void*) { return (ITestFactory*)new PluginTestFactory(); }
	static FUID cid;

	OBJ_METHODS(PluginTestFactory, FObject)
	DEF_INTERFACES_1(ITestFactory, FObject)
	REFCOUNT_METHODS(FObject)
};

}
```

```TestFactory.cpp
#include "TestFactory.h"
#include "base/source/fstring.h"
#include "public.sdk/source/vst/testsuite/vsttestsuite.h"

// 自作のテストクラス
#include "CalculationTest.h"
#include "EditControllerTest.h"
#include "AudioProcessorTest.h"

namespace Steinberg {
// おまじない
DEF_CLASS_IID(ITest)
DEF_CLASS_IID(ITestSuite)
DEF_CLASS_IID(ITestFactory)
// GUIDは自分で生成してください
FUID PluginTestFactory::cid(0xXXXXXXXX, 0xXXXXXXXX, 0xXXXXXXXX, 0xXXXXXXXX);

tresult PLUGIN_API PluginTestFactory::createTests(FUnknown* context, ITestSuite* parentSuite)
{
	FUnknownPtr<Vst::ITestPlugProvider> plugProvider(context);
	if (plugProvider)
	{
		// 自作テストの登録
		parentSuite->addTest("Calculation Test", owned(new CalculationTest()));
		parentSuite->addTest("Edit Controller Test", owned(new EditControllerTest(plugProvider)));
		parentSuite->addTest("Audio Processor Test", owned(new AudioProcessorTest(plugProvider, kSample32)));
	}
	return kResultTrue;
}
}
```

ファクトリークラスは、以下のようにしてプラグイン本体に登録します。VST3.7から含まれるVST3_Project_Generatorで生成したプロジェクトの場合、XXXentry.cppというソースファイルがあると思います。（XXXは自分で指定したprefix）

```XXXentry.cpp
#include "TestFactory.h"

	// (省略)
	// BEGIN_FACTORY_DEFとEND_FACTORYの間に書いてください

	DEF_CLASS2(INLINE_UID_FROM_FUID(PluginTestFactory::cid),
		PClassInfo::kManyInstances,
		kTestClass,
		stringPluginName "Test Factory",
		0,
		"",
		"",
		"",
		PluginTestFactory::createInstance)

	// (省略)
```


# ２．コンポーネントに依存しないテスト
テストクラスはITestインタフェースを実装します。
数値計算などVST3のコンポーネントに依存しないテストの場合、次のように書けます。

```CalculationTest.h
#pragma once
#include "base/source/fobject.h"
#include "pluginterfaces/test/itest.h"

using namespace Steinberg;
class CalculationTest : public ITest, public FObject
{
public:
	// ITestインタフェース
	bool PLUGIN_API setup() SMTG_OVERRIDE;
	bool PLUGIN_API run(ITestResult* testResult) SMTG_OVERRIDE;
	bool PLUGIN_API teardown() SMTG_OVERRIDE;
	const tchar* PLUGIN_API getDescription() SMTG_OVERRIDE;
	// おまじない
	OBJ_METHODS(CalculationTest, FObject)
	DEF_INTERFACES_1(ITest, FObject)
	REFCOUNT_METHODS(FObject)
private:
	// テスト対象関数
	int add(int a, int b);
	int sub(int a, int b);
};
```

```CalculationTest.cpp
#include "pluginterfaces/base/fstrdefs.h"
#include "base/source/fstring.h"
#include "CalculationTest.h"

using namespace Steinberg;
//------------------------------------------------------------------------
// Tests
//
bool PLUGIN_API CalculationTest::setup()
{
	return true;
}

bool PLUGIN_API CalculationTest::run(ITestResult* testResult)
{
	bool result = true;

	result &= (add(1, 2) == 3); // 足し算のテスト例
	result &= (sub(5, 3) == 2); // 引き算のテスト例

	if (result)
	{  // 正常時のメッセージを出す例
		testResult->addMessage(String("Calculation Test OK"));
	}
	else
	{  // 異常時のメッセージを出す例
		testResult->addErrorMessage(String("Calculation Test Failed"));
	}
	return result;
}

bool PLUGIN_API CalculationTest::teardown()
{
	return true;
}

const tchar* PLUGIN_API CalculationTest::getDescription()
{
	return STR("Calculation Tests");
}

//------------------------------------------------------------------------
// Functions
//
int CalculationTest::add(int a, int b)
{
	return a + b;
}

int CalculationTest::sub(int a, int b)
{
	return a - b;
}
```

## 注意点
ここで皆さんに悪いお知らせがあります。
VST3 SDKはtResultというbool風の型がよく使われていて、kResultOk/kResultTrue=0は正常ステータスを表し、kResultFalse=1は異常ステータスを示します。
一方、validatorのテストで戻す値はbool型で、true=1が正常ステータス、false=0が異常ステータスとなり、1と0の意味がtResultと逆転します。
複数のテストの結果ステータスを論理演算したりするときにハマるので気をつけてください。

# ３．EditControllerのテスト

EditControllerクラスのテストは以下のように記述します。
今回テスト対象とするプラグインは、kVolumeIdという音量パラメータをひとつ持っているという想定です。

```EditControllerTest.h
#pragma once
#include "base/source/fobject.h"
#include "pluginterfaces/test/itest.h"
#include "pluginterfaces/vst/ivsttestplugprovider.h"

using namespace Steinberg;
using namespace Vst;

class EditControllerTest : public ITest, public FObject
{
public:
	EditControllerTest(ITestPlugProvider* plugProvider);
	// ITestインタフェース
	bool PLUGIN_API setup() SMTG_OVERRIDE;
	bool PLUGIN_API run(ITestResult* testResult) SMTG_OVERRIDE;
	bool PLUGIN_API teardown() SMTG_OVERRIDE;
	const tchar* PLUGIN_API getDescription() SMTG_OVERRIDE;
	// おまじない
	OBJ_METHODS(EditControllerTest, FObject)
	DEF_INTERFACES_1(ITest, FObject)
	REFCOUNT_METHODS(FObject)
protected:
	ITestPlugProvider* plugProvider;
};
```

```EditControllerTest.cpp
#include "pluginterfaces/base/fstrdefs.h"
#include "base/source/fstring.h"
#include "params.h" // 自作プラグインのパラメータIDの定義
#include "EditControllerTest.h"

using namespace Steinberg;

EditControllerTest::EditControllerTest(ITestPlugProvider* plugProvider)
	: plugProvider(plugProvider)
{
}

bool PLUGIN_API EditControllerTest::setup()
{
	return true;
}

bool PLUGIN_API EditControllerTest::run(ITestResult* testResult)
{
	// 前処理
	// setupで前処理をするとエラーが起きてもテストNGにできないので
	// 失敗の可能性がある前処理はrunでおこなう
	auto controller = plugProvider->getController();
	if (!controller)
	{
		testResult->addErrorMessage(String("Unknown IEditController"));
		return false;
	}

	// 初期化関数controller->initialize()はvalidatorが呼ぶのでここでは不要

	// ここからテスト本体
	bool result = true;

	// パラメータset後すぐにgetして値が一致するかのテスト
	result &= (controller->setParamNormalized(kVolumeId, 0.5) == kResultOk);
	ParamValue expected = 0.5;
	ParamValue actual = controller->getParamNormalized(kVolumeId);
	if (expected != actual)
	{
		result = false;
		String s;
		testResult->addErrorMessage(String("getParamNormalized()")
			+ String(" expected: ") + s.printFloat(expected)
			+ String(" actual: ") + s.printFloat(actual));
	}
	// ここまでテスト本体

	// 後処理
	plugProvider->releasePlugIn(nullptr, controller);
	return (result);
}

bool PLUGIN_API EditControllerTest::teardown()
{
	return true;
}

const tchar* PLUGIN_API EditControllerTest::getDescription()
{
	return STR("Edit Controller Test");
}
```

# ４．AudioProcessorのテスト

いよいよ大物の信号処理のテストです。自前で全部書くと大変なのでvalidatorソースのProcessTestクラスを継承して流用します。
まず、以下の4つのソースコードをプロジェクトに追加します。

* VST3_SDK/public.sdk/source/vst/testsuite/testbase.cpp
* VST3_SDK/public.sdk/source/vst/testsuite/processing/process.cpp
* VST3_SDK/public.sdk/source/vst/hosting/processdata.cpp
* VST3_SDK/public.sdk/source/vst/utility/stringconvert.cpp

```AudioProcessorTest.cpp
#pragma once
#include "base/source/fobject.h"
#include "pluginterfaces/test/itest.h"
#include "public.sdk/source/vst/testsuite/processing/process.h"
#include "params.h" // 自作プラグインのパラメータIDの定義

using namespace Steinberg;
using namespace Vst;

// パラメータ変更情報クラス
class ProcessDataChange : public IParameterChanges, public FObject
{        // 手抜き実装なので1種類のパラメータの1つの値しか扱えません
public:  // このくらい多用されるクラスはちゃんとした実装をSDKに含んでほしいなー
	ProcessDataChange()
	{
		paramQueue.init(kVolumeId, 1);
	}
	int32 PLUGIN_API getParameterCount() SMTG_OVERRIDE
	{
		return paramQueue.getPointCount();
	};
	IParamValueQueue* PLUGIN_API getParameterData(int32 index) SMTG_OVERRIDE
	{
		return &paramQueue;
	};
	IParamValueQueue* PLUGIN_API addParameterData(const ParamID& id, int32& index) SMTG_OVERRIDE
	{
		return &paramQueue;
	};

	OBJ_METHODS(ProcessDataChange, FObject)
	DEF_INTERFACES_1(IParameterChanges, FObject)
	REFCOUNT_METHODS(FObject)

protected:
	// これはSDKで用意されているQueueクラスだけど、なぜかaddPoint()の実装が空っぽだったりして中途半端で使いにくい
	ParamChanges paramQueue;
};

// -------------------------------------------------------------
// AudioProcessorテストクラス宣言はここから
class AudioProcessorTest : public ProcessTest
{
public:
	AudioProcessorTest(ITestPlugProvider* plugProvider, ProcessSampleSize sampl);
	// ITestインタフェース
	bool PLUGIN_API setup() SMTG_OVERRIDE;
	bool PLUGIN_API run(ITestResult* testResult) SMTG_OVERRIDE;
	bool PLUGIN_API teardown() SMTG_OVERRIDE;
	const tchar* PLUGIN_API getDescription() SMTG_OVERRIDE;
protected:
	ProcessDataChange processDataChange;
};
```

長くてそろそろ読むのも飽きてきましたね。
信号処理のテストとしては、振幅±1のサイン波を1周期与えて、音量調整した結果振幅の最大値がどうなったかをテストします。

```AudioProcessorTest.cpp
#define _USE_MATH_DEFINES
#include <cmath>
#include <algorithm>
#include "pluginterfaces/base/fstrdefs.h"
#include "base/source/fstring.h"

#include "params.h" // 自作プラグインのパラメータIDの定義
#include "AudioProcessorTest.h"

using namespace Steinberg;

AudioProcessorTest::AudioProcessorTest(ITestPlugProvider* plugProvider, ProcessSampleSize sampl)
	: ProcessTest(plugProvider, sampl)
{
}

bool PLUGIN_API AudioProcessorTest::setup()
{
	if (ProcessTest::setup() == false)
	{
		return false;
	}

	// Volumeパラメータを0.5にセット
	processData.inputParameterChanges = &processDataChange;
	int32 index = 0;
	IParamValueQueue* paramQueue = processData.inputParameterChanges->addParameterData(kVolumeId, index);
	if (paramQueue)
	{
		((ParamChanges*)paramQueue)->setPoint(index, 0, 0.5);
	}

	// 入力バッファに振幅±1のサイン波1周期分をセット
	int32 numChannels = processData.inputs[0].numChannels;
	Vst::Sample32** in = processData.inputs[0].channelBuffers32;
	for (int32 ch = 0; ch < numChannels; ch++)
	{
		Vst::Sample32* pIn = in[ch];
		for (int32 i = 0; i < processData.numSamples; i++)
		{
			*pIn = (Sample32)sin((double(i) / (double)processData.numSamples * 2.0 * M_PI));
			pIn++;
		}
	}

	return true;
}

bool PLUGIN_API AudioProcessorTest::run(ITestResult* testResult)
{
	if (!testResult || !audioEffect)
		return false;

	bool result = true;

	// 32bitと64bitの両方に対応しているかテスト
	result &= (audioEffect->canProcessSampleSize(kSample32) == kResultOk);
	result &= (audioEffect->canProcessSampleSize(kSample64) != kResultOk);

	audioEffect->setProcessing(true);

	// process()実行
	result &= (audioEffect->process(processData) == kResultOk);
	if (result == false)
	{
		audioEffect->setProcessing(false);
		return false;
	}

	// 波形をチェック
	int32 bus = 0;
	int32 numChannels = processData.outputs[bus].numChannels;
	Vst::Sample32** out = processData.outputs[bus].channelBuffers32;
	// 簡易的に左チャンネルだけチェック
	int32 leftCh = 0;
	// ボリュームを絞っているので最大値が0.5であることをテスト
	ParamValue expected = 0.5;
	ParamValue actual = *std::max_element(out[leftCh], out[leftCh] + processData.numSamples);
	if (expected != actual)
	{
		result = false;
		String s;
		testResult->addErrorMessage(String("getParamNormalized()")
			+ String(" expected: ") + s.printFloat(expected)
			+ String(" actual: ") + s.printFloat(actual));
	}

	/*
	// 波形データのダンプ
	Vst::Sample32* pOut = out[leftCh];
	for (int32 i = 0; i < processData.numSamples; i++)
	{
		String s;
		testResult->addMessage(String("data:") + s.printFloat(*pOut));
		pOut++;
	}
	*/

	audioEffect->setProcessing(false);
	return true;
}

bool PLUGIN_API AudioProcessorTest::teardown()
{
	return ProcessTest::teardown();
}

const tchar* PLUGIN_API AudioProcessorTest::getDescription()
{
	return STR("Audio Processor Test");
}
```

# ５．実行結果

実行結果はこんな感じです。Visual Studioの出力ウィンドウからのコピペです。

```
　　（省略）
1>[Accuracy: Block, 1 Parameters, Change every100 Samples]
1>Info:  ===Accuracy: Block, 1 Parameters, Change every100 Samples ====================================
1>Info:  64bit Audio Processing not supported.
1>[Succeeded]
1>
1>[Accuracy: Sample, 1 Parameters, Change every100 Samples]
1>Info:  ===Accuracy: Sample, 1 Parameters, Change every100 Samples ====================================
1>Info:  64bit Audio Processing not supported.
1>[Succeeded]
1>
※ ↑はvalidatorの標準テスト。前回記事参照
1>-------------------------------------------------------------
1>TestSuite : VolumeTest Factory
1>-------------------------------------------------------------
1>
1>[Calculation Test]
1>Info:  Calculation Test OK
1>[Succeeded]
1>
1>[Edit Controller Test]
1>[Succeeded]
1>
1>[Audio Processor Test]
1>[Succeeded]
1>
1>-------------------------------------------------------------
1>Result: 49 tests passed, 0 tests failed
1>-------------------------------------------------------------
```




いかがでしたか？　来週もまた見てくださいねー。んがくく。

[VST3制作の過去記事一覧はこちら](https://qiita.com/aike@github/items/05a14911bb89db8b6c2c)