Skip to content

Implement IIRFilter class with detailed documentation and methods for… #11407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 55 additions & 49 deletions audio_filters/iir_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,87 @@


class IIRFilter:
r"""
N-Order IIR filter
Assumes working with float samples normalized on [-1, 1]
"""
Represents an N-order Infinite Impulse Response (IIR) filter.

This class implements a digital filter that operates on floating-point samples normalized

Check failure on line 8 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:8:89: E501 Line too long (93 > 88)
to the range [-1, 1].

---
Attributes:
order (int): The order of the filter.
a_coeffs (list[float]): The coefficients of the denominator polynomial (feedback coefficients).

Check failure on line 13 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:13:89: E501 Line too long (103 > 88)
b_coeffs (list[float]): The coefficients of the numerator polynomial (feedforward coefficients).

Check failure on line 14 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:14:89: E501 Line too long (104 > 88)
input_history (list[float]): History of input samples for processing.
output_history (list[float]): History of output samples for processing.

Implementation details:
Based on the 2nd-order function from
https://en.wikipedia.org/wiki/Digital_biquad_filter,
this generalized N-order function was made.
Note:
This class assumes that the filter operates in real-time and processes one sample at a time.

Check failure on line 19 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:19:89: E501 Line too long (100 > 88)

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)
Example:
>>> filt = IIRFilter(2)
>>> filt.set_coefficients([1.0, -1.0, 0.5], [1.0, 0.0, -0.5])
>>> output = filt.process(0.5)
"""

def __init__(self, order: int) -> None:
self.order = order
"""
Initializes the IIRFilter with the specified order.

# a_{0} ... a_{k}
Args:
order (int): The order of the filter.

Note:
The initial coefficients are set to unity (1.0) for both the numerator and denominator.

Check failure on line 35 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:35:89: E501 Line too long (99 > 88)
"""
self.order = order
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
self.input_history = [0.0] * order
self.output_history = [0.0] * order

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.

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=48000)
>>> filt = IIRFilter(2)
>>> filt.set_coefficients(a_coeffs, b_coeffs)
"""
if len(a_coeffs) < self.order:
a_coeffs = [1.0, *a_coeffs]
Sets the coefficients for the IIR filter.

Args:
a_coeffs (list[float]): The coefficients of the denominator polynomial (feedback coefficients).

Check failure on line 48 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:48:89: E501 Line too long (107 > 88)
b_coeffs (list[float]): The coefficients of the numerator polynomial (feedforward coefficients).

Check failure on line 49 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:49:89: E501 Line too long (108 > 88)

Raises:
ValueError: If the length of `a_coeffs` or `b_coeffs` does not match the order of the filter.

Check failure on line 52 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

audio_filters/iir_filter.py:52:89: E501 Line too long (105 > 88)
"""
if len(a_coeffs) != self.order + 1:
msg = (
f"Expected a_coeffs to have {self.order + 1} elements "
f"for {self.order}-order filter, got {len(a_coeffs)}"
raise ValueError(
"Expected {} coefficients for `a_coeffs`, got {}".format(
self.order + 1, len(a_coeffs)
)

Check failure on line 58 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP032)

audio_filters/iir_filter.py:56:17: UP032 Use f-string instead of `format` call

Check failure on line 58 in audio_filters/iir_filter.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (EM103)

audio_filters/iir_filter.py:56:17: EM103 Exception must not use a `.format()` string directly, assign to variable first
)
raise ValueError(msg)

if len(b_coeffs) != self.order + 1:
msg = (
f"Expected b_coeffs to have {self.order + 1} elements "
f"for {self.order}-order filter, got {len(a_coeffs)}"
raise ValueError(
"Expected {} coefficients for `b_coeffs`, got {}".format(
self.order + 1, len(b_coeffs)
)
)
raise ValueError(msg)

self.a_coeffs = a_coeffs
self.b_coeffs = b_coeffs

def process(self, sample: float) -> float:
"""
Calculate y[n]
Processes a single input sample through the IIR filter.

>>> filt = IIRFilter(2)
>>> filt.process(0)
0.0
Args:
sample (float): The input sample to be filtered.

Returns:
float: The filtered output sample.

Example:
>>> filt = IIRFilter(2)
>>> filt.set_coefficients([1.0, -1.0, 0.5], [1.0, 0.0, -0.5])
>>> output = filt.process(0.5)
"""
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]
Expand Down
Loading