|
2 | 2 |
|
3 | 3 |
|
4 | 4 | class IIRFilter:
|
5 |
| - r""" |
6 |
| - N-Order IIR filter |
7 |
| - Assumes working with float samples normalized on [-1, 1] |
| 5 | + """ |
| 6 | + Represents an N-order Infinite Impulse Response (IIR) filter. |
| 7 | +
|
| 8 | + This class implements a digital filter that operates on floating-point samples normalized |
| 9 | + to the range [-1, 1]. |
8 | 10 |
|
9 |
| - --- |
| 11 | + Attributes: |
| 12 | + order (int): The order of the filter. |
| 13 | + a_coeffs (list[float]): The coefficients of the denominator polynomial (feedback coefficients). |
| 14 | + b_coeffs (list[float]): The coefficients of the numerator polynomial (feedforward coefficients). |
| 15 | + input_history (list[float]): History of input samples for processing. |
| 16 | + output_history (list[float]): History of output samples for processing. |
10 | 17 |
|
11 |
| - Implementation details: |
12 |
| - Based on the 2nd-order function from |
13 |
| - https://en.wikipedia.org/wiki/Digital_biquad_filter, |
14 |
| - this generalized N-order function was made. |
| 18 | + Note: |
| 19 | + This class assumes that the filter operates in real-time and processes one sample at a time. |
15 | 20 |
|
16 |
| - Using the following transfer function |
17 |
| - 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}} |
18 |
| - we can rewrite this to |
19 |
| - 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) |
| 21 | + Example: |
| 22 | + >>> filt = IIRFilter(2) |
| 23 | + >>> filt.set_coefficients([1.0, -1.0, 0.5], [1.0, 0.0, -0.5]) |
| 24 | + >>> output = filt.process(0.5) |
20 | 25 | """
|
21 | 26 |
|
22 | 27 | def __init__(self, order: int) -> None:
|
23 |
| - self.order = order |
| 28 | + """ |
| 29 | + Initializes the IIRFilter with the specified order. |
| 30 | +
|
| 31 | + Args: |
| 32 | + order (int): The order of the filter. |
24 | 33 |
|
25 |
| - # a_{0} ... a_{k} |
| 34 | + Note: |
| 35 | + The initial coefficients are set to unity (1.0) for both the numerator and denominator. |
| 36 | + """ |
| 37 | + self.order = order |
26 | 38 | self.a_coeffs = [1.0] + [0.0] * order
|
27 |
| - # b_{0} ... b_{k} |
28 | 39 | self.b_coeffs = [1.0] + [0.0] * order
|
29 |
| - |
30 |
| - # x[n-1] ... x[n-k] |
31 |
| - self.input_history = [0.0] * self.order |
32 |
| - # y[n-1] ... y[n-k] |
33 |
| - self.output_history = [0.0] * self.order |
| 40 | + self.input_history = [0.0] * order |
| 41 | + self.output_history = [0.0] * order |
34 | 42 |
|
35 | 43 | def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None:
|
36 | 44 | """
|
37 |
| - Set the coefficients for the IIR filter. These should both be of size order + 1. |
38 |
| - a_0 may be left out, and it will use 1.0 as default value. |
39 |
| -
|
40 |
| - This method works well with scipy's filter design functions |
41 |
| - >>> # Make a 2nd-order 1000Hz butterworth lowpass filter |
42 |
| - >>> import scipy.signal |
43 |
| - >>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, |
44 |
| - ... btype='lowpass', |
45 |
| - ... fs=48000) |
46 |
| - >>> filt = IIRFilter(2) |
47 |
| - >>> filt.set_coefficients(a_coeffs, b_coeffs) |
48 |
| - """ |
49 |
| - if len(a_coeffs) < self.order: |
50 |
| - a_coeffs = [1.0, *a_coeffs] |
| 45 | + Sets the coefficients for the IIR filter. |
51 | 46 |
|
52 |
| - if len(a_coeffs) != self.order + 1: |
53 |
| - msg = ( |
54 |
| - f"Expected a_coeffs to have {self.order + 1} elements " |
55 |
| - f"for {self.order}-order filter, got {len(a_coeffs)}" |
56 |
| - ) |
57 |
| - raise ValueError(msg) |
| 47 | + Args: |
| 48 | + a_coeffs (list[float]): The coefficients of the denominator polynomial (feedback coefficients). |
| 49 | + b_coeffs (list[float]): The coefficients of the numerator polynomial (feedforward coefficients). |
58 | 50 |
|
| 51 | + Raises: |
| 52 | + ValueError: If the length of `a_coeffs` or `b_coeffs` does not match the order of the filter. |
| 53 | + """ |
| 54 | + if len(a_coeffs) != self.order + 1: |
| 55 | + raise ValueError(f"Expected {self.order + 1} coefficients for `a_coeffs`, got {len(a_coeffs)}") |
59 | 56 | if len(b_coeffs) != self.order + 1:
|
60 |
| - msg = ( |
61 |
| - f"Expected b_coeffs to have {self.order + 1} elements " |
62 |
| - f"for {self.order}-order filter, got {len(a_coeffs)}" |
63 |
| - ) |
64 |
| - raise ValueError(msg) |
| 57 | + raise ValueError(f"Expected {self.order + 1} coefficients for `b_coeffs`, got {len(b_coeffs)}") |
65 | 58 |
|
66 | 59 | self.a_coeffs = a_coeffs
|
67 | 60 | self.b_coeffs = b_coeffs
|
68 | 61 |
|
69 | 62 | def process(self, sample: float) -> float:
|
70 | 63 | """
|
71 |
| - Calculate y[n] |
| 64 | + Processes a single input sample through the IIR filter. |
72 | 65 |
|
73 |
| - >>> filt = IIRFilter(2) |
74 |
| - >>> filt.process(0) |
75 |
| - 0.0 |
| 66 | + Args: |
| 67 | + sample (float): The input sample to be filtered. |
| 68 | +
|
| 69 | + Returns: |
| 70 | + float: The filtered output sample. |
| 71 | +
|
| 72 | + Example: |
| 73 | + >>> filt = IIRFilter(2) |
| 74 | + >>> filt.set_coefficients([1.0, -1.0, 0.5], [1.0, 0.0, -0.5]) |
| 75 | + >>> output = filt.process(0.5) |
76 | 76 | """
|
77 | 77 | result = 0.0
|
78 |
| - |
79 |
| - # Start at index 1 and do index 0 at the end. |
80 | 78 | for i in range(1, self.order + 1):
|
81 | 79 | result += (
|
82 | 80 | self.b_coeffs[i] * self.input_history[i - 1]
|
|
0 commit comments