Home

Awesome

<!-- omit in toc -->

CTAG Dynamic Range Compressor

<!-- omit in toc -->

An audio compressor plugin created with JUCE

<img src="Documentation/CTAGDRC_Snap.png" width="70%"> <!-- omit in toc -->

Contents

Introduction

This is a simple vst3/au compressor plugin that was build with the goal in mind to create a good sounding compressor for a wide variety of signals and applications. The GUI is kept minimalistic to improve usability and not clutter the screen. The project was mostly used to further my understanding and knowledge of digital signal processing and digital audio effects. A demonstration is available on YouTube.

Features

Manual

General

Gain Computer

Ballistics

Parameter Automation

Metering

Technical

Design Approach

In general there are two topologies for compressor design:

Most modern compressors use a feedforward topology due to limitations of the feedback topology such as the inability to allow a lookahead functionality aswell as to work as a limiter due to inifnite negative amplification needed.

Therefore the topology of choice is the feedforward topology

alt text

The control circuit is fed with a copy of the input signal and calculates the needed attenuations which are then multiplied with the input signal. One might recognize this diagram since it pretty much resembles an analog VCA-Compressor. In a VCA-Compressor the voltage controlled amplifier attenuates a signal according to an external control voltage coming from the control circuit.

Overview

Knowing that we are building "the ideal VCA-Compressor" the interesting question is how the control voltage is actually calculated in a digital implementation.

A basic compressor diagram:

alt text

Lets take a look at the components.

Level Detection

Two approaches - peak detection which is based on the absolute value of the signal and rms detection which is based on the square of the signal. Both are easily implemented in the digital domain, but rms detection introduces a significant delay, therefore peak detection was the choice for this implementation.

In C++ this is as simple as doing:

float input = std::abs(sample)

Log and Lin Conversion

One of the reason many compressors sound different. Calculation of attenuations and smoothing can be done either in linear or log. domain.

Converting from linear to log (decibel):

float log = 20 * std::log10 (gain)

and converting from log. to linear:

float lin = std::pow (10.0, decibels * 0.05)

In this implementation the gain computer as well as the ballistics are operating in the log. domain.

Gain Computer

The gain computer calculates how much the input signal needs to be attenuated given the current chosen characteristics. It also generates the control voltage which is used to attenuate the signal in the gain stage.

A typical compression curve looks like:

alt text

The Treshold, Ratio and Knee parameters define the input/output characteristics of the compressor.

If we ignore the knee parameter for now, the compressor starts to attenuate the signal according to the ratio once it exceeds the threshold.

alt text

Resulting in:

alt text

With xG being the input, yG the output, T the threshold and R the ratio.

In order to soften the compression we can smooth the transition between no compression and compression. This is called soft knee. The knee knob adjusts the dB-range which is equally distributed on both sides of the threshold.

To implement this we can replace the equation above with a second order interpolation function [1]:

alt text

With W being the knee-width and (xG - T) being the overshoot. When W is set to 0dB this function is identical to the hard knee.

A simplified implementation:

float applyCompression(const float& input)
{
    const float kneeHalf = knee/2.0;
    const float overshoot = input - threshold;

    if (overshoot <= -kneeHalf)
        return 0.0f;
    if (overshoot > -kneeHalf && overshoot <= kneeHalf)
        return 0.5f * slope * square(overshoot + kneeHalf) / knee;

    return slope * overshoot;
}

This function returns the calculated attenuations which are then being fed into the smoothing filter aka. peak detector.

Ballistics

The time constants are another component that differs alot from compressor to compressor. The attack is usually defined as the time it takes for the compressor to attenuate the signal once the signal exceeds the threshold. Likewise release is defined as the time it takes for the compressor to recover once the signal falls under the threshold.

In a digital implementation the attack and release times are usually introduced through a digital one-pole filter, also known as smoothing filter.

alt text

Where r[n] is the input, s[n] the output and alpha the filter coefficient. With the step response:

alt text

The attack/release coefficients are then calculated as follows:

alt text

This is true when the rise time of the step response is considered to go from 0% to 63% or 1 - 1/e of the final value. Meaning, tauAttack starts at 0 and goes to 1. Therefore attack time is the time it takes for the level to reach 0.63. TauRelease starts at 1 and goes to 0. The release time is the time it takes for the level to reach 1-0.63 = 0.37. [2]

Someone else might consider the rise time of the step response to go from 10% to 90% of the final value or define it as time to change level by so many dBs.

Improved designs are proposed in [1] and [2].

Smooth branching peak detector:

alt text

C++ implementation:

float processPeakBranched(const float& in)
{
    //Smooth branched peak detector
    if (in < state01)
        state01 = alphaAttack * state01 + (1 - alphaAttack) * in;
    else
        state01 = alphaRelease * state01 + (1 - alphaRelease) * in;

    return static_cast<float>(state01); //yL
}

Smooth decoupled peak detector:

alt text

C++ implementation:

float processPeakDecoupled(const float& in)
{
    //Smooth decoupled peak detector
    const double input = static_cast<double>(in);
    state02 = jmax(input, alphaRelease * state02 + (1 - alphaRelease) * input);
    state01 = alphaAttack * state01 + (1 - alphaAttack) * state02;
    return static_cast<float>(state01); //yL
}

The definition of the time constants, design of smoothing filter and the placement of the filter are all variables that change the sound of the compressor.

For this implementation the smooth branching peak detector was placed behind the gain computer to smooth the calculated attenuations. Due to the placement, the detector does not have to operate on the whole dynamic range of the input signal.

Parameter Automation

The sidechain configuration including components for parameter automation and look-ahead. alt text

Automating the time constants

The attack and release times can be automated using the crest factor of the input signal. The crest factor is defined as the ratio of peak signal level to root means squared (rms) signal level over a given duration.

alt text

This method is a useful short term signal measure to determine the nature of a signal. For a steady-state signal the rms value will be close to the peak value, therefore the crest factor will be relatively small.

The ideal time frame for the measurement was informally evaluated in [4] and chosen to be 200ms. Choosing the same time frame for both the peak and the rms detector ensures that the rms value will always be smaller than the peak value.

Since most transients in a signal are a burst of high amplitude over a short duration (typically < 10ms) the contribution to the rms value is relatively small in comparison to the peak value. This means, the crest factor will be high for transient-rich signals and small for steady-state signals.

As shown in [4] we can combine a peak and rms detector as follows:

alt text

And then calculate the time constants like:

alt text

Implemented in C++ this could look like:

//Init accumulators
if (!peakState) peakState = src[0];
if (!rmsState) rmsState = src[0];

//Calculate crest factor and ballistics
for (int i = 0; i < numSamples; ++i)
{

    //Square of input signal
    const double s = static_cast<double>(src[i])* static_cast<double>(src[i]);

    //Update peak state
    peakState = jmax(s, a1 * peakState + b1 * s);

    //Update rms state
    rmsState = a1 * rmsState + b1 * s;

    //calculate squared crest factor
    const double c = peakState / rmsState;
    cFactor = c > 0.0 ? c : 0.0;

     //calculate ballistics
    if (cFactor > 0.0)
    {
        attackTimeInSeconds = 2 * (maxAttackTime / cFactor);
        releaseTimeInSeconds = 2 * (maxReleaseTime / cFactor) - attackTimeInSeconds;
    }
}
Automating the makeup gain

The makeup gain automation in this implementation is not ideal. It is based on the calculated attenuations smoothed over a long time frame, then added back to the compressed signal to achieve ouput volume that is closer to the perceived loudness of the input signal. Since added makeup gain is a smoothed over a time frame, it is still time-varying and therefore changes the gain over time. This should rather be an approximation to get closer to the perceived loudness of the input signal, thus a static gain increase.

In [3] aswell as in [4] ideas are discussed on how to tackle this problem.

LookAhead

LookAhead is mostly used in limiters, meaning, compressors with a ratio of infinity:1 and instantanious attack. LookAhead, like the name already suggests, looks ahead and anticipates incoming peaks.

It is used to lower/prevent distortion that occurs when the gain reduction happens too fast. This might be very noticeable when the signal has low frequency content and the attack time is faster than a single cycle of the waveform.

Most books and articles on the internet implement this feature by simply delaying the input signal. This means that the calculated attenuations will affect the signal a chosen time (mostly 5ms-15ms) before the transient will effectively hit the compressor. So - what will happen if an incoming peak is shorter than the chosen time?

The limiter will most likely miss it and even if the release time is considerably long the peak won't be attenuated by the correct amount.

A different approach - In the article [5] a different implementation is proposed by Daniel Rudrich. We delay the input signal aswell as the calculated attenuations. Then use the delay time to fade in the aggressive gain reduction happening when a peak occurs. Instead of instantly going to the final value of the needed attenuation, we use the 5-15ms time frame to ramp towards that value, thus effecively preventing distortion.

Comparing the two implementations

Only delaying the input signal:

alt text

Fading in the aggressive gain reductions:

alt text

It's quite visible that in the first figure the peak isn't completely attenuated and the gain reduction kicks in abruptly. This will result in an audible click and distortion.

In the second figure the gain reduction is faded in slowly, thus preventing any of the unwanted effects.

References