|
||||
Introduction
If you're here then I suppose you might be a plugin developer frustrated with Steinberg's VST3 SDK - and their recent announcement that they will fully depreciate VST2 within 24-months of January 2022 - and thus you might suddenly have found yourself asking, "How do I work this? How did I get here? This is not my beautiful VST2!" If you are reading this sometime in 2022 or 2023, you might find yourself in some kind of existential crisis, needing to port your old VST2 plugins to the VST3 standard or else face losing all current and potential Cubase customers (and you don't want to use JUCE, for whatever reason). Well brother (or sister), I'm here to tell you it's ok; take a deep breath, calm down, and we'll get through this digital crisis together! Yes, VST3 is an over-engineered, poorly documented, obtuse framework that no developer really likes. And sure, it's a shame that Steinberg feels the need to fully depreciate VST2 when it still works perfectly (yes, even on Arm64 processors if you just replace the outdated GUI code!) And yes, you are in for weeks or months of pain as you needlessly fritter away your all-too-short-existence on redoing a bunch of your work that you've already done, but hey - that's software baby!
Getting Started
Many years ago, maybe a decade back, I downloaded the VST3 SDK, opened up and - as I'm sure as many other developers have done since - said "Nope!" closed the damn thing, and completely forgot about it...until now! The first thing you'll want to do is download the VST3 SDK from...somewhere (I'm not linking to it because half the Steinberg links I click on these days 404). Next, Google "Will Pirkle How to setup the VST3 SDK's Sample Plugin Projects," and hopefully his video showing you how to use CMake to build the .lib files that the example projects need still exists. After that, I recommend taking one of those example projects and trying to build and/or modify it to run your plugin in (I'm using the note_expression_synth project because I needed MIDI stuff. It's probably not the best project to use as there is a bunch of extra junk in there. You might start with again or adelay if you don't need MIDI, and some of the MDA examples are a bit cleaner than Steinberg's code).
In your project, you will need to setup unique FUID identifiers in this format:
To be used here: (your plugin's entry point in againentry.cpp, factory.cpp, ect.):
DEF_CLASS2 INLINE_UID_FROM_FUID(Steinberg::Vst::MySynth::ControllerWithUI::cid),
Essentially, this replaces the 4-character unique identifier of VST2 as well as some of the VST2 attributes like isSynth() and in more in line with how AAX does things. How can you generate the FUID number in the correct format? Steinberg's Project Generator will do it for you. Unfortunately for me, that thing doesn't work so I made a console application to generate unique FUIDs - if you are brave enough to use executable files from strangers, you can download it here and it will generate random FUID identifiers for you to use in your projects.
if (data.symbolicSampleSize == kSample32)
processAudioBuffer<Sample32>((Sample32**)out, data.outputs[0].numChannels, data.numSamples); //<---Actually process the audio block as you would in VST2
else if(data.symbolicSampleSize == kSample64)
processAudioBuffer<Sample64>((Sample64**)out, data.outputs[0].numChannels, data.numSamples); //<---Actually process the audio block as you would in VST2
Don't worry about Samples64, and Samples32, they are just typedef floats and doubles because Steinberg!!!
}
Again we encounter the bifurcated nature of the design: the Controller keeps track of the Parameter type in a ParameterContainer, well the Processor keeps track of ParamValue types loosely or in a data type like GlobalParameterState - double the work, double the fun! I suppose Steinberg's intention here is for you to do sample-accurate automation or something but the end result is a bit of a mess!There are many intricacies associated with communicating parameter information between the Processor and Controller classes, and we will discuss them at length later on. ParamValue is just another typedef double (the VST3 standard calls for keeping these normalized to values between 0 and 1). Parameter is the actual Parameter Class.
People Programmers might have a long discussions about the best way to implement a GUI class in VST3, but I believe the easiest way is to sublcass the VST3Editor and VST3EditorDelegate classes. Steinberg really, really, really wants you to use their WYSIWYG editor and .uidesc description files. Well, that's one way to do things if you are building plugins from scratch, but having already built all my GUI views progmatically, I'd much prefer to throw the WYSIWYG editor in the trash and reuse all my old code, so I will show you exactly what functions you need to override to get things working correctly. Plus, as far as I can tell, WYSIWIG editor is just an abstraction layer sitting on top of the old VSTGUI code, although there have been some changes here and there.
I'll also show you how to get window resizing working properly...although I've noticed not too many plugins have embraced it ;)
Parameter Handling - A Closer Look
Since so much of what we do revolves around user parameters, let's have a closer look at those first. If you are using the note_expression_synth example project, then a GlobalParameterState struct has been defined for you. It's not strictly necessary to use this datatype: you could also just hold the parameters in your Processor as loose doubles or in another data structure you've defined, but it's there so I'm using it. The important thing to remember is that these values are local to the Processor and will need to be set whenever they are changed in the Controller (and vice-a-versa). Why not just used getParameterNormalized() whenever you need the value of a parameter? Strictly speaking, there's no way to do this from the Processor, because these are now Controller functions that can't be called from the Processor (unless maybe perhaps if you have created an IConnectionPoint, which we will try to avoid).
So then, how do we set local parameter values? Two ways: first, by getting the state:
Host Writing to Processor Parameters:
if (!s.writeDouble(windowHeight))
if (!s.writeDouble(windowWidth))
This is how the Host/DAW can get, set, and store/restore parameter values: by retrieving or setting the "state." When your plugin is first loaded, the Host will call tresult Processor::getState (IBStream* stream), tresult Processor::setState (IBStream* stream), tresult Controller::setComponentState (IBStream* state):
Host Reading From Processor Parameters:
if (!s.readDouble (masterVolume))
if (!s.readDouble(multiThread))
if (!s.readDouble(cutAttack))
if (!s.readDouble(windowHeight))
if (!s.readDouble(windowWidth))
After a parameter change or on close, the Host will call tresult Processor::getState (IBStream* stream) to save the "state" (the values of your local Parameters).
Setting Controller Parameters with Processor Parameters:
}
The implication should be clear: if you want parameter values to persist beyond the current session of your plugin and be restored when the user reloads a project:
That should take care of Parameter values sent from the Processer to the Controller/Host, but what about the other way around? Well, believe it or not, this is also handled in our old friend, the process() function:
Setting Processor Parameters When Controller Parameters Have Changed:
Please take careful note: YOU MUST CALL tresult Controller::performEdit (ParamID tag, ParamValue valueNormalized) WHEN SETTING CONTROLLER PARAMETERS OR THE CHANGE WILL NEVER BE PUSHED INTO THE IParameterChanges* QUEUE, AND YOUR PROCESSOR WON'T KNOW THAT THE VALUE HAS CHANGED!!!
Notice the extra Parameter that does stuff: kSustainPedal. This is a hack in my plugin to implement support for the sustain pedal as there is no built-in Midi CC support in VST3 (more on that later!). I capture the data and stuff it into an Event buffer I have created for use elsewhere.
The Controller parameter values are fairly simple to implement. They are of type Parameter* and are held in ParameterContainer parameters object of the EditController base class. You can add parameters in the initialize function of your Controller class:
Adding Parameters to the Controller
Notice the parameter entry for kSustainPedal from earlier: the way VST3 handles MIDI events is to let the Host pass a long whatever MIDI event it feels like passing a long. Hopefully, THE HOST TAKES ALL MIDI CC EVENTS, CONVERTS THEM INTO THE STEINBERG EVENT TYPE, AND PASSES THEM AS PARAMETER CHANGES ;). Please take note: YOU NEED TO ADD THESE AS PARAMETERS HERE OTHERWISE THEY WON'T WORK IN CERTAIN DAWS - you don't have to write them to the global parameter state or keep track of their values, but they must exist as actual parameters in the parameter container to work correctly! There is a special function you need to overwrite to tell the Host what MIDI events you want to receive and what their corresponding tags will be:
Defining What Midi Events We Will Handle
Unless you need to do something special with the parameters in the Controller there isn't any reason to override tresult Controller::setParamNormalized (ParamID tag, ParamValue value) or tresult Controller::performEdit (ParamID tag, ParamValue valueNormalized). However, maybe you do want to do something in the Controller instead of the Processor? Again, there is some confusion about what the Processor is supposed to do and what the Controller is supposed to do so you will have to make decisions about where exactly to set up data structures and other things you may need...but I digress. If you do need to do something with the Controller's parameters, makes sure to call the base class function too:
//Do whatever Wacky stuff you need to do!
tresult PLUGIN_API Processor::getState(IBStream* state)
{
IBStreamer s (stream, kLittleEndian);
if (!s.writeDouble(masterVolume))
return kResultFalse;
if (!s.writeDouble(multiThread))
return kResultFalse;
if (!s.writeDouble(cutAttack))
return kResultFalse;
return kResultFalse;
return kResultFalse;
tresult PLUGIN_API Processor::setState (IBStream* stream){
IBStreamer s (stream, kLittleEndian);
return kResultFalse;
return kResultFalse;
return kResultFalse;
return kResultFalse;
return kResultFalse;
tresult PLUGIN_API Controller::setComponentState (IBStream* state){
GlobalParameterState gps;
tresult result = gps.setState (state);
if (result == kResultTrue){
setParamNormalized (kParamMasterVolume, gps.masterVolume);//Value of masterVolume is either the default value you have set it to or a value the host has previously saved
setParamNormalized(kmultiThreading, gps.multiThread);//Value of multiThread is either the default value you have set it to or a value the host has previously saved
setParamNormalized(kCutAttack, gps.cutAttack);//Value of cutAttack is either the default value you have set it to or a value the host has previously saved
setParamNormalized(kParamWindowHeight, gps.windowHeight);//Recovering resized window height from previous sessions
setParamNormalized(kParamWindowWidth, gps.windowWidth);//Recovering resized window width from previous sessions
return result;
}
1.) Define and set local Parameters, using ParamValue (double), doubles, and/or GlobalParameterState or a similar data structure.
2.) Make sure these are properly implemented in Processor::getState() and Processor::setState() functions;
3.) Make sure that the Controller::setComponentState() is also properly implemented to set the controller's parameter values to the saved state.
tresult PLUGIN_API Processor::process(ProcessData& data){
if (data.inputParameterChanges){
int32 count = data.inputParameterChanges->getParameterCount();
for (int32 i = 0; i < count; i++){
IParamValueQueue* queue = data.inputParameterChanges->getParameterData(i);
if (queue){
int32 sampleOffset;
ParamValue value;
ParamID pid = queue->getParameterId();
if (queue->getPoint(queue->getPointCount() - 1, sampleOffset, value) == kResultTrue){
switch (pid)
{
case kParamMasterVolume:{
masterVolume = value;
}
break;
case kmultiThreading:{
multiThread = value;
}
break;
case kCutAttack:{
cutAttack = value;
}
break;
case kSustainPedal:{
if (numberOfEvents < ACTIVEEVENTSIZE) {
activeEventBuffer[numberOfEvents].type = Event::kNoteExpressionValueEvent;
activeEventBuffer[numberOfEvents].noteOn.velocity = value;
activeEventBuffer[numberOfEvents].noteOn.noteId = kSustainPedal;
activeEventBuffer[numberOfEvents].sampleOffset = sampleOffset;
++numberOfEvents;
}
break;
}
tresult PLUGIN_API Controller::initialize (FUnknown* context){
tresult result = Controller::initialize (context){
if (result == kResultTrue){
Parameter* param;
param = new RangeParameter (USTRING("Master Volume"), kParamMasterVolume, USTRING("%"), 0, 1, .8); //Hopefully the EditController Class frees these in its desctructor!!! ;)
param->setPrecision (1);
parameters.addParameter (param);
param = new RangeParameter(USTRING("MultiThreading"), kmultiThreading, USTRING("%"), 0, 1, 1);
param->setPrecision(1);
parameters.addParameter(param);
param = new RangeParameter(USTRING("Cut Attack"), kCutAttack, USTRING("%"), 0, 1, 0);
param->setPrecision(1);
parameters.addParameter(param);
param = new RangeParameter(USTRING("Window Width"), kParamWindowWidth, USTRING("%"), 0, 1, .0632);
param->setPrecision(1);
parameters.addParameter(param);
param = new RangeParameter(USTRING("Window Height"), kParamWindowHeight, USTRING("%"), 0, 1, .0293);
param->setPrecision(1);
parameters.addParameter(param);
param = new RangeParameter(USTRING("Sustain Pedal"), kSustainPedal, USTRING("%"), 0, 1, 0.f);
param->setPrecision(1);
parameters.addParameter(param);
}
return kResultTrue;
tresult PLUGIN_API Controller::getMidiControllerAssignment(int32 busIndex, int16 midiChannel, CtrlNumber midiControllerNumber, ParamID& tag){
if (busIndex == 0 && midiChannel == 0 && midiControllerNumber == kCtrlSustainOnOff){
tag = kSustainPedal;
return kResultTrue;
}
else
return kResultFalse;
tresult Controller::setParamNormalized ( ParamID tag, ParamValue value){
tresult res = EditController::setParamNormalized(tag, value);
return res;
}
The way I do things is to push Midi events into a C-Style array and process them sample-accurately in my Audio Processing Loop:
template <typename SampleType> SampleType Processor::processAudioBuffer(SampleType** out, int32 numChannels,
int32 sampleFrames, unsigned int& eventCount) {
//Setup the rest of your Audio Processing Loop and then
tresult PLUGIN_API ControllerWithUI::notify(IMessage* message);
virtual void willClose(VST3Editor* editor) { theView = NULL; } //Let's not do stuff with the GUI if it's been closed or doesn't exist!
REFCOUNT_METHODS (Controller)
myVST3Editor* theView;
IPlugView* PLUGIN_API createView (FIDString name) SMTG_OVERRIDE>/span>;
static FUnknown* createInstance (void*) { return (IEditController*)new ControllerWithUI (); }
static FUID cid;
Now...you could of course go even lower-level than the VST3Editor class; perhaps inherit from CPluginView and not have everything feeling like like such a hack? Of course, to me things like converting MIDI CC Data to Parameters already makes the SDK feel like a hack, and personally, the lower I tried to go, the more time and energy it looked like I was going to have to spend to get things working correctly, but if you have extra days to whittle away on reverse-engineering the VST3 SDK, then by all means, go for it!
Here is the createView() function:
return tView;
}
myEditorVST3Editor(Steinberg::Vst::EditController* controller)
//Load resource bitmaps as in VST2 with a resource file...
hBackground = new CBitmap(CResourceDescription(IDB_BACKGROUND));
AMKnob = new CBitmap(CResourceDescription(IDB_KNOB_BKGR));
}
//whatever objects you need in your GUI....
CView* createView(const UIAttributes& attrs, const IUIDescription* desc);
CView* verifyView(CView* view, const UIAttributes& attributes, const IUIDescription* description);
bool PLUGIN_API open(void* parent, const PlatformType& type);
void PLUGIN_API close();
CMessageResult notify(CBaseObject* /*sender*/, const char* message);
Steinberg::tresult onSize(Steinberg::ViewRect* newSize);
virtual bool beforeSizeChange(const CRect& newSize, const CRect& oldSize);
bool requestResize(const CPoint& newSize);
void valueChanged(CControl* pControl);
Steinberg::tresult PLUGIN_API checkSizeConstraint(Steinberg::ViewRect* rect);
CBitmap* hBackground;
CBitmap* AMKnob;
CAnimKnob* reverbKnob;
CTextEdit* sampleDisplay;
CView* myEditorVST3Editor::verifyView(CView* view, const UIAttributes& attributes, const IUIDescription* description) {>
return view;
}
getFrame()->setViewAddedRemovedObserver(this); //We appropriated this block from VST3Editor::Open()
getFrame()->setTransparency(true);
getFrame()->registerMouseObserver(this);
getFrame()->enableTooltips(tooltipsEnabled);
IPlatformFrameConfig* config = nullptr;
getFrame()->open(parent, type, config);
if (delegate)
delegate->didOpen(this);
this->requestResize(CPoint(tRect.right, tRect.bottom));
VSTGUI::CRect size(0, 0, 0, 0);
CPoint point(0, 0);
size(65, 240, 65 + AMKnob->getWidth(), 268); //adding a knob to our CFrame as in VST2
reverbKnob = new CAnimKnob(size, this, kParamMasterVolume, 128, 28, AMKnob, point);
reverbKnob->setDefaultValue(0.0f);
reverbKnob->setTransparency(true);
reverbKnob->setValue(controller->getParamNormalized(kParamMasterVolume));
reverbKnob->isDirty();
frame->addView(reverbKnob);
//We save the size of our plugin's window as parameters in our plugin and resize the view if the user has changed it.
Steinberg::ViewRect tRect;
this->getSize(&tRect);
tRect.right = Steinberg::int32((controller->getParamNormalized(kParamWindowWidth)+0.00001) * 10000.f); //We keep our parameters in the 0 to 1 range even if we actually need ints
tRect.bottom = Steinberg::int32((controller->getParamNormalized(kParamWindowHeight)+0.00001)*10000.f); //We add a tiny amount before multiplying to compensate for lack of precision between the two types, could also use round()
plugFrame->resizeView(this, &tRect);
Steinberg::IdleUpdateHandler::start();
return true;
CCoord width = newSize.x;
CCoord height>/span> = newSize.y;
controller->setParamNormalized(kParamWindowHeight, height/ 10000.f) ;
controller->performEdit(kParamWindowHeight, controller->getParamNormalized(kParamWindowHeight)); //Notifies the Host, adds to IParameterChanges queue
controller->setParamNormalized(kParamWindowWidth, width / 10000.f) ;
controller->performEdit(kParamWindowWidth, controller->getParamNormalized(kParamWindowWidth)); //Notifies the Host, adds to IParameterChanges queue
double widthScale = width / frame->getWidth();
double heightScale = height / frame->getHeight();
CRect tRect = frame->getViewSize();
frame->setAutosizingEnabled(false);
CGraphicsTransform scaled = frame->getTransform();
scaled = scaled.scale(widthScale, heightScale);
frame->setTransform(scaled);
frame->setAutosizingEnabled(true);
frame->invalid();
return Steinberg::kResultTrue;
if (frame == NULL) return Steinberg::kResultFalse;
if (getFrame()){
if (rect->getWidth() >= minSize.x && rect->getHeight() >= minSize.y) {//*Note - only seems necessary to perform this check because of how Cubase inits the window, all other DAWs seem to work fine without it!
return Steinberg::kResultTrue;}
else return Steinberg::kResultFalse;//If the resize is smaller than we want we just return.
if (delegate)
delegate->willClose(this);
for (ParameterChangeListenerMap::const_iterator it = paramChangeListeners.begin(), end = paramChangeListeners.end(); it != end; ++it)
it->second->release();
paramChangeListeners.clear();
if (frame){
Communication between Processor, Controller, and GUI
if (!strcmp(message->getMessageID(), "Resources Loaded")) {
if (theView != NULL)
theView->Tab1NeedsDirty=true;
Conclusions
To a degree, we are all hapless victims of the choices of others, so it's maybe worth considering what affect your your actions and decisions will have on others, especially if you are in a position to make decisions that affect thousands or millions of people. Well, time to drink another Scotch!