Audio Ordeal

Music Production, Podcast, and DJ Tutorials

How to Build a VST: Compressor

8 min read

Compressors are one of the key plugins used in mixing and mastering. In this tutorial I’ll show you how to make one from scratch. At the end of this tutorial you’ll have built a working compressor with attack/release controls and a soft/hard knee selector. 

Project Setup

Add circularbuffer files to projucer project and into the source file directory

Create a new audio plugin called Compressor with Projucer. Copy and paste the CircularBuffer files from the last tutorial into the source folder. Then add exisiting files in Projucer, select CircularBuffer.cpp and CircularBuffer.h

You can download the files here.

https://github.com/aRycroft/JuceTutorial5

Add compressor.h and compressor.cpp to the projucer project

Now select Add New CPP & Header File… and create two files called Compressor.cpp and Compressor.h.

Compressor Class

Open the project in your ide and head to Compressor.h.

Add the following base code to Compressor.h.

#include "CircularBuffer.h"
#pragma once

class Compressor {
public:
private:
};

Add these variables in the private section of the class. tav is the averaging time used when calculating rms.

CircularBuffer buffer;
float tav, rms, gain;

For now the compressor class will have one public function and a constructor that takes no arguments.

Compressor();
float compressSample(float data, float thresh, float ratio, float attack, float release, float knee);

Compressor.h should look like this,

Create definitions for the two functions in Compressor.cpp.

In the Compressor() constructor class initialise the CircularBuffer and set tav, rms and gain to default values.

buffer = CircularBuffer(150, 20);
tav = 0.01;
rms = 0;
gain = 1;

How Compressors Work

There are 3 main operations we need for a compressor.
1. Calculate how loud the incoming signal is.
2. Calculate how much to reduce the gain by.
3. Smooth the gain transition to avoid artefacts.

1. Gain Detector

In the Limiter tutorial we used a peak detector to determine when to activate the limiter. This time we’re going to find the RMS value of the waveform. RMS is the continous power of a waveform over time.

rms = (1 - tav) * rms + tav * std::pow(data, 2.0f); //1
float dbRMS = 10 * std::log10(rms); //2

//1 Adjusts the rms value depending on the incoming signal.
//2 Converts this value to the decibel scale.

2. Gain reduction

Users of this compressor will be able to decide the compression ratio. This is how much the signal is compressed by once it passes a certain threshold.

We convert the ratio to a slope, which is a decimal value.

(Ratio) 1:4 -> (Slope) 0.7
(Ratio) 1:2 -> (Slope) 0.5

Next we multiply (threshold – dbRMS) by this slope factor. Then check that it is below 0 to make sure the signal is never boosted if the rms value is above the threshold.

Add gain reduction calculation
float slope = 1 - (1 / ratio); //1
float dbGain = std::min(0.0f, (slope * (thresh - dbRMS))); //2
float newGain = std::pow(10, dbGain / 20); //3

//1 Calculate slope from ratio
//2 Find the gain to be applied in db, and make sure it’s less than 0.0f
//3 Calculate the newGain in linear scale

3. Smooth transition

Now we know what gain to apply to the current signal we need to smooth the transition to this new value. We do this using attack and release times.

float coeff;
if (newGain < gain) coeff = attack; //1
else coeff = release; //2
gain = (1 - coeff) * gain + coeff * newGain; //3

//1 Declare coeff variable, if newGain is less than current gain set to attack variable
//2 Else set to release
//3
Adjust gain based on new gain and coeff

Finally we need to set and get values from our CircularBuffer

float compressedSample = gain * buffer.getData();
buffer.setData(data);
buffer.nextSample();
return compressedSample;

Coding PluginProcessor

We now have a working Compressor class! But we aren’t doing anything with it yet.

Head to PluginProcessor.h.

#include "Compressor.h" //1

Array allCompressors; //2

Include the new Compressor class at the top of the file, and declare a new array of Compressors.

Now in PluginProcessor.cpp

//Prepare To Play
for (int channel = 0; channel < getNumOutputChannels(); channel++) {
   allCompressors.add(Compressor());
}
//Process Block
for (int i = 0; i < buffer.getNumSamples(); i++) {
   for (int channel = 0; channel < getTotalNumOutputChannels(); channel++) {
auto* data = buffer.getWritePointer(channel); Compressor* comp = &allCompressors.getReference(channel); //1 data[i] = comp->compressSample(data[i], -30.0f, 20.0f, 0.01f, 0.4f, 0.0f); //2
} }

In PrepareToPlay create an array of Compressors.

In ProcessBlock loop through samples and channels.
// 1 Get reference Compressor for the current channel.
// 2 Calculate the compressed samples with some initial values passed into the compressSample function.

Build your plugin now and you should hear the incoming audio being compressed. Try changing the values passed to the compressor class, then rebuilding to hear the difference.

GUI

For this plugin we’re going to make a GUI using ValueStateTree. As of April 2020 I believe this is the best way to make a pluginGUI in Juce.

We’ll create a ValueStateTree in our plugin and let the Editor access and edit the tree. The PluginProcessor will only be able to read values from the tree. Using a ValueStateTree means we can easily save and recall the state of the plugin, as well as add parameter automation in a DAW.

Creating valuestateTree
//PluginProcessor.h
AudioProcessorValueTreeState state;
//PluginProcessor.cpp
,
state(*this, nullptr, Identifier("params"), {

}
)

Declare an AudioProcessorValueTreeState called state in PluginProcessor.h then add a constructor in the AudioProcessor Constructor list.

We’ll add the parameters to the StateTree in this constructor.

Add threshold parameter to constructor
std::make_unique<AudioParameterFloat>(
"thresh",
"Threshold",
NormalisableRange<float>(-60.0f, 20.0f, 0.01f),
10.0f),

This creates a unique pointer to an AudioParameterFloat with
identifier “thresh”,
name “Threshold”,
numberRange betwwen -60.0f -> 20.0f with spacing 0.01f,
default value of 10.0f

Add the following AudioParameterFloat parameters using the same method.

“ratio” 1.0f -> 20.0f
“knee” 0.0f -> 24.0f
“attack” 0.01f -> 500.0f
“release” 0.01f -> 2000.0f

 std::make_unique(
"thresh",
"Threshold",
NormalisableRange(-60.0f, 20.0f, 0.01f),
10.0f),
std::make_unique(
"ratio",
"Ratio",
NormalisableRange(1.0f, 20.0f, 0.01f),
2.0f),
std::make_unique(
"knee",
"KneeWidth",
NormalisableRange(0.0f, 24.0f, 0.01f),
0.0f),
std::make_unique(
"attack",
"Attack",
NormalisableRange(0.01f, 500.0, 0.01f),
100.0f),
std::make_unique(
"release",
"Release",
NormalisableRange(0.01f, 2000.0f, 0.01f),
500.0f)

Now our statetree is setup in PluginProcessor we need to pass it into PluginEditor.

Head to PluginEditor.h and declare a pointer to an AudioProcessorValueTreeState called params.

Also change the constructor to receive an AudioProcessorValueTreeState object.

Now in PluginEditor.cpp edit the constructor to receive the state and point params to the state object.

Back in PluginProcessor.cpp change the editor constructor to include passing the statetree.

Now we’ve passed the stateTree to the editor we need to create some sliders to control the values in the tree.

In PluginEditor.h create this typedef at the top of the file.

typedef AudioProcessorValueTreeState::SliderAttachment SliderAttachment;

This saves writing AudioProcessorValueTreeState::SliderAttachment everytime we want to use it.

Declare these sliders, labels and sliderattachments in PluginEditor.h. A slider attachment will allow values in the stateTree to be changed by a slider.

Slider threshSlider, slopeSlider, kneeSlider, attackSlider, releaseSlider;
Label threshLabel, slopeLabel, kneeLabel, attackLabel, releaseLabel;
std::unique_ptr<SliderAttachment> threshAttachment, slopeAttachment, kneeAttachment, attackAttachment, releaseAttachment;

Add the following function to your PluginEditor, you can declare it as private in the header file as it won’t be called anywhere else.

void CompressorAudioProcessorEditor::addSlider(String name, String labelText, Slider& slider, Label& label, std::unique_ptr& attachment) {
addAndMakeVisible(slider);
attachment.reset(new SliderAttachment(params, name, slider));
label.setText(labelText, dontSendNotification);
label.attachToComponent(&slider, true);
addAndMakeVisible(label);
}

We’ll use this to quickly add sliders, labels and attachments to the GUI.

Now we can write this code in the Editor constructor to add all the sliders we want to the GUI and attach them to the stateTree.

addSlider("thresh", "Threshold", threshSlider, threshLabel, threshAttachment);
addSlider("ratio", "Ratio", slopeSlider, slopeLabel, slopeAttachment);
addSlider("knee", "Knee", kneeSlider, kneeLabel, kneeAttachment);
addSlider("attack", "Attack", attackSlider, attackLabel, attackAttachment);
addSlider("release", "Release", releaseSlider, releaseLabel, releaseAttachment);

Finally set the bounds of the slider components in the resized() function.

threshSlider.setBounds(100, 0, 200, 50);
slopeSlider.setBounds(100, 50, 200, 50);
kneeSlider.setBounds(100, 100, 200, 50);
attackSlider.setBounds(100, 150, 200, 50);
releaseSlider.setBounds(100, 200, 200, 50);

Now we’re finished coding the PluginEditor but we’re not using the values in PluginProcessor.

//PluginProcessor.h
float* threshParam, *slopeParam, *kneeParam, *attackParam, *releaseParam;
//PluginProcessor.cpp -> PrepareToPlay
threshParam = state.getRawParameterValue("thresh");
scopeParam = state.getRawParameterValue("ratio");
kneeParam = state.getRawParameterValue("knee");
attackParam = state.getRawParameterValue("attack");
releaseParam = state.getRawParameterValue("release");

Declare these pointers in the header file and set them to values in the stateTree in prepareToPlay.

These pointers don’t directly store the float value. They point to the memory address where the value is stored.

Now we can pass these values into our CompressSample function. We can do some maths on the attack and release times to convert from seconds to milliseconds and from a linear to time scale.

float at = 1 - std::pow(MathConstants::euler, ((1 / getSampleRate()) * -2.2f) / (*attackParam / 1000.0f));
float rt = 1 - std::pow(MathConstants::euler, ((1 / getSampleRate()) * -2.2f) / (*releaseParam / 1000.0f));

for (int i = 0; i < buffer.getNumSamples(); i++) {
for (int channel = 0; channel < getTotalNumOutputChannels(); channel++) {
auto* data = buffer.getWritePointer(channel);
Compressor* comp = &allCompressors.getReference(channel);
data[i] = comp->compressSample(data[i], *threshParam, *slopeParam, at, rt, *kneeParam);
}
}

The last thing we’re going to with the gui is add code to save and recall the StateTree we just created. We can do this by changing two functions in PluginProcessor.cpp.

void CompressorAudioProcessor::getStateInformation(MemoryBlock& destData)
{
auto stateTree = state.copyState();
std::unique_ptr<XmlElement> xml(stateTree.createXml());
copyXmlToBinary(*xml, destData);
}

void CompressorAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
{
std::unique_ptr<XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));
if (xmlState.get() != nullptr && xmlState->hasTagName(state.state.getType())) {
state.replaceState(ValueTree::fromXml(*xmlState));
}
}

At this point we’ve got a fully working Compressor, with parameters that are saved when you stop using the plugin and recalled when you come back.

Adding a Soft Knee (OPTIONAL)

The compressor we built so far had a hard knee. That means when an RMS is detected above the threshold, the ratio is set instantaneously to the new ratio. By introducing an area where if an RMS is detected inside it the slope is changed along a curve, we can create a soft knee. This is regarded as giving a compressor a more natural sound, it has a subtle effect on the sound.

There are many ways to interpolate between two points but we’re going to use lagrange interpolation.

First off we’re going to write a function to do this in Compressor.cpp.

float Compressor::interpolatePoints(float* xPoints, float* yPoints, float detectedValue) {
float result = 0.0f;
int n = 2;

 for (int i = 0; i < n; i++){
float term = 1.0f;
for (int j = 0; j < n; j++{
if (j != i) {
term *= (detectedValue - xPoints[j]) / (xPoints[i] - xPoints[j]);
}
}
result += term * yPoints[i];
}
return result;
}

Don’t worry about understanding how this function works too much, all you need to know is that it takes an array of two xpoints, two y points and approximates a value between them. 

This value will replace our slope value.

if (knee > 0 && dbRMS > (thresh - knee / 2.0) && dbRMS < (thresh + knee / 2.0)) {
   float kneeBottom = thresh - knee /    2.0, kneeTop = thresh + knee / 2.0;
float xPoints[2], yPoints[2];
xPoints[0] = kneeBottom;
xPoints[1] = kneeTop;
xPoints[1] = std::fmin(0.0f, kneeTop);
yPoints[0] = 0.0f;
yPoints[1] = slope;
slope = interpolatePoints(&xPoints[0], &yPoints[0], thresh);
thresh = kneeBottom;
}

This block of code sets up some variables for the interpolation function we just wrote.

First it checks the current rms value is in the knee zone, between thresh – knee / 2.0 and thresh + knee / 2.0.

Then we make two arrays of points, 
xPoints holds the db values of the bottom and top of the knee region.
yPoints hold the slope values.

Once these arrays are set up we interpolate a new slope value and set the threshold of the compressor to the lower knee value.

Rebuild the plugin and you should see/hear a difference when you change the knee slider. Don’t worry if it doesn’t sound drastically different:)

That’s the end of this compressor tutorial, code can be found at the following link.

https://github.com/aRycroft/JuceTutorial6

6 thoughts on “How to Build a VST: Compressor

  1. Thanks so much. I have finished all the lessons. You did a great job and I learned a lot. I would be stoked if you kept going!

    1. Thanks Ethan! Is there anything you’d be interested in learning about in the next lessons? Thinking of doing delays/reverbs next..

      1. Hi Alex thanks for the tutorial. Would love to see a tutorial on pitch correction / auto tuning. Is that possible?

  2. Hey Alex! A delay and a reverb would be awesome! Eventually, I want to build a convolution synth, where you take the FFT, manipulate it and then iFFT it. It was a really interesting thing that you could mess with in Synthmaker, which was a graphical DSP thing. It is way ahead of my level atm, but maybe a Convolution reverb would also be a rad tutorial that would be on elf the stepping stones. Thanks again for your time and efforts! Best, Ethan

  3. Hey Alex, I just wanted to say that these tutorials have been helping me a lot. Thanks for making them.

Leave a Reply

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