Files
3d_audio/lib_audio_dsp/python/audio_dsp/dsp/biquad.py
Steven Dan d8b2974133 init
2025-12-11 09:43:42 +08:00

1300 lines
39 KiB
Python

# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""The biquad DSP block."""
import warnings
from copy import deepcopy
import numpy as np
import numpy.typing as npt
import scipy.signal as spsig
import matplotlib.pyplot as plt
from docstring_inheritance import inherit_numpy_docstring
from audio_dsp.dsp import utils as utils
from audio_dsp.dsp import generic as dspg
class biquad(dspg.dsp_block):
"""
A second order biquadratic filter instance.
This implements a direct form 1 biquad filter, using the
coefficients provided at initialisation:
`a0*y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]`
For efficiency the biquad coefficients are normalised by a0 and the
output `a` coefficients multiplied by -1.
When the coefficients are updated, the biquad states are reset. This
helps avoid large errors, but can make this implementation unsuitable
for real time control. For real time control, :py:class:`biquad_slew` may be a
better choice.
Parameters
----------
coeffs : list[float]
List of normalised biquad coefficients in the form in the form
`[b0, b1, b2, -a1, -a2]/a0`
Attributes
----------
coeffs : list[float]
List of normalised float biquad coefficients in the form in the
form `[b0, b1, b2, -a1, -a2]/a0`, rounded to int32 precision.
int_coeffs : list[int]
List of normalised int biquad coefficients in the form in the
form `[b0, b1, b2, -a1, -a2]/a0`, scaled and rounded to int32.
b_shift : int
The number of right shift bits applied to the b coefficients.
The default coefficient scaling allows for a maximum coefficient
value of 2, but high gain shelf and peaking filters can have
coefficients above this value. Shifting the b coefficients down
allows coefficients greater than 2, with the cost of b_shift
bits of precision.
"""
def __init__(
self,
coeffs: list[float],
fs: int,
n_chans: int = 1,
Q_sig: int = dspg.Q_SIG,
):
super().__init__(fs, n_chans, Q_sig)
self.update_coeffs(coeffs)
def update_coeffs(self, new_coeffs: list[float]):
"""Update the saved coefficients to the input values.
Parameters
----------
new_coeffs : list[float]
The new coefficients to be updated.
"""
self.b_shift = _get_bshift(new_coeffs)
self.coeffs, self.int_coeffs = _round_and_check(new_coeffs, self.b_shift)
self._check_gain()
# reset states to avoid clicks
self.reset_state()
def process(self, sample: float, channel: int = 0) -> float:
"""
Filter a single sample using direct form 1 biquad using floating
point maths.
"""
y = (
self.coeffs[0] * sample
+ self.coeffs[1] * self._x1[channel]
+ self.coeffs[2] * self._x2[channel]
+ self.coeffs[3] * self._y1[channel]
+ self.coeffs[4] * self._y2[channel]
)
y = utils.saturate_float(y, self.Q_sig)
self._x2[channel] = self._x1[channel]
self._x1[channel] = sample
self._y2[channel] = self._y1[channel]
self._y1[channel] = y
y = y * (1 << self.b_shift)
y = utils.saturate_float(y, self.Q_sig)
return y
def process_int(self, sample: float, channel: int = 0) -> float:
"""
Filter a single sample using direct form 1 biquad using int32
fixed point maths.
The float input sample is quantized to int32, and returned to
float before outputting
"""
sample_int = utils.float_to_fixed(sample, self.Q_sig)
# process a single sample using direct form 1
y = utils.int64(
sample_int * self.int_coeffs[0]
+ self._x1[channel] * self.int_coeffs[1]
+ self._x2[channel] * self.int_coeffs[2]
+ self._y1[channel] * self.int_coeffs[3]
+ self._y2[channel] * self.int_coeffs[4]
)
# the b_shift can be combined with the >> 30, which reduces
# quantization noise, but this results in saturation at an
# earlier point, and so is not used here for consistency
y = utils.int64(y + (1 << 29))
y = utils.int32_mult_sat_extract(y, 1, 30)
# save states
self._x2[channel] = utils.int32(self._x1[channel])
self._x1[channel] = utils.int32(sample_int)
self._y2[channel] = utils.int32(self._y1[channel])
self._y1[channel] = utils.int32(y)
# compensate for coefficients
y = utils.int64(y << self.b_shift)
y = utils.saturate_int32(y)
y_flt = utils.fixed_to_float(y, self.Q_sig)
return y_flt
def process_xcore(self, sample: float, channel: int = 0) -> float:
"""
Filter a single sample using direct form 1 biquad using int32
fixed point maths, with use of the XS3 VPU.
The float input sample is quantized to int32, and returned to
float before outputting.
"""
if isinstance(sample, float):
sample_int = utils.float_to_fixed(sample, self.Q_sig)
elif isinstance(sample, int):
sample_int = sample
else:
raise TypeError("input must be float or int")
# process a single sample using direct form 1. In the VPU the
# ``>> 30`` comes before accumulation
y = utils.vlmaccr(
[
sample_int,
self._x1[channel],
self._x2[channel],
self._y1[channel],
self._y2[channel],
],
self.int_coeffs,
)
y = utils.saturate_int32(y)
# save states
self._x2[channel] = utils.int32(self._x1[channel])
self._x1[channel] = utils.int32(sample_int)
self._y2[channel] = utils.int32(self._y1[channel])
self._y1[channel] = utils.int32(y)
# compensate for coefficients
y = utils.int64(y << self.b_shift)
y = utils.saturate_int32(y)
if isinstance(sample, float):
return utils.fixed_to_float(y, self.Q_sig)
else:
return y
def process_frame_int(self, frame: list[np.ndarray]) -> list[np.ndarray]:
"""
Take a list frames of samples and return the processed frames,
using a bit exact int implementation.
A frame is defined as a list of 1-D numpy arrays, where the
number of arrays is equal to the number of channels, and the
length of the arrays is equal to the frame size.
Parameters
----------
frame : list
List of frames, where each frame is a 1-D numpy array.
Returns
-------
list
List of processed frames, with the same structure as the input frame.
"""
n_outputs = len(frame)
frame_size = frame[0].shape[0]
output = deepcopy(frame)
for chan in range(n_outputs):
this_chan = output[chan]
for sample in range(frame_size):
this_chan[sample] = self.process_int(this_chan[sample], channel=chan)
return output
def freq_response(
self, nfft: int = 1024
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""
Calculate the frequency response of the biquad filter.
The biquad filter coefficients are scaled and returned to
numerator and denominator coefficients, before being passed to
`scipy.signal.freqz` to calculate the frequency response.
Parameters
----------
nfft : int
The number of points to compute in the frequency response,
by default 1024.
Returns
-------
tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]
A tuple containing the frequency vector and the complex
frequency response.
"""
b = [self.coeffs[0], self.coeffs[1], self.coeffs[2]]
b = _apply_biquad_bshift(b, -self.b_shift)
a = [1, -self.coeffs[3], -self.coeffs[4]]
f, h = spsig.freqz(b, a, worN=nfft, fs=self.fs) # type: ignore it thinks a is supposed to be an int?
return f, h
def _check_gain(self):
"""
Check if the gain of the biquad filter is greater than the
available headroom. If so, display a warning.
"""
_, h = self.freq_response()
max_gain = np.max(utils.db(h))
if max_gain > dspg.HEADROOM_DB:
warnings.warn(
"biquad gain (%.1f dB) is > headroom" % (max_gain)
+ " (%.0f dB), overflow may occur" % dspg.HEADROOM_DB
+ " unless signal level has previously been reduced"
)
def reset_state(self):
"""Reset the biquad saved states to zero."""
self._x1 = [0.0] * self.n_chans
self._x2 = [0.0] * self.n_chans
self._y1 = [0.0] * self.n_chans
self._y2 = [0.0] * self.n_chans
class biquad_slew(biquad):
"""
A second order biquadratic filter instance that slews between
coefficient updates.
This implements a direct form 1 biquad filter, using the
coefficients provided at initialisation:
`a0*y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]`
For efficiency the biquad coefficients are normalised by a0 and the
output `a` coefficients multiplied by -1.
When the target coefficients are updated, the applied coefficients
are slewed towards the new target values. This makes this implementation
suitable for real time control. A table of the first 10 slew shifts is shown below:
+------------+--------------------+
| slew_shift | Time constant (ms) |
+============+====================+
| 1 | 0.03 |
+------------+--------------------+
| 2 | 0.07 |
+------------+--------------------+
| 3 | 0.16 |
+------------+--------------------+
| 4 | 0.32 |
+------------+--------------------+
| 5 | 0.66 |
+------------+--------------------+
| 6 | 1.32 |
+------------+--------------------+
| 7 | 2.66 |
+------------+--------------------+
| 8 | 5.32 |
+------------+--------------------+
| 9 | 10.66 |
+------------+--------------------+
| 10 | 21.32 |
+------------+--------------------+
Parameters
----------
slew_shift : int
The shift value used in the exponential slew.
Attributes
----------
target_coeffs : list[float]
List of normalised float target biquad coefficients in the form in the
form `[b0, b1, b2, -a1, -a2]/a0`, rounded to int32 precision. The coeffs
are slewed towards these values.
target_coeffs_int : list[int]
List of normalised int target biquad coefficients in the form in the
form `[b0, b1, b2, -a1, -a2]/a0`, scaled and rounded to int32. The int_coeffs
are slewed towards these values.
"""
def __init__(
self,
coeffs: list[float],
fs: int,
n_chans: int = 1,
slew_shift: int = 6,
Q_sig: int = dspg.Q_SIG,
):
dspg.dsp_block.__init__(self, fs, n_chans, Q_sig)
# call the superclass during init only
biquad.update_coeffs(self, coeffs)
# set target equal to initial
self.target_coeffs = deepcopy(self.coeffs)
self.target_coeffs_int = deepcopy(self.int_coeffs)
self.slew_shift = slew_shift
self.remaining_shifts = 0
def update_coeffs(self, new_coeffs: list[float]):
"""Update the saved coefficients to the input values.
Parameters
----------
new_coeffs : list[float]
The new coefficients to be updated.
"""
old_b_shift = self.b_shift
self.b_shift = _get_bshift(new_coeffs)
self.target_coeffs, self.target_coeffs_int = _round_and_check(new_coeffs, self.b_shift)
b_shift_change = old_b_shift - self.b_shift
if b_shift_change > 0:
# we can't shift safely until we know we have headroom
self.remaining_shifts = b_shift_change
self.b_shift += self.remaining_shifts
if b_shift_change < 0:
b_shift_change = -b_shift_change
self.coeffs[:3] = [x * 2**-b_shift_change for x in self.coeffs[:3]]
self.int_coeffs[:3] = [x >> b_shift_change for x in self.int_coeffs[:3]]
for chan in range(self.n_chans):
if type(self._y1[chan]) is int:
self._y1[chan] = self._y1[chan] >> b_shift_change
self._y2[chan] = self._y2[chan] >> b_shift_change
else:
self._y1[chan] = self._y1[chan] * 2**-b_shift_change
self._y2[chan] = self._y2[chan] * 2**-b_shift_change
@property
def slew_shift(self):
"""The shift value used in the exponential slew."""
return self._slew_shift
@slew_shift.setter
def slew_shift(self, value):
self._slew_shift = value if value > 1 else 1
def process(self, sample: float, channel: int = 0) -> float:
"""
``process`` is not implemented for the slewing biquad, as the
coefficient slew is shared across the channels.
"""
raise NotImplementedError
def process_int(self, sample: float, channel: int = 0) -> float:
"""
``process_int`` is not implemented for the slewing biquad, as the
coefficient slew is shared across the channels.
Parameters
----------
sample : float
The input sample to be processed.
channel : int, optional
The channel index to process the sample on. Default is 0.
"""
raise NotImplementedError
def process_xcore(self, sample: float, channel: int = 0) -> float:
"""
``process_xcore`` is not implemented for the slewing biquad, as the
coefficient slew is shared across the channels.
"""
raise NotImplementedError
def process_channels(self, sample_list: list[float]) -> list[float]:
"""
Slew the biquad coefficients towards the target, then filter the
samples in each channel using floating point maths.
Each sample is filtered using direct form 1 biquad using
floating point maths.
"""
if self.remaining_shifts > 0:
tmp_target = deepcopy(self.target_coeffs)
tmp_target[:3] = [x * (2 ** (-self.remaining_shifts)) for x in tmp_target[:3]]
for n in range(5):
self.coeffs[n] += (tmp_target[n] - self.coeffs[n]) * 2**-self.slew_shift
if (
abs(self.coeffs[0]) < 1
and abs(self.coeffs[1]) < 1
and abs(self.coeffs[2]) < 1
and all(abs(x) < 1 for x in self._y1)
and all(abs(x) < 1 for x in self._y2)
):
# we now have the headroom to shift
self.coeffs[:3] = [x * 2 for x in self.coeffs[:3]]
self._y1 = self._y1 * 2
self._y2 = self._y2 * 2
self.remaining_shifts -= 1
self.b_shift -= 1
else:
for n in range(5):
self.coeffs[n] += (self.target_coeffs[n] - self.coeffs[n]) * 2**-self.slew_shift
out_samples = deepcopy(sample_list)
for channel in range(len(sample_list)):
# use basic biquad
out_samples[channel] = super().process(sample_list[channel], channel)
return out_samples
def process_channels_xcore(self, sample_list: list[float]) -> list[float]:
"""
Slew the biquad coefficients towards the target, then filter the
samples in each channel using fixed point maths.
Each sample is filtered using direct form 1 biquad using int32
fixed point maths, with use of the XS3 VPU.
The float input sample is quantized to int32, and returned to
float before outputting.
"""
if self.remaining_shifts > 0:
# change in b_shift to manage, target_coeffs have less headroom, so add the headroom back
tmp_target = deepcopy(self.target_coeffs_int)
tmp_target[:3] = [utils.int32(x >> self.remaining_shifts) for x in tmp_target[:3]]
# do the slew
for n in range(5):
self.int_coeffs[n] += (
utils.saturate_int32_vpu(tmp_target[n] - self.int_coeffs[n]) >> self.slew_shift
)
# see if we have headroom to do the shift
if (
abs(self.int_coeffs[0]) < (2**30)
and abs(self.int_coeffs[1]) < (2**30)
and abs(self.int_coeffs[2]) < (2**30)
and all(abs(x) < (2**30) for x in self._y1)
and all(abs(x) < (2**30) for x in self._y2)
):
# we now have the headroom to shift
self.int_coeffs[:3] = [utils.int32(x << 1) for x in self.int_coeffs[:3]]
self._y1 = [utils.int32(x << 1) for x in self._y1]
self._y2 = [utils.int32(x << 1) for x in self._y2]
self.remaining_shifts -= 1
self.b_shift -= 1
else:
# no change in b_shift to manage, so can just slew
for n in range(5):
self.int_coeffs[n] += (
utils.saturate_int32_vpu(self.target_coeffs_int[n] - self.int_coeffs[n])
>> self.slew_shift
)
out_samples = deepcopy(sample_list)
for channel in range(len(sample_list)):
# use basic biquad process
out_samples[channel] = super().process_xcore(sample_list[channel], channel)
return out_samples
def _round_to_q30(coeffs: list[float]) -> tuple[list[float], list[int]]:
"""
Round a list of filter coefficients to Q1.30 format and int32
precision. The coefficients should already have any b_shift applied.
Returns the rounded coefficients in float and int formats
"""
rounded_coeffs = [0.0] * len(coeffs)
int_coeffs = [0] * len(coeffs)
Q = 30
for n in range(len(coeffs)):
# scale to Q1.30 ints, note this is intentionally not multiplied
# (2**Q -1) to keep 1.0 as 1.0
rounded_coeffs[n] = round(coeffs[n] * (1 << Q))
# check for overflow
if not (-(1 << 31)) <= rounded_coeffs[n] <= utils.Q_max(31):
raise ValueError(
"Filter coefficient will overflow (%.4f, %d), reduce gain" % (coeffs[n], n)
)
int_coeffs[n] = utils.int32(rounded_coeffs[n])
# rescale to floats
rounded_coeffs[n] = rounded_coeffs[n] / (1 << Q)
return rounded_coeffs, int_coeffs
def _apply_biquad_gain(coeffs: list[float], gain_db: float) -> list[float]:
"""Apply linear gain to the b coefficients."""
gain = 10 ** (gain_db / 20)
coeffs[0] = coeffs[0] * gain
coeffs[1] = coeffs[1] * gain
coeffs[2] = coeffs[2] * gain
return coeffs
def _apply_biquad_bshift(coeffs: list[float], b_shift: int) -> list[float]:
"""
Apply linear bitshift to the b coefficients.
This can be used for high gain shelf and peaking filters, where the
filter coefficients are greater than 2, and so cannot be represented
in Q1.30 format.
"""
gain = 2**-b_shift
coeffs[0] = coeffs[0] * gain
coeffs[1] = coeffs[1] * gain
coeffs[2] = coeffs[2] * gain
return coeffs
def _normalise_biquad(coeffs: list[float]) -> list[float]:
"""
Normalise biquad coefficients by dividing by a0 and making a1 and a2
negative.
Expected input format: [b0, b1, b2, a0, a1, a2]
Expected output format: [b0, b1, b2, -a1, -a2]/a0
"""
if len(coeffs) != 6:
raise ValueError("expected list of 6 biquad coefficients")
# divide by a0, make a1 and a2 negative
coeffs = [
coeffs[0] / coeffs[3],
coeffs[1] / coeffs[3],
coeffs[2] / coeffs[3],
-coeffs[4] / coeffs[3],
-coeffs[5] / coeffs[3],
]
return coeffs
def _round_and_check(coeffs: list[float], b_shift: int = 0) -> tuple[list[float], list[int]]:
"""
Apply any b_shift to biquad coefficients, then round to int32
precision, and check the poles are inside the unit circle.
"""
# round to int32 precision
if len(coeffs) != 5:
raise ValueError("coeffs should be in the form [b0 b1 b2 -a1 -a2]")
coeffs = _apply_biquad_bshift(coeffs.copy(), b_shift)
coeffs, int_coeffs = _round_to_q30(coeffs)
# check filter is stable
poles = np.roots([1, -coeffs[3], -coeffs[4]])
if np.any(np.abs(poles) >= 1):
raise ValueError("Poles lie outside the unit circle, the filter is unstable")
return coeffs, int_coeffs
def _check_filter_freq(filter_freq, fs):
if filter_freq > fs / 2:
warnings.warn("filter_freq must be less than fs/2, saturating to fs/2", UserWarning)
filter_freq = fs / 2
return filter_freq
def _check_max_gain(gain, max_gain):
if gain > max_gain:
warnings.warn(
f"gain_db must be less than {max_gain:.2f}, saturating to {max_gain:.2f}", UserWarning
)
gain = max_gain
return gain
def _get_bshift(coeffs: list[float]) -> int:
if len(coeffs) != 5:
raise ValueError("coeffs should be in the form [b0 b1 b2 -a1 -a2]")
b_coeffs = coeffs[:3]
max_b = np.max(np.abs(b_coeffs))
if max_b != 0:
shr = int(np.floor(np.log2(max_b)))
else:
return 0
return shr if (shr >= 0) else 0
def make_biquad_bypass(fs: int) -> list[float]:
"""
Create a bypass biquad filter. Only the b0 coefficient is set.
Parameters
----------
fs : int
The sample rate of the audio signal.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
"""
coeffs = np.array([1.0, 0.0, 0.0, 0.0, 0.0], dtype=float).tolist()
return coeffs
def make_biquad_mute(fs: int) -> list[float]:
"""
Create a biquad filter coefficients list that represents a mute
filter. All the coefficients are 0.
Parameters
----------
fs : int
The sampling frequency in Hz.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2].
"""
coeffs = [0.0, 0.0, 0.0, 0.0, 0.0]
return coeffs
def make_biquad_gain(fs: int, gain_db: float) -> list[float]:
"""
Calculate the coefficients for a biquad filter with a specified
linear gain.
Parameters
----------
fs : int
The sampling frequency in Hz.
gain_db : float
The desired gain in decibels.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
"""
coeffs = make_biquad_bypass(fs)
coeffs = _apply_biquad_gain(coeffs, gain_db)
return coeffs
def make_biquad_lowpass(fs: int, filter_freq: float, q_factor: float) -> list[float]:
"""Create coefficients for a lowpass biquad filter.
Parameters
----------
fs : int
The sample rate of the audio signal.
filter_freq : float
The cutoff frequency of the filter.
q_factor : float
The Q factor of the filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than fs/2.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) / (2 * q_factor)
b0 = (+1.0 - np.cos(w0)) / 2.0
b1 = +1.0 - np.cos(w0)
b2 = (+1.0 - np.cos(w0)) / 2.0
a0 = +1.0 + alpha
a1 = -2.0 * np.cos(w0)
a2 = +1.0 - alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_highpass(fs: int, filter_freq: float, q_factor: float) -> list[float]:
"""Create coefficients for a highpass biquad filter.
Parameters
----------
fs : int
The sample rate of the audio signal.
filter_freq : float
The cutoff frequency of the highpass filter.
q_factor : float
The Q factor of the highpass filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than fs/2.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) / (2 * q_factor)
b0 = (1.0 + np.cos(w0)) / 2.0
b1 = -(1.0 + np.cos(w0))
b2 = (1.0 + np.cos(w0)) / 2.0
a0 = +1.0 + alpha
a1 = -2.0 * np.cos(w0)
a2 = +1.0 - alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
# Constant 0 dB peak gain
def make_biquad_bandpass(fs: int, filter_freq: float, BW) -> list[float]:
"""Create coefficients for a biquad bandpass filter.
Parameters
----------
fs : int
The sampling frequency.
filter_freq : float
The center frequency of the bandpass filter.
BW : float
The bandwidth of the bandpass filter in octaves, measured
between -3 dB points.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If filter_freq is greater than fs/2.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) * np.sinh(np.log(2) / 2 * BW * w0 / np.sin(w0))
b0 = alpha
b1 = +0.0
b2 = -alpha
a0 = +1.0 + alpha
a1 = -2.0 * np.cos(w0)
a2 = +1.0 - alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
# Constant 0 dB peak gain
def make_biquad_bandstop(fs: int, filter_freq: float, BW: float) -> list[float]:
"""Create coefficients for a biquad bandstop filter.
Parameters
----------
fs : int
The sampling frequency.
filter_freq : float
The center frequency of the bandstop filter.
BW : float
The bandwidth of the bandstop filter in octaves, measured
between -3 dB points
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than half of the sample rate.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) * np.sinh(np.log(2) / 2 * BW * w0 / np.sin(w0))
b0 = +1.0
b1 = -2.0 * np.cos(w0)
b2 = +1.0
a0 = +1.0 + alpha
a1 = -2.0 * np.cos(w0)
a2 = +1.0 - alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_notch(fs: int, filter_freq: float, q_factor: float) -> list[float]:
"""Create a biquad notch filter.
Parameters
----------
fs : int
The sampling frequency.
filter_freq : float
The center frequency of the notch filter.
q_factor : float
The Q factor of the notch filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than half of the sample rate.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) / (2.0 * q_factor)
b0 = +1.0
b1 = -2.0 * np.cos(w0)
b2 = +1.0
a0 = +1.0 + alpha
a1 = -2.0 * np.cos(w0)
a2 = +1.0 - alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_allpass(fs: int, filter_freq: float, q_factor: float) -> list[float]:
"""
Create coefficients for a biquad allpass filter.
Parameters
----------
fs : int
The sample rate of the audio signal.
filter_freq : float
The center frequency of the allpass filter.
q_factor : float
The Q factor of the allpass filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than half of the sample rate.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) / (2.0 * q_factor)
b0 = +1.0 - alpha
b1 = -2.0 * np.cos(w0)
b2 = +1.0 + alpha
a0 = +1.0 + alpha
a1 = -2.0 * np.cos(w0)
a2 = +1.0 - alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_peaking(
fs: int, filter_freq: float, q_factor: float, boost_db: float
) -> list[float]:
"""Create coefficients for a biquad peaking filter.
Parameters
----------
fs : int
The sampling frequency in Hz.
filter_freq : float
The center frequency of the peaking filter in Hz.
q_factor : float
The Q factor of the peaking filter.
boost_db : float
The boost in decibels applied by the filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than half of the sample rate.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
A = np.sqrt(10 ** (boost_db / 20))
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) / (2.0 * q_factor)
b0 = +1.0 + alpha * A
b1 = -2.0 * np.cos(w0)
b2 = +1.0 - alpha * A
a0 = +1.0 + alpha / A
a1 = -2.0 * np.cos(w0)
a2 = +1.0 - alpha / A
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_constant_q(
fs: int, filter_freq: float, q_factor: float, boost_db: float
) -> list[float]:
"""Create coefficients for a biquad peaking filter with constant Q.
Constant Q means that the bandwidth of the filter remains constant
as the gain varies. It is commonly used for graphic equalisers.
Parameters
----------
fs : int
The sample rate of the audio signal.
filter_freq : float
The center frequency of the filter in Hz.
q_factor : float
The Q factor of the filter.
boost_db : float
The boost in decibels applied to the filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than half of the sample rate.
References
----------
- Zoelzer, U. (2011). DAFX: Digital Audio Effects. John Wiley & Sons, Table 2.4
- https://www.musicdsp.org/en/latest/Filters/37-zoelzer-biquad-filters.html
"""
filter_freq = _check_filter_freq(filter_freq, fs)
V = 10 ** (boost_db / 20)
w0 = 2.0 * np.pi * filter_freq / fs
K = np.tan(w0 / 2)
if boost_db > 0:
b0 = 1 + V * K / q_factor + K**2
b1 = 2 * (K**2 - 1)
b2 = 1 - V * K / q_factor + K**2
a0 = 1 + K / q_factor + K**2
a1 = 2 * (K**2 - 1)
a2 = 1 - K / q_factor + K**2
else:
V = 1 / V
b0 = 1 + (K / q_factor) + K**2
b1 = 2 * (K**2 - 1)
b2 = 1 - (K / q_factor) + K**2
a0 = 1 + (V * K / q_factor) + K**2
a1 = 2 * (K**2 - 1)
a2 = 1 - (V * K / q_factor) + K**2
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_lowshelf(
fs: int, filter_freq: float, q_factor: float, gain_db: float
) -> list[float]:
"""Create coefficients for a lowshelf biquad filter.
The Q factor is defined in a similar way to standard low pass, i.e.
> 0.707 will yield peakiness (where the shelf response does not
monotonically change). The level change at f will be boost_db/2.
Parameters
----------
fs : int
The sample rate of the audio signal.
filter_freq : float
The cutoff frequency of the filter.
q_factor : float
The Q factor of the filter.
gain_db : float
The gain in decibels of the filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than half of the sample rate.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
A = 10.0 ** (gain_db / 40.0)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) / (2 * q_factor)
b0 = A * ((A + 1) - (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha)
b1 = 2 * A * ((A - 1) - (A + 1) * np.cos(w0))
b2 = A * ((A + 1) - (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha)
a0 = (A + 1) + (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha
a1 = -2 * ((A - 1) + (A + 1) * np.cos(w0))
a2 = (A + 1) + (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_highshelf(
fs: int, filter_freq: float, q_factor: float, gain_db: float
) -> list[float]:
"""Create coefficients for a highshelf biquad filter.
The Q factor is defined in a similar way to standard high pass, i.e.
> 0.707 will yield peakiness. The level change at f will be
boost_db/2.
Parameters
----------
fs : int
The sample rate of the audio signal.
filter_freq : float
The cutoff frequency of the filter.
q_factor : float
The Q factor of the filter.
gain_db : float
The gain in decibels of the filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If the filter frequency is greater than half of the sample rate.
"""
filter_freq = _check_filter_freq(filter_freq, fs)
A = 10.0 ** (gain_db / 40.0)
w0 = 2.0 * np.pi * filter_freq / fs
alpha = np.sin(w0) / (2 * q_factor)
b0 = A * ((A + 1) + (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha)
b1 = -2 * A * ((A - 1) + (A + 1) * np.cos(w0))
b2 = A * ((A + 1) + (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha)
a0 = (A + 1) - (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha
a1 = 2 * ((A - 1) - (A + 1) * np.cos(w0))
a2 = (A + 1) - (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
def make_biquad_linkwitz(fs: int, f0: float, q0: float, fp: float, qp: float) -> list[float]:
"""
Create coefficients for a Linkwitz Transform biquad filter.
The Linkwitz Transform is commonly used to change the low frequency
roll off slope of a loudspeaker. When applied to a loudspeaker, it
will change the cutoff frequency from f0 to fp, and the quality
factor from q0 to qp.
Parameters
----------
fs : int
The sampling frequency of the audio signal.
f0 : float
The original cutoff frequency of the filter.
q0 : float
The original quality factor of the filter at f0.
fp : float
The target cutoff frequency for the filter.
qp : float
The target quality factor for the filter.
Returns
-------
list[float]
The coefficients of the biquad filter in the order
[b0, b1, b2, -a1, -a2]. The coefficients are normalised by a0
such that ``a0 = 1``.
Raises
------
ValueError
If either f0 or fp is greater than fs/2.
References
----------
- Linkwitz Transform: https://www.linkwitzlab.com/filters.htm#9
- Linkwitz Transform in MiniDSP: https://www.minidsp.com/applications/advanced-tools/linkwitz-transform
"""
f0 = _check_filter_freq(f0, fs)
fp = _check_filter_freq(fp, fs)
fc = (f0 + fp) / 2
# these are translated from the MiniDSP spreadsheet
d0i = (2 * np.pi * f0) ** 2
d1i = (2 * np.pi * f0) / q0
c0i = (2 * np.pi * fp) ** 2
c1i = (2 * np.pi * fp) / qp
gn = (2 * np.pi * fc) / (np.tan(np.pi * fc / fs))
cci = c0i + gn * c1i + (gn**2)
a0 = cci
a1 = 2 * (c0i - (gn**2))
a2 = c0i - gn * c1i + (gn**2)
b0 = d0i + gn * d1i + (gn**2)
b1 = 2 * (d0i - (gn**2))
b2 = d0i - gn * d1i + (gn**2)
coeffs = [b0, b1, b2, a0, a1, a2]
coeffs = _normalise_biquad(coeffs)
return coeffs
if __name__ == "__main__":
fs = 48000
biquad_1 = biquad(make_biquad_notch(fs, 20, 1), 0, Q_sig=30)
biquad_2 = biquad(make_biquad_notch(fs, 20, 1), 3, Q_sig=30)
biquad_3 = biquad(make_biquad_notch(fs, 20, 1), 0, Q_sig=27)
biquad_4 = biquad(make_biquad_notch(fs, 20, 1), 3, Q_sig=27)
biquad_5 = biquad(make_biquad_notch(fs, 20, 1), 3, Q_sig=27)
t = np.arange(fs * 4) / fs
# signal = spsig.chirp(t, 20, 1, 20000, 'log', phi=-90)
signal = np.sin(2 * np.pi * 997 * t, dtype=np.float64)
output_1 = np.zeros(len(signal))
output_2 = np.zeros(len(signal))
output_3 = np.zeros(len(signal))
output_4 = np.zeros(len(signal))
output_5 = np.zeros(len(signal))
for n in range(len(signal)):
output_1[n] = biquad_1.process_xcore(signal[n])
output_2[n] = biquad_2.process_xcore(signal[n])
output_3[n] = biquad_3.process_xcore(signal[n])
output_4[n] = biquad_4.process_xcore(signal[n])
output_5[n] = biquad_5.process(signal[n])
# plt.plot(signal)
# plt.plot(output_1)
# plt.plot(output_2)
plt.psd(output_1, 1024 * 16, fs, window=spsig.windows.blackmanharris(1024 * 16))
plt.psd(output_2, 1024 * 16, fs, window=spsig.windows.blackmanharris(1024 * 16))
plt.psd(output_3, 1024 * 16, fs, window=spsig.windows.blackmanharris(1024 * 16))
plt.psd(output_4, 1024 * 16, fs, window=spsig.windows.blackmanharris(1024 * 16))
plt.psd(output_5, 1024 * 16, fs, window=spsig.windows.blackmanharris(1024 * 16))
ax = plt.gca()
ax.set_xscale("log")
plt.legend(["Q30", "Q30, b_shift 3", "Q27", "Q27, b_shift 3", "double"])
plt.show()
pass
exit()