From 2dcb4142609cc3b06ab68a56c14cf221c93f64f1 Mon Sep 17 00:00:00 2001 From: Martmists Date: Sun, 10 Oct 2021 11:17:01 +0200 Subject: [PATCH 01/18] Add IIR Filter and Butterworth design functions Signed-off-by: Martmists --- audio_filters/__init__.py | 0 audio_filters/butterworth_filter.py | 149 ++++++++++++++++++++++++++++ audio_filters/iir_filter.py | 73 ++++++++++++++ audio_filters/show_response.py | 61 ++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 audio_filters/__init__.py create mode 100644 audio_filters/butterworth_filter.py create mode 100644 audio_filters/iir_filter.py create mode 100644 audio_filters/show_response.py diff --git a/audio_filters/__init__.py b/audio_filters/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py new file mode 100644 index 000000000000..b9794401fee8 --- /dev/null +++ b/audio_filters/butterworth_filter.py @@ -0,0 +1,149 @@ +from math import sqrt, sin, tau, cos + +from audio_filters.iir_filter import IIRFilter + + +class ButterworthFilter: + """ + Create 2nd-order IIR filters with Butterworth design. + + Code based on https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html + Alternatively you can use scipy.signal.butter, which should yield the same results. + """ + + @staticmethod + def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = (1 - _cos) / 2 + b1 = 1 - _cos + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) + return filt + + @staticmethod + def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = (1 + _cos) / 2 + b1 = - 1 - _cos + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) + return filt + + @staticmethod + def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = _sin / 2 + b1 = 0 + b2 = - b0 + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + @staticmethod + def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = 1 - alpha + b1 = -2 * _cos + b2 = 1 + alpha + + filt = IIRFilter(2) + filt.set_coefficients([b2, b1, b0], [b0, b1, b2]) + return filt + + @staticmethod + def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + A = 10 ** (gain_db / 40) + + b0 = 1 + alpha * A + b1 = -2 * _cos + b2 = 1 - alpha * A + a0 = 1 + alpha / A + a1 = -2 * _cos + a2 = 1 - alpha / A + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + @staticmethod + def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + A = 10 ** (gain_db / 40) + pmc = (A+1) - (A-1)*_cos + ppmc = (A+1) + (A-1)*_cos + mpc = (A-1) - (A+1)*_cos + pmpc = (A-1) + (A+1)*_cos + aa2 = 2*sqrt(A)*alpha + + b0 = A * (pmc + aa2) + b1 = 2 * A * mpc + b2 = A * (pmc - aa2) + a0 = ppmc + aa2 + a1 = -2 * pmpc + a2 = ppmc - aa2 + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + @staticmethod + def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + A = 10 ** (gain_db / 40) + pmc = (A+1) - (A-1)*_cos + ppmc = (A+1) + (A-1)*_cos + mpc = (A-1) - (A+1)*_cos + pmpc = (A-1) + (A+1)*_cos + aa2 = 2*sqrt(A)*alpha + + b0 = A * (ppmc + aa2) + b1 = -2 * A * pmpc + b2 = A * (ppmc - aa2) + a0 = pmc + aa2 + a1 = 2 * mpc + a2 = pmc - aa2 + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py new file mode 100644 index 000000000000..d7e76d750df2 --- /dev/null +++ b/audio_filters/iir_filter.py @@ -0,0 +1,73 @@ +from typing import List + + +class IIRFilter: + """ + N-Order IIR filter + Assumes working with float samples normalized on [-1, 1] + + --- + + Implementation details: + Using the following transfer function + H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}}{a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}} + we can rewrite this to + y[n]={\frac{1}{a_{0}}}\left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)-\left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right) + """ + def __init__(self, order: int): + self.order = order + + # a_{0} ... a_{k} + self.a_coeffs = [1.0] + [0.0] * order + # b_{0} ... b_{k} + self.b_coeffs = [1.0] + [0.0] * order + + # x[n-1] ... x[n-k] + self.input_history = [0.0] * self.order + # y[n-1] ... y[n-k] + self.output_history = [0.0] * self.order + + def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]): + """ + Set the coefficients for the IIR filter. These should both be of size order + 1. + a_0 may be left out, and it will use 1.0 as default value. + + This method works well with scipy's filter design functions + >>> # Make a 2nd-order 1000Hz butterworth lowpass filter + >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, btype='lowpass', fs=samplerate) + >>> filt = IIRFilter(2) + >>> filt.set_coefficients(a_coeffs, b_coeffs) + """ + if len(a_coeffs) < self.order: + a_coeffs = [1.0] + a_coeffs + + if len(a_coeffs) != self.order + 1: + raise ValueError(f"Expected a_coeffs to have {self.order + 1} elements for {self.order}-order filter, got {len(a_coeffs)}") + + if len(b_coeffs) != self.order + 1: + raise ValueError(f"Expected b_coeffs to have {self.order + 1} elements for {self.order}-order filter, got {len(a_coeffs)}") + + self.a_coeffs = a_coeffs + self.b_coeffs = b_coeffs + + def process(self, sample: float): + """ + Calculate y[n] + """ + result = 0.0 + + # Start at index 1 and do index 0 at the end. + for i in range(1, self.order+1): + result += ( + self.b_coeffs[i] * self.input_history[i-1] - self.a_coeffs[i] * self.output_history[i-1] + ) + + result = (result + self.b_coeffs[0] * sample) / self.a_coeffs[0] + + self.input_history[1:] = self.input_history[:-1] + self.output_history[1:] = self.output_history[:-1] + + self.input_history[0] = sample + self.output_history[0] = result + + return result diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py new file mode 100644 index 000000000000..ecd3bef7997d --- /dev/null +++ b/audio_filters/show_response.py @@ -0,0 +1,61 @@ +from math import pi + +import matplotlib.pyplot as plt +import numpy as np + + +def show_frequency_response(filter, samplerate): + """ + Show frequency response of a filter + """ + + size = 512 + inputs = [1] + [0] * (size - 1) + outputs = [0] * size + for i in range(size): + outputs[i] = filter.process(inputs[i]) + + filler = [0] * (samplerate - size) # zero-padding + outputs += filler + fft_out = np.abs(np.fft.fft(outputs)) + fft_db = 20 * np.log10(fft_out) + + # Frequencies on log scale from 24 to nyquist frequency + plt.xlim(24, samplerate/2-1) + plt.xlabel("Frequency (Hz)") + plt.xscale('log') + + # Display within reasonable bounds + lowest = min([-20, np.min(fft_db[1:samplerate//2-1])]) + highest = max([20, np.max(fft_db[1:samplerate//2-1])]) + plt.ylim(max([-80, lowest]), min([80, highest])) + plt.ylabel("Gain (dB)") + + plt.plot(fft_db) + plt.show() + + +def show_phase_response(filter, samplerate): + """ + Show phase response of a filter + """ + + size = 512 + inputs = [1] + [0] * (size - 1) + outputs = [0] * size + for i in range(size): + outputs[i] = filter.process(inputs[i]) + + filler = [0] * (samplerate - size) # zero-padding + outputs += filler + fft_out = np.angle(np.fft.fft(outputs)) + + # Frequencies on log scale from 24 to nyquist frequency + plt.xlim(24, samplerate / 2 - 1) + plt.xlabel("Frequency (Hz)") + plt.xscale('log') + + plt.ylim(-2*pi, 2*pi) + plt.ylabel("Phase shift (Radians)") + plt.plot(np.unwrap(fft_out, -2*pi)) + plt.show() From 01574c88d923c1ba8c8c0013404ea93b4bbb2f88 Mon Sep 17 00:00:00 2001 From: Martmists Date: Sun, 10 Oct 2021 11:20:57 +0200 Subject: [PATCH 02/18] naming conventions and missing type hints Signed-off-by: Martmists --- audio_filters/butterworth_filter.py | 50 ++++++++++++++--------------- audio_filters/iir_filter.py | 2 +- audio_filters/show_response.py | 4 +-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index b9794401fee8..12018dfe9602 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -87,14 +87,14 @@ def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = _sin = sin(w0) _cos = cos(w0) alpha = _sin / (2 * q_factor) - A = 10 ** (gain_db / 40) + big_a = 10 ** (gain_db / 40) - b0 = 1 + alpha * A + b0 = 1 + alpha * big_a b1 = -2 * _cos - b2 = 1 - alpha * A - a0 = 1 + alpha / A + b2 = 1 - alpha * big_a + a0 = 1 + alpha / big_a a1 = -2 * _cos - a2 = 1 - alpha / A + a2 = 1 - alpha / big_a filt = IIRFilter(2) filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) @@ -106,16 +106,16 @@ def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: flo _sin = sin(w0) _cos = cos(w0) alpha = _sin / (2 * q_factor) - A = 10 ** (gain_db / 40) - pmc = (A+1) - (A-1)*_cos - ppmc = (A+1) + (A-1)*_cos - mpc = (A-1) - (A+1)*_cos - pmpc = (A-1) + (A+1)*_cos - aa2 = 2*sqrt(A)*alpha - - b0 = A * (pmc + aa2) - b1 = 2 * A * mpc - b2 = A * (pmc - aa2) + big_a = 10 ** (gain_db / 40) + pmc = (big_a+1) - (big_a-1)*_cos + ppmc = (big_a+1) + (big_a-1)*_cos + mpc = (big_a-1) - (big_a+1)*_cos + pmpc = (big_a-1) + (big_a+1)*_cos + aa2 = 2*sqrt(big_a)*alpha + + b0 = big_a * (pmc + aa2) + b1 = 2 * big_a * mpc + b2 = big_a * (pmc - aa2) a0 = ppmc + aa2 a1 = -2 * pmpc a2 = ppmc - aa2 @@ -130,16 +130,16 @@ def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: fl _sin = sin(w0) _cos = cos(w0) alpha = _sin / (2 * q_factor) - A = 10 ** (gain_db / 40) - pmc = (A+1) - (A-1)*_cos - ppmc = (A+1) + (A-1)*_cos - mpc = (A-1) - (A+1)*_cos - pmpc = (A-1) + (A+1)*_cos - aa2 = 2*sqrt(A)*alpha - - b0 = A * (ppmc + aa2) - b1 = -2 * A * pmpc - b2 = A * (ppmc - aa2) + big_a = 10 ** (gain_db / 40) + pmc = (big_a+1) - (big_a-1)*_cos + ppmc = (big_a+1) + (big_a-1)*_cos + mpc = (big_a-1) - (big_a+1)*_cos + pmpc = (big_a-1) + (big_a+1)*_cos + aa2 = 2*sqrt(big_a)*alpha + + b0 = big_a * (ppmc + aa2) + b1 = -2 * big_a * pmpc + b2 = big_a * (ppmc - aa2) a0 = pmc + aa2 a1 = 2 * mpc a2 = pmc - aa2 diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index d7e76d750df2..e5cf24dd6870 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -50,7 +50,7 @@ def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]): self.a_coeffs = a_coeffs self.b_coeffs = b_coeffs - def process(self, sample: float): + def process(self, sample: float) -> float: """ Calculate y[n] """ diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index ecd3bef7997d..8006739192af 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -4,7 +4,7 @@ import numpy as np -def show_frequency_response(filter, samplerate): +def show_frequency_response(filter, samplerate: int): """ Show frequency response of a filter """ @@ -35,7 +35,7 @@ def show_frequency_response(filter, samplerate): plt.show() -def show_phase_response(filter, samplerate): +def show_phase_response(filter, samplerate: int): """ Show phase response of a filter """ From 97704073bf02d16c2021aafeed09d9a7f0137c25 Mon Sep 17 00:00:00 2001 From: Martmists Date: Sun, 10 Oct 2021 11:27:26 +0200 Subject: [PATCH 03/18] Link wikipedia in IIRFilter Signed-off-by: Martmists --- audio_filters/butterworth_filter.py | 21 +++++++++++++++++++++ audio_filters/iir_filter.py | 3 +++ 2 files changed, 24 insertions(+) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index 12018dfe9602..e8953d6d25c3 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -13,6 +13,9 @@ class ButterworthFilter: @staticmethod def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a low-pass filter + """ w0 = tau * frequency / samplerate _sin = sin(w0) _cos = cos(w0) @@ -31,6 +34,9 @@ def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) @staticmethod def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a high-pass filter + """ w0 = tau * frequency / samplerate _sin = sin(w0) _cos = cos(w0) @@ -49,6 +55,9 @@ def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) @staticmethod def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a band-pass filter + """ w0 = tau * frequency / samplerate _sin = sin(w0) _cos = cos(w0) @@ -68,6 +77,9 @@ def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) @staticmethod def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates an all-pass filter + """ w0 = tau * frequency / samplerate _sin = sin(w0) _cos = cos(w0) @@ -83,6 +95,9 @@ def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) @staticmethod def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a peak filter + """ w0 = tau * frequency / samplerate _sin = sin(w0) _cos = cos(w0) @@ -102,6 +117,9 @@ def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = @staticmethod def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a low-shelf filter + """ w0 = tau * frequency / samplerate _sin = sin(w0) _cos = cos(w0) @@ -126,6 +144,9 @@ def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: flo @staticmethod def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a high-shelf filter + """ w0 = tau * frequency / samplerate _sin = sin(w0) _cos = cos(w0) diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index e5cf24dd6870..471110462bb2 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -9,6 +9,9 @@ class IIRFilter: --- Implementation details: + Based on the 2nd-order function from https://en.wikipedia.org/wiki/Digital_biquad_filter, + this generalized N-order function was made. + Using the following transfer function H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}}{a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}} we can rewrite this to From adc612d3e2da185dd0888607861e8de6f93c5df3 Mon Sep 17 00:00:00 2001 From: Martmists Date: Mon, 11 Oct 2021 09:19:54 +0200 Subject: [PATCH 04/18] Add doctests and None return types Signed-off-by: Martmists --- audio_filters/butterworth_filter.py | 21 +++++++++++++++++++++ audio_filters/iir_filter.py | 8 ++++++-- audio_filters/show_response.py | 10 ++++++++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index e8953d6d25c3..f80657556d2a 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -15,6 +15,9 @@ class ButterworthFilter: def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ Creates a low-pass filter + + >>> ButterworthFilter.make_lowpass(1000, 48000) + """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -36,6 +39,9 @@ def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ Creates a high-pass filter + + >>> ButterworthFilter.make_highpass(1000, 48000) + """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -57,6 +63,9 @@ def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ Creates a band-pass filter + + >>> ButterworthFilter.make_bandpass(1000, 48000) + """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -79,6 +88,9 @@ def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ Creates an all-pass filter + + >>> ButterworthFilter.make_allpass(1000, 48000) + """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -97,6 +109,9 @@ def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ Creates a peak filter + + >>> ButterworthFilter.make_peak(1000, 48000, 6) + """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -119,6 +134,9 @@ def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ Creates a low-shelf filter + + >>> ButterworthFilter.make_lowshelf(1000, 48000, 6) + """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -146,6 +164,9 @@ def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: flo def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ Creates a high-shelf filter + + >>> ButterworthFilter.make_highshelf(1000, 48000, 6) + """ w0 = tau * frequency / samplerate _sin = sin(w0) diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index 471110462bb2..b17e7e40f4c3 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -17,7 +17,7 @@ class IIRFilter: we can rewrite this to y[n]={\frac{1}{a_{0}}}\left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)-\left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right) """ - def __init__(self, order: int): + def __init__(self, order: int) -> None: self.order = order # a_{0} ... a_{k} @@ -30,7 +30,7 @@ def __init__(self, order: int): # y[n-1] ... y[n-k] self.output_history = [0.0] * self.order - def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]): + def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]) -> None: """ Set the coefficients for the IIR filter. These should both be of size order + 1. a_0 may be left out, and it will use 1.0 as default value. @@ -56,6 +56,10 @@ def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]): def process(self, sample: float) -> float: """ Calculate y[n] + + >>> filt = IIRFilter(2) + >>> filt.process(0) + 0 """ result = 0.0 diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 8006739192af..70e91a31f1fa 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -1,10 +1,16 @@ from math import pi +from typing import Protocol import matplotlib.pyplot as plt import numpy as np -def show_frequency_response(filter, samplerate: int): +class FilterType(Protocol): + def process(self, sample: float) -> float: + pass + + +def show_frequency_response(filter: FilterType, samplerate: int) -> None: """ Show frequency response of a filter """ @@ -35,7 +41,7 @@ def show_frequency_response(filter, samplerate: int): plt.show() -def show_phase_response(filter, samplerate: int): +def show_phase_response(filter: FilterType, samplerate: int) -> None: """ Show phase response of a filter """ From ce8bf6d21319f892281947bacbd13e69301f853d Mon Sep 17 00:00:00 2001 From: Martmists Date: Mon, 11 Oct 2021 09:24:23 +0200 Subject: [PATCH 05/18] More doctests Signed-off-by: Martmists --- audio_filters/show_response.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 70e91a31f1fa..77f299b89b5c 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -7,12 +7,22 @@ class FilterType(Protocol): def process(self, sample: float) -> float: + """ + Calculate y[n] + + >>> filt = FilterType() + >>> filt.process(0) + """ pass def show_frequency_response(filter: FilterType, samplerate: int) -> None: """ Show frequency response of a filter + + >>> from audio_filters.iir_filter import IIRFilter + >>> filt = IIRFilter(4) + >>> show_frequency_response(filt, 48000) """ size = 512 @@ -44,6 +54,10 @@ def show_frequency_response(filter: FilterType, samplerate: int) -> None: def show_phase_response(filter: FilterType, samplerate: int) -> None: """ Show phase response of a filter + + >>> from audio_filters.iir_filter import IIRFilter + >>> filt = IIRFilter(4) + >>> show_phase_response(filt, 48000) """ size = 512 From 92e58da52f8c8217a86be06ee2da85ff75c1b7c0 Mon Sep 17 00:00:00 2001 From: Martmists Date: Mon, 11 Oct 2021 12:05:59 +0200 Subject: [PATCH 06/18] Requested changes Signed-off-by: Martmists --- audio_filters/butterworth_filter.py | 363 ++++++++++++++-------------- audio_filters/show_response.py | 5 +- 2 files changed, 184 insertions(+), 184 deletions(-) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index f80657556d2a..803beb6b81bf 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -3,189 +3,188 @@ from audio_filters.iir_filter import IIRFilter -class ButterworthFilter: +""" +Create 2nd-order IIR filters with Butterworth design. + +Code based on https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html +Alternatively you can use scipy.signal.butter, which should yield the same results. +""" + + +def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a low-pass filter + + >>> make_lowpass(1000, 48000) + + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = (1 - _cos) / 2 + b1 = 1 - _cos + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) + return filt + + +def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: """ - Create 2nd-order IIR filters with Butterworth design. + Creates a high-pass filter - Code based on https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html - Alternatively you can use scipy.signal.butter, which should yield the same results. + >>> make_highpass(1000, 48000) + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = (1 + _cos) / 2 + b1 = - 1 - _cos + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) + return filt + + +def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a band-pass filter + + >>> make_bandpass(1000, 48000) + + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = _sin / 2 + b1 = 0 + b2 = - b0 + + a0 = 1 + alpha + a1 = -2 * _cos + a2 = 1 - alpha + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt - @staticmethod - def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: - """ - Creates a low-pass filter - - >>> ButterworthFilter.make_lowpass(1000, 48000) - - """ - w0 = tau * frequency / samplerate - _sin = sin(w0) - _cos = cos(w0) - alpha = _sin / (2 * q_factor) - - b0 = (1 - _cos) / 2 - b1 = 1 - _cos - - a0 = 1 + alpha - a1 = -2 * _cos - a2 = 1 - alpha - - filt = IIRFilter(2) - filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) - return filt - - @staticmethod - def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: - """ - Creates a high-pass filter - - >>> ButterworthFilter.make_highpass(1000, 48000) - - """ - w0 = tau * frequency / samplerate - _sin = sin(w0) - _cos = cos(w0) - alpha = _sin / (2 * q_factor) - - b0 = (1 + _cos) / 2 - b1 = - 1 - _cos - - a0 = 1 + alpha - a1 = -2 * _cos - a2 = 1 - alpha - - filt = IIRFilter(2) - filt.set_coefficients([a0, a1, a2], [b0, b1, b0]) - return filt - - @staticmethod - def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: - """ - Creates a band-pass filter - - >>> ButterworthFilter.make_bandpass(1000, 48000) - - """ - w0 = tau * frequency / samplerate - _sin = sin(w0) - _cos = cos(w0) - alpha = _sin / (2 * q_factor) - - b0 = _sin / 2 - b1 = 0 - b2 = - b0 - - a0 = 1 + alpha - a1 = -2 * _cos - a2 = 1 - alpha - - filt = IIRFilter(2) - filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) - return filt - - @staticmethod - def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: - """ - Creates an all-pass filter - - >>> ButterworthFilter.make_allpass(1000, 48000) - - """ - w0 = tau * frequency / samplerate - _sin = sin(w0) - _cos = cos(w0) - alpha = _sin / (2 * q_factor) - - b0 = 1 - alpha - b1 = -2 * _cos - b2 = 1 + alpha - - filt = IIRFilter(2) - filt.set_coefficients([b2, b1, b0], [b0, b1, b2]) - return filt - - @staticmethod - def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: - """ - Creates a peak filter - - >>> ButterworthFilter.make_peak(1000, 48000, 6) - - """ - w0 = tau * frequency / samplerate - _sin = sin(w0) - _cos = cos(w0) - alpha = _sin / (2 * q_factor) - big_a = 10 ** (gain_db / 40) - - b0 = 1 + alpha * big_a - b1 = -2 * _cos - b2 = 1 - alpha * big_a - a0 = 1 + alpha / big_a - a1 = -2 * _cos - a2 = 1 - alpha / big_a - - filt = IIRFilter(2) - filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) - return filt - - @staticmethod - def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: - """ - Creates a low-shelf filter - - >>> ButterworthFilter.make_lowshelf(1000, 48000, 6) - - """ - w0 = tau * frequency / samplerate - _sin = sin(w0) - _cos = cos(w0) - alpha = _sin / (2 * q_factor) - big_a = 10 ** (gain_db / 40) - pmc = (big_a+1) - (big_a-1)*_cos - ppmc = (big_a+1) + (big_a-1)*_cos - mpc = (big_a-1) - (big_a+1)*_cos - pmpc = (big_a-1) + (big_a+1)*_cos - aa2 = 2*sqrt(big_a)*alpha - - b0 = big_a * (pmc + aa2) - b1 = 2 * big_a * mpc - b2 = big_a * (pmc - aa2) - a0 = ppmc + aa2 - a1 = -2 * pmpc - a2 = ppmc - aa2 - - filt = IIRFilter(2) - filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) - return filt - - @staticmethod - def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: - """ - Creates a high-shelf filter - - >>> ButterworthFilter.make_highshelf(1000, 48000, 6) - - """ - w0 = tau * frequency / samplerate - _sin = sin(w0) - _cos = cos(w0) - alpha = _sin / (2 * q_factor) - big_a = 10 ** (gain_db / 40) - pmc = (big_a+1) - (big_a-1)*_cos - ppmc = (big_a+1) + (big_a-1)*_cos - mpc = (big_a-1) - (big_a+1)*_cos - pmpc = (big_a-1) + (big_a+1)*_cos - aa2 = 2*sqrt(big_a)*alpha - - b0 = big_a * (ppmc + aa2) - b1 = -2 * big_a * pmpc - b2 = big_a * (ppmc - aa2) - a0 = pmc + aa2 - a1 = 2 * mpc - a2 = pmc - aa2 - - filt = IIRFilter(2) - filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) - return filt + +def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates an all-pass filter + + >>> make_allpass(1000, 48000) + + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + + b0 = 1 - alpha + b1 = -2 * _cos + b2 = 1 + alpha + + filt = IIRFilter(2) + filt.set_coefficients([b2, b1, b0], [b0, b1, b2]) + return filt + + +def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a peak filter + + >>> make_peak(1000, 48000, 6) + + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + big_a = 10 ** (gain_db / 40) + + b0 = 1 + alpha * big_a + b1 = -2 * _cos + b2 = 1 - alpha * big_a + a0 = 1 + alpha / big_a + a1 = -2 * _cos + a2 = 1 - alpha / big_a + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + +def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a low-shelf filter + + >>> make_lowshelf(1000, 48000, 6) + + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + big_a = 10 ** (gain_db / 40) + pmc = (big_a+1) - (big_a-1)*_cos + ppmc = (big_a+1) + (big_a-1)*_cos + mpc = (big_a-1) - (big_a+1)*_cos + pmpc = (big_a-1) + (big_a+1)*_cos + aa2 = 2*sqrt(big_a)*alpha + + b0 = big_a * (pmc + aa2) + b1 = 2 * big_a * mpc + b2 = big_a * (pmc - aa2) + a0 = ppmc + aa2 + a1 = -2 * pmpc + a2 = ppmc - aa2 + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt + + +def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: + """ + Creates a high-shelf filter + + >>> make_highshelf(1000, 48000, 6) + + """ + w0 = tau * frequency / samplerate + _sin = sin(w0) + _cos = cos(w0) + alpha = _sin / (2 * q_factor) + big_a = 10 ** (gain_db / 40) + pmc = (big_a+1) - (big_a-1)*_cos + ppmc = (big_a+1) + (big_a-1)*_cos + mpc = (big_a-1) - (big_a+1)*_cos + pmpc = (big_a-1) + (big_a+1)*_cos + aa2 = 2*sqrt(big_a)*alpha + + b0 = big_a * (ppmc + aa2) + b1 = -2 * big_a * pmpc + b2 = big_a * (ppmc - aa2) + a0 = pmc + aa2 + a1 = 2 * mpc + a2 = pmc - aa2 + + filt = IIRFilter(2) + filt.set_coefficients([a0, a1, a2], [b0, b1, b2]) + return filt diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 77f299b89b5c..70cda6d9e3da 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -11,9 +11,10 @@ def process(self, sample: float) -> float: Calculate y[n] >>> filt = FilterType() - >>> filt.process(0) + >>> filt.process(0.0) + 0.0 """ - pass + return 0.0 def show_frequency_response(filter: FilterType, samplerate: int) -> None: From 4fe509d193cf2406ea1948c9c9101144fa342e4c Mon Sep 17 00:00:00 2001 From: Martmists Date: Mon, 11 Oct 2021 12:11:57 +0200 Subject: [PATCH 07/18] run pre-commit Signed-off-by: Martmists --- audio_filters/butterworth_filter.py | 55 ++++++++++++++++++----------- audio_filters/iir_filter.py | 25 +++++++++---- audio_filters/show_response.py | 14 ++++---- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index 803beb6b81bf..566aef5530ac 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -1,8 +1,7 @@ -from math import sqrt, sin, tau, cos +from math import cos, sin, sqrt, tau from audio_filters.iir_filter import IIRFilter - """ Create 2nd-order IIR filters with Butterworth design. @@ -11,7 +10,9 @@ """ -def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: +def make_lowpass( + frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) +) -> IIRFilter: """ Creates a low-pass filter @@ -35,7 +36,9 @@ def make_lowpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) return filt -def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: +def make_highpass( + frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) +) -> IIRFilter: """ Creates a high-pass filter @@ -48,7 +51,7 @@ def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) alpha = _sin / (2 * q_factor) b0 = (1 + _cos) / 2 - b1 = - 1 - _cos + b1 = -1 - _cos a0 = 1 + alpha a1 = -2 * _cos @@ -59,7 +62,9 @@ def make_highpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) return filt -def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: +def make_bandpass( + frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) +) -> IIRFilter: """ Creates a band-pass filter @@ -73,7 +78,7 @@ def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) b0 = _sin / 2 b1 = 0 - b2 = - b0 + b2 = -b0 a0 = 1 + alpha a1 = -2 * _cos @@ -84,7 +89,9 @@ def make_bandpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) return filt -def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) -> IIRFilter: +def make_allpass( + frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2) +) -> IIRFilter: """ Creates an all-pass filter @@ -105,7 +112,9 @@ def make_allpass(frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)) return filt -def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: +def make_peak( + frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2) +) -> IIRFilter: """ Creates a peak filter @@ -130,7 +139,9 @@ def make_peak(frequency: int, samplerate: int, gain_db: float, q_factor: float = return filt -def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: +def make_lowshelf( + frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2) +) -> IIRFilter: """ Creates a low-shelf filter @@ -142,11 +153,11 @@ def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: flo _cos = cos(w0) alpha = _sin / (2 * q_factor) big_a = 10 ** (gain_db / 40) - pmc = (big_a+1) - (big_a-1)*_cos - ppmc = (big_a+1) + (big_a-1)*_cos - mpc = (big_a-1) - (big_a+1)*_cos - pmpc = (big_a-1) + (big_a+1)*_cos - aa2 = 2*sqrt(big_a)*alpha + pmc = (big_a + 1) - (big_a - 1) * _cos + ppmc = (big_a + 1) + (big_a - 1) * _cos + mpc = (big_a - 1) - (big_a + 1) * _cos + pmpc = (big_a - 1) + (big_a + 1) * _cos + aa2 = 2 * sqrt(big_a) * alpha b0 = big_a * (pmc + aa2) b1 = 2 * big_a * mpc @@ -160,7 +171,9 @@ def make_lowshelf(frequency: int, samplerate: int, gain_db: float, q_factor: flo return filt -def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)) -> IIRFilter: +def make_highshelf( + frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2) +) -> IIRFilter: """ Creates a high-shelf filter @@ -172,11 +185,11 @@ def make_highshelf(frequency: int, samplerate: int, gain_db: float, q_factor: fl _cos = cos(w0) alpha = _sin / (2 * q_factor) big_a = 10 ** (gain_db / 40) - pmc = (big_a+1) - (big_a-1)*_cos - ppmc = (big_a+1) + (big_a-1)*_cos - mpc = (big_a-1) - (big_a+1)*_cos - pmpc = (big_a-1) + (big_a+1)*_cos - aa2 = 2*sqrt(big_a)*alpha + pmc = (big_a + 1) - (big_a - 1) * _cos + ppmc = (big_a + 1) + (big_a - 1) * _cos + mpc = (big_a - 1) - (big_a + 1) * _cos + pmpc = (big_a - 1) + (big_a + 1) * _cos + aa2 = 2 * sqrt(big_a) * alpha b0 = big_a * (ppmc + aa2) b1 = -2 * big_a * pmpc diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index b17e7e40f4c3..922e0682dd98 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -2,14 +2,15 @@ class IIRFilter: - """ + r""" N-Order IIR filter Assumes working with float samples normalized on [-1, 1] --- Implementation details: - Based on the 2nd-order function from https://en.wikipedia.org/wiki/Digital_biquad_filter, + Based on the 2nd-order function from + https://en.wikipedia.org/wiki/Digital_biquad_filter, this generalized N-order function was made. Using the following transfer function @@ -17,6 +18,7 @@ class IIRFilter: we can rewrite this to y[n]={\frac{1}{a_{0}}}\left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)-\left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right) """ + def __init__(self, order: int) -> None: self.order = order @@ -37,7 +39,9 @@ def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]) -> None This method works well with scipy's filter design functions >>> # Make a 2nd-order 1000Hz butterworth lowpass filter - >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, btype='lowpass', fs=samplerate) + >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, + ... btype='lowpass', + ... fs=samplerate) >>> filt = IIRFilter(2) >>> filt.set_coefficients(a_coeffs, b_coeffs) """ @@ -45,10 +49,16 @@ def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]) -> None a_coeffs = [1.0] + a_coeffs if len(a_coeffs) != self.order + 1: - raise ValueError(f"Expected a_coeffs to have {self.order + 1} elements for {self.order}-order filter, got {len(a_coeffs)}") + raise ValueError( + f"Expected a_coeffs to have {self.order + 1} elements for {self.order}" + f"-order filter, got {len(a_coeffs)}" + ) if len(b_coeffs) != self.order + 1: - raise ValueError(f"Expected b_coeffs to have {self.order + 1} elements for {self.order}-order filter, got {len(a_coeffs)}") + raise ValueError( + f"Expected b_coeffs to have {self.order + 1} elements for {self.order}" + f"-order filter, got {len(a_coeffs)}" + ) self.a_coeffs = a_coeffs self.b_coeffs = b_coeffs @@ -64,9 +74,10 @@ def process(self, sample: float) -> float: result = 0.0 # Start at index 1 and do index 0 at the end. - for i in range(1, self.order+1): + for i in range(1, self.order + 1): result += ( - self.b_coeffs[i] * self.input_history[i-1] - self.a_coeffs[i] * self.output_history[i-1] + self.b_coeffs[i] * self.input_history[i - 1] + - self.a_coeffs[i] * self.output_history[i - 1] ) result = (result + self.b_coeffs[0] * sample) / self.a_coeffs[0] diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 70cda6d9e3da..d353297b5e84 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -38,13 +38,13 @@ def show_frequency_response(filter: FilterType, samplerate: int) -> None: fft_db = 20 * np.log10(fft_out) # Frequencies on log scale from 24 to nyquist frequency - plt.xlim(24, samplerate/2-1) + plt.xlim(24, samplerate / 2 - 1) plt.xlabel("Frequency (Hz)") - plt.xscale('log') + plt.xscale("log") # Display within reasonable bounds - lowest = min([-20, np.min(fft_db[1:samplerate//2-1])]) - highest = max([20, np.max(fft_db[1:samplerate//2-1])]) + lowest = min([-20, np.min(fft_db[1 : samplerate // 2 - 1])]) + highest = max([20, np.max(fft_db[1 : samplerate // 2 - 1])]) plt.ylim(max([-80, lowest]), min([80, highest])) plt.ylabel("Gain (dB)") @@ -74,9 +74,9 @@ def show_phase_response(filter: FilterType, samplerate: int) -> None: # Frequencies on log scale from 24 to nyquist frequency plt.xlim(24, samplerate / 2 - 1) plt.xlabel("Frequency (Hz)") - plt.xscale('log') + plt.xscale("log") - plt.ylim(-2*pi, 2*pi) + plt.ylim(-2 * pi, 2 * pi) plt.ylabel("Phase shift (Radians)") - plt.plot(np.unwrap(fft_out, -2*pi)) + plt.plot(np.unwrap(fft_out, -2 * pi)) plt.show() From 3f71475e8b816a54efb3254c63ccda89db02f2ad Mon Sep 17 00:00:00 2001 From: Martmists Date: Fri, 15 Oct 2021 02:05:43 +0200 Subject: [PATCH 08/18] Make mypy stop complaining about ints vs floats Signed-off-by: Martmists --- audio_filters/show_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index d353297b5e84..c11105fb0c78 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -28,7 +28,7 @@ def show_frequency_response(filter: FilterType, samplerate: int) -> None: size = 512 inputs = [1] + [0] * (size - 1) - outputs = [0] * size + outputs = [0.0] * size for i in range(size): outputs[i] = filter.process(inputs[i]) @@ -63,7 +63,7 @@ def show_phase_response(filter: FilterType, samplerate: int) -> None: size = 512 inputs = [1] + [0] * (size - 1) - outputs = [0] * size + outputs = [0.0] * size for i in range(size): outputs[i] = filter.process(inputs[i]) From 6a69c43d9f19fdaf8e28c712d9336529a589b817 Mon Sep 17 00:00:00 2001 From: Martmists Date: Fri, 15 Oct 2021 08:50:28 +0200 Subject: [PATCH 09/18] Use slower listcomp to make it more readable Signed-off-by: Martmists --- audio_filters/show_response.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index c11105fb0c78..ca7c2ff220fa 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -28,9 +28,7 @@ def show_frequency_response(filter: FilterType, samplerate: int) -> None: size = 512 inputs = [1] + [0] * (size - 1) - outputs = [0.0] * size - for i in range(size): - outputs[i] = filter.process(inputs[i]) + outputs = [filter.process(item) for item in inputs] filler = [0] * (samplerate - size) # zero-padding outputs += filler @@ -63,9 +61,7 @@ def show_phase_response(filter: FilterType, samplerate: int) -> None: size = 512 inputs = [1] + [0] * (size - 1) - outputs = [0.0] * size - for i in range(size): - outputs[i] = filter.process(inputs[i]) + outputs = [filter.process(item) for item in inputs] filler = [0] * (samplerate - size) # zero-padding outputs += filler From 72ab2839f228c9ba518a757ddb39e95894f3abf3 Mon Sep 17 00:00:00 2001 From: Martmists Date: Fri, 15 Oct 2021 08:54:18 +0200 Subject: [PATCH 10/18] Make doctests happy Signed-off-by: Martmists --- audio_filters/butterworth_filter.py | 18 ++++++------------ audio_filters/iir_filter.py | 3 ++- audio_filters/show_response.py | 4 +--- requirements.txt | 1 + 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index 566aef5530ac..723de061bf7a 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -16,8 +16,7 @@ def make_lowpass( """ Creates a low-pass filter - >>> make_lowpass(1000, 48000) - + >>> filter = make_lowpass(1000, 48000) """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -42,8 +41,7 @@ def make_highpass( """ Creates a high-pass filter - >>> make_highpass(1000, 48000) - + >>> filter = make_highpass(1000, 48000) """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -68,8 +66,7 @@ def make_bandpass( """ Creates a band-pass filter - >>> make_bandpass(1000, 48000) - + >>> filter = make_bandpass(1000, 48000) """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -95,8 +92,7 @@ def make_allpass( """ Creates an all-pass filter - >>> make_allpass(1000, 48000) - + >>> filter = make_allpass(1000, 48000) """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -118,8 +114,7 @@ def make_peak( """ Creates a peak filter - >>> make_peak(1000, 48000, 6) - + >>> filter = make_peak(1000, 48000, 6) """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -145,8 +140,7 @@ def make_lowshelf( """ Creates a low-shelf filter - >>> make_lowshelf(1000, 48000, 6) - + >>> filter = make_lowshelf(1000, 48000, 6) """ w0 = tau * frequency / samplerate _sin = sin(w0) diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index 922e0682dd98..348616cf4eb9 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -39,6 +39,7 @@ def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]) -> None This method works well with scipy's filter design functions >>> # Make a 2nd-order 1000Hz butterworth lowpass filter + >>> import scipy.signal >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, ... btype='lowpass', ... fs=samplerate) @@ -69,7 +70,7 @@ def process(self, sample: float) -> float: >>> filt = IIRFilter(2) >>> filt.process(0) - 0 + 0.0 """ result = 0.0 diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index ca7c2ff220fa..681840888f02 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -10,9 +10,7 @@ def process(self, sample: float) -> float: """ Calculate y[n] - >>> filt = FilterType() - >>> filt.process(0.0) - 0.0 + >>> None # We can't test Protocols """ return 0.0 diff --git a/requirements.txt b/requirements.txt index 4867de26f8f1..0b5ff882d378 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,4 @@ sympy tensorflow types-requests xgboost +scipy From fb839f55f398a626e54b6bab1efa2d62ec8c49f7 Mon Sep 17 00:00:00 2001 From: Martmists Date: Fri, 15 Oct 2021 13:12:47 +0200 Subject: [PATCH 11/18] Remove scipy Signed-off-by: Martmists --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0b5ff882d378..4867de26f8f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,3 @@ sympy tensorflow types-requests xgboost -scipy From 05e8e1b483786b5faf4f072b670f7e6bd43d0fd5 Mon Sep 17 00:00:00 2001 From: Martmists Date: Wed, 20 Oct 2021 16:26:33 +0200 Subject: [PATCH 12/18] Test coefficients from bw filters Signed-off-by: Martmists --- audio_filters/butterworth_filter.py | 17 +++++++++++++++-- audio_filters/iir_filter.py | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index 723de061bf7a..0f523de84403 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -17,6 +17,8 @@ def make_lowpass( Creates a low-pass filter >>> filter = make_lowpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.004277569313094809, 0.008555138626189618, 0.004277569313094809] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -42,6 +44,8 @@ def make_highpass( Creates a high-pass filter >>> filter = make_highpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9957224306869052, -1.9914448613738105, 0.9957224306869052] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -67,6 +71,8 @@ def make_bandpass( Creates a band-pass filter >>> filter = make_bandpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.06526309611002579, 0, -0.06526309611002579] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -93,6 +99,8 @@ def make_allpass( Creates an all-pass filter >>> filter = make_allpass(1000, 48000) + >>> filter.a_coeffs + filter.b_coeffs + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9077040443587427, -1.9828897227476208, 1.0922959556412573] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -115,6 +123,8 @@ def make_peak( Creates a peak filter >>> filter = make_peak(1000, 48000, 6) + >>> filter.a_coeffs + filter.b_coeffs + [1.0653405327119334, -1.9828897227476208, 0.9346594672880666, 1.1303715025601122, -1.9828897227476208, 0.8696284974398878] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -141,6 +151,8 @@ def make_lowshelf( Creates a low-shelf filter >>> filter = make_lowshelf(1000, 48000, 6) + >>> filter.a_coeffs + filter.b_coeffs + [3.0409336710888786, -5.608870992220748, 2.602157875636628, 3.139954022810743, -5.591841778072785, 2.5201667380627257] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -171,8 +183,9 @@ def make_highshelf( """ Creates a high-shelf filter - >>> make_highshelf(1000, 48000, 6) - + >>> filter = make_highshelf(1000, 48000, 6) + >>> filter.a_coeffs + filter.b_coeffs + [2.2229172136088806, -3.9587208137297303, 1.7841414181566304, 4.295432981120543, -7.922740859457287, 3.6756456963725253] """ w0 = tau * frequency / samplerate _sin = sin(w0) diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index 348616cf4eb9..3677ab4a3dfa 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -42,7 +42,7 @@ def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]) -> None >>> import scipy.signal >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, ... btype='lowpass', - ... fs=samplerate) + ... fs=48000) >>> filt = IIRFilter(2) >>> filt.set_coefficients(a_coeffs, b_coeffs) """ From 0fdd49e6f5950b6454c8098fcdb3b0c927891ac0 Mon Sep 17 00:00:00 2001 From: Martmists Date: Sat, 23 Oct 2021 18:15:52 +0200 Subject: [PATCH 13/18] Protocol test Co-authored-by: Christian Clauss --- audio_filters/show_response.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 681840888f02..8602fbedd806 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -10,7 +10,8 @@ def process(self, sample: float) -> float: """ Calculate y[n] - >>> None # We can't test Protocols + >>> issubclass(FilterType, Protocol) + True """ return 0.0 From dedc0d5a3f9a347142676d770dc8464057580cf3 Mon Sep 17 00:00:00 2001 From: Martmists Date: Sat, 23 Oct 2021 18:23:56 +0200 Subject: [PATCH 14/18] Make requested change Signed-off-by: Martmists --- audio_filters/show_response.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 8602fbedd806..fe58fdf9e074 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -16,6 +16,20 @@ def process(self, sample: float) -> float: return 0.0 +def get_bounds(fft_results: np.ndarray, samplerate: int): + """ + Get bounds for printing fft results + + >>> import numpy + >>> array = numpy.linspace(-20.0, 20.0, 1000) + >>> get_bounds(array, 1000) + (-20, 20) + """ + lowest = min([-20, np.min(fft_results[1:samplerate // 2 - 1])]) + highest = max([20, np.max(fft_results[1:samplerate // 2 - 1])]) + return lowest, highest + + def show_frequency_response(filter: FilterType, samplerate: int) -> None: """ Show frequency response of a filter @@ -40,9 +54,8 @@ def show_frequency_response(filter: FilterType, samplerate: int) -> None: plt.xscale("log") # Display within reasonable bounds - lowest = min([-20, np.min(fft_db[1 : samplerate // 2 - 1])]) - highest = max([20, np.max(fft_db[1 : samplerate // 2 - 1])]) - plt.ylim(max([-80, lowest]), min([80, highest])) + bounds = get_bounds(fft_db, samplerate) + plt.ylim(max([-80, bounds[0]]), min([80, bounds[1]])) plt.ylabel("Gain (dB)") plt.plot(fft_db) From ef2b9524451b2afda2340d36c9fd3836408e465d Mon Sep 17 00:00:00 2001 From: Martmists Date: Sat, 23 Oct 2021 18:25:16 +0200 Subject: [PATCH 15/18] Types Signed-off-by: Martmists --- audio_filters/show_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index fe58fdf9e074..d645dcb22385 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -1,5 +1,5 @@ from math import pi -from typing import Protocol +from typing import Protocol, Tuple, Union import matplotlib.pyplot as plt import numpy as np @@ -16,7 +16,7 @@ def process(self, sample: float) -> float: return 0.0 -def get_bounds(fft_results: np.ndarray, samplerate: int): +def get_bounds(fft_results: np.ndarray, samplerate: int) -> Tuple[Union[int, float], Union[int, float]]: """ Get bounds for printing fft results From 9848caf9cc01a3c7efa6426d03329022dc01fb9b Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 23 Oct 2021 19:29:15 +0200 Subject: [PATCH 16/18] Apply suggestions from code review --- audio_filters/iir_filter.py | 4 ++-- audio_filters/show_response.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index 3677ab4a3dfa..e9d9590b3a03 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -1,4 +1,4 @@ -from typing import List +from __future__ import annotations class IIRFilter: @@ -32,7 +32,7 @@ def __init__(self, order: int) -> None: # y[n-1] ... y[n-k] self.output_history = [0.0] * self.order - def set_coefficients(self, a_coeffs: List[float], b_coeffs: List[float]) -> None: + def set_coefficients(self, a_coeffs: List[float], b_coeffs: list[float]) -> None: """ Set the coefficients for the IIR filter. These should both be of size order + 1. a_0 may be left out, and it will use 1.0 as default value. diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index d645dcb22385..90a05a174607 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from math import pi -from typing import Protocol, Tuple, Union +from typing import Protocol import matplotlib.pyplot as plt import numpy as np @@ -16,7 +18,7 @@ def process(self, sample: float) -> float: return 0.0 -def get_bounds(fft_results: np.ndarray, samplerate: int) -> Tuple[Union[int, float], Union[int, float]]: +def get_bounds(fft_results: np.ndarray, samplerate: int) -> tuple[int | float, int | float]: """ Get bounds for printing fft results @@ -25,8 +27,8 @@ def get_bounds(fft_results: np.ndarray, samplerate: int) -> Tuple[Union[int, flo >>> get_bounds(array, 1000) (-20, 20) """ - lowest = min([-20, np.min(fft_results[1:samplerate // 2 - 1])]) - highest = max([20, np.max(fft_results[1:samplerate // 2 - 1])]) + lowest = min([-20, np.min(fft_results[1 : samplerate // 2 - 1])]) + highest = max([20, np.max(fft_results[1 : samplerate // 2 - 1])]) return lowest, highest From 2d9ccc4d04b64c66a33df6f4b04b716345d6a47c Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 23 Oct 2021 19:45:31 +0200 Subject: [PATCH 17/18] Apply suggestions from code review --- audio_filters/iir_filter.py | 2 +- audio_filters/show_response.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/audio_filters/iir_filter.py b/audio_filters/iir_filter.py index e9d9590b3a03..aae320365012 100644 --- a/audio_filters/iir_filter.py +++ b/audio_filters/iir_filter.py @@ -32,7 +32,7 @@ def __init__(self, order: int) -> None: # y[n-1] ... y[n-k] self.output_history = [0.0] * self.order - def set_coefficients(self, a_coeffs: List[float], b_coeffs: list[float]) -> None: + def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None: """ Set the coefficients for the IIR filter. These should both be of size order + 1. a_0 may be left out, and it will use 1.0 as default value. diff --git a/audio_filters/show_response.py b/audio_filters/show_response.py index 90a05a174607..6e2731a58419 100644 --- a/audio_filters/show_response.py +++ b/audio_filters/show_response.py @@ -18,7 +18,9 @@ def process(self, sample: float) -> float: return 0.0 -def get_bounds(fft_results: np.ndarray, samplerate: int) -> tuple[int | float, int | float]: +def get_bounds( + fft_results: np.ndarray, samplerate: int +) -> tuple[int | float, int | float]: """ Get bounds for printing fft results From 6cb4e2c0aef964bd53ddda753c6fc2bef2cc0ed0 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 23 Oct 2021 19:57:53 +0200 Subject: [PATCH 18/18] Update butterworth_filter.py --- audio_filters/butterworth_filter.py | 35 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/audio_filters/butterworth_filter.py b/audio_filters/butterworth_filter.py index 0f523de84403..409cfeb1d95c 100644 --- a/audio_filters/butterworth_filter.py +++ b/audio_filters/butterworth_filter.py @@ -17,8 +17,9 @@ def make_lowpass( Creates a low-pass filter >>> filter = make_lowpass(1000, 48000) - >>> filter.a_coeffs + filter.b_coeffs - [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.004277569313094809, 0.008555138626189618, 0.004277569313094809] + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.004277569313094809, + 0.008555138626189618, 0.004277569313094809] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -44,8 +45,9 @@ def make_highpass( Creates a high-pass filter >>> filter = make_highpass(1000, 48000) - >>> filter.a_coeffs + filter.b_coeffs - [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9957224306869052, -1.9914448613738105, 0.9957224306869052] + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9957224306869052, + -1.9914448613738105, 0.9957224306869052] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -71,8 +73,9 @@ def make_bandpass( Creates a band-pass filter >>> filter = make_bandpass(1000, 48000) - >>> filter.a_coeffs + filter.b_coeffs - [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.06526309611002579, 0, -0.06526309611002579] + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.06526309611002579, + 0, -0.06526309611002579] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -99,8 +102,9 @@ def make_allpass( Creates an all-pass filter >>> filter = make_allpass(1000, 48000) - >>> filter.a_coeffs + filter.b_coeffs - [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9077040443587427, -1.9828897227476208, 1.0922959556412573] + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9077040443587427, + -1.9828897227476208, 1.0922959556412573] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -123,8 +127,9 @@ def make_peak( Creates a peak filter >>> filter = make_peak(1000, 48000, 6) - >>> filter.a_coeffs + filter.b_coeffs - [1.0653405327119334, -1.9828897227476208, 0.9346594672880666, 1.1303715025601122, -1.9828897227476208, 0.8696284974398878] + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [1.0653405327119334, -1.9828897227476208, 0.9346594672880666, 1.1303715025601122, + -1.9828897227476208, 0.8696284974398878] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -151,8 +156,9 @@ def make_lowshelf( Creates a low-shelf filter >>> filter = make_lowshelf(1000, 48000, 6) - >>> filter.a_coeffs + filter.b_coeffs - [3.0409336710888786, -5.608870992220748, 2.602157875636628, 3.139954022810743, -5.591841778072785, 2.5201667380627257] + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [3.0409336710888786, -5.608870992220748, 2.602157875636628, 3.139954022810743, + -5.591841778072785, 2.5201667380627257] """ w0 = tau * frequency / samplerate _sin = sin(w0) @@ -184,8 +190,9 @@ def make_highshelf( Creates a high-shelf filter >>> filter = make_highshelf(1000, 48000, 6) - >>> filter.a_coeffs + filter.b_coeffs - [2.2229172136088806, -3.9587208137297303, 1.7841414181566304, 4.295432981120543, -7.922740859457287, 3.6756456963725253] + >>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE + [2.2229172136088806, -3.9587208137297303, 1.7841414181566304, 4.295432981120543, + -7.922740859457287, 3.6756456963725253] """ w0 = tau * frequency / samplerate _sin = sin(w0)