In the last lesson I showed you how to build a limiter VST that works on one channel of audio. Find the code so far at this link.
Last time we wrote some code in the processBlock to create a circular audio buffer. It would be nice to not have to copy this logic everytime we wanted a circular buffer. We can encapsulate the code we wrote into a CircularBuffer class that we can use in multiple projects without copying and pasting code.
In your Projucer project right click in the file viewer and select ‘Add New Cpp & Header file..’
Call the files CircularBuffer.cpp and CircularBuffer.h, make sure they are in the source folder.
In CircularBuffer.h write this base code that declares a class called CircularBuffer. The class name should always be the same as the name of the file.
class CircularBuffer {
public:
private:
};
In the header file we need to declare what functions this class will have. We write function prototypes that say what the function will return, and what inputs it has.
We want to be able to write and read data in the buffer, but we don’t need to decide at which index.
float getData();
void setData(float data);
getData doesn’t require any variables, it will just return the data at the current writeIndex.
setData doesn’t need an index, it will only set the data at the current readIndex.
In CircularBuffer.h public section add the getData and setData function signatures.
We also need a way to move the read and writeIndexes.
void nextSample();
Add the nextSample signature.
So far we’ve added function signatures in the public section of the class, which means any other class can call these functions. We need to declare some private variables to handle the buffer logic. These won’t be accessible by other classes.
int writeIndex;
int readIndex;
int delayLength;
Add writeIndex, readIndex and delayLength to the private header section.
We’ll also need an AudioSampleBuffer to store the samples. To declare this we need to write this line to include the source code for this type of buffer.
#include “../JuceLibraryCode/JuceHeader.h”
Now we can declare the following variable.
AudioSampleBuffer buffer;
Add #include and AudioSampleBuffer
The final thing we need to do in the header file is add a constructor method. This is called once when a new CircularBuffer object is created, and deals with any intialisation that needs to happen.
C++ also requires you to write a default constructor, this is called with no arguments.
CircularBuffer();
CircularBuffer(int bufferSize, int delayLength);
Adding a constructor and default constructor function.
Now we’ve declared everything we need in the header file to start coding CircularBuffer.cpp.
We need to create definitions of each of these functions in the cpp file. In visual studio by clicking on the functions and pressing alt + enter then selecting create definition, it will do this for you automatically. Otherwise make your CircularBuffer.cpp file look like the picture below.
Your CircularBuffer.cpp file should look like this.
Add this code to the default constructor. It just sets an empty AudioSampleBuffer and all other variables to 0.
buffer = AudioSampleBuffer();
writeIndex = readIndex = delayLength = 0;
Default constructor method.
Add this code to CircularBuffer(int bufferSize, int delayLength);
buffer = AudioSampleBuffer(1, bufferSize); //1
buffer.clear(); //2
writeIndex = delayLength; //3
readIndex = 0; //4
this->delayLength = delayLength; //5
1- Call the constructor for an AudioSampleBuffer.
2- Clear all samples in the buffer.
3- The writeIndex should be delayLength spaces infront of the readIndex.
4- Set readIndex to 0;
5- Save the delayLength passed into the constructor in a variable in the object.
CircularBuffer constructor with initialisation code.
Next code the getData and setData functions.
return buffer.getSample(0, readIndex); //1
buffer.setSample(0, writeIndex, data); //2
1- Finds the sample in the readIndex position of the buffer.
2- Sets data as the value in the buffer at the writeIndex position.
getData and setData functions.
We can reuse some of the code we wrote in the Limiter processBlock for the nextSample function.
int bufferLength = buffer.getNumSamples();
readIndex = ((bufferLength + writeIndex) – delayLength) % bufferLength;
writeIndex = (writeIndex + 1) % bufferLength;
This is similar to what we wrote in the processBlock, it finds the next read and write indexes.
At this point our CircularBuffer class has all the functionality we need to use it in the PluginProcessor.
Take the following steps to use it in the PluginProcessor.
1- Add #include “CircularBuffer.h” in PluginProcessor.h.
2- Replace AudioSampleBuffer delayBuffer; with CircularBuffer delayBuffer; in the header file.
3- Replace the AudioSampleBuffer initialisation code in prepareToPlay with delayBuffer = CircularBuffer(10, 1);
After taking these steps we can replace the code we previously wrote in the processBlock function.
float limitedSample = gain * delayBuffer.getData();
delayBuffer.setData(sample);
delayBuffer.nextSample();
Replace the old circular buffer code with the class functions we just wrote. You can tidy up by deleting the old writeIndex, readIndex, delayIndex and bufferLength variables.
Build your plugin now and it should work exactly how it did before. By encapsulating the CircularBuffer logic into it’s own class you can now use it in any of your projects. It also makes the processBlock a lot tidier and easier to read.
After writing that class it’s very easy to add more channels to our plugin. For each channel we’ll need a dedicated CircularBuffer. We can store all of these in an Array and select each one depending on the channel of audio currently being processed in the processBlock.
Juce has a built-in array that we’ll use for this.
Array < CircularBuffer > allBuffers;
Declare this in you header file. The <> brackets just mean we want an array of CircularBuffer objects.
Next in prepareToPlay add the following code.
allBuffers = Array < CircularBuffer > ();
for (int channel = 0; channel < getNumOutputChannels(); channel++) {
allBuffers.add(CircularBuffer(10, 1));
}
Here we initialise the allBuffers array and add a new CircularBuffer for each channel of output audio in the plugin.
prepareToPlay with the new initialisation code.
Write another for loop inside the main process loop that goes from 0 to the total number of output channels.
auto* data = buffer.getWritePointer(channel);
CircularBuffer* delayBuffer = &allBuffers.getReference(channel);
These lines of code get the data and delayBuffer for the current channel.
Don’t worry about the syntax in the second line of code, we need to use it so we get a pointer to the object stored in the array not a copy. Read up about C pointers if you’d like to know more 🙂
You will also need to change the code at the bottom of the processBlock to use (->) instead of (.)
processBlock with stereo functionality.
The last thing we’ll do in this tutorial is make a quick and easy UI for the plugin. Start by moving the limiterThresh, attackTime and releaseTime variables to the PluginProcessor header file, in the public section.
We also need to set some default values for the PluginEditor to be able to change the values.
PluginProcessor.h with new public declarations.
Now we can access these variables from the PluginEditor class.
In PluginEditor.h declare three new Sliders.
Slider threshold, at, rt;
In PluginEditor.cpp write the following code to add the sliders to the gui.
addAndMakeVisible(&threshold);
threshold.setValue(0);
threshold.setRange(-60.0f, 10.0f, 0.001);
addAndMakeVisible(&at);
at.setRange(0.0f, 10.0f, 0.001);
addAndMakeVisible(&rt);
rt.setRange(0.0f, 10.0f, 0.001);
Add this code to create 3 sliders, these don’t affect anything yet.
Then in the resized function set where they will be drawn in the gui.
threshold.setBounds(50, 50, 200, 50);
at.setBounds(50, 150, 200, 50);
rt.setBounds(50, 250, 200, 50);
This sets where the sliders will be placed in the gui.
Add these lines of code to the PluginEditor constructor.
These functions set the variables we previously exposed in the PluginProcessor class.
threshold.onValueChange = [this] {
processor.limiterThresh= std::pow(10, (threshold.getValue() / 20));
};
at.onValueChange = [this] {
processor.attackTime= 1 – std::pow(MathConstants < float > ::euler, ((1 / processor.getSampleRate()) * -2.2f) / at.getValue());
};
rt.onValueChange = [this] {
processor.releaseTime= 1 – std::pow(MathConstants < float > ::euler, ((1 / processor.getSampleRate()) * -2.2f) / rt.getValue());
};
Theses functions also convert from a decibel to linear scale for the threshold, and from time to linear for attackTime and releaseTime.
Add these functions to allow the PluginEditor to change values in PluginProcessor.
This is all the code I’ll be doing in this tutorial. Build the plugin and try it out! It’s fairly basic but I think it sounds pretty good.
If you’re looking for more to do, think about how you could encapsulate this code further. Currently if the left channel of audio is loud and turning on the limiter, the right channel is limited too. Could you write a new class to also encapsulate the gain and xPeak variables? This would mean each channel is limited independently.
You could also move all the code currently in ProcessBlock to the new class. Meaning you would only need to call one function for each sample in the block. This would make it really easy to add a limiter effect to any other plugins you’re making.
Find the code for this tutorial here.