Porting VST2 plugins to the VST3 standard (A Quick and Dirty Overview of VST3)

Introduction

6/15/2022

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:


FUID ControllerWithUI::cid (0x123abc!@, 0x123abc!@, 0x123abc!@, 0x123abc!@);
FUID ProcessorWithUIController::cid (0x123abc!@, 0x123abc!@, 0x123abc!@, 0x123abc!@);

To be used here: (your plugin's entry point in againentry.cpp, factory.cpp, ect.):


#define stringPluginName"My Synth Plugin"
BEGIN_FACTORY_DEF (stringCompanyName, stringCompanyWeb, stringCompanyEmail)
DEF_CLASS2 (INLINE_UID_FROM_FUID(Steinberg::Vst::MySynth::ProcessorWithUIController::cid),
PClassInfo::kManyInstances,
kVstAudioEffectClass,
stringPluginName " With UI",
Vst::kDistributable,
Vst::PlugType::kInstrumentSynth,
FULL_VERSION_STR,
kVstVersionString,
Steinberg::Vst::MySynth::ProcessorWithUIController::createInstance)

DEF_CLASS2 INLINE_UID_FROM_FUID(Steinberg::Vst::MySynth::ControllerWithUI::cid),

PClassInfo::kManyInstances,
kVstComponentControllerClass,
stringPluginName " With UI",
0,
"",
FULL_VERSION_STR,
kVstVersionString,
Steinberg::Vst::MySynth::ControllerWithUI::createInstance)

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.

Core Principles of VST3
The main difference between VST2 and VST3 is in the design philosophy. In VST2, you had one master class (AudioEffect) handling the audio block, processing MIDI events, ect. In communist Russia, there are no MIDI Events! - Sorry, I couldn't resist a Yakov, but it's true: in VST3 there is no true MIDI, we will discuss this later - In VST3, operations have been separated into two components: a "Controller" and a "Processor." Why? Who knows, but the design philosophy is to keep these two components separate, so that's what we will attempt to do. The following is a quick list of VST2 functions and their VST3 counterparts:

Audio Block Processing:
VST2: void processReplacing (float** inputs, float** outputs, VstInt32 sampleFrames)
VST3:void PLUGIN_API Processor::process(ProcessData& data){
//This function is used to process both the AudioBlock and Event Data. To process the Audio Block, you might call something like this:
void** out = getChannelBuffersPointer(processSetup, data.outputs[0]);

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!!!
}

You might notice that Steinberg has created a template function that depends on the data type the Host wants to pass. You can override tresult canProcessSampleSize(int32 symbolicSampleSize) in your Processor class to set this value to floats/doubles if you don't want to use the template and instead process only 32-bit or 64-bit samples.
Event/Midi Processing:
VST2: VstInt32 processEvents (VstEvents* ev)
VST3: void PLUGIN_API Processor::process(ProcessData& data){
//This function is used to process both the AudioBlock and Event Data. To process the Event Data, you might call something like this:
IEventList* eventList = data.inputEvents;
if (eventList){
int32 numEvent = eventList->getEventCount ();
for (int32 i = 0; i < numEvent; i++){
Event event;
if (eventList->getEvent (i, event) == kResultOk){
switch (event.type)
{
case Event::kNoteOnEvent:
//do noteOnStuff
break;
case Event::kNoteOffEvent: //do note off stuff
break;
}
}
}
}

}
Here we have reached the first major difference between VST2 and VST3: We are no longer able to get the Midi Status Byte, instead we have to rely on the Steinberg Event type! This may not seem like a big deal, but this type doesn't really support non-note events, so for things like MIDI CC automation, we are forced to use a weird parameter hack (more on that later). For now, let's just say we probably don't want to process our events here because we can only access kNoteOnEvent, kNoteOffEvent, and a handful of other event types Steinberg has bothered to define. Incidentally, if you need MIDI events in your plugin:
Processor Init
tresult PLUGIN_API Processor::initialize(FUnknown* context) {
tresult result = AudioEffect::initialize(context);
if (result == kResultTrue){
addAudioOutput(STR16("Audio Output"), SpeakerArr::kStereo); //We want a pointer to the outputs. For an audio processing class, you would also want a pointer to the inputs.
addEventInput(STR16("Event Input"), 1); //We want to be notified of MIDI Events
}
return result;
}
Parameter Handling:
VST2: float getParameter (VstInt32 index), void setParameter (VstInt32 index, float value), kNumParams, ect.
VST3: tresult Controller::setParamNormalized (ParamID tag, ParamValue value), ParamValue Controller::getParamNormalized (ParamID tag), GlobalParameterState, tresult Processor::setState (IBStream* stream), tresult Processor::getState (IBStream* stream), tresult Controller::setComponentState (IBStream* state), Parameter, ParamValue

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.

GUI Editor:
VST2: AEffGUIEditor
VST3: VST3EditorDelegate, VST3Editor

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:
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;

if (!s.writeDouble(windowHeight))
return kResultFalse;

if (!s.writeDouble(windowWidth))
return kResultFalse;

return kResultTrue;
}

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:
tresult PLUGIN_API Processor::setState (IBStream* stream){


IBStreamer s (stream, kLittleEndian);

if (!s.readDouble (masterVolume))
return kResultFalse;

if (!s.readDouble(multiThread))
return kResultFalse;

if (!s.readDouble(cutAttack))
return kResultFalse;

if (!s.readDouble(windowHeight))
return kResultFalse;

if (!s.readDouble(windowWidth))
return kResultFalse;

}

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:
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;
}

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:


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.

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:
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;
}
}
}
}
}
Whenever tresult Controller::setParamNormalized AND tresult Controller::performEdit (ParamID tag, ParamValue valueNormalized) have been called, the Host will push the parameter change into the ProcessData argument it passes to theProcessor::process(ProcessData& data) function - data.inputParameterChanges (of type IParameterChanges*). There is an also a data.outputParameterChanges object if you need to send outgoing parameter changes to....somewhere?

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
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;

}

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
tresult PLUGIN_API Controller::getMidiControllerAssignment(int32 busIndex, int16 midiChannel, CtrlNumber midiControllerNumber, ParamID& tag){

//The Host will handle Noteon and Notoff events and push them into data.inputEvents. For other Midi events, it will push them into data.inputParameterChanges with the tag set here!!!
if (busIndex == 0 && midiChannel == 0 && midiControllerNumber == kCtrlSustainOnOff){
tag = kSustainPedal;
return kResultTrue;
}
else
return kResultFalse;
}
Please Note: Steinberg has defined 133 (enum) MIDI types that correspond to the standard MIDI CC numbers (i.e., kCtrlModWheel = 1, kCtrlSustainOnOff=64, ect.) Here, I have changed the tag from kCtrlSustainOnOff to kSustainPedal. Why? Two reasons: 1.) I have inserted it at the end of my list of Parameter Enums because it's a parameter and 2.)The Steinberg-defined enums might conflict with the parameters you have defined! FOR EXAMPLE: IF YOU ASSIGN kCtrlModWheel TAG TO THE MOD WHEEL, AND IF YOU HAVE A PARAMETER THAT HAS ALSO BEEN DEFINED WITH AN ENUM EQUAL TO 1, EVERYTIME YOU USE THE MODWHEEL, YOU WILL ALSO CHANGE THAT PARAMETER, AS setParamNormalized(1, value) WILL BE CALLED called IN BOTH INSTANCES)!!! This obviously has the potential to create major bugs!

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:


tresult Controller::setParamNormalized ( ParamID tag, ParamValue value){

//Do whatever Wacky stuff you need to do!
tresult res = EditController::setParamNormalized(tag, value);
return res;
}

Sequencing Events and MIDI CC
With VST3, you have some degree of control over how you would like to sequence MIDI Events. Changing parameters and addressing events once per sample block is probably fine at lower latencies, but you may also want to try and be "sample accurate." For parameters changed by GUI Controls, I have not figured out how to generate sample-offset numbers (this may work with the WYSIWYG editor, I don't know). However, I personally don't need my GUI controls to be sample-accurate (and I'm assuming most people who need this have already done it implementing some sort of internal parameter smoothing in earlier versions of their plugins). For MIDI events, the Host will provide you with sample offset numbers.

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:

(Called from Within Processor::process(ProcessData& data))

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

for (int l = 0; l<sampleFrames; ++l) {
if (eventCount > 0
for (int i = 0; i < eventCount; ++i){
if (activeEventBuffer[i].sampleOffset == l){
processEvent(activeEventBuffer[i]);
}
}
}
//Do the rest of your Audio Processing Loop
}
Why? Because I feel iterating over a C-Style array once per sample is probably faster than calling Steinberg's tresult getEvent (int32 index, Event& e) function, but I could be wrong. You can also cull unwanted events this way. More importantly, you can create your own Events and post them to your array from the MIDI CC Parameter changes discussed earlier, so that all MIDI events can be processed in the same function as in VST2 and not all over the damn place ;) However, you can also implement your sequencing more simply like this:
(Called from Within Processor::process(ProcessData& data))
template <typename SampleType> SampleType Processor::processAudioBuffer(SampleType** out, int32 numChannels,
int32 sampleFrames, ProcessData& data){
//Setup the rest of your Audio Processing Loop and then
int32 numEvents = data.inputEvents ? data.inputEvents->getEventCount() : 0;
Event e;
for (int l = 0; l<sampleFrames; ++l){
if (numEvents > 0){
for (int i = 0; i < numEvents; ++i) {
data.inputEvents->getEvent(i, e);
if((e->sampleOffset=l)processEvent(e);
}
}
}
//Do the rest of your Audio Processing Loop and then
}
It's really up to you how you want to sequence events.
GUI and Window Resizing
Ok, here we go! VST3 seems to be designed around using the WYSIWYG editor and .uidesc files, so if you are willing to do that, you can probably ignore this section. As far as I can tell, .uidesc files are just some abstraction/parsing nonsense built upon the core VST3 GUI framework, which itself is just a modified version of the old VST2 GUI framework (all your favorite classes are still here!). We start by deriving a UI+Controller class from the Controller class we have already defined:
Controller (With UI) Class
class ControllerWithUI : public Controller, public VSTGUI::VST3EditorDelegate
{
public:
ControllerWithUI()
:theView(NULL){}

tresult PLUGIN_API ControllerWithUI::notify(IMessage* message);
myVST3Editor* theView;
IPlugView* PLUGIN_API createView (FIDString name) SMTG_OVERRIDE>/span>;
static FUnknown* createInstance (void*) { return (IEditController*)new ControllerWithUI (); }
static FUID cid;

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)

};
There isn't a whole lot to this class as it inherits mostly everything from the Controller and VST3EditorDelegate classes. In fact, you could probably just inherent VSTEditorDelegate into your Controller class and not have an extra UI class but...this is how Steinberg does it in their example project. I have added one object: a pointer to the actual GUI - myVST3Editor* theView. I have done this so that I can communicate from the Processor to the GUI later, but you may not need this functionality. Additionally, notify() will become an important function later on, so we will also override it.

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:

Delegate Creates the View
IPlugView* PLUGIN_API ControllerWithUI::createView (FIDString _name)
{
myVST3Editor* tView = new myVST3Editor(this);
theView = tView;


return tView;
}

I have created the view and saved it to my theView pointer for later use. myVST3Editor has one argument, a pointer to the UI Controller Class: Normally, a class inherited from VST3Editor also takes argument for its name and the .uidesc file, but we won't be using those here. Now, the actual editor class:
The Actual GUI View Class
class myVST3Editor : public VSTGUI::VST3Editor {
public:

myEditorVST3Editor(Steinberg::Vst::EditController* controller)

:VST3Editor::VST3Editor(controller, "My Plugin VST3", "note_expression_synth.uidesc"){

//Load resource bitmaps as in VST2 with a resource file...
hBackground = new CBitmap(CResourceDescription(IDB_BACKGROUND));
AMKnob = new CBitmap(CResourceDescription(IDB_KNOB_BKGR));
}


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);

//whatever objects you need in your GUI....
CBitmap* hBackground;
CBitmap* AMKnob;
CAnimKnob* reverbKnob;
CTextEdit* sampleDisplay;

};
Unfortunately, we will have to override many functions to hack things into working properly. For starters, look at the constructor: when we call the constructor of the base class, we have to pass these three arguments: VST3Editor::VST3Editor(controller, "My Midi Plugin VST3", "note_expression_synth.uidesc") - THIS IS BECAUSE THE VST3EDITOR CLASS HAS NO DEFAULT CONSTRUCTOR BECUASE STEINBERG REALLY, REALLY, REALLY WANTS YOU TO USE THEIR WYSIWYG EDITOR!!! Also, if you try to pass a NULL pointer to the .uidesc argument, everything will crash. Admittedly, it is a bit of a hack to be deriving from the VST3Editor class when we are not using the .uidesc files, but my goal was to adopt the Steinberg example projects into what I needed; there are probably better, easier, or cleaner ways to get a progmatic GUI working with VST3, but you will have to dip even lower into the framework ;). Anyway, I've opted to just send it the descriptor file already built into the note_expression_synth project - we will override all the proper functions and insert our own CFrame instead of using the CFrame generated by the .uidesc file:

CView* myEditorVST3Editor::createView(const UIAttributes& attrs, const IUIDescription* desc) {>
return frame;
}


CView* myEditorVST3Editor::verifyView(CView* view, const UIAttributes& attributes, const IUIDescription* description) {>
return view;
}

These two classes we just need to override to get rid of .uidesc nonsense in the VST3Editor base class.
Opening the Editor
bool PLUGIN_API myEditorVST3Editor::open(void* parent, const PlatformType& type){
VSTGUI::CRect bFrameRect(0, 0, hBackground->getWidth(), hBackground->getHeight());
frame = new CFrame(bFrameRect, this);
minSize.x = hBackground->getWidth();
minSize.y = hBackground->getHeight();


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);


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()

this->requestResize(CPoint(tRect.right, tRect.bottom));
plugFrame->resizeView(this, &tRect);
Steinberg::IdleUpdateHandler::start();


return true;

}
In my plugin I have decided to make the window size two parameters so that any resizing the user does can be saved/restored. requestResize() sets up the size of my view and resizeView() is a request to the Host to resize the view:
Setup the View for Resizing
bool myEditorVST3Editor::requestResize(const CPoint& newSize) {
if (!plugFrame)
return Steinberg::kResultFalse;
if (frame == NULL)
return Steinberg::kResultFalse;


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;


}
After you call resizeView() to inform the Host that you want a resize, the Host will then call onSize() to actually resize the view:
Resize the View
Steinberg::tresult PLUGIN_API myEditorVST3Editor::onSize(Steinberg::ViewRect* newSize){


if (frame == NULL) return Steinberg::kResultFalse;


Steinberg::int32 tWidth((controller->getParamNormalized(kParamWindowWidth) + 0.00001) * 10000.f); //We don't want to resize the window unless we or the user have called for a resize.
Steinberg::int32 tHeight((controller->getParamNormalized(kParamWindowHeight) + 0.00001)*10000.f);
if (tWidth != newSize->getWidth() || tHeight !=newSize->getHeight()) return Steinberg::kResultFalse;


if (getFrame()){

CRect r(newSize->left, newSize->top, newSize->right, newSize->bottom);
CRect currentSize;
getFrame()->getSize(currentSize);
if (r == currentSize)
return Steinberg::kResultTrue;
}
auto oldState = requestResizeGuard;
requestResizeGuard = true;
auto result = VSTGUIEditor::onSize(newSize);
requestResizeGuard = oldState;
return result;
}
So then, for the plugin to request a resize, the order is requestResize(), plugFrame->resizeView() and then onSize() by the Host. When the Host wants to request a resize, the Host will call checkSizeRestraints():
Host Wants to Resize
Steinberg::tresult PLUGIN_API myEditorVST3Editor::checkSizeConstraint(Steinberg::ViewRect* rect) {


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!

CPoint tPoint(rect->getWidth(), rect->getHeight());
requestResize(tPoint);


return Steinberg::kResultTrue;}
else return Steinberg::kResultFalse;//If the resize is smaller than we want we just return.

}
In my plugin, checkSizeConstraint()calls our requestResize() function to set the plugin up for resizing, after which the Host will once again call onSize(). Cubase appears to break resizing a bit as it wants to resize your window to weird dimensions when you first load your plugin, so we have to put in a check to make sure the size isn't too small. Any CController object in your plugin (knobs, switches, ect.) will call valueChanged():
Value Changed
void myEditorVST3Editor::valueChanged(CControl* pControl){
if (controller->setParamNormalized(pControl->getTag(), pControl->getValue()) == Steinberg::kResultTrue)
controller->performEdit(pControl->getTag(), controller->getParamNormalized(pControl->getTag())); //Remember to call performEdit or the change won't be pushed into the inputParameterChanges queue!!!
return;
}
Close() is borrowed from VST3Editor::Close(), but with the .uidesc junk removed:
Closing the Editor
void PLUGIN_API myEditorVST3Editor::close() {
Steinberg::IdleUpdateHandler::stop();


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){

getFrame()->unregisterMouseObserver(this);
getFrame()->removeAll(true);
int32_t refCount = getFrame()->getNbReference();
if (refCount == 1){
getFrame()->close();
frame = nullptr;
}
else{
getFrame()->forget();
}
}}

Communication between Processor, Controller, and GUI

Suppose you need to tell the GUI to update something from the Processor - how do you do this? Welcome to the world of Notify() and IMessage! You can send up an IMessage in your Processor or Controller like this:
Allocate and Send a Message
IMessage* resourcesLoaded = allocateMessage();
resourcesLoaded->setMessageID("Resources Loaded");
sendMessage(resourcesLoaded);
resourcesLoaded->release();
And catch it in the Processor or Controller like this:
Catch the Message
tresult PLUGIN_API ControllerWithUI::notify(IMessage* message) {
if (!message)
return kInvalidArgument;


if (!strcmp(message->getMessageID(), "Resources Loaded")) {
if (theView != NULL)
theView->Tab1NeedsDirty=true;

return kMessageNotified;
}
return kMessageUnknown;
}
I have sent an IMessage from the Processor to the Controller, caught it in the Controller, and then set a flag in my GUI class. Why did I set a flag instead of calling directly into my GUI class? BECAUSE THERE'S NO GUARANTEE THE HOST WILL PUT THE NOTIFY CONTROLLER FUNCTION ON THE MAIN THREAD AND GUI STUFF ONLY WORKS ON THE MAIN THREAD. How can you make sure you are on the main thread when doing GUI stuff? I recommend doing any GUI stuff you need to do in the Notify() function of your GUI:
Update the GUI
CMessageResult myEditorVST3Editor::notify(CBaseObject* /*sender*/, const char* message){
if (Tab1NeedsDirty) {//Do GUI stuff
closeLoadingtab();
Tab1NeedsDirty = false;
}
if (message == CVSTGUITimer::kMsgTimer) {//Refresh the GUI
if (frame)
frame->idle();
return kMessageNotified;
}
return kMessageUnknown;
}
And now hopefully you see why: the Notify() function in your GUI gets sent periodic messages by a timer to update the frame ( frame->idle()) which redraws your view. This is the ideal place to put your GUI code, as it's pretty much guaranteed to be on the main thread if the Host wants to actually ever refresh the view - you could probably also just send iMessagesto myEditorVST3Editor::Notify() directly instead of setting flags.

Conclusions

Well that's it! Hopefully, now you understand the basics of how you might go about porting your VST2 plugins to the VST3 format. VST3 isn't wildly dissimilar to VST2, it's basically the same thing with extra nonsense improvements and limitations thrown in, alongside a few " advantages" like the silence flag, which I'd completely forgotten about until now!
//in your Processor::Process() funtion
if (data.inputs[0].silenceFlags != 0){
//means the upstream plugin set the silence flag to 0. If you are an effect plugin, maybe set silent flags like this:
data.outputs[0].silenceFlags = 0x11;
//if not, set the silence flags to 0 so the next plugin knows to process our outputs
data.outputs[0].silenceFlags = 0;
}
It's not that I hate Steinberg (or Apple, or Google, or any other large tech company) - it's their plugin format and they certainly have the right to do whatever they want with it - but I sometimes wonder if these large companies realize how much of an impact they have on small developers. You change your framework, you change your API, even slightly, and you have just created thousands, maybe millions of hours of extra work that real, actual humans will need to do in order to get back to where they were before the change. Especially with something like VST2, it's rather frustrating, as we had an existing framework that was working just fine, and could have just been updated without completely rewriting the whole thing. But I think two things tend to happen in large companies:
1.) The original developers leave, nobody understands how the old code works, and so everything gets rewritten.
2.) Highly paid developers have nothing better to do than completely redesign working things in order to justify their large salaries.

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!