Skip to content

Simple audio filters #5230

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

Merged
merged 18 commits into from
Oct 23, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
Empty file added audio_filters/__init__.py
Empty file.
191 changes: 191 additions & 0 deletions audio_filters/butterworth_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
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:
"""
Creates a low-pass filter

>>> ButterworthFilter.make_lowpass(1000, 48000)
<audio_filters.iir_filter.IIRFilter object at 0x7f3dbb93d040>
"""
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)
<audio_filters.iir_filter.IIRFilter object at 0x7f3dbb93d040>
"""
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)
<audio_filters.iir_filter.IIRFilter object at 0x7f3dbb93d040>
"""
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)
<audio_filters.iir_filter.IIRFilter object at 0x7f3dbb93d040>
"""
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)
<audio_filters.iir_filter.IIRFilter object at 0x7f3dbb93d040>
"""
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)
<audio_filters.iir_filter.IIRFilter object at 0x7f3dbb93d040>
"""
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)
<audio_filters.iir_filter.IIRFilter object at 0x7f3dbb93d040>
"""
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
80 changes: 80 additions & 0 deletions audio_filters/iir_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import List


class IIRFilter:
"""
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,
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
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

# 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]) -> 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
>>> 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) -> float:
"""
Calculate y[n]

>>> filt = IIRFilter(2)
>>> filt.process(0)
0
"""
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
67 changes: 67 additions & 0 deletions audio_filters/show_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from math import pi
from typing import Protocol

import matplotlib.pyplot as plt
import numpy as np


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
"""

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: FilterType, samplerate: int) -> None:
"""
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()