No history yet

Advanced DSP Techniques

Beyond Basic Filtering

You already know how to build simple low-pass and high-pass filters. Let's move on to something more flexible: shelving filters. These are the workhorses of equalizers, allowing you to boost or cut frequencies above or below a certain point without removing them entirely.

A first-order low-shelf filter adjusts the bass frequencies. Its behavior is defined by a transfer function that looks a bit more involved than what you've seen before, but it's manageable when broken down.

H(z)=b0+b1z11+a1z1H(z) = \frac{b_0 + b_1 z^{-1}}{1 + a_1 z^{-1}}

To make this a working low-shelf filter, we need to calculate those coefficients based on the desired gain and corner frequency. The math involves a few steps, but the result gives us direct formulas for our C++ code. Here's a simplified way to implement the core processing logic for such a filter inside a JUCE plugin's processBlock method.

// Assume b0, b1, a1 are pre-calculated
// member variables based on gain and frequency.
// z1 is a member variable storing the previous sample.

for (int channel = 0; channel < totalNumInputChannels; ++channel)
{
    auto* channelData = buffer.getWritePointer (channel);

    for (int i = 0; i < buffer.getNumSamples(); ++i)
    {
        // Get the current input sample
        float in = channelData[i];

        // Apply the filter formula:
        // y[n] = b0*x[n] + b1*x[n-1] - a1*y[n-1]
        float out = b0 * in + b1 * z1[channel] - a1 * z1[channel];
        
        // Store the state for the next sample
        z1[channel] = in;

        // Write the output
        channelData[i] = out;
    }
}

This is a direct form I (DF-I) implementation. While simple, it can be susceptible to numerical precision issues. For professional plugins, structures like the Transposed Direct Form II are often preferred for better stability.

Shaping Sound with Modulation

Modulation is about change over time. In audio, it means using one signal, the modulator, to alter a property of another signal, the carrier. This simple concept is behind classic effects like tremolo, vibrato, and flangers.

Let's start with Amplitude Modulation (AM), which changes the volume of the carrier. If you modulate a sine wave with a slower sine wave, you get a tremolo effect.

y(t)=A(1+msin(2πfmt))sin(2πfct)y(t) = A \cdot (1 + m \cdot \sin(2\pi f_m t)) \cdot \sin(2\pi f_c t)

A close cousin is Ring Modulation, which is essentially AM without the added carrier signal. It simply multiplies the carrier and modulator. This creates a distinctive, often metallic and inharmonic sound because it produces new frequencies at the sum and difference of the original frequencies.

y(t)=sin(2πfct)sin(2πfmt)y(t) = \sin(2\pi f_c t) \cdot \sin(2\pi f_m t)

In practice, you'll implement these using a low-frequency oscillator (LFO) as your modulator. In a JUCE plugin, you would typically have a class for your LFO that generates a sine, triangle, or square wave at a user-defined rate. Then, in your processBlock, you multiply its output with your audio signal sample by sample.

// Inside a processBlock loop

for (int i = 0; i < buffer.getNumSamples(); ++i)
{
    // Get the next value from our LFO
    // The LFO updates its phase internally
    float lfoValue = lfo.getNextSample(); 

    // For Tremolo (AM)
    // 'depth' is our modulation index, from 0 to 1
    float modulator = 1.0f - (depth * 0.5f) + (lfoValue * depth * 0.5f);

    // For Ring Mod
    // float modulator = lfoValue;

    // Apply modulation to the input sample
    float inputSample = channelData[i];
    channelData[i] = inputSample * modulator;
}

Analyzing Time and Frequency

The standard Fourier Transform is great for seeing the frequency content of a signal, but it has a major blind spot: it averages this content over the entire signal duration. You lose all information about when those frequencies occurred.

To build effects like pitch shifters, vocoders, or spectral filters, we need a tool that can see how the frequency spectrum changes over time. This is where the Short-Time Fourier Transform (STFT) comes in.

Lesson image

The process is straightforward:

  1. Windowing: Take a small chunk of the audio signal (e.g., 1024 samples) and apply a windowing function (like a Hann window) to it. This smooths the edges to prevent spectral artifacts.
  2. FFT: Perform a Fast Fourier Transform on this windowed chunk to get its frequency spectrum (magnitude and phase).
  3. Process: Manipulate the magnitude and phase information. To pitch shift, you might resample the data. For a spectral gate, you might zero out bins below a certain magnitude.
  4. IFFT: Perform an Inverse FFT to convert the processed spectrum back into a time-domain signal chunk.
  5. Overlap-Add: Slide the window forward by a smaller amount than its full length (e.g., 256 samples for 75% overlap) and repeat. The resulting output chunks are overlapped and added together to reconstruct the final, continuous audio stream.

This overlap-add method is crucial for avoiding clicks and pops at the window boundaries.

Implementing the STFT in C++ requires a robust FFT library. JUCE provides a convenient wrapper around highly optimized libraries like FFTW or Apple's vDSP. The process involves creating an instance of juce::dsp::FFT, feeding it your windowed data, and then performing the forward transform. After processing the complex data, you use the same object to perform the inverse transform.

#include <juce_dsp/juce_dsp.h>

// In your processor's constructor
juce::dsp::FFT fft(10); // For FFT size of 2^10 = 1024
juce::dsp::WindowingFunction<float> window(1024, juce::dsp::WindowingFunction<float>::hann);

// In your processing loop
// 1. Copy 1024 samples to a buffer
// 2. Apply the window
window.multiplyWithWindowingTable(buffer, 1024);

// 3. Perform forward FFT
fft.performFrequencyOnlyForwardTransform(buffer);

// ... Your spectral processing happens here ...

// To go back, you would use performRealOnlyInverseTransform
// after processing both real and imaginary parts.

Much of what a DSP has to do for end-user audio applications is based on filters (crossover networks and equalisation).

These advanced techniques—complex filters, versatile modulation, and time-frequency analysis—form the backbone of countless professional audio plugins. Mastering their implementation is a significant step toward creating truly sophisticated audio tools.

Quiz Questions 1/5

What is the primary function of a shelving filter in an equalizer?

Quiz Questions 2/5

Which audio effect is created by multiplying a carrier signal (the audio) with a modulator signal (an LFO) without adding the original carrier back into the output?