[VST 개발] 전반적인 폴더 트리 구조 및 튜토리얼 따라하기 (2)
지난번에 이어서 이번에는 VST SDK 에서 제공하는 예제 튜토리얼을 작성해보도록 하자
예제 코드는 여기서 볼 수 있다
developer.steinberg.help/display/VST/Creating+a+new+audio+plug-in
Creating a new audio plug-in - VST - Steinberg Developer Help
메타 데이터의 끝으로 건너뛰기 Yvan Grabit님이 작성, 11월 17, 2020에 최종 변경 메타 데이터의 시작으로 이동
(여담이지만, 현재 2021년 1월 31일 기준으로 VST 문서가 2가지로 분리되어 있다. 하나는 깃허브를 기반으로 한 소개 페이지고 다른 하나는 스타인버그의 help안에 만들어져있다. 거의 비슷한 내용이지만 가끔 여긴 없는게 저긴 있어서 헷갈린다)
steinbergmedia.github.io/vst3_doc/index.html
Steinberg Plug-in Interfaces Documentation
steinbergmedia.github.io
developer.steinberg.help/display/VST/VST+Home
VST Home - VST - Steinberg Developer Help
메타 데이터의 끝으로 건너뛰기 Yvan Grabit님이 작성, 8월 03, 2020에 최종 변경 메타 데이터의 시작으로 이동 Welcome to the world of VST 3 This part of the Steinberg Developer Resource is a portal dedicated to developers of V
developer.steinberg.help
본격적인 예제 튜토리얼 작성에 앞서, 어떤 형태의 예제가 만들어질지 살펴보자

GUI 를 만들지 않을 것이기 때문에 VST3 PluginTestHost 상에서는 이런 모양의 플러그인이 만들어질 것이다
간단하게 입력되는 오디오 소스의 음량을 슬라이더를 통해 조절하고,
현재 조절되는 값은 옆의 0.5000 와 같이 표시된다
엄청 간단하지만 실제로 배우게 될 것은 다음과 같다
1. 파라미터 추가
: 이 플러그인에서는 "음량을 조절하는 슬라이더" 를 추가하게 된다
2. 파라미터 값을 확인
: 이 플러그인에서는 "조절하는 슬라이더의 위치에 따른 값" 을 파라미터의 값으로 받아오게 된다
3. 오디오 신호를 받아서 음량을 조절한 뒤 출력
: 이 플러그인에서는 "입력되는 오디오 샘플의 크기에 파라미터의 값을 곱해서" 음량을 조절하고 출력신호로 보내게 된다
먼저 지난번에 설정한대로 프로젝트를 만들었다고 가정하고, 실제 코드를 작성해보자
1. 파라미터를 추가해보자
이전 글에서도 작성했지만, 모든 VST3 의 파라미터는 고유의 id를 지정해주어야 한다
먼저 Header Files 의 cids.h 에 다음과 같이 작성한다
#pragma once
#include "pluginterfaces/base/funknown.h"
#include "pluginterfaces/vst/vsttypes.h"
enum GainParams: Steinberg::Vst::ParamID {
kParamGainId = 102 //파라미터에 사용될 id 번호
};
namespace MyCompanyName {...생략...}
namespace 윗쪽에 전체 프로젝트에서 사용하게 될 파라미터들의 enum 그룹을 만들고, 파라미터에 사용할 id 값의 변수를 만들어준다
(튜토리얼에서는 102로 지정했지만 실제로는 아무 숫자나 해도 상관없을 것으로 보인다. 나중에 다른 파라미터의 id 값과 중복되지만 않으면 된다)
그리고 Source Files 의 controller.cpp 에서 initialize 함수를 찾아서 그 안에 다음과 같이 작성해준다
#include "controller.h"
#include "cids.h"
using namespace Steinberg;
namespace MyCompanyName {
tresult PLUGIN_API GainControllerController::initialize (FUnknown* context) {
tresult result = EditControllerEx1::initialize (context);
if (result != kResultOk) {
return result;
}
parameters.addParameter(STR("GAIN"), //1. 파라미터의 타이틀
STR("dB"), //2. 파라미터의 단위
0, //3. 파라미터의 동작 모드
0.5, //4. 파라미터의 기본 값
Vst::ParameterInfo::kCanAutomate, //5. 파라미터의 플래그(속성)
GainParams::kParamGainId, //6. 파라미터의 id값
0, //7. 파라미터의 유닛 id값
STR("vol")); //8. 파라미터의 숏타이틀
return result;
}
...이하 생략
parameters의 addParameter 함수를 통해 파라미터를 생성할 수 있다
(parameters 는 ParameterContainer를 상속하는 값으로 기본적으로 controller.h 의 vsteditcontroller.h 를 통해 선언되어 있다
ParameterContainer 는 사용가능한 Parameter 들의 집합체라고 생각하면 된다)
addParameter의 인수(Arguments)는 최대 8가지인데, 천천히 살펴보면 쉽게 알수 있는 것들이다
가장 첫번째 인수부터 마지막 인수까지는 코드 위 주석을 참고하면 쉽게 파악 가능하다. 일단 여기까지 작성하고 빌드를 해보자

빌드를 해보면 다음과 같이 슬라이더와 여러가지 항목들을 볼 수 있게 된다
각 번호는 코드의 주석에 있는 번호와 동일하다
(6번 id의 경우 드러나는 부분은 아니기 때문에, 보이지는 않는다)
여기서 조금 주의깊게 봐야할 부분은 3, 5번의 파라미터의 동작모드와 파라미터의 플래그이다
VST SDK 에서는 3번 파라미터의 동작모드(VST SDK 에서는 StepCount 즉, 스텝의 횟수라고 한다)를 통해 값의 변화량을 조절할 수 있다
0으로 세팅할 경우 연속적인 값들을 입력 받을 수 있고, 1로 할 경우에는 2가지 값, 2로 할 경우에는 3가지 값으로 움직이게 된다
말보단 그림이 빠르니 각각의 0, 1, 2로 했을때 어떻게 동작하는지 살펴보자



각각 파라미터를 0이 아닌 다른 숫자로 했을 때의 동작들이다.
0일때는 연속적인 값들을 입력값으로 받을 수 있지만,
1일때는 On/Off 두가지,
2일때는 0/0.5/1.0 의 값을 입력으로 받아올 수 있다
즉, 동작 모드를 어떤수로 조절하는지에 따라 슬라이더 형식(연속적인 값)의 움직임을 만들어 내거나,
버튼(2가지 상태)혹은 셀렉터(특정한 상태)형식의 움직임을 만들어 낼 수 있는 것이다
*만약 이 숫자를 2 이상으로 한다면 그만큼 더 잘게 쪼개진 값으로 나뉘어진 불연속적인 값들만 선택할 수 있게 된다
이제 5번 파라미터의 플래그를 살펴보자.
플래그는 kCanAutomate, kIsBypass, kIsReadOnly, kIsWrapAround, kIsList, kIsHidden, kIsProgramChange 로 나뉘어지며 각각은 표시될 모양을 변경해줄뿐 아니라 어떤 형태인지 host application에 자동으로 전달해준다
추후 자세하게 살펴보도록 하고, 지금은 Automation이 가능한 kCanAutomate 로 설정해주고 넘어가보도록 하자
2. 이제 프로세스를 만들어보자
이제 만들어진 컨트롤러의 값을 토대로 음량을 조절할 수 있도록 코드를 작성해보자
먼저 processor.h 에 입력 값을 저장할 변수를 만들어주도록 한다
#pragma once
#include "public.sdk/source/vst/vstaudioeffect.h"
namespace MyCompanyName {
class GainControllerProcessor : public Steinberg::Vst::AudioEffect {
public:
...중략...
protected:
Steinberg::Vst::ParamValue mGain = 1.0; //파라미터의 값을 저장할 mGain 변수
}
}
mGain 이라는 변수의 타입은 ParamValue 이다. 즉 Parameter의 값을 저장할 수 있는 변수라는 의미로 해석해도 무관할듯하다
이제 process.cpp 에서 값을 처리할 수 있도록 해보자
제일 처음 해야할 일은 mGain에 우리가 만든 Parameter 를 통해 변화되는 값을 넣어주는 일이다
*한가지 체크할 점은 Parameter 의 값은 VST 내부라기보다는 Host Application(DAW)에서 받아오는 것이다.
즉, 보여지는 것들에 대한 정보는 VST 가 가지고 있지만 이 것을 실제 화면에 그리고, 또 움직이게 하는 것은 Host 이며, 이 Host 를 통해 사용자의 움직임을 받은 뒤 넘겨준다는 것이다.
#include "processor.h"
#include "cids.h"
#include "base/source/fstreamer.h"
#include "pluginterfaces/vst/ivstparameterchanges.h"
using namespace Steinberg;
namespace MyCompanyName {
...중략...
tresult PLUGIN_API GainControllerProcessor::process (Vst::ProcessData& data) {
if (data.inputParameterChanges) {
// 1. 입력된 파라미터의 변화값이 있는지 체크 한 뒤
int32 numParamsChanged = data.inputParameterChanges -> getParameterCount();
// 2. 변화한 값을 가지고 있는 파라미터의 갯수를 저장한다
for (int32 index = 0; index < numParamsChanged; index++) {
// 3. 파라미터의 갯수만큼 for 문을 돌린다
if (auto* paramQueue = data.inputParameterChanges -> getParameterData(index)) {
// 4. 파라미터의 인덱스 값을 가져온다
Vst::ParamValue value;
// 5. 파라미터의 값을 저장할 변수
int32 sampleOffset;
// 6. 현재 샘플 데이터의 범위
int32 numPoints = paramQueue -> getPointCount();
// 7. 주어진 큐 값의 포인트 카운트를 가져온다
switch (paramQueue -> getParameterId ()){
// 8. 파라미터의 아이디 값을 가져온다
case GainParams::kParamGainId:
// 9. 파라미터의 아이디 값이 우리가 지정한 kParamGainId 와 동일하다면
if (paramQueue -> getPoint(numPoints - 1, sampleOffset, value) == kResultTrue)
mGain = value;
// 10. 주어진 파라미터의 포인트와 샘플 범위에 해당하는 값을 찾아오고 이게 빈 값이 아니라면 mGain에 value를 넣어라
break;
}
}
}
}
...이하 생략...
}
솔직히 포인터가 난무하기 때문에 그냥 보기엔 매우 어렵다
(다행스럽게도 프로젝트를 처음 열었을 때 switch전까지는 주석으로 처리되어 있어서 작성에 있어서는 큰 문제는 없다)
그나마 다행인건 저 포인터들이 가리키는 함수들의 결과값들은 그리 어려운 것이 아니기 때문에 해석자체는 어렵지는 않다
정말 쉽게 풀어보자면
(파라미터 입력) -> (입력된 파라미터들을 리스트화) -> (리스트 중에 Gain 에 해당하는 id가 있다면) -> (그 id의 해당 값을 찾아서 저장)
이렇게 보면 된다
주의 할 점은 중간의 for문이 들어가는 것은 입력된 파라미터 값이 하나가 아닐수도 있기 때문이다
(즉, 음량도 조절하면서 동시에 EQ 값도 조절하는 슬라이더도 같이 생성되어 움직일수도 있다)
이 부분은 GUI의 샘플 블록에 대한 부분도 포함되어 있기 때문에 복잡하지만 찬찬히 뜯어 보면 그리 어렵지는 않다.
여기에 대해서는 나중에 시원하게 결론을 내린 뒤에 다시 작성하도록 하겠다
이제 저 if 문 아래로 다음과 같은 코드를 작성한다
#include "processor.h"
#include "cids.h"
#include "base/source/fstreamer.h"
#include "pluginterfaces/vst/ivstparameterchanges.h"
using namespace Steinberg;
namespace MyCompanyName {
...중략...
tresult PLUGIN_API GainControllerProcessor::process (Vst::ProcessData& data) {
if (data.inputParameterChanges) {
...중략...
}
if (data.numInputs == 0 || data.numSamples == 0)
return kResultOk;
// 1. 만약에 데이터의 오디오 인풋 버스 갯수가 0이거나 프로세스 되는 갯수가 0일때는 ok로 리턴시켜라
// 아마 이건 입력이 없거나 처리되는 샘플이 없을때는 그냥 디폴트 상태로 만들어주는 조건이라 판단된다
int32 numChannels = data.inputs[0].numChannels;
// 2. 오디오 입력의 채널 갯수를 저장한다(즉 몇개의 인풋 갯수를 가지고 입력이 들어오고 있는지)
void** in = getChannelBuffersPointer (processSetup, data.inputs[0]);
void** out = getChannelBuffersPointer (processSetup, data.outputs[0]);
// in과 out이라는 이중 포인터를 만들어서 인풋과 아웃풋 채널의 버퍼의 포인터를 저장한다
}
...이하 생략...
}
이걸 보고 C++ 에서 포인터를 제대로 이해 못하면 큰일나겠다라는 생각이 들었다
코드 자체는 간단하지만 작성이 더럽게 복잡하다
일단 입력 값이 들어오면 in, out 이라는 이중 포인터를 만들어서 버퍼를 담고 있는 포인터의 주소를 저장한다
(아마 지금 현재 채널에 들어오고 있는 버퍼의 주소를 역참조 하기 위함이라고 생각한다. 왜 인지는 이 다음 코드에서 알수 있다)
여기까지 작성하면 아마 에러가 날수도 있다. 저 getChannelBuffersPointer 가 에러가 날텐데 이럴때는 이렇게 해결하면 된다
#include "processor.h"
#include "cids.h"
#include "base/source/fstreamer.h"
#include "pluginterfaces/vst/ivstparameterchanges.h"
#include "public.sdk/source/vst/vstaudioprocessoralgo.h"
// 프로세서 알고리듬이 저장되어 있는 헤더파일을 불러온다
using namespace Steinberg;
...생략...
이제 에러를 해결했으면 계속 진행해보자
int32 numChannels = data.inputs[0].numChannels;
void** in = getChannelBuffersPointer (processSetup, data.inputs[0]);
void** out = getChannelBuffersPointer (processSetup, data.outputs[0]);
----여기서부터 다시 작성----
data.outputs[0].silenceFlags = 0;
// silenceFlags 를 0으로 만들어준다(이건 나중에 설명)
float gain = mGain;
// gain 이라는 float 변수에 mGain 값을 넣어준다
for (int32 i = 0; i < numChannels; i++) {
// 채널 갯수만큼 계속 for문을 돌려준다
// 2개의 스피커를 사용하는 스테레오일 경우 0, 1을 계속 돌려준다
int32 samples = data.numSamples;
// data.numSamples 만큼 샘플 갯수를 가져오고
Vst::Sample32* ptrIn = (Vst::Sample32*)in[i];
// 입력된 버퍼의 주소 배열의 값들을 ptrIn이라는 변수에 넣는다
Vst::Sample32* ptrOut = (Vst::Sample32*)out[i];
// 출력될 버퍼의 주소 배열의 값들을 ptrOut이라는 변수에 넣는다
Vst::Sample32 tmp;
while (--samples >= 0) {
// 샘플 갯수를 계속 줄여서 0이 될때까지
tmp = (*ptrIn++) * gain;
// 입력된 버퍼의 주소 배열들의 실제 값에 gain의 값을 곱해서 tmp에 저장한 뒤
(*ptrOut++) = tmp;
// 이 값을 출력 버퍼의 주소의 값으로 넣는다
}
}
여기서도 포인터의 향연이다
그래도 역시 구조는 크게 어렵지 않다
요점은 입력 channel 수만큼 버퍼를 가져온 뒤, gain 값을 인풋 값에 곱한 뒤 이걸 다시 아웃풋으로 돌려주는 것이다
단지, 이 작업을 포인터를 통해 해준다는 점이 특징인데, 아마 이는 오디오 버퍼에 작성할 값을 직접 바꿔주는게 아니라 오디오 버퍼의 주소들을 참조해서 값을 넣어준다는 걸로 해석된다
(이래야 메모리의 누수도 없고, 안전하게 버퍼의 값을 바꿔줄 수 있는게 아닌가 하고 추측하는 중이다)
여기까지 입력하고 Host Application을 실행한 뒤 동작시켜보면 입력 신호의 음량이 슬라이더의 움직임에 따라 커지거나 작아지는 것을 확인할 수 있다
*TIP
XCode 에서 빌드한 뒤, 제대로 나오는지 확인하기 위해서 Host를 키는 일은 매우 불편하다. 또한 궁금한 값들을 디버그해보고 싶은데 Host에서는 나오질 않으니 이것도 나름 보고 싶다할때는 XCode 의 Scheme 을 Host Application으로 지정해주면 된다
XCode 프로젝트를 연 뒤, 가장 윗쪽 메뉴에서 Products - Scheme - Edit Scheme... 누르면 다음과 같이 나온다

여기서 중간의 Executable 을 원하는 Host Application 으로 선택해주면 빌드할 때마다 선택한 Host Application 이 실행되게 된다
또한 이상태에서 VST3를 불러오면 디버그 창에서 메시지도 볼 수 있게 된다