Introduction

After working on the VST3 development notes series of post and implementing another VST plugin, I extracted this learning experience into a new lightweight framework called Jamba. It is now available on github.

Details

Starting from scratch developing a VST plugin is quite a task (as was reflected in the previous series of posts). The Jamba framework offers a set of wrapper classes, makefiles, concepts, abstractions and utilities to help with the bootstrapping process. You are still writing a VST plugin with the VST3 SDK (you can actually pretty much override any feature of the framework to bypass it).

The framework takes care of some of the biggest pain points like saving and restoring the state in a thread safe fashion.

The Jamba Sample Gain plugin is the documentation plugin for this framework and demonstrates most of the features the framework has to offer.

The Jamba framework also provides a simple command line executable which creates a fully functionning (Jamba enabled) plugin: check Quick Starting Guide.

Features

Building the Jamba framework started from a set of questions that needed to be answered in order to build the plugins that I was writing. This led to the decision and design of the framework. Here are some of the questions:

How do I create a VST3 plugin that depends on the SDK?

Even the latest 3.6.10 version of the VST3 SDK, which provides instructions for the first time on how to build your own plugin, has it backward with your plugin being built part of the SDK.

Jamba solution is to include jamba.cmake in your project (and having installed the SDK of course), and everything is taken care for you with the dependencies in the proper direction, meaning your plugin depends on the SDK not the other way around. In other words, your plugin is a self contained project that can be easily built by others.

What code do I need to change if I copy/paste an example?

The VST3 SDK comes with a set of examples and it is pretty hard and messy to copy/paste one of them to create your own plugin since it is hard to know what needs to be changed (for example, you need to generate UUIDs…). There is also a lot of boilerplate code.

Jamba offers a simple command line executable which creates a fully functionning (Jamba enabled) plugin: check Quick Starting Guide.

The plugin generated is ready to be built including those unique UUIDs. The only thing that cannot be generated is the 4 letter code for VST2 plugins since they need to be registered on the Steinberg website…

How do I save and restore the state?

Even the again sample code provided with the VST3 SDK is not thread safe (check getState => fGain, fGainReduction and bBypass are being accessed from the GUI thread (getState is called by the GUI thread), yet they are owned and modified by the processor code).

Jamba takes care of the state management entirely and does it in a thread safe fashion by using the ...::LockFree::SingleElementQueue and ...::LockFree::AtomicValue concepts. It also takes care of versioning the state so that it is easier to upgrade later on.

How to I map my VST parameter to a <Type> ?

VST parameters are represented by a double in the [0.0, 1.0] range.

For example, there are many ways to map a value in this range to a boolean (the C way: 0 is false, anything else is true / the “fair” way: [0.0, 0.5[ is false, [0.5, 1.0] is true, etc…).

What matters is that the mapping should be consistent throughout the code.

Jamba offers typed parameters (a converter is provided at creation time) so that the code doesn’t deal with this conversion and only deal with the Type (ex: RTVstParam<bool>).

How do I stop duplicating VST related code?

In the VST world, the GUI is the one registering the VST parameters with the SDK. The Real Time (RT) / processing code is also using them. The Processor::getState methods saves the state and the Controller::setComponentState needs to read this state in the same order to initialize the values. There is a lot of duplication and redundancy which can lead to bugs.

Jamba puts the parameters front and center: you declare them once and they are reused throughout the code and Jamba takes care of registering them and storing them.

Check JSGainParameters for an example of parameter declaration.

How do I store a user input text field?

In my A/B switch plugin, the user can input the name of the A and/or B source. This obviously needs to be saved so that it is restored when the plugin is later on reopened.

Jamba introduces a new kind of parameter called Jamba parameters which extend the “double in the [0.0, 1.0] range” restriction of VST parameters to any type (providing a serializer). In this case, the text field is a JmbParam<UTF8String> and Jamba will save it automatically. Jamba also introduces a TextField view that will manage it.

Check Jamba Sample Gain plugin for an example of using such a parameter.

...
JmbParam<UTF8String> fInputTextParam; // the input text field
...

// the free form input text - this param WILL be saved in its owner (GUI) state
fInputTextParam =
  jmb<UTF8StringSerializer>(EJSGainParamID::kInputText, STR16("Input Text"))
    .guiOwned()
    .defaultValue("Hello from GUI")
    .add();

How do I exchange data between the RT and GUI?

In my VAC-6V plugin, the RT is the one keeping track of the history of peak values that are rendered in the UI. This is quite a challenging problem (thread safe, messaging, timer, view that may or may not be created, etc…).

Jamba makes this problem very simple by using a Jamba parameter and declaring it shared. The RT code simply calls broadcast on it with the new value. The GUI view simply uses this parameter as it would a normal VST parameter.

Check Jamba Sample Gain plugin for an example of using such a parameter.

// In RT code (JSGainProcessor.cpp)
Stats stats{};
stats.fSampleRate = processSetup.sampleRate;
stats.fMaxSinceReset = fState.fMaxSinceReset;
stats.fResetTime = Clock::getCurrentTimeMillis();
fState.fStats.broadcast(stats); // send to GUI

// In GUI code JSGainStatsView.cpp
std::ostringstream s;
s << "Rate=" << fStatsParam->fSampleRate
  << "| Max=" << toDbString(fStatsParam->fMaxSinceReset)
  << "| Dur.=" << computeDurationString(Clock::getCurrentTimeMillis() - 
                                        fStatsParam->fResetTime);
...
rdc.drawString(s.str(), ...);

How do I exchange data between the GUI and RT?

Although a bit more rare than the other way around, Jamba uses the same mechanism for communicating from the GUI to RT: a shared Jamba parameter (this time guiOwned).

// In GUI code JSGainSendMessageView.cpp
UIMessage msg{};
auto const &text = fState->fInputText.getValue(); // note the use of the JmbParam<UTF8String> param
text.copy(msg.fText, sizeof(msg.fText) / sizeof(msg.fText[0]));
fState->fUIMessage.broadcast(msg); // send to RT

// In RT code (JSGainProcessor.cpp)
// Detect the fact that the GUI has sent a message to the RT.
auto uiMessage = fState.fUIMessage.pop();
if(uiMessage)
{
  DLOG_F(INFO, "Received message from UI <%s> / timestamp = %lld",
               uiMessage->fText, uiMessage->fTimestamp);
}

How do I write a custom view?

The views are the main UI components (a label, a knob, a slider, etc…). Jamba allows you to write custom views very easily including adding them to the Editor so that they can be used like native components.

Check Jamba Sample Gain plugin for several examples on how to write a custiom view.

class JSGainStatsView : public PluginCustomView<JSGainGUIState>,
                        public ITimerCallback
{
public:
  // Constructor
  explicit JSGainStatsView(const CRect &iSize) : PluginCustomView(iSize)
  {}

  const CColor &getTextColor() const { return fTextColor;  }
  void setTextColor(const CColor &iColor) { fTextColor = iColor; }
  
  FontPtr getFont() const { return fFont; }
  void setFont(FontPtr iFont) { fFont = iFont; }

  void registerParameters() override;

  void draw(CDrawContext *iContext) override;
...
protected:
  CColor fTextColor{};
  FontSPtr fFont{nullptr};

  GUIJmbParam<Stats> fStatsParam{};

public:
  class Creator : public CustomViewCreator<JSGainStatsView, CustomView>
  {
  public:
    explicit Creator(char const *iViewName = nullptr,
                     char const *iDisplayName = nullptr) noexcept :
      CustomViewCreator(iViewName, iDisplayName)
    {
      registerColorAttribute("text-color",
                             &JSGainStatsView::getTextColor,
                             &JSGainStatsView::setTextColor);
      registerFontAttribute("font",
                            &JSGainStatsView::getFont,
                            &JSGainStatsView::setFont);
    }
  };

};

How do I use multiple parameters to render a view?

As an example, the VAC-6V plugin displays peak values (one parameter) with a color depending on another parameter (soft clipping level).

Jamba makes all parameters accessible to any custom view, and by default (by calling registerXX(xxx, bool iSubscribeToChanges = true)) the view is notified when the parameter changes which makes it trivial to implement a view depending on multiple parameters.

Check LinkedSliderView custom view for an example.

// calback when fLinkedSlider or fLink changes
void GenericLinkedSliderView::onParameterChange(ParamID iParamID)
{
  if(fLinkedSlider.getParamID() == iParamID)
  {
    if(fLink && fSlider != fLinkedSlider)
      fSlider.copyValueFrom(fLinkedSlider);
    return;
  }

  if(fLink.getParamID() == iParamID)
  {
    if(fLink && fSlider.getValue() < fLinkedSlider.getValue())
      fSlider.copyValueFrom(fLinkedSlider);
    return;
  }
}

How do I use 4 frames for a toggle?

The VST3 SDK comes with a toggle that uses only 2 frames.

Jamba introduces:

  • Jamba – Toggle Button (on/off)
  • Jamba – Momentary Button (on when pressed)

which are 2 views handling either 2 or 4 frames (the extra 2 frames deal with the state: pressed/not pressed).

How do I write unit tests for my plugin?

When you are not familiar with cmake and C++ development, coming up with a solution to write unit tests is a bit challenging and quite time consuming.

Although there are many ways to accomplish this, jamba.cmake offers an integration with Google Test as a starting point. When following the Quick Starting Guide, the plugin comes with a single unit test example so that it is easy to add more tests without having to deal with the details.

You can obviously use your own test framework if you wish to.

How do I build/test/edit/validate/install my plugin?

Although the tools are part of the VST3 SDK, it is very manual and time consuming to figure out how to compile and run them with the right set of arguments.

By including jamba.cmake (jamba_dev_scripts(${target})), Jamba generates a convenient shell/bat script :

./jamba.sh -h
Usage:  jamba.sh [-hdrn] <command>

 -h : usage help
 -d : use Debug build config (default)
 -r : use Release build config (Debug if not defined)
 -n : dry run (prints what it is going to do)

Commands: 
 ---- VST Commands ----
 clean    : clean all builds
 build    : build the VST plugin
 edit     : start the GUI editor (Debug only)
 install  : install the VST plugin in their default location
 test     : run the unit tests
 validate : run the VST3 validator
 ---- Audio Unit Commands ----
 build-au   : build the Audio Unit wrapper plugin
 install-au : install the Audio Unit plugin in its default location
 ---- Generic Commands ----
 archive : generate the zip file containing the plugin(s) and README/License
 prod    : run test/validate/archive (default to Release, override with -d)
 ---- CMake target ----
 <target> : invoke cmake with the provided target

How do I build for macOS and Windows 10?

Jamba comes with support for both macOS and Windows 10 out of the box and you can write your code on one platform and compile/build on the other as well (as long as the source of truth is the CMakeLists.txt). The files that are important are:

  • CMakeLists.txt which contains the list of sources (you need to add your own source files in the vst_sources list). Example.
  • CMakeLists.txt which contains the list of resources (graphics) for the UI (jamba_add_vst3_resource). Note that Jamba automatically takes care of handling the resources for macOS and Windows 10 (which is different). Example.

How do I wrap my plugin as an Audio Unit?

The VST3 SDK provides a wrapper to generate an audio unit from the VST3 plugin. Using it out of the box is quite challenging (good luck hunting down the “Core Audio SDK” referenced in the comments…). Jamba (new in 2.1.0) encapsulates all the details for you. By following the Quick Starting Guide you will have a plugin configured to support Audio Unit immediately.

Last edited: 2018/10/07