Skip to main content

Command Palette

Search for a command to run...

SynthQuest 6: How to Build the GUI for Your VST in JUCE

Episode - 6

Updated
4 min read
SynthQuest 6: How to Build the GUI for Your VST in JUCE
H

a web developer who likes to do a bit of audio dev

Welcome back to the SynthQuest series, where we’re building a VST from scratch. This is Episode 6, and today we are going to implement the createParams() function along with the GUI. If this is your first time here, make sure to check out the previous episodes to catch up. Let’s start by creating the createParams() function:

juce::AudioProcessorValueTreeState::ParameterLayout BasicOscAudioProcessor::createParams() {}

Inside the function, we are going to create a vector that is going to save the pointers to the juce::RangedAudioParameter class, which is nothing but a base class for all juce plugin parameters.

std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;

Okay, so in our case, we are going to use AudioParameterChoice (for choosing different sound waves/oscillators) and AudioParameterFloat (for gain, filter, and other things). Let’s create a parameter for the oscillators:

params.push_back(std::make_unique<juce::AudioParameterChoice>("OSC", "Oscillator", juce::StringArray{"Sine", "Saw", "Square", "Triangle"}, 0));

We are pushing a unique pointer to the juce::AudioParameterChoice object to the vector; this ensures automatic allocation and deallocation, no need for new and delete. Now, in the constructor, we are passing a few parameters; the first one is the ID, then the name of the parameter, then a juce::StringArray, and lastly the default value.

Let’s create the adsr parameters :

params.push_back(std::make_unique<juce::AudioParameterFloat>("ATTACK", "Attack", juce::NormalisableRange{ 0.0f, 1.0f }, 0.0f));

params.push_back(std::make_unique<juce::AudioParameterFloat>("DECAY", "Decay", juce::NormalisableRange{ 0.0f, 1.0f }, 0.0f));

params.push_back(std::make_unique<juce::AudioParameterFloat>("SUSTAIN", "Sustain", juce::NormalisableRange{ 0.0f, 1.0f }, 1.0f));

params.push_back(std::make_unique<juce::AudioParameterFloat>("RELEASE", "Release", juce::NormalisableRange{ 0.0f, 3.0f }, 0.1f));

Then lastly, we are going to return the vector:

return { params.begin(), params.end() };

So, the whole createParams() function should look like this :

Let’s move to the GUI section now, open the pluginEditor.h file and declare the sliders and stuff. We need all the 4 ADSR sliders and a drop-down menu (comboBox) for choosing between different oscillators. There is this juce class for sliders already, so we are just going to use that :

juce::Slider attackSlider;

juce::Slider decaySlider;

juce::Slider sustainSlider;

juce::Slider releaseSlider;

Also, we are going to write them in the private section. Then we are going to declare a comboBox object for the drop-down :

juce::ComboBox oscSelector;

Now, let’s understand a new concept. What are attachments? To connect the sliders and knobs in the pluginEditor to pluginProcessor file, we use respective attachment classes. Let’s take an example. Previously, we created parameters in the createParams() function, which was fed to the AudioProcessorValueTreeState class. Now, we need to link those to the GUI. For that, we are going to declare attachments.

std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attackAttachment;

std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> decayAttachment;

std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> sustainAttachment;

std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> releaseAttachment;

std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> oscSelectAttachment;

So, now your pluginEditor.h file should look something like this :

Now, head over to the pluginEditor.cpp file and implement the above things. Firstly, we are going to implement the attachments, so let’s take the attack attachment, for example :

attackAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(audioProcessor.theValueTree, "ATTACK", attackSlider);

We are making a unique pointer to the juce::AudioProcessorValueTreeState::SliderAttachment class with three arguments: firstly, the apvts class we declared, then the ID of the parameter (here “ATTACK”), and then the slider. Now, you know how the AudioProcessorValueTreeState class helps with parameters. These should be written in the constructor of the BasicOscAudioProcessorEditor class. We will do this for all the attachments :

decayAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(audioProcessor.theValueTree, "DECAY", decaySlider);

sustainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(audioProcessor.theValueTree, "SUSTAIN", sustainSlider);

releaseAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(audioProcessor.theValueTree, "RELEASE", releaseSlider);

oscSelectAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment>(audioProcessor.theValueTree, "OSC", oscSelector);

Now, let’s define the type of slider we want and where to align the text box, and also make the slider visible on the GUI :

attackSlider.setSliderStyle(juce::Slider::SliderStyle::LinearVertical);

attackSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, true, 50, 25);

addAndMakeVisible(attackSlider);

Now, for the rest of the ADSR sliders :

decaySlider.setSliderStyle(juce::Slider::SliderStyle::LinearVertical);

decaySlider.setTextBoxStyle(juce::Slider::TextBoxBelow, true, 50, 25);

addAndMakeVisible(decaySlider);

sustainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearVertical);

sustainSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, true, 50, 25);

addAndMakeVisible(sustainSlider);

releaseSlider.setSliderStyle(juce::Slider::SliderStyle::LinearVertical);

releaseSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, true, 50, 25);

addAndMakeVisible(releaseSlider);

In the drop-down menu, we will now add the other wave types :

oscSelector.addItem("Sine", 1);

oscSelector.addItem("Saw", 2);

oscSelector.addItem("Square", 3);

oscSelector.addItem("Triangle", 4);

Now setting the default wave as sine :

oscSelector.setSelectedId(audioProcessor.theValueTree.getRawParameterValue("OSC")->load() + 1);

I’ll explain what is happening here, audioProcessor.theValueTree.getRawParameterValues(“OSC”)->load() will return 0 as the value because, in the createParams() function, we set 0 as the default value; however, ComboBox indexes start from 1. To match them correctly, we add 1.

Now you can ask why not directly write 1 in the setSelectedId() function? Well, if you do that, then whenever you switch between different VST windows in a DAW and then open our VST, it is going to be a sine wave again, even if you changed it.

oscSelector.setJustificationType(juce::Justification::centred);

addAndMakeVisible(oscSelector);

We just centered the comboBox and made it visible. The file should look something like this :

And the rest of the code follows.

Now, in the paint function, we can change the colour of the background and stuff :

g.fillAll (juce::Colour(251, 133, 0));

This is a juce::Colour class which can take RGB values on its constructor. This will change the background colour of our VST to a shade of orange. You can run the code and see for yourself.

You can check out the synth-quest-6 branch in the GitHub repository for the code till here. In the next episode, we are going to write the resized() function, we set the height, width, x, and y basically we position the sliders. Obrigado, and see you in the next one!

SynthQuest

Part 2 of 7

SynthQuest is a 7-part blog series on building a Synth VST plugin using C++ and JUCE. From setup to sound design, it walks you through DSP, GUI, and plugin logic; perfect for devs diving into audio programming.

Up next

SynthQuest 5: Implementing ADSR

Episode - 5