DSP 103

In the previous DSP article we discussed the biquad filter structure and how to get filter coefficients using Python’s Scipy. In this article we will go a little deeper into the biquad and how to scale the gain of each section.

Lets go back to the 6th order lowpass filter and plot the frequency response of each section:

import numpy as np
import scipy.signal as signal
import matplotlib.pyplot as plt

fs = 48000 # the sample rate [Hz]
fc = 1000  # the desired cutoff frequency [Hz]

order = 6
sos = signal.butter(order, fc, output='sos', fs=fs)

# plot each section
plt.figure(1)
for bq in sos:
    w,h = signal.sosfreqz([bq], fs=fs)
    plt.semilogx(w, 20*np.log10(np.abs(h)))
plt.xlabel("Frequency [Hz]")
plt.ylabel("Magnitude [dB]")
plt.title("Frequency response")
plt.show()

The resulting plot is shown below:

Figure 1: Response of each section

We can see that two biquad sections have a gain of +50 dB while one has a gain of almost -100 dB. Together they provide a total gain of 0 dB. When calculating biquads using floating-point arthmetic these huge gain differences don’t cause problems. However, in fixed-point environments they do.

Luckily, scaling a biquad is very easy: simply multiply (or divide) all the b-coefficients of one section by the same number to change its gain. The question is then: by what factor must each section be scaled?

For lowpass filters we’d like to keep the total gain at the lowest frequency (DC) the same. For highpass filters we’d like to keep the total gain at the highest frequency (Nyquist) the same.

Gain calculation

The DC gain of a biquad is easy to calculate: sum the b-coefficients and divide by the sum of the a-coefficients, taking into account that $a_0=1$.

For example, the first section of the filter has the following coefficients:

    B = [ 6.15535185e-08  1.23107037e-07  6.15535185e-08 ]
    A = [ 1.00000000e+00 -1.76088036e+00  7.76074924e-01 ]

The sum of the B coefficients is 2.46214074e-7 and the sum of the A coefficients is 0.015194564. Dividing the former by the latter gives a DC gain of 1.62041e-05, which is clearly very small. In fact it’s -95.8 dB.

By the same method the DC gain of the remaining sections can be determined to be: 255.35436 (48.1 dB) and 241.67526 (47.7 dB).

As a sanity check we can determine the total DC gain, which is -95.8 + 48.1 + 47.7 = 0 dB, as expected.

DC gain calculation can be added to the Python script:

for bq in sos:
    w,h = signal.sosfreqz([bq], fs=fs)
    plt.semilogx(w, 20*np.log10(np.abs(h)))
    gain = np.sum(bq[0:3])/np.sum(bq[3:6])
    print(f"DC gain = {gain}   {20*np.log10(gain)} dB")

Calculating the high-frequency (Nyquist) gain of a biquad section is similarly easy. Sum the coefficients just like before but with alternating signs: $$ A_{sum} = a_0 - a_1 + a_2 $$ $$ B_{sum} = b_0 - b_1 + b_2 $$ And divide to get the gain: $$ gain_{hf} = \dfrac{A_{sum}}{B_{sum}} $$

This can also be added to the Python script. Here we use the dot product function np.dot to change the sign where appropriate and sum the coefficients:

    gain_hf = np.dot(bq[0:3],[1,-1,1])/np.dot(bq[3:6],[1,-1,1])
    print(f"Nyquist gain = {gain_hf}   {20*np.log10(gain_hf)} dB")

All three sections have a nyquist gain of -infinity because it’s a lowpass filter.

Scaling

One possible scaling for the lowpass filter is to set each section DC gain to 0 dB. This will keep the total DC gain at 0 dB, as it was before. To scale each section, we must divide the b-coefficients by the DC gain of the section:

for index in range(0,len(sos)):
    bq = sos[index]
    # calculate the DC gain of the biquad section
    gain = np.sum(bq[0:3])/np.sum(bq[3:6])
    # scale the biquad
    bq[0:3] = bq[0:3]/gain
    # write back the scaled biquad
    sos[index] = bq
    print(f"Scaled biquad: {bq}")

Results

After running this, we get the following output for the sections:

Scaled biquad: [ 0.00379864  0.00759728  0.00379864  1.         -1.76088036  0.77607492]
Scaled biquad: [ 0.00391613  0.00783225  0.00391613  1.         -1.81534108  0.83100559]
Scaled biquad: [ 0.00413778  0.00827557  0.00413778  1.         -1.91809148  0.93464262]

The frequency responses of each scaled section is shown below:

Figure 2: Response of each section

From the plot above we can see that, even-though we scaled the DC gain to 0 dB, there is still peaking above 0 dB at 1 kHz. If a number system other than floating-point is used, the reader should account for some headroom to allow for this peaking. Therefore, it is always good practice to plot the frequency response of each section individually to get a sense of the signal swing at internal nodes of the biquad chain.

In addition, this scaling will only work for a lowpass filter because the desired DC gain is 0 dB at DC. For a high-pass filter, the Nyquist gain must be used for scaling.