DSP Labs
  • INTRODUCTION
  • BILL OF MATERIALS
  • 1. OVERVIEW AND INSTALLATION
    • 1.1 Hardware
    • 1.2 Software
      • CubeMX
      • SW4STM32
      • Eclipse tips
    • 1.3 First project!
  • 2. AUDIO PASSTHROUGH
    • 2.1 Audio I/O theory
      • Microphone
      • Stereo decoder
    • 2.2 Updating peripherals
    • 2.3 Wiring audio I/O
    • 2.4 Coding passthrough
  • 3. ALIEN VOICE EFFECT
    • 3.1 How it works
    • 3.2 Real-time DSP tips
    • 3.3 Real-time with Python
    • 3.4 C implementation
  • 4. DIGITAL FILTER DESIGN
    • 4.1 Design approaches
    • 4.2 Real-time implementation
  • 5. GRANULAR SYNTHESIS
    • 5.1 How it works
    • 5.2 Implementation
  • 6. LINEAR PREDICTION
    • 6.1 Theory behind LPC
    • 6.2 Implementation
  • 7. DFT PITCH SHIFTING
    • 7.1 How it works
    • 7.2 Python implementation
Powered by GitBook
On this page
  • Analysis of our current (simple) filter
  • FIR filters
  • Moving average filter
  • IIR filters

Was this helpful?

  1. 4. DIGITAL FILTER DESIGN

4.1 Design approaches

As before, we will first understand the problem and design (various) solutions in Python. We will then implement a real-time version in Python so that implementing it on the microcontroller will be primarily that of porting Python to C. Let's get started!

In the following analysis, we assume a sampling frequency of 16 kHz16 \text{ kHz}16 kHz, as this is the sampling rate of our test recording. This parameter can be changed in the referenced scripts.

TASK 1: You are strongly encouraged to play around with the scripts mentioned below!

Analysis of our current (simple) filter

In the previous chapter, we implemented a very simple high pass filter by subtraction the previous sample from the current one, namely:

y[n]=x[n]−x[n−1].y[n] = x[n] - x[n-1].y[n]=x[n]−x[n−1].

From this difference equation, we could perform an analysis of its frequency response by taking the Z-transform:

Y(z)=X(z)+X(z)⋅z−1,Y(z) = X(z) + X(z) \cdot z^{-1},Y(z)=X(z)+X(z)⋅z−1,

from which we obtain the transfer function:

H(z)=Y(z)X(z)=1−z−1.H(z) = \frac{Y(z)}{X(z)} = 1 - z^{-1}.H(z)=X(z)Y(z)​=1−z−1.

For certain system, we can also compute the Fourier Transform, which may be more intuitive to understand as we can "read-off" the attenuation at various frequencies. For our simple HPF, the frequency response is shown below.

Figure: Frequency response of our simple HPF (log scale).

As noted in the last chapter, this filter does not have a sharp cutoff, which means frequencies of interest within the voice spectrum will be attenuated.

We can observe this with a simple example: we will take a zero-mean audio signal and add an artificial offset to it; we will then apply our simple HPF and compare the filtered signal with the zero-mean one.

Figure: Original zero-mean signal (blue) and signal with artificial offset (orange).

Figure: Signal with artificial offset (blue) and HPF'ed signal (orange).

Figure: Original zero-mean signal (blue) and HPF'ed signal (orange).

If you would be just looking at the resulting plots, you would think the high pass filtered (HPF'ed) signal is completely different from the original! However, if you listen to the output, you will hear the same sentence but with the lower frequencies severely attenuated.

Nonetheless, looking at this plot and hearing the output, we can conclude that this simple filter will simply not cut it.

FIR filters

Our simple filter from before is a special case of a causal discrete-time Finite Impulse Response (FIR) filter. Causal means that only current and previous samples are used, while FIR implies only current and past input samples are used. For such a filter of order NNN, we can write the output as:

y[n]=b0x[n]+b1x[n−1]+⋯+bNx[n−N]=∑i=0Nbi⋅x[n−i],y[n] = b_0 x[n] + b_1 x[n-1] + \cdots + b_N x[n-N] = \sum_{i=0}^N b_i \cdot x[n-i],y[n]=b0​x[n]+b1​x[n−1]+⋯+bN​x[n−N]=i=0∑N​bi​⋅x[n−i],

where bib_ibi​ are often called the filter coefficients. A common way to visualize such filters is with a block diagram as seen below.

For our simple HPF, we had N=1N=1N=1 with b0=−b1=1b_0= -b_1 = 1b0​=−b1​=1.

Figure: Comparing FIR filters of different order with our simple HPF filter from before.

Although our simple HPF from before attenuates DC more than most of the FIR filters considered above, the other filters would preserve important signal in the voice spectrum , i.e. 0 dB0 \text{ dB}0 dB (unit gain / passband) above 100 Hz100 \text{ Hz}100 Hz. However, using 414141 taps barely attenuates the DC component while 321321321 taps demands a large amount of memory.

Let's take a look at the resulting output when we pass the artificial DC-biased signal from before.

Figure: Signal with artificial offset filtered by 414141-tap FIR filter.

Figure: Signal with artificial offset filtered by 321321321-tap FIR filter.

As expected from the frequency response curves we saw earlier, the 414141-tap filter is unable to attenuate the DC offset, while the 321321321-tap filter is able to do so (but at the cost of more memory and computation). Nonetheless, both filters preserve the original signal much better than what we saw with the simple 222-tap HPF we were using earlier. The resulting filtered signals should sound nearly identical to the original (zero-mean) file. The 414141-tap output, on the other hand, will begin with a small click due to the DC offset.

Moving average filter

Recalling our original motivation of the HPF, we discussed a simple solution for removing the DC offset by subtracting the average/mean of the signal. This can be done with a single line of Python when we have a recording, but is not possible when we are streaming an input, as we do not have access to future values!

m[n]=x[n]+x[n−1]+⋯+x[N−1]N.m[n] = \dfrac{x[n] + x[n-1] + \cdots + x[N-1]}{N}.m[n]=Nx[n]+x[n−1]+⋯+x[N−1]​.

The average at time index nnn can also be computed using the previous average at index n−1n-1n−1 in order to reduce the number of computations, particularly if NNN is large.

m[n]=m[n−1]+x[n]N−x[n−N]N.m[n] = m[n-1] + \dfrac{x[n]}{N} - \dfrac{x[n-N]}{N}.m[n]=m[n−1]+Nx[n]​−Nx[n−N]​.

The above form falls under the category of infinite impulse response (IIR) filters, which will be discussed shortly, as the output depends on past output value(s).

With this estimate of the average, we can remove it from the current sample towards the goal of removing the DC offset:

y[n]=x[n]−m[n].y[n] = x[n] - m[n].y[n]=x[n]−m[n].

We can observe a tradeoff between a large number of coefficients and a desirable (low) cutoff frequency versus a small number of coefficients with a better attenuation at DC but an undesirable (high) cutoff frequency. Nonetheless, we can obtain desirable performance without using as many coefficients as we saw earlier with the window method.

IIR filters

So are we stuck with using FIR filters with lots of coefficients? Fortunately not!

With recursion, we can compute filters that theoretically have an infinite impulse response (IIR). The filters we saw earlier have a finite impulse response since they rely on a finite number of past input samples. With IIR filters, we also use past output values, and this recursion is what creates an infinite impulse response. The difference equation of an IIR filter is typically written as such:

y[n]=∑i=0Nbi⋅x[n−i]+∑j=1Mai⋅y[n−i].y[n] = \sum_{i=0}^N b_i \cdot x[n-i] + \sum_{j=1}^{M} a_i \cdot y[n-i].y[n]=i=0∑N​bi​⋅x[n−i]+j=1∑M​ai​⋅y[n−i].

Notice that aia_iai​ starts with i=1i=1i=1; in literature you might see an a0a_0a0​ coefficient but this will be typically set to a0=1a_0=1a0​=1. In total, we have N+M+1N+M+1N+M+1 coefficients, meaning this many multiplies for the output.

Looking at the "Time Domain" row, as we are interested in DC removal, we can observe two approaches of interest:

  1. Moving average (which as we saw earlier could be implemented more efficiently as an IIR filter).

  2. Single pole IIR filter.

We will now look into this second approach, as we will be implementing this filter for DC removal.

y[n]=x[n]−x[n−1]+Ry[n−1],y[n] = x[n] - x[n-1] + Ry[n-1],y[n]=x[n]−x[n−1]+Ry[n−1],

where RRR specifies the location of the pole. By taking the Z-transform:

Y(z)=X(z)+X(z)⋅z−1+RY(z)⋅z−1,Y(z) = X(z) + X(z) \cdot z^{-1} + RY(z) \cdot z^{-1},Y(z)=X(z)+X(z)⋅z−1+RY(z)⋅z−1,

we can obtain the following transfer function:

H(z)=Y(z)X(z)=1−z−11−Rz−1.H(z) = \frac{Y(z)}{X(z)} = \frac{1 - z^{-1}}{1 - Rz^{-1}}.H(z)=X(z)Y(z)​=1−Rz−11−z−1​.

Although R=0.95R=0.95R=0.95 produces a filter with a more desirable cutoff frequency, the roll-off is still the same, as we can observe roughly parallel lines with a decay of roughly 18 dB/decade18 \text{ dB/decade}18 dB/decade. We can produce a sharper filter by cascading multiple single pole filters. A cascade of NNN stages corresponds to raising the frequency response to the power of NNN, namely:

(H(z))N=(1−z−1)N(1−Rz−1)N.\left(H(z)\right)^N = \frac{(1 - z^{-1})^N}{(1 - Rz^{-1})^N}.(H(z))N=(1−Rz−1)N(1−z−1)N​.

As NNN increases, more coefficients (and samples) will be needed to compute the output, which means more computations and memory needed to store the coefficients and past samples. An NNN-stage filter would require (2N+1)(2N+1)(2N+1) coefficients, (N+1)(N+1)(N+1) past input samples, and NNN past output samples.

For example, for N=2N=2N=2:

(H(z))2=(1−z−1)2(1−Rz−1)2=1−2z−1+z−21−2Rz−1+R2z−2,(H(z))^2 = \frac{(1 - z^{-1})^2}{(1 - Rz^{-1})^2} = \frac{1 - 2z^{-1} + z^{-2}}{1 - 2Rz^{-1} + R^2z^{-2}},(H(z))2=(1−Rz−1)2(1−z−1)2​=1−2Rz−1+R2z−21−2z−1+z−2​,

from which we can read off the coefficients b=[1,−2,1]\mathbf{b} = [1, -2, 1]b=[1,−2,1] and a=[1,−2R,R2]\mathbf{a} = [1, -2R, R^2]a=[1,−2R,R2].

For N=3N=3N=3, we could use the filter from N=2N=2N=2 to compute the coefficients:

(H(z))3=(H(z))2⋅1−z−11−Rz−1=1−3z−1+3z−2−z−31−3Rz−1+3R2z−2−R3z−3,(H(z))^3 = (H(z))^2 \cdot \frac{1 - z^{-1}}{1 - Rz^{-1}} = \frac{1 - 3z^{-1} + 3z^{-2} - z^{-3}}{1 - 3Rz^{-1} + 3R^2z^{-2} - R^3z^{-3}},(H(z))3=(H(z))2⋅1−Rz−11−z−1​=1−3Rz−1+3R2z−2−R3z−31−3z−1+3z−2−z−3​,

yielding the coefficients b=[1,−3,3,−1]\mathbf{b} = [1, -3, 3, -1]b=[1,−3,3,−1] and a=[1,−3R,3R2,−R3]\mathbf{a} = [1, -3R, 3R^2, -R^3]a=[1,−3R,3R2,−R3].

For a general NNN, the transfer function (and thus coefficients) can be computed in this recursive fashion:

(H(z))N=(H(z))N−1⋅1−z−11−Rz−1.(H(z))^N = (H(z))^{N-1} \cdot \frac{1 - z^{-1}}{1 - Rz^{-1}}.(H(z))N=(H(z))N−1⋅1−Rz−11−z−1​.

We can observe this sharper decay for higher values of NNN. Quantitavely, it is around 18N dB/decade18N \text{ dB/decade}18N dB/decade for NNN stages. Moreover, comparing to our FIR analysis from before, we need much less coefficients in order to obtain a filter with a desirable performance!

Previous4. DIGITAL FILTER DESIGNNext4.2 Real-time implementation

Last updated 6 years ago

Was this helpful?

From such an expression, we can create the standard as seen below. With such a plot, we can extract a lot of useful information, such as stability and causality.

Figure: Pole-zero plot of our simple HPF. Thanks to for the visualization function.

You can run to generate the plots below and to listen to the filtered signal.

Figure: Causal discrete-time FIR filter of order NNN. The top part is an NNN-stage delay line with N+1N + 1N+1 taps. Each unit delay is a z−1z^{-1}z−1 operator in Z-transform notation. .

With more coefficients, filters with sharper roll-offs can be achieved, but at the cost of more computation (and memory to store the coefficients and previous sample values). For selecting the values of the coefficients, there are different design criteria. A common approach is the , which we will consider here for different orders NNN.

The above figure can be generated with , which uses the function from the scipy.signal library in order to compute the coefficients with the window method.

The above figure can be generated with , which again uses firwin to design the filter and to apply the filter.

We can however perform a that is estimate the mean from the past NNN samples and subtract this from the current sample value. The moving average can be written as the following FIR operation:

The figure below, generated by , demonstrates the performance of the above filter for varying values of NNN.

A more efficient and better performing DC removal moving average filter is discussed . As we will not adopt this approach in our implementation, we leave this resource for the interested reader.

As with FIR filter design, there are various design approaches and criteria. The table below from the very useful online provides a nice summary of the different design approaches for a variety of applications.

Figure: Filters can be divided by their use, and how they are implemented. .

Another name for this filter is a , and it can be expressed with the following difference equation:

A larger RRR results in a more selective filter (not suppressing desired lower frequencies), but at the expense of an impulse response with a longer tail. More on this tradeoff can be read .

In the above figure, generated by , we vary the value of RRR to demonstrate the resulting response. As RRR approaches 111, we obtain a narrower notch at DC, that is the attenuation "begins" at a lower frequency. The value R=0R=0R=0 corresponds precisely to the simple two-tap filter we saw earlier.

But let's see what the frequency response looks like! The figure below is generated with .

The figure below, generated with , shows that even the single stage (N=1N=1N=1) does a sufficient job in removing the DC offset for the (artificially generated) signal with a DC offset. This filter is implemented with just 333 coefficients, whereas the FIR filter needed 181181181 taps with the window method and 404040 taps with the moving average approach for comparable performance.

In the next section, we will see how we can implement the second order IIR filter, also known as a , in real-time with Python.

pole-zero plot
this software
this script
Source
window method
this script
firwin
this script
lfilter
moving average
this script
here
DSP guide
Source
DC blocker
here
this script
this script
this script
biquad filter