SynthQuest 5: Implementing ADSR


Welcome back to the SynthQuest series, where we’re building a VST from scratch. This is Episode 5, and today’s quest is to implement ADSR and open our synth inside a DAW for the first time. If you’re joining us for the first time, be sure to check out the previous episodes to catch up. Alright, let’s begin with a small video clip of 27 seconds to hear ADSR (attack/decay/sustain/release) in action–
Now, I’ll provide the literal definitions of all these, but after reading, watch the video again (using headphones), and you will get a good idea:
Attack: The time it takes for a sound to rise from silence to its peak amplitude when a note is pressed.
Decay: The time it takes for the sound to drop from its peak amplitude to the sustain level.
Sustain: Steady amplitude level that the sound holds as long as the note is held.
Release: The time it takes for the sound to fade from the sustain level to silence when the note is released.
We will declare a juce::ADSR object as a private member in the SynthVoice class in pluginProcessor.h file. At the same time, we’ll also declare juce::ADSR::Parameters object; this will be useful later.
juce::ADSR adsr;
juce::ADSR::Parameters adsrParams;
After that, go to the pluginProcessor.cpp file and set the sample rate in the SynthVoice::prepareToPlay() function, write:
adsr.setSampleRate(sampleRate);
Then in the SynthVoice::startNote() function write
adsr.noteOn();
This lets the ADSR object know where to start the attack phase of the envelope. Also,
osc.setFrequency(juce::MidiMessage::getMidiNoteInHertz(midiNoteNumber));
There are two functions above, one is getMidiNoteInHertz(), which converts the midiNoteNumber to its respective frequency, and then that frequency is fed to the osc.setFrequency() function. The oscillator needs to know which frequency to play.
In the SynthVoice::stopNote() function:
adsr.noteOff();
This starts the release phase of the envelope. Check out the official documentation to learn more about this class and its functions here.
Let’s go to the SynthVoice::renderNextBlock() function and apply this ADSR to the audio buffer for it to show effect:
adsr.applyEnvelopeToBuffer(outputBuffer, startSample, numSamples);
With this line, we now have a fully-fledged basic synthesizer which can play any note you tap on, and you can write your melodies now. Build the program by clicking on build solution or by pressing Ctrl+Shift+B. If your build is successful, it should show something like this:
The main .vst3 file’s location (in the terminal) for me is – E:\Files\C Projects\juce projects\basicOsc\basicOsc\Builds\VisualStudio2022\x64\Debug\VST3\basicOst.vst3\Contents\x86_64-win\basicOsc.vst3
Now, to test our plugin in a DAW, first of course we’ll need a DAW, so let’s download one. Reaper would be a good option; it is free and lightweight. You can download Reaper from here. Its installation is really easy. Create this folder if it’s not there already C:\Program Files\Common Files\VST3
, and paste the .vst3 file in that folder. After installation, open Reaper and go to options->preferences->Plug-ins->VST and then see if that folder is there or not, if not add it by clicking on edit path list and rescan for plugins. In most cases, it is there by default.
After this, go to track->insert virtual instrument on new track, then just search for the VST and open it:
Open the VST and then press Alt+B for the MIDI controller.
Every time you build the project, you’ll have to copy the .vst3 file to the VST3 folder for your DAW to recognise it.
Alright, now let’s try to understand what an AudioProcessorValueTreeState (APVTS) class is. See, in a plugin some parameters or knobs can be modulated to change the sound, and we know that to make GUI elements in our plugin, we have to edit the pluginEditor files, but to efficiently link GUI components to the DSP code and ensure parameter changes are properly made, we need to take help of the APVTS class.
Let’s write its code, open the pluginProcessor.h file, and declare an APVTS object publicly in the AudioProcessor class.
juce::AudioProcessorValueTreeState theValueTree;
And after this, we need a helper function which will help us to create new parameters (like gain, pan, etc) and it will feed the parameters to the APVTS object. This function returns a ParameterLayout, which will be required when we initialize the APVTS in the pluginProcessor.cpp file.
juce::AudioProcessorValueTreeState::ParameterLayout createParams();
I am going to declare it privately in the AudioProcessor class of the pluginProccessor.h file. After this, your pluginProcessor.h file should look like this:
Now, let’s head over to the pluginProcessor.cpp file and initialize our APVTS object.
theValueTree(*this, nullptr, "Parameters", createParams())
This is the line of code, the first argument is the parent class, the second one is the undoManager (we don’t need this now, that’s why nullptr), third one is the name of the parameter group, and the fourth one is the parameter creation helper function which returns a ParameterLayout class having all the parameters. Now, your constructor should look like this:
You can check out the synth-quest-5 branch in the GitHub repository for the code till here. In the next episode, we’ll be implementing the createParams() function and start with the GUI of the VST. Merci, and see you in the next one!
Subscribe to my newsletter
Read articles from Harshit Sarma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
