This set of Jupyter notebooks (raw Github link) covers the second book in Julius O. Smith III's excellent four-book series on audio DSP, Introduction to Digital Filters: with Audio Applications. This book takes the concepts from Mathematics of the DFT and runs with it, getting deep into digital filter theory, analysis, design and applications. The difficulty level goes up quite a bit here - lots of complex analysis tricks are used to derive results and a substantial amount of algebraic manipulation and creativity in the complex domain is used throughout the book, and required to solve the exercises. Analytic vs graphical approaches to understanding filtersReaders of this blog will know that I love visualizations, and in this domain I especially found that visually exploring the concepts really helped to develop my intuition. It's easy to get bogged down trying to derive exact analytic solutions for a filter's response. I spent many maddening/rewarding hours desperately trying random trig identities for half a page to try and get some closed solution, only to find at the end that it could have been done in a few steps by using some complex algebra trick. However, when you step away from the symbolic and analytic approach and just start moving the poles and zeros of a filter around and seeing what happens, the intimidation wears off a bit and it starts to feel within the realm of possibility to maybe actually design a filter response using some pretty intuitive visual reasoning. Designing a second-order filter to reject 60 Hz humFor example, say we want to design a filter (as we are asked to do in the first exercise of Chapter 8) to attenuate 60 Hz hum, given a sampling rate of 40 KHz. I'll walk through my more analytic attempt at the problem, and then I'll give a visual explanation of a more canonical notch filter that cuts past a lot of analytic hair-pulling. If you'd like you can also just skip to the graphical approach. An analytic approachNote: For brevity I'm going to assume some notational and terminology knowledge in this section. In the next section, I will back up and explain things more fully :) We want our filter to have a zero at 60 Hz. For now, let's consider only FIR filters (with no feedback component). We are given the constraint that the filter must be second-order. The factored form of a second-order FIR filter is , where and are the zeros of the frequency response. If we want the amplitude response to equal zero at normalized frequencies and , we can simply choose complex filter coefficients such that when . Here is an example for clarity: In [4]: from scipy.signal import freqz In [5]: # Pick a couple of arbitrary frequencies to reject. w1_reject = 0.8 w2_reject = 2.2 N = 512 q1 = np.exp(1j * w1_reject) q2 = np.exp(1j * w2_reject) B = [1, -(q1 + q2), q1 * q2]; A = [1] w, h = freqz(B, A, worN = N) plt.plot(w, np.abs(h)) for w_reject in [w1_reject, w2_reject]: plt.axvline(x=w_reject, linestyle='--', c='k') plt.grid(True) If we only want to reject a single frequency (as in this problem), we can set , making our expanded general factored transfer function . In [6]: # Only reject a single frequency with a second-order FIR filter with complex coefficients. w_reject = 2.2 N = 512 q = np.exp(1j * w_reject) B = [1, -2*q, q ** 2]; A = [1] w, h = freqz(B, A, worN = N) plt.plot(w, np.abs(h)) plt.axvline(x=w_reject, linestyle='--', c='k') plt.grid(True) We could technically be done here since the problem doesn't stipulate any more constraints, nor does it stipulate how wide the passband should be. However, I don't think this is what the question is after. If we want a second-order FIR filter with a real coefficient (and ), such that the amplitude response is equal to for normalized frequency , we set the amplitude response to and solve for
The only way for the magnitude of a complex number to be is if the complex number itself is . I know it's obvious but I needed to think about this for a while and found out the hard way that this works cleanly after solving with trig by expanding using Euler's identity. Here's a much simpler way:
We are given the sampling interval . When ,
Finally, our naive second-order 60 Hz hum-reject-filter for a sampling rate of Khz is . In [7]: fs = 40_000 B = [1, -2*np.cos(3 * np.pi / 1000), 1] H = np.fft.fft(B, n=fs) f = np.arange(0, fs // 2) plt.axvline(x=60, linestyle='--', c='k', label='Target reject frequency (60 Hz)') plt.loglog(f, np.abs(H[:fs//2])) plt.legend() plt.grid(True) How is the frequency response not ideal? What is a simple way to obtain a better frequency response at the price of higher filter order? (Hint: Consider .) We can see from the plot (and from the equation) that the response is nowhere near flat outside of the "stopband" (nor is it flat anywhere in the spectrum). Let's get a grip on the hint we're given: In [8]: theta = np.linspace(0, 2 * np.pi, 1000) _ = plt.plot(theta, 1 - np.abs(np.cos(theta)) ** 1000) This tells us that behaves as what looks like an ideal reject response when as . We can choose any frequency to "notch" here by subtracting it from above. That is, we should be pretty happy with an amplitude response that looks like I scratched my head for awhile thinking of how to design a transfer function to match this kind of response. I ended up finding this SO answer that helped. (I will discuss this more in the next section). The constant In [9]: fs = 40_000 f_r = 3 * np.pi / 1000 a = 1.01 B = [1, -2*np.cos(f_r), 1] A = [1, -2 * a * np.cos(f_r), a ** 2] H = ((1 + a) / 2) * np.fft.fft(B, n=fs) / np.fft.fft(A, n=fs) f = np.arange(0, fs // 2) plt.axvline(x=60, linestyle='--', c='k', label='Target reject frequency (60 Hz)') plt.loglog(f, np.abs(H[:fs//2])) plt.legend() plt.grid(True) A graphical approachIn this case, as mentioned above, I managed to find a Stack Overflow answer that helped me solve this particular problem. But the help came in a fully-formed transfer function, How did they arrive at this somewhat perplexing transfer function? How would I be able to come up with a similarly clever formula to solve a problem like this? I will get to what I think is a satisfying answer using some geometric intuition after a little background! Let's start with the definition of a transfer function: The transfer function of a linear time-invariant discrete-time filter is defined as , where denotes the transform of the filter output signal , and denotes the transform of the filter input signal . Here, is just any number in the complex plane. However, what we really care about is the frequency response, which is only evaluated on the unit circle in the plane. Thus we can set to , where , is the frequency in Hz, and is the sampling interval in seconds. Now that we only care about the frequency response, we can think of the transfer function in more familiar terms, as a ratio of the DFT of the output to the DFT of the input. This is a frequency domain interpretation of digital filters. In the time domain, for linear time-invariant filters we are always simply multiplying the current input , past inputs , and past outputs , by some coefficients and . That is, any linear time-invariant filter can be formulated as . JOS provides a derivation that I won't reproduce here, which uses basic properties of the DFT to unite these equations into a factored form expressed in terms of poles and zeros (along with some scaling factor ): Here we care about the amplitude response, . That is, the amplitude response of a filter is defined as the absolute value of its transfer function evaluated on the unit circle. I will skip yet another derivation to get to the point, which is that we can finally express the amplitude reponse of any linear time-invariant filter as , where, again, is a pole of the filter, is a zero, and is a scaling constant. Remember that transfer function in the Stack Overflow answer? Here it is again: Taking the amplitude response, We finally have the tools to interpret this in a visual, geometric way! Clearly, that term is our scaling factor . We can safely ignore it since it will scale all frequencies the same amount. That term there is a frequency at which we are evaluating the amplitude response of the filter. The term is what the Stack Overflow dude decided to use to express the angle of the placement of the poles and zeros of our notch filter. Thus, the zeros, lie exactly on the unit circle, and the poles, , are placed at the same angle as the poles, and then "pushed out" by radius . Let's see what these poles and zeros look like in the complex plane, and their effect on the frequency response, as we change : As the poles are moved closer to the zeros, the frequency response magnitude increases everywhere except at the angle (and ) at which we've placed our poles and zeros. Which is exactly what we want! To understand why this is the case, look back at the amplitude response (removing the constant scaling factor): Here we can see that the frequency response magnitude at frequency is given by the product of vector lengths from the zeros to the point on the unit circle, divided by the product of vectors lengths from the poles to that same point. (The phase response is similarly given by the sum of angles of the zero vectors, subtracted by the angles of the pole vectors. But I won't be discussing phase reponse in this post.) Let's now see that same animation but with these vectors explicitly drawn. The red point in the amplitude response chart is the result of taking the ratio of the product of the orange (numerator) vectors over the product of the blue (denominator) vectors. The frequency response is obtained by finding this ratio for all frequencies (all such points along the unit circle). Since the zeros lie on the unit circle, the response will evaluate to exactly for . As the poles get closer to the zeros, the difference between the product of the blue vectors and the product of the orange vectors gets smaller and smaller, and so their ratio approaches . However, until they are exactly equal, the orange (zero) vectors will still asymptotically approach as , while the poles will still have some positive magnitude, resulting in a ratio (response magnitude) close to (and equal to when ). With this interpretation we've transformed a somewhat abstract mathy goal (design a filter that rejects 60 Hz hum) into a problem that, at least for me, is much more intuitive: How can we arrange points in a 2D plane such that the ratio of vector-lengths from those points to the unit circle is near zero at the angle corresponding to 60 Hz, but near one everywhere else? Visualizations for common audio filtersWe can deepen our intuition further by moving the poles and zeros of some common filter families, and seeing what effect it has on the amplitude, phase and impulse responses. (The impulse response is the output of the filter when the input is a unit impulse - a single 1 followed by an infinite number of 0's.) One-zero filterOne-pole filterTwo-pole filterTwo-zero filterBiquad sectionBiquad allpass sectionComplex one-pole resonatorDC-blockerThe code that created these animations is in the notebook for Appendix B: Elementary Audio Digital Filters. The graphical interpretation of digital filters originally came from this section of the book. I hope playing around with these animations helps someone else feel a little less intimidated by digital filters! |
|