Control the cutoff with just one coefficient!

You have probably seen it: a lowpass filter digital audio workstation (DAW) plugin.

It could have had a roll-off and a resonance control knob or slider.

But it definitely had the cutoff frequency control.

If you learned a little bit about digital signal processing (DSP), you may have come across formulas for different types of filters. However, these formulas typically require to have all their coefficients recalculated as soon as the cutoff frequency changes. That means that their real-time control is inefficient, computationally speaking.

How to design and implement a lowpass or a highpass filter, where adjusting the cutoff frequency requires a recalculation of just one parameter?

That is the topic of this article 🙂

Let’s start with the basics.

Lowpass Filter

For the purpose of this article, we’ll define a lowpass filter as a filter that attenuates frequencies above a certain frequency, called the cutoff frequency.

The cutoff frequency is typically defined as the frequency at which the attenuation is already 3 dB.

The frequencies below the cutoff frequency aren’t affected by the filter.

The amplitude response (how each frequency is attenuated at the output of the filter) of a lowpass filter is shown in Figure 1.

Figure 1. Lowpass filter amplitude response.

Highpass Filter

Contrary to a lowpass filter, a highpass filter attenuates all frequencies below the cutoff frequency.

The amplitude response of a highpass filter is shown in Figure 2.

Figure 2. Highpass filter amplitude response.

The Need for a Simple Control-to-Coefficients Mapping

Let’s recap a “traditional” method of designing an IIR lowpass filter:

  1. Design the analog prototype.
  2. Digitize it with the bilinear transform.

For example, in the bilinear transform tutorial, we digitized the Butterworth lowpass of order 2. The resulting transfer function formula was

H2(z)=W2+2W2z1+W2z21+W2+W2+2(W21)z1+(W2W2+1)z2,(1)H_2(z) = \frac{W^2 + 2W^2 z^{-1} + W^2z^{-2}}{1 + W \sqrt{2} + W^2 + 2(W^2 - 1)z^{-1} + (W^2 - W\sqrt{2} + 1)z^{-2}}, \quad (1)

where W=tan(ωcT/2)W = \tan(\omega_\text{c} T / 2) and ωc\omega_\text{c} is the desired cutoff frequency of the digital filter in radians per second.

Note that if we change the cutoff frequency ωc\omega_\text{c}, we need to calculate 6 filter coefficients!

(A reminder: a filter coefficient is a scalar at each power of the zz variable in the numerator and the denominator.)

If we wanted to control the cutoff frequency in real time, for example, during a live performance, or using an envelope, the computational overhead could be troublesome.

Can we have a simple mapping: 1 filter control change requires 1 coefficient change?

That is the promise of allpass-based parametric filters.

To understand them, we first need to recap a few facts about the allpass filter.

Allpass Filter Revisited

An allpass filter is a filter that does not attenuate or boost any frequencies but introduces a frequency-dependent delay.

That means that a single allpass filter won’t introduce any audible change in the signal. Only when we use this filter in some context, can we hear its true power.

If you want to learn more about the allpass filter itself, check out my comprehensive “Allpass Filter: All You Need to Know” article here.

What is a “frequency-dependent delay”? Well, the higher the frequency, the later it will appear at the filter’s output.

The amount of phase delay can be seen in the phase response of the allpass filter. In Figure 3, you can see such responses for various values of the break frequency (I explain the break frequency later).

Figure 3. Phase response of a first-order allpass filter for different break frequencies fbf_\text{b}. fsf_s is the sampling rate.

If this delay was large and we put a signal with a flat spectrum at the input, we could hear a tone rising in frequency at the output; the lowest frequency would appear immediately at the output, whereas the highest would appear last, because it has the largest delay.

In practice, this delay is too small to be audible. We can, however, observe its effect on the waveform in the time domain.

This effect can be seen in Figure 4. There, 3 nicely aligned sines (left) pass through an allpass filter and appear misaligned at the output (right).

Figure 4. (Left) A superposition of 3 sines. (Right) The same 3 sines after passing through an allpass filter.

At the output, the frequency content is the same but the relative phase of the sines changed. At the same time, the output sounds exactly as the input.

Phase Cancellation

The break frequency of an allpass filter is the frequency at which the phase shift is exactly π2-\frac{\pi}{2}.

We can control the break frequency of an allpass filter of any order with a single coefficient that appears in simple formulas for the final filter coefficients.

Here is the formula for the transfer function of the allpass filter:

HAP1(z)=a1+z11+a1z1,(2)H_{\text{AP}_1}(z) = \frac{a_1 + z^{-1}}{1 + a_1z^{-1}}, \quad (2)

where

a1=tan(πfb/fs)1tan(πfb/fs)+1.(3)a_1 = \frac{\tan(\pi f_\text{b} / f_s) - 1}{\tan(\pi f_\text{b} / f_s) + 1}. \quad (3)

This formula is the bilinear transform of the analog allpass.

If you don’t understand it, don’t worry; all you need to know is that the break frequency is easily controllable.

Now, at the Nyquist frequency (half of the sampling rate), the phase shift is exactly π-\pi so the tone corresponding to that frequency is exactly inverted in phase.

(Phase inversion is sometimes marked as \varnothing in DAWs.)

If we add a signal and its phase-inverted version, a phase cancellation will occur; we will obtain an all-zero signal, i.e., silence.

An example of this can be seen in Figure 5.

Figure 5. A sum of two sines with the relative phase shift of π\pi results in phase cancellation.

A phase cancellation means perfect attenuation, right? Could we possibly use this property in a lowpass or a highpass filter?

Allpass-Based Lowpass Filter

What will happen if we add the output of the first-order allpass filter to the original input signal (the so-called direct path) as in Figure 6? [Zölzer11].

Figure 6. Allpass-based lowpass filter structure.

Since the phase shift at the Nyquist frequency is π-\pi, we’ll obtain a phase cancellation at this frequency.

At the direct current (DC) (frequency of 0 Hz), the output signal is not shifted with respect to the input (the signals are said to be in-phase). If we add two sines that have the same frequency and are in phase, we effectively obtain a sine at the same frequency which has the amplitude equal to the sum of amplitudes of the original sines.

In the case of the discussed structure, the DC component at the input and at the output are identical. Therefore, the amplitude of the input DC component will double. Hence the multiplication by 12\frac{1}{2} so that we don’t exceed the [1,1][-1, 1] range and avoid clipping.

Ok, we know that at the output of the structure from Figure 6, the 0 Hz component will be doubled in amplitude and the Nyquist frequency component will vanish (have amplitude equal to 0). What will happen between these frequencies?

Between these frequencies, the amplitude of sines will be gradually attenuated as the input signal and the output of the allpass filter gradually move out of phase with increasing frequency.

The resulting magnitude transfer function can be seen in Figure 7. We obtained a lowpass filter!

Figure 7. Magnitude transfer function of the resulting lowpass filter.

Cutoff Frequency Control

As I promised, the cutoff frequency of this lowpass filter is very easy to control. We just need to set the a1a_1 coefficient of the allpass filter according to Equation 3, which controls the frequency at which the phase shift of the allpass is exactly π2-\frac{\pi}{2}. The a1a_1 coefficient can then be used as a regular filter coefficient.

We, thus, obtained a one-to-one control-to-coefficient mapping!

Allpass-Based Highpass Filter

What if instead of adding the output of the allpass to the input signal, we subtracted it?

The corresponding structure is shown in Figure 8.

Figure 8. Allpass-based highpass filter structure.

By multiplying the output of the allpass by 1-1 we invert all the components in phase.

Therefore, the frequency component at the Nyquist frequency, which was inverted in phase by the allpass filter, gets inverted again and is back in phase with the corresponding component of the input signal.

So the Nyquist frequency component before the multiplication by 12\frac{1}{2} in the structure in Figure 8 is doubled in amplitude.

Conversely, the DC component, which was previously in phase, is now negated. Therefore, the DC component is missing in the output signal of the structure from Figure 8.

In between these two frequencies, we get an increase in the magnitude of the transfer function with increasing frequency.

The magnitude transfer function can be seen in Figure 9.

Figure 9. Magnitude transfer function of the resulting highpass filter.

We, thus, obtained a high-pass filter!

Its cutoff frequency can again be controlled with just one coefficient as in the lowpass case (because we merely introduced the multiplication by 1-1).

Great, we have just designed easily controllable lowpass and highpass filters! How can we implement them in code?

Python Implementation

Listing 1 shows the implementation of the allpass-based lowpass/highpass and includes extensive comments.

Listing 1. Allpass-based lowpass/highpass filter.

#!/usr/bin/python3
from scipy import signal
import numpy as np
import soundfile as sf
from pathlib import Path


def generate_white_noise(duration_in_seconds, sampling_rate):
    duration_in_samples = int(duration_in_seconds * sampling_rate)
    return np.random.default_rng().uniform(-1, 1, duration_in_samples)


def a1_coefficient(break_frequency, sampling_rate):
    tan = np.tan(np.pi * break_frequency / sampling_rate)
    return (tan - 1) / (tan + 1)


def allpass_filter(input_signal, break_frequency, sampling_rate):
    # Initialize the output array
    allpass_output = np.zeros_like(input_signal)

    # Initialize the inner 1-sample buffer
    dn_1 = 0

    for n in range(input_signal.shape[0]):
        # The allpass coefficient is computed for each sample
        # to show its adaptability
        a1 = a1_coefficient(break_frequency[n], sampling_rate)

        # The allpass difference equation
        # Check the article on the allpass filter for an 
        # in-depth explanation
        allpass_output[n] = a1 * input_signal[n] + dn_1

        # Store a value in the inner buffer for the 
        # next iteration
        dn_1 = input_signal[n] - a1 * allpass_output[n]
    return allpass_output


def allpass_based_filter(input_signal, cutoff_frequency, \
    sampling_rate, highpass=False, amplitude=1.0):
    # Perform allpass filtering
    allpass_output = allpass_filter(input_signal, \
        cutoff_frequency, sampling_rate)

    # If we want a highpass, we need to invert 
    # the allpass output in phase
    if highpass:
        allpass_output *= -1

    # Sum the allpass output with the direct path
    filter_output = input_signal + allpass_output

    # Scale the amplitude to prevent clipping
    filter_output *= 0.5

    # Apply the given amplitude
    filter_output *= amplitude

    return filter_output


def white_noise_filtering_example():
    sampling_rate = 44100
    duration_in_seconds = 5

    # Generate 5 seconds of white noise
    white_noise = generate_white_noise(duration_in_seconds, sampling_rate)
    input_signal = white_noise

    # Make the cutoff frequency decay with time ("real-time control")
    cutoff_frequency = np.geomspace(20000, 20, input_signal.shape[0])

    # Actual filtering
    filter_output = allpass_based_filter(input_signal, \
        cutoff_frequency, sampling_rate, highpass=False, amplitude=0.1)

    # Store the result in a file
    output_dir = Path('assets', 'wav', 'posts', 'fx', \
        '2022-05-08-allpass-based-lowpass-and-highpass-filters')
    output_dir.mkdir(parents=True, exist_ok=True)
    filename = 'filtered_white_noise.flac'
    sf.write(output_dir / filename, filter_output, sampling_rate)


def main():
    white_noise_filtering_example()


if __name__ == '__main__':
    main()

The resulting audio file should sound similar to the following:

Can you notice how the cutoff frequency lowers over time? We achieved this easily thanks to the one-to-one control-to-coefficient mapping.

Summary

In this article, we discussed an easy and popular method of obtaining a lowpass or a highpass filter; by combining an allpass filter and the direct path.

The allpass filter delays the input frequency components. The phase delay increases with frequency.

At DC, the phase shift is 0. At the break frequency the phase shift is π2-\frac{\pi}{2}. At the Nyquist frequency the phase shift is π-\pi.

Adding (subtracting) the allpass output to (from) the direct path creates phase cancellation at the Nyquist frequency (DC component). We, thus, obtain a lowpass (highpass) filter.

The real power of this structure can be seen in a real-time implementation… So that’s what we’ll do next!

Bibliography

[Zölzer11] Zölzer Udo, DAFX: Digital Audio Effects. 2nd ed., Helmut Schmidt University, Hamburg, Germany, John Wiley & Sons Ltd, 2011.