Audio Ordeal

Music Production, Podcast, and DJ Tutorials

How to build a VST – Lesson 5: Limiter 2

7 min read

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.

https://github.com/aRycroft/JuceTutorial4

Circular Buffer Class

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.

Right click folder to add new cpp and header files

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.

CircularBuffer default class setup

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.

Add the function siganture for getData and setData to CircularBuffer.h

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 void nextSample(); to header file

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 integer variables writeIndex, readIndex and delayLength to private section of header file

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;

 
#include "../JuceLibraryCode/JuceHeader.h" then declare AudioSampleBuffer buffer in private section.

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);

Declare a constructor function with BufferSize and delayLength as input variables.

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.

Create function definitions for everything declared in the header file.

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.

Call the constructor for the AudioSampleBuffer, set writeIndex to delayLength and readIndex to 0.

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;

Write code in the nextSample function to calculate the next write and read indexes.

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.

Adding more channels

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 (.)  

Get a reference to the current channel's CircularBuffer and calculate the next sample using this buffer.

processBlock with stereo functionality.

Quick UI

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.

Move the limiterThresh, attackTime and releaseTime to pluginprocessor.h

PluginProcessor.h with new public declarations.

Now we can access these variables from the PluginEditor class.

Declare three new slider objects.

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 and make visible all the previously declared sliders, and set their bounds.

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);

Set out where the gui elements will be

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.

More Encapsulation??

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.

https://github.com/aRycroft/JuceTutorial5

Leave a Reply

Copyright © Tom Jarvis 2020 All rights reserved. | Newsphere by AF themes.