With real-time center frequency and bandwidth control!

In one of the previous articles, we discussed how to implement a simple lowpass and a highpass filter using the first-order allpass filter. That filter had a real-time cutoff frequency control.

Now, we can take it to the next level and design a bandpass and a bandstop filter with a second-order allpass filter. This design will allow us to control the center frequency and the bandwidth (or alternatively, the Q factor) in real time!

We’ll discuss this design and its properties, listen to a few examples, and look at a sample Python implementation at the end.

Table of Contents

  1. What Is a Bandstop (Notch) Filter?
  2. What Is a Bandpass Filter?
  3. Recap: The Second-Order Allpass Filter
    1. Transfer Function
    2. Phase Response
  4. Allpass-Based Bandstop Filter
    1. DSP Diagram
    2. Magnitude Response
    3. Real-Time Control
    4. Implementation
  5. Allpass-Based Bandpass Filter
    1. DSP Diagram
    2. Magnitude Response
    3. Real-Time Control
    4. Implementation
  6. Applications
    1. Filter Sweep
    2. Hear band (in filters)
    3. Phaser
  7. Summary

What Is a Bandstop (Notch) Filter?

A bandstop filter (also called a notch filter) is a filter that attenuates frequencies in a certain frequency range.

This frequency range is determined by a center frequency and a bandwidth.

Figure 1. Bandstop filter amplitude response.

The center frequency points to the frequency with the largest attenuation, the “dip” in the magnitude response of the filter.

The bandwidth determines how wide will the “dip” or “valley” be around the center frequency.

In order for the bandstop filter to sound equally wide in bandwidth at all center frequencies, we often use the Q or quality factor or Q-factor parameter. It is defined as

Q=fcBW,(1)Q = \frac{f_c}{BW}, \quad (1)

where BWBW is the bandwidth in Hz and fcf_c is the center frequency in Hz.

A “constant-Q” filter allows for a narrower band in the low frequencies (where human hearing is more sensitive to frequency) and for a wider band in the high frequencies (where human hearing is less sensitive to frequency).

Note that we cannot control the amount of attenuation in the bandstop filter. That is possible only in a peaking (band) filter, which is not the topic of this article.

What Is a Bandpass Filter?

A bandpass filter is a filter that attenuates all frequencies apart from a specified range.

This range is defined in terms of the center frequency and the bandwidth, both expressed in Hz.

Figure 2. Bandpass filter amplitude response.

As in the case of the bandstop filter, we can specify the bandwidth using the Q (quality factor, Q-factor) parameter. Constant-Q filters retain the same “perceptual width” of the passed-through frequency range. The relation between the center frequency, the bandwidth, and Q is given by Equation 1.

Recap: The Second-Order Allpass Filter

The main building block of bandpass and bandstop filters is the second-order allpass filter.

An allpass filter is a filter that does not attenuate any frequencies but it introduces a frequency-dependent phase shift.

Let’s recap a few facts about this filter.

Transfer Function

The transfer function of the second-order allpass filter is

HAP2(z)=c+d(1c)z1+z21+d(1c)z1cz2,(2)H_{\text{AP}_2}(z) = \frac{-c + d(1-c) z^{-1} + z^{-2}}{1 + d(1-c) z^{-1} - c z^{-2}}, \quad (2)

where

c=tan(πBW/fs)1tan(πBW/fs)+1,(3)c = \frac{\tan(\pi BW / f_s) - 1}{\tan(\pi BW / f_s) + 1}, \quad (3)
d=cos(2πfb/fs),(4)d = - \cos(2\pi f_\text{b} / f_s), \quad (4)

BWBW is the bandwidth in Hz, fbf_\text{b} is the break frequency in Hz, and fsf_s is the sampling rate in Hz. The break frequency specifies the frequency at which the phase shift is π-\pi. The bandwidth specifies the width of the transition band in which the phase shift goes from 0 to 2π-2\pi.

Phase Response

The phase response of the second-order allpass filter is visible in Figure 3.

Figure 3. Phase response of a second-order allpass filter for different break frequencies frequencies fbf_\text{b} and bandwidth BW/fs=0.022BW / f_s = 0.022.

As you can see, the phase shift is 0 at 0 Hz and gradually changes to 2π-2\pi. The steepness of the phase response is determined by the bandwidth BWBW parameter expressed in Hz.

You can already guess that the bandwidth parameter of the second-order allpass filter translates to the bandwidth parameter of bandpass and bandstop filters. Accordingly, the break frequency corresponds to the center frequency. How?

Thanks to the phase cancellation effect, if we add two tones at the same frequency but with relative phase shift of π\pi, they will cancel each other. A shift by π\pi is equivalent to a multiplication of the tone by -1.

With this knowledge we can now employ the second-order allpass filter for bandpass or bandstop filtering.

Allpass-Based Bandstop Filter

If we add the output of the second-order allpass filter to its input signal, at the break frequency we will obtain a phase cancellation. Why?

At the break frequency, the phase delay is π-\pi. Adding two tones at the break frequency with the relative phase shift of π\pi, we effectively eliminate them from the resulting signal. As the phase shift deviates from π\pi further away from the break frequency, the cancellation is less and less effective.

DSP Diagram

Here is a block diagram of the bandstop filter.

Figure 4. DSP diagram of the allpass-based bandstop filter.

AP2(z)\text{AP}_2(z) denotes the second-order allpass filter.

The output of the second-order allpass filter is added to the direct path. We multiply the result by 12\frac{1}{2} to stay in the [-1, 1] range (input is in [-1, 1] range, allpass’s output is in [-1, 1] range so their sum is in the [-2, 2] range; we want to scale that back down to [-1, 1], otherwise we’ll possibly clip the signal).

Magnitude Response

Here is a magnitude transfer function of the bandstop filter from Figure 4 with the center frequency at 250 Hz and QQ equal to 3.

Figure 5. Magnitude transfer function of the bandstop filter.

At the center frequency, we get the biggest attenuation which decreases the further away we get from the center frequency. We can see how selective in frequency this filter is.

Real-Time Control

As this filter requires quite easy computations to control the center frequency and the bandwidth, we can alter its parameters in real time.

As an example, here’s a white noise signal filtered with the bandstop filter, whose center frequency varies from 100 to 16000 Hz over time and Q is equal to 3.

To visualize what’s happening here, take a look at the spectrogram of the audio file.

Figure 6. Spectrogram of the bandstop filtering example.

On the x-axis we have the time, on the y-axis we have the log-scaled frequency, and color indicates the amplitude level of the frequency at a specific time point in decibels full-scale (dBFS).

As you can see, the dip travels exponentially (mind the log scale!) from low to high frequencies. Thus, we can hear the so-called “filter sweep”.

Implementation

You will find a sample implementation of the bandstop filter in Python at the end of this article.

Allpass-Based Bandpass Filter

The allpass-based bandpass filter differs from the bandstop filter only in the sign of the allpass filter output. In case of the bandpass, we invert the output of the allpass in phase so that the phase cancellation occurs at the 0 Hz frequency and the Nyquist frequency. Because the tone at the break frequency gets reversed twice, it is in phase with the input signal. Therefore, the summation results in doubling of the amplitude of the tone corresponding to the break frequency of the allpass.

DSP Diagram

In Figure 7, there’s a block diagram of the presented bandpass filter.

Figure 7. DSP diagram of the allpass-based bandpass filter.

AP2(z)\text{AP}_2(z) denotes the second-order allpass filter.

The multiplication by 12\frac{1}{2} is just to preserve the [-1, 1] amplitude range of the signal.

Magnitude Response

In Figure 8, there’s the magnitude response of the bandpass filter with center frequency set to 250 Hz and QQ set to 3.

Figure 8. Magnitude transfer function of the bandpass filter.

As you can see, it actually passes through only the frequencies in the specified band.

Real-Time Control

Exactly as the bandstop filter, the bandpass filter can be easily controlled in real time.

Here’s an audio sample with a bandpass-filtered white noise, where the center frequency varies from 100 Hz to 16000 Hz and Q is equal to 3.

You can observe the effect of the bandpass filter on the spectrogram of the above audio file (Figure 9).

Figure 9. Spectrogram of the bandpass filtering example.

Once again, the y-axis is a log-frequency axis, the x-axis is a time axis, and color intensity corresponds to the sound level in decibels full-scale (dBFS).

Implementation

Here is a sample Python implementation of both filters: the bandpass and the bandstop.

The code generates 5 seconds of white noise and then filters it with time-varying bandstop and bandpass filters respectively. The center frequency in both cases changes exponentially from 100 Hz to 16000 Hz (this code was used to generate the previous examples in this article).

The code is heavily commented so you should have no problems in understanding.

Listing 1. Allpass-based bandstop and bandpass filtering implementation & filter sweep application.

import numpy as np
import scipy.signal as sig
import soundfile as sf


def apply_fade(signal):
    """Apply a fade-in and a fade-out to the 1-dimensional signal"""
    # Use a half-cosine window
    window = sig.hann(8192)
    # Use just the half of it
    fade_length = window.shape[0] // 2
    # Fade-in
    signal[:fade_length] *= window[:fade_length]
    # Fade-out
    signal[-fade_length:] *= window[fade_length:]
    # Return the modified signal
    return signal


def second_order_allpass_filter(break_frequency, BW, fs):
    """
    Returns b, a: numerator and denominator coefficients
    of the second-order allpass filter respectively

    Refer to scipy.signal.lfilter for the explanation
    of b and a arrays.

    The coefficients come from the transfer function of
    the allpass (Equation 2 in the article).

    Parameters
    ----------
    break_frequency : number
        break frequency of the allpass in Hz
    BW : number
        bandwidth of the allpass in Hz
    fs : number
        sampling rate in hz

    Returns
    -------
    b, a : array_like
        numerator and denominator coefficients of
        the second-order allpass filter respectively
    """
    tan = np.tan(np.pi * BW / fs)
    c = (tan - 1) / (tan + 1)
    d = - np.cos(2 * np.pi * break_frequency / fs)
    
    b = [-c, d * (1 - c), 1]
    a = [1, d * (1 - c), -c]
    
    return b, a


def bandstop_bandpass_filter(input_signal, Q, center_frequency, fs, bandpass=False):
    """Filter the given input signal

    Parameters
    ----------
    input_signal : array_like
        1-dimensional audio signal
    Q : float
        the Q-factor of the filter
    center_frequency : array_like
        the center frequency of the filter in Hz
        for each sample of the input
    fs : number
        sampling rate in Hz
    bandpass : bool, optional
        perform bandpass filtering if True, 
        bandstop filtering otherwise, by default False

    Returns
    -------
    array_like
        filtered input_signal according to the parameters
    """
    # For storing the allpass output
    allpass_filtered = np.zeros_like(input_signal)
    
    # Initialize filter's buffers
    x1 = 0
    x2 = 0
    y1 = 0
    y2 = 0
    
    # Process the input signal with the allpass
    for i in range(input_signal.shape[0]):
        # Calculate the bandwidth from Q and center frequency
        BW = center_frequency[i] / Q
        
        # Get the allpass coefficients
        b, a = second_order_allpass_filter(center_frequency[i], BW, fs)
        
        x = input_signal[i]
        
        # Actual allpass filtering:
        # difference equation of the second-order allpass
        y = b[0] * x + b[1] * x1 +  b[2] * x2 - a[1] * y1 - a[2] * y2
        
        # Update the filter's buffers
        y2 = y1
        y1 = y
        x2 = x1
        x1 = x
        
        # Assign the resulting sample to the output array
        allpass_filtered[i] = y
    
    # Should we bandstop- or bandpass-filter?
    sign = -1 if bandpass else 1
    
    # Final summation and scaling (to avoid clipping)
    output = 0.5 * (input_signal + sign * allpass_filtered)

    return output


def main():
    """
    Sample application of bandpass and bandstop
    filters: filter sweep.
    """
    # Parameters
    fs = 44100
    length_seconds = 6
    length_samples = fs * length_seconds
    Q = 3

    # We have a separate center frequency for each sample
    center_frequency = np.geomspace(100, 16000, length_samples)

    # The input signal
    noise = np.random.default_rng().uniform(-1, 1, (length_samples,))
    
    # Actual filtering
    bandstop_filtered_noise = bandstop_bandpass_filter(noise, Q, center_frequency, fs)
    bandpass_filtered_noise = bandstop_bandpass_filter(noise, Q, center_frequency, fs, 
                                                                        bandpass=True)
    
    # Make the audio files not too loud
    amplitude = 0.5
    bandstop_filtered_noise *= amplitude
    bandpass_filtered_noise *= amplitude
    
    # Apply the fade-in and the fade-out to avoid clicks
    bandstop_filtered_noise = apply_fade(bandstop_filtered_noise)
    bandpass_filtered_noise = apply_fade(bandpass_filtered_noise)
    
    # Write the output audio file
    sf.write('bandstop_filtered_noise.flac', bandstop_filtered_noise, fs)
    sf.write('bandpass_filtered_noise.flac', bandpass_filtered_noise, fs)


if __name__=='__main__':
    main()

Applications

Apart from just filtering, bandpass and bandstop filters can be used in a variety of audio effect applications.

Filter Sweep

Filter sweep is a very strong effect that can add a powerful character to the sound. That’s exactly the effect that you heard in the bandstop-filtering example.

“Listen” to a Frequency Band (In Audio Plugins)

Audio plugins that use information from a frequency range to control their behavior often have a “listen” functionality that allows you to listen to the frequency band you specified and adjust it.

For example, Tonmann Deesser plugin has the “listen” button to be able to hear the frequency range that is being compressed. With the “listen” functionality we can find the most audible range with the “s” consonant to compress and avoid compressing the desired signal.

Figure 10. Tonmann Deesser plugin has the “listen” functionality.

Alternatively, “listen” can be used to focus on just one part of the spectrum while making edits.

Phaser

If we modulate the center frequency of the bandstop filter over time, for example, using a low-frequency oscillator (LFO), we can easily obtain the phaser effect.

Better yet, if we have a series of bandstop filters, the effect will be truly awesome! Think Van Halen’s Eruption level of awesome!

The actual application of the phaser effect will be a topic of an another article but you can already experiment with the attached implementation code!

Summary

In this article, we learned how to implement efficient, real-time-controllable bandpass and bandstop filters using the second-order allpass filter.

Bandpass and bandstop filters are one of the basic effects in the audio programmer’s arsenal. If you want to know which elements make up the audio plugin developer toolbox, check out my free audio plugin developer checklist.