Tutorial: A compressor in Pure Data
Compressors have become more than just gain control units, they can be just as important as EQs in shaping a sound and sometimes even more so. For the mathematically inclined, a compressor works with a transfer function, or in plain speak, it changes its input in a predictable way. The controls of a compressor help specify this transfer function. The most common controls include: threshold (specifies when the compressor kicks in, usually in decibels), ratio (the amount a signal is compressed once it crosses the threshold), attack (the time taken for the compressor to begin compressing once the signal crosses the threshold), release (the time taken for the signal to return to ‘normal’, i.e., for the compressor to stop having an effect) and make-up gain (a post compression gain). It is quite common for a compressor to have other controls like specifying an alternate side-chain signal, filtering of the side-chain signal, choice between RMS and peak detection or look ahead (where the signal is delayed and then compressed).
Building a compressor in Pure Data (or Max) can be fairly straightforward – depending on the functionality you are looking for. For the purpose of this post I will include the following controls:
- Attack and release (with a unified control to keep things simple)
- A choice between peak and RMS detection
- Make-up gain
A typical compressor works by analysing the input signal and applying a reduction in gain to this same input signal based on the parameters specified (threshold, ratio, etc). A simplified schematic:
Most of the magic and sound trickery in a compresor is in the way it analyses the signal and computes the gain. There’s lots of information about this available on the web (including the excellent post by Herbert Goldberg that Shaun mentioned the other day). For this post, I have followed a fantastic paper on compressor design and implemented a portion of it into Pure Data Vanilla (I have mentioned the Max equivalent objects along the way and have included a comparison chart towards the end of this post). To also keep things simple I have unified the attack and release times, i.e., the attack and release times are the same. This is not entirely ideal as it decreases the flexibility and creative use of the compressor.
Calculating the RMS of the side-chain signal in both Pd and Max is fairly straightforward. In Pd it is calculated with the [env~] object and in Max with the [average~] object (check the help file in Max for the object as you will have to switch it to RMS mode).
The RMS of a signal is calculated by windowing the signal (followed by more maths) and the [env~] object allows the window size to be specified in samples, which I have set to 512. The [env~] object also follows a strange decibel convention (as with most dB related objects in Pd). 100dB equates to no gain (or a signal multiplied by 1, [*~ 1]), anything above 100 is a positive change in gain (103dB is [*~ 1.412]) and anything below hundred is a negative change (97dB is [*~ 0.707]).
The [*~] object in Pd (or Max) is commonly used like an amplifier – to scale signals. It can compute only linear numbers, not decibels. The [dbtorms~] object helps make the output of [env~] compatible with [*~] (it converts dB to linear amplitude).
The peak of a signal is detected by taking the absolute of the input signal (check the above mentioned paper if you want more information). In Pd and Max this can be done using the [abs~] object.
Calculating the gain change for the compressor can be tricky. Fortunately it has been specified as a formula (worry not, the maths is not too hardcore) in the above mentioned paper:
If the level of the signal is below the threshold,
output = input
If the level of the signal is above the threshold,
output = threshold + (input - threshold)/ratio
This formula is for a hard knee compressor. A soft knee compressor is a little more complicated.
To implement this formula in Pd it would be best to break it down into smaller chunks (unless you want to get handy with the expr/expr~ objects). First:
input - threshold
The output of that calculation is divided by ratio:
(input - threshold)/ratio
The output of that is added with the threshold:
threshold + (input - threshold)/ratio
To make the patch tidier and easier to use, it would be best to use sends and receives with a $0 tag (this helps create local send and receives, just incase you have multiple instances of the patch running). The sends and receives will also make it easer to communicate with the rest of the patch and a GUI if required. Furthermore, division (/) calculations are more expensive than multiplication (*) so it would be better to replace the [/~] with a [*~] for the ratio calculation and calculate the inverse of the ratio (1/ratio) in the message domain instead of the signal domain elsewhere in the patch.
The last step of the gain computation process requires the output of the formula to be divided by the input. I’ve also added in the signal detection components and created a sub-patch for each detection type (the [+~ 0] doesn’t do anything other than keeping the patch neat).
The RMS gain computation sub-patch:
The Peak gain computation sub-patch:
Clip and Response:
To stop the compressor from gaining the signal (positively) when the threshold is at 0 I’ve used the [clip~] object:
The next step is to add the attack/release control. Since it is a unified control (both the attack and release have the same time), I have used a low pass filter as a ramp generator. By converting time (in ms) to frequency (frequency = 1000/time), the lowpass filter can be used as an attack and release control. For ease of use and control I have also created a receive object to control the frequency of the filter.
The lookahead feature in a compressor works by delaying the input signal before the gain control stage (not the signal being analysed, the image at the top of the post illustrates this).
A delay in Pd can be created using [delwrite~] and [delread~] objects (in Max use [tapin~] and [tapout~]). Here’s what it looks like:
The [delwrite~] object requires a name followed by the maximum delay time in milliseconds and the [delread~] object required the name of the [delwrite~] object, while the delay time can be specified at its inlet.
If the delay objects are patched like the way they are in the above image, Pd will be unable to delay a signal less than its block size (if this goes over your head just remember that Pd won’t be able to delay a signal less than 1.4ms in a typical setup). To overcome this problem the delay objects must be encapsulated into sub-patches that are connected by a dummy cable (for more information about this look up Pd help). I have also created a receive object to change the delay/lookahead time remotely.
Here’s an overview of the patch so far:
Adding a make-up gain feature is as simple as using another [*~] after the gain control, although with the [line~] object to smoothen incoming control data (I’ve created a receive object named $0-gain):
If the patch is run in its above state, both the Peak and RMS detection modules will work simultaneously. What is needed is a switch, that turns one module on and the other off.
If the [switch~] object is placed in a Pd subpatch, it can be used to toggle its DSP state. Sending a “1” toggles the DSP of that sub-patch (i.e., make the sub-patch work) and a “0” turns it off.
Here’s the Peak sub-patch with the [switch~] object and an inlet to control its state (the RMS sub-patch is similar):
The parent patch needs something that turns the switch~ of one subpatch on, while turning the other off. Here’s how I’ve implemented it:
The [== 0] inverts the input. If it receives a 0, it spits out a 1 and vice-versa. The above setup ensures that if [r $0-rmspeak] is sent a 0, the RMS module is activated and the peak module deactivated.
I’m not a fan of patches with tens of inlets. I’d rather use message boxes so I can keep patches organised and tidy while also being able to save settings with the patch. To control the compressor I’ve included a single inlet connected to a route object:
If the [route] object is sent messages “threshold -10” and “ratio 2”, it will spit out “-10” from the first inlet and “2” from the second inlet and so on. Each outlet is connected to their respective sends (that send the data to their respective receives). I have also also included other objects to standardise and translate the incoming data.
I wanted the compressor to be used like most other digital compressors out there – with the controls in the dBFS scale.
- Threshold: Threshold values need to be specified from a range of 0dB to -100dB. To make the values compatible, I first convert the values to the Pd dB scale (which I mentioned earlier) by adding 100 and then using the [dbtorms] object to convert the dB value to a linear amplitude value (the name of the [dbtorms] object is a bit misleading, it should be named [dbtolinear]).
- Ratio: The gain computation formula used the inverse of the ratio. The [swap] and [/] objects calculate the inverse of the ratio
- Response: Specified in milliseconds, the value is converted to frequency (for the low pass filter)
- Makeup gain: Similar to the threshold, dBFS values are converted to linear amplitude values
Here’s the final patch, with some additional features to initialise the compressor and send/receive values to a UI object (click to zoom):
Here’s the patch with a GUI abstraction:
Feel free to download the patches (below). Any thoughts, comments or suggestions are welcome!
A comparison of Pd and Max objects (note: Max uses a dBFS scale unlike Pd and the [average~] object outputs linear amplitude values):
EDIT: The aim was to get a compressor working in Pd Vanilla. With Pd Extended (and even Max) there are a lot more objects, like [slide~] and [rampsmooth~] that can make things a bit easier.