In Part 2, we created a fully functioning VST2/VST3 plugin whose sole action was to remove 3dB of headroom to the input signal (no UI). Part 3 focuses on adding a UI with a control to actually do the primary job of the A/B switch: switching between input A and input B.

Check Part 1 for platform requirements and assumptions.

Setup

This project is available on github: vst-ab-switch. Make sure you checkout the blog-part3-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

Creating the UI

As described in Part 2, using the editorhost app (or deploying the plugin in an actual DAW), you can right click on the background to open the editor. The documentation that comes with the SDK describes the editor a little bit.

editorapp
This is what the editor will look like at the end of this exercise

Changing the background color

The top right area shows the parameters that you can edit for the “view” that is selected in the top left area (called Editor). To change the background color (of the only current view), simply click on the background-color selection box. I selected ~BlueCColor~ to make it different from the default.

Adding simple text

The bottom right section has multiple tabs. The first one is called Views and lists all the views that the editor knows about. Each view corresponds to a C++ class (feel free to look at the source code of the SDK to see what each class does, although the comments/documentation is quite limited). You then drag and drop a view from this list into the editor. I dragged the one called Label (C++ class being CTextLabel) and changed some properties like the title, background color, etc… to create the A/B Switch title.

If you want to change the font to one that is not listed, you need to go into the Fonts tab in the bottom right section and add the font there. It will then appear in the dropdown selection.

From what I could understand, when adding a font, it simply adds the name of the font in the xml file, not the actual font. It is very unclear to me what would happen in the event the font does not exist on a user machine. That is why I don’t think it is a good idea to add a font and would recommend that the text that appears in the UI of the plugin be part of the background image (scaling/HiDPI is handled!).

Adding the switch/toggle

To add the toggle, I used the OnOff Button view (COnOffButton) and resized it to 50×50. An on/off button is a view that has 2 states which can get toggled by clicking on it.

In order for the button to have an image that will change when the toggle is clicked, you need to:

  • create one image that contains the 2 frames on top of each other. Or in other words, the image is twice as tall (see the file resource/Control_AudioSwitch.png).
Control_AudioSwitch.png
The image contains 2 frames
  • on the Bitmaps tab of the bottom right panel, click the + and select the image
  • now in the parameters area, you can now select the image you just added, in the dropdown of the bitmap entry.
I spent a huge amount of time trying to figure out why this was not working at first. I actually found a bug in the SDK (see forum): the filename of the image is parsed to determine the scale (used for HiDPI) and so my image (originally) called Control_AudioSwitch_50x100.png ended up being treated as a 50x scale.

If you exit the Editing mode (which you can do by clicking on the x next to Editing in the menu bar), you can now click on the image and it changes to say In A or In B.

Saving

Once you are done, you save the new UI by clicking File/Save As.... The file needs to be saved under the resource folder and to match what the C++ code expects (see ABSwitchController constructor): it should be called ABSwitch.uidesc. The image(s) also need to be under the resource folder.

You may need to edit the file generated to remove the absolute path that gets saved for the bitmap (see ABSwitch.uidesc) checked in.

The file contains a <custom>...</custom> section which represents the state of the editor and can be safely removed (although it is probably good to keep it while you are working on the UI).

Bonus: automatic scaling

Based on the helloworld sample that comes with the SDK (and the bug I found), I realized that if you generate an image called Control_AudioSwitch_2x.png and add it to the Bitmaps section, the UI will automatically use it on HiDPI screen like retina displays!

Adding a parameter to the code

Now that we have a UI, we need to tie the UI to the code so that toggling the switch actually does something.

Declaring the parameter in the controller

The controller (ABSwitchController) declares the parameter we just created in the initialize method:

// the toggle that switches between A and B input
parameters.addParameter(STR16 ("Audio Switch"), // title
                        STR16 ("A/B"), // units
                        1, // stepCount => 1 means toggle
                        0, // defaultNormalizedValue => we start with A (0)
                        Vst::ParameterInfo::kCanAutomate, // flags
                        ABSwitchParamID::kAudioSwitch, // tag
                        0, // unitID => not using units at this stage
                        STR16 ("Switch")); // shortTitle
  • parameters is a field that comes from the EditController class that the controller inherits from and is used to declare/register a parameter
  • a stepCount of 1 indicates it is a toggle
  • the defaultNormalizedValue specifies the original value of the parameter, which we set to 0 (which represents Input A)
  • the tag (defined in ABSwitchCIDs.h) is very important as it is the value that ties all the pieces together (the value comes from an enum and is set to 1000). Note that it is unclear to me what the actual allowed range for this tag is. Can it start at 0 for example? The helloworld sample code uses 500 and 1000… so I used a similar value.
  • I have no idea how short the short title is supposed to be (could not find an obvious explanation)

In order for the controller to be able to restore its state (for example after loading a project which contains this plugin), the setComponentState method needs to be implemented:

tresult ABSwitchController::setComponentState(IBStream *state)
{
  // we receive the current state of the component (processor part)
  if(state == nullptr)
    return kResultFalse;

  // using helper to read the stream
  IBStreamer streamer(state, kLittleEndian);

  // ABSwitchParamID::kAudioSwitch
  float savedParam1 = 0.f;
  if(!streamer.readFloat(savedParam1))
    return kResultFalse;
  setParamNormalized(ABSwitchParamID::kAudioSwitch, savedParam1);

  return kResultOk;

The state is provided as an IBStream and using the helper class IBStreamer you can read what was previously saved (see processor). Note how the value read from the stream is assigned to the proper parameter by using the setParamNormalized and tag previously used during registration!

Adding the parameter to the UI

In the UI editor, you now add a tag in the Tags tab of the bottom right area. I called it Param_AudioSwitch with a value of 1000 (which is the tag defined in the controller!).

Then the on/off button view needs to be edited to select this tag in the control-tag entry.

Don’t forget to save the UI.

Handling the parameter in the processor

Quite surprisingly, you don’t register/declare the parameter in the processor. You just use it. That being said, the parameter is usually represented by an actual concept in the processor code. In this case, the field fSwitchState (whose type is backed up by an enum class for clarity) represents which input is currently used.

Persistence

Similarly to the controller, you need to implement a couple of methods to read and write the parameter to handle persistence (for example, loading/saving a project).

// called to restore the state in the processor
tresult ABSwitchProcessor::setState(IBStream *state)
{
  if(state == nullptr)
    return kResultFalse;

  IBStreamer streamer(state, kLittleEndian);

  // ABSwitchParamID::kAudioSwitch
  float savedParam1 = 0.f;
  if(!streamer.readFloat(savedParam1))
    return kResultFalse;

  fSwitchState = ESwitchStateFromValue(savedParam1);

  return kResultOk;
}

This method is very similar to the one implemented in the controller with the difference that it saves the parameter value in the local fSwitchState field.

// called to save the state
tresult ABSwitchProcessor::setState(IBStream *state)
{
  IBStreamer streamer(state, kLittleEndian);
  streamer.writeFloat(fSwitchState == ESwitchState::kA ? 0 : 1.0f);
  return kResultOk;
}

This method is the one that generates the stream in the first place. In this case the streamer is used to write the content of the local fSwitchState field to the stream.

Updates

Finally, the last remaining piece of the puzzle is how the processor gets notified of changes in the UI (or through automation). Every time the process method is called, the ProcessData argument contains an entry referencing all the parameters that have changed since the last call to process (for clarity I have extracted this step into a separate method).

void ABSwitchProcessor::processParameters(IParameterChanges &inputParameterChanges)
{
  int32 numParamsChanged = inputParameterChanges.getParameterCount();
  for(int i = 0; i < numParamsChanged; ++i)
  {
    IParamValueQueue *paramQueue = inputParameterChanges.getParameterData(i);
    if(paramQueue != nullptr)
    {
      ParamValue value;
      int32 sampleOffset;
      int32 numPoints = paramQueue->getPointCount();

      // we read the "last" point (ignoring multiple changes for now)
      if(paramQueue->getPoint(numPoints - 1, sampleOffset, value) == kResultOk)
      {
        switch(paramQueue->getParameterId())
        {
          case kAudioSwitch:
            fSwitchState = ESwitchStateFromValue(value);
            break;

          default:
            // shouldn't happen?
            break;
        }
      }
    }
  }
}
  • the outer loop iterates over every change (in this case there should be at most 1)
  • the code is a little bit complicated because you do not get just a single value but potentially multiple values: every call to process handles several samples and the parameter may actually change at different points in time during this window. The “Parameters and Automation” section in the VST SDK documentation is actually pretty good at describing what could happen (check the diagram at the bottom of the page!).
  • for our case, we are simplifying and simply taking the last known value in the interval for our value
  • note how the switch statement determines which parameter we are talking about
As you can see there is a lack of consistency: in the UI editor and the controller, it is called tag. Here it is called parameterId. But it is the same thing!
Use

Now that we know how the parameter gets persisted and updated, we can simply use it. After all this, the actual value is stored in the fSwitchState field, ready to be used in the rest of the code.

In the initialize method we register another stereo audio input (B):

...
  // 2 ins (A and B) => 1 out
  addAudioInput(STR16 ("Stereo In A"), SpeakerArr::kStereo);
  addAudioInput(STR16 ("Stereo In B"), SpeakerArr::kStereo);
  addAudioOutput(STR16 ("Stereo Out"), SpeakerArr::kStereo);
...

The logic is now almost trivial!

...
  int inputIndex = 0;

  // this is where the "magic" happens => determine which input we use (A or B)
  if(data.numInputs > 1)
    inputIndex = fSwitchState == ESwitchState::kA ? 0 : 1;

  AudioBusBuffers &stereoInput = data.inputs[inputIndex];
  AudioBusBuffers &stereoOutput = data.outputs[0];
  
...

  // simply copy the samples
  memcpy(out[i], in[i], sampleFramesSize);

...

The fSwitchState field is used to determine which stereo input we should use. The rest of the logic simply copies the input samples to the output stereo pair (removed the gain section from Part 2) using memcpy for each channel (left and right).

Conclusion

At this stage we have a fully working A/B switch. It’s not pretty, it doesn’t have all the bells and whistles of the Rack Extension but the core part of the plugin is doing what it is supposed to do. The final version should be very similar to the Rack Extension (minus CV handling which is not available in the VST world :( ).

Next

Check out Part 4 for some more notes and comments regarding the remainder of the implementation.

Last edited: 2018/03/24