In Part 1, I described my first steps in installing the VST3 SDK and compiling a basic plugin in its own environment. Part2 focuses on the content of a plugin. For this exercise, I am reimplementing the A/B Audio Switch rack extension since the logic is fairly simple while still touching many areas of plugin development.

Check Part 1 for platform requirements and assumptions.

Setup

This project is available on github: vst-ab-switch. Make sure you checkout the blog-part2-369 tag in order to follow along with this part (or browse the code directly on github).

As mentioned in Part 1, I am now using CLion exclusively but it should be fairly trivial to generate the Xcode project or using cmake directly as explained in Part 1.

All the source files are located under src/cpp

Processor and Controller

As described in the documentation, a plugin is made up of 2 main concepts:

  • the processor in charge of doing all the actual audio processing (deal with audio samples, midi events, etc…)
  • the controller in charge of the UI (vu meters, knobs, mouse events, etc…)

Each concept is represented by a main class, in this case ABSwitchProcessor and ABSwitchController which inherit/implement the required interfaces/classes.

Main entry point(s)

When the host/DAW loads the plugin, it needs to get a handle to the classes. This is a bit convoluted and frankly not explained at all in the documentation. So this is what I could gather from looking at the examples and SDK source code.

Class IDs

Each class defines a unique ID (actually each class in the SDK has a unique ID as well). The file ABSwitchCIDs.h declares the unique ID for the 2 main classes:

static const ::Steinberg::FUID ABSwitchProcessorUID(0x8d605466, 0x25154967, 0x85ddbb25, 0x8ac01235);
static const ::Steinberg::FUID ABSwitchControllerUID(0x82aea4a3, 0x5b4e4a5f, 0xa3d68b1a, 0x8a1b69c5);

The helloworld plugin that comes with the SDK specifies: “you can use GUID creator tools like www.guidgenerator.com in order to generate those IDS. I generated these using java java.lang.UUID.randomUUID() (see javadoc), but feel free to use the website suggested.

VST2 entry point

The VST3 SDK contains a helper wrapper to wrap a VST3 plugin into a VST2 plugin. The main entry point for VST2 is defined in ABSwitchVST2.cpp:

::AudioEffect *createEffectInstance(audioMasterCallback audioMaster)
{
  return Steinberg::Vst::Vst2Wrapper::create(GetPluginFactory(),
                                             pongasoft::VST::ABSwitchProcessorUID,
                                             'TBDx',
                                             audioMaster);
}

It simply uses the ID of the processor. Note that the VST2 unique ID is set to TBDx as I have not registered one with Steinberg yet…

If you are familiar with VST2, the main entry point is actually the function

VST_EXPORT AEffect* VSTPluginMain(audioMasterCallback audioMaster);

but the VST3 wrapper classes define this method which internally calls createEffectInstance

VST3 entry point

For VST3, the main entry point is actually the following C-style export function:

IPluginFactory* PLUGIN_API GetPluginFactory()

This information is in the VST3 documentation but quite buried… It is on the VST 3 API / VST-MA page (selected in the sidebar) under the section Plug-ins / Module Factory.

Once the host gets access to the factory, it can then call it back to instantiate the processor and controller.

Thankfully the implementation of this method, the factory itself and the class registration is all done via a set of macros (see file ABSwitchVST3.cpp):

BEGIN_FACTORY_DEF ("pongasoft",
                   "https://www.pongasoft.com",
                   "mailto:support@pongasoft.com")

    // ABSwitchProcessor processor
    DEF_CLASS2 (INLINE_UID_FROM_FUID(::pongasoft::VST::ABSwitchProcessorUID),
                PClassInfo::kManyInstances,  // cardinality
                kVstAudioEffectClass,    // the component category (do not changed this)
                stringPluginName,      // here the Plug-in name (to be changed)
                Vst::kDistributable,  // means that component and controller could be distributed on different computers
                "Fx",          // Subcategory for this Plug-in (to be changed)
                FULL_VERSION_STR,    // Plug-in version (to be changed)
                kVstVersionString,    // the VST 3 SDK version (do not changed this, use always this define)
                pongasoft::VST::ABSwitchProcessor::createInstance)  // function pointer called when this component should be instantiated

    // ABSwitchController controller
    DEF_CLASS2 (INLINE_UID_FROM_FUID(::pongasoft::VST::ABSwitchControllerUID),
                PClassInfo::kManyInstances,  // cardinality
                kVstComponentControllerClass,// the Controller category (do not changed this)
                stringPluginName
                  "Controller",  // controller name (could be the same than component name)
                0,            // not used here
                "",            // not used here
                FULL_VERSION_STR,    // Plug-in version (to be changed)
                kVstVersionString,    // the VST 3 SDK version (do not changed this, use always this define)
                pongasoft::VST::ABSwitchController::createInstance)// function pointer called when this component should be instantiated

END_FACTORY

// from ABSwitchProcessor.h
class ABSwitchProcessor : public AudioEffect {
public:
  // .....
  static FUnknown *createInstance(void * /*context*/) {
    return (IAudioProcessor *) new ABSwitchProcessor();
  }
  // .....
}

// from ABSwitchController.h
class ABSwitchController : public EditController {
public:
  // .....
  static FUnknown *createInstance(void * /*context*/) { 
    return (IEditController *) new ABSwitchController();
  }
  // .....
}

The macro uses the IDs previously created as well as the factory methods (createInstance) from the processor and controller.

Attaching the controller to the processor

Finally, the controller gets attached to the processor via its ID. See file ABSwitchProcessor.cpp:

ABSwitchProcessor::ABSwitchProcessor() : AudioEffect()
{
  setControllerClass(ABSwitchControllerUID);
}

The processor

The processor (defined in ABSwitchProcessor.h) needs to implement both IComponent and IAudioProcessor as per the documentation. The SDK class AudioEffect implements the basic methods and is used as a starting point for the processor.

Initializing the processor

During the initialization phase of the processor, we define the inputs and outputs of the plugin (see file ABSwitchProcessor.cpp):

tresult PLUGIN_API ABSwitchProcessor::initialize(FUnknown *context)
{
  tresult result = AudioEffect::initialize(context);
  if(result != kResultOk)
  {
    return result;
  }

  //---create Audio In/Out buses------
  addAudioInput(STR16 ("Stereo In"), SpeakerArr::kStereo);
  addAudioOutput(STR16 ("Stereo Out"), SpeakerArr::kStereo);

  return result;
}
In order for the processor to declare that it can handle 64 bits processing, the method canProcessSampleSize needs to be overridden (see file ABSwitchProcessor.cpp).

The actual processing

The actual audio processing takes place in this method (see file ABSwitchProcessor.cpp):

tresult PLUGIN_API ABSwitchProcessor::process(ProcessData &data)
{
  // ... see source code
}
The code is mostly copied from the again sample provided with the SDK.

A few points:

  • all the info necessary is provided in the data parameter
  • data.inputs[0] refers to the Stereo In pair defined during initialization
  • data.outputs[0] refers to the Stereo Out pair defined during initialization
  • in[0] represents the first channel of the Stereo In pair (the left channel). It is an array of samples of size data.numSamples. in[1] represents the other channel (right).
  • out[0] represents the first channel of the Stereo Out pair (the left channel). It is an array of samples of size data.numSamples. out[1] represents the other channel (right).
  • the type of a sample (ex: in[0][0] is the first sample of the left input pair) is determined by data.symbolicSampleSize. Note how the code uses a templated function (defined in ABSwitchProcess.h) so as not to repeat the code for 32 or 64 bits.
The code is generic and doesn’t care how many channels are defined for input or output: it simply multiplies every input sample by 0.5 to generate an output sample.

The controller

The controller (defined in ABSwitchController.h) needs to implement IEditController as per the documentation. The SDK class EditController implements the basic methods and is used as a starting point for this plugin.

Creating the main view

The controller main method is quite simple and returns an instance of VSTGUI::VST3Editor:

IPlugView *ABSwitchController::createView(const char *name)
{
  if(name && strcmp(name, ViewType::kEditor) == 0)
  {
    return new VSTGUI::VST3Editor(this, "view", fXmlFile);
  }
  return nullptr;
}

This class will read the provided xml file to build the GUI (it is empty so it will render a black square…). This class has an editing mode which allows you to create the UI on the fly with an editor (by right clicking in the UI)! The documentation (section VSTGUI 4 / Inline UI Editing for VST3 (WYSIWYG)) explains the various steps to make it happen (seems complicated), but in my experience it is already built-in in the makefiles so it is quite easy:

# from AddVST3Library.cmake
target_compile_definitions(${target} PUBLIC $<$<CONFIG:Debug>:VSTGUI_LIVE_EDITING=1>)

As long as you compile in Debug mode the editor is enabled. Simple.

Although you can start your DAW to load the plugin, the SDK comes with a simple editorhost application (similar to the validator application (see Part 1)) which lets you load the plugin and edit it:

# you may need to compile this app separately
./vst3-sdk.369/Debug/bin/editorhost.app/Contents/MacOS/editorhost ./vst-ab-switch/Debug/VST3/pongasoft_ABSwitch.vst3

What we have so far

At this stage we have a fully functioning VST3 and VST2 plugin which simply removes 3dB from the input signal (so we can verify that the plugin gets called properly) and has a blank UI with editing built-in.

Next

Now that we have a fully functioning plugin which does something trivial, let’s build the UI and add the toggle to switch between input A and input B. This is the point of Part 3.

Last edited: 2018/03/17