pub struct Biquad<C> {
pub ba: [C; 5],
}Expand description
Biquad IIR (second order section)
A biquadratic IIR filter supports up to two zeros and two poles in the transfer function.
The Biquad performs the following operation to compute a new output sample y0 from a new
input sample x0 given its configuration and previous samples:
y0 = b0*x0 + b1*x1 + b2*x2 + a1*y1 + a2*y2
This implementation here saves storage and improves caching opportunities by decoupling filter configuration (coefficients, limits and offset) from filter state and thus supports both (a) sharing a single filter between multiple states (“channels”) and (b) rapid switching of filters (tuning, transfer) for a given state without copying either state of configuration.
§Filter architecture
Direct Form 1 (DF1) and Direct Form 2 transposed (DF2T) are the only IIR filter structures with an (effective in the case of TDF2) single summing junction this allows clamping of the output before feedback.
DF1 allows atomic coefficient change because only inputs and outputs are stored. The summing junction pipelining of TDF2 would require incremental coefficient changes and is thus less amenable to online tuning.
DF2T needs less state storage (2 instead of 4). This is in addition to the coefficient storage (5 plus 2 limits plus 1 offset)
DF2T is less efficient and less accurate for fixed-point architectures as quantization
happens at each intermediate summing junction in addition to the output quantization. This is
especially true for common i64 + i32 * i32 -> i64 MACC architectures.
One could use wide state storage for fixed point DF2T but that would negate the storage
and processing advantages.
§Coefficients
ba: [T; 5] = [b0, b1, b2, a1, a2] is the coefficients type.
To represent the IIR coefficients, this contains the feed-forward
coefficients b0, b1, b2 followed by the feed-back coefficients
a1, a2, all five normalized such that a0 = 1.
The summing junction of the BiquadClamp filter also receives an offset u
and applies clamping such that min <= y <= max.
See crate::iir::coefficients::Filter and crate::iir::pid::Builder for ways to generate coefficients.
§Fixed point
Coefficient scaling for fixed point (i.e. integer) processing relies on [dsp_fixedpoint::Q].
Choose the number of fractional bits to meet coefficient range (e.g. potentially a1 = 2
for a double integrator) and guard bits.
§PID controller
The IIR coefficients can be mapped to other transfer function representations, for example PID controllers as described in https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw and https://arxiv.org/abs/1508.06319.
Using a Biquad as a template for a PID controller achieves several important properties:
- Its transfer function is universal in the sense that any biquadratic transfer function can be implemented (high-passes, gain limits, second order integrators with inherent anti-windup, notches etc) without code changes preserving all features.
- It inherits a universal implementation of “integrator anti-windup”, also and especially in the presence of set-point changes and in the presence of proportional or derivative gain without any back-off that would reduce steady-state output range.
- It has universal derivative-kick (undesired, unlimited, and un-physical amplification of set-point changes by the derivative term) avoidance.
- An offset at the input of an IIR filter (a.k.a. “set-point”) is equivalent to an offset at the summing junction (in output units). They are related by the overall (DC feed-forward) gain of the filter.
- It stores only previous outputs and inputs. These have direct and invariant interpretation (independent of coefficients and offset). Therefore it can trivially implement bump-less transfer between any coefficients/offset sets.
- Cascading multiple IIR filters allows stable and robust implementation of transfer functions beyond biquadratic terms.
Fields§
§ba: [C; 5]Coefficients
[b0, b1, b2, a1, a2]
Such that
y0 = (b0*x0 + b1*x1 + b2*x2 + a1*y1 + a2*y2)/(1 << F)
where x0, x1, x2 are current, delayed, and doubly delayed inputs and
y0, y1, y2 are current, delayed, and doubly delayed outputs.
Note the a1, a2 sign. The transfer function is:
H(z) = (b0 + b1*z^-1 + b2*z^-2)/((1 << F) - a2*z^-1 - a2*z^-2)
Implementations§
Source§impl<C: Clamp + Copy> Biquad<C>
impl<C: Clamp + Copy> Biquad<C>
Sourcepub const IDENTITY: Self
pub const IDENTITY: Self
A unity gain filter
let x0 = 3.0;
let y0 = Biquad::<f32>::IDENTITY.process(&mut DirectForm1::default(), x0);
assert_eq!(y0, x0);Sourcepub const HOLD: Self
pub const HOLD: Self
A “hold” filter that ingests input and maintains output
let mut state = DirectForm1::<f32>::default();
state.xy[2] = 2.0;
let x0 = 7.0;
let y0 = Biquad::<f32>::HOLD.process(&mut state, x0);
assert_eq!(y0, 2.0);
assert_eq!(state.xy, [x0, 0.0, y0, y0]);Sourcepub const fn proportional(k: C) -> Self
pub const fn proportional(k: C) -> Self
A filter with the given proportional gain at all frequencies
let x0 = 3.0;
let y0 = Biquad::<f32>::proportional(2.0).process(&mut DirectForm1::default(), x0);
assert_eq!(y0, 2.0 * x0);Trait Implementations§
Source§impl<'de, C> Deserialize<'de> for Biquad<C>where
C: Deserialize<'de>,
impl<'de, C> Deserialize<'de> for Biquad<C>where
C: Deserialize<'de>,
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl<C: Copy + 'static, T> From<[T; 5]> for Biquad<C>where
T: AsPrimitive<C>,
Normalized and sign-flipped coefficients
[b0, b1, b2, a1, a2]
impl<C: Copy + 'static, T> From<[T; 5]> for Biquad<C>where
T: AsPrimitive<C>,
Normalized and sign-flipped coefficients
[b0, b1, b2, a1, a2]
Source§impl<C: PartialOrd> PartialOrd for Biquad<C>
impl<C: PartialOrd> PartialOrd for Biquad<C>
Source§impl<T: 'static + Copy, C: Copy + Mul<T, Output = A>, A: Add<Output = A> + AsPrimitive<T>> SplitProcess<T, T, DirectForm1<T>> for Biquad<C>
let mut state = DirectForm1 {
xy: [0.0, 1.0, 2.0, 3.0],
};
let x0 = 4.0;
let y0 = Biquad::<f32>::IDENTITY.process(&mut state, x0);
assert_eq!(y0, x0);
assert_eq!(state.xy, [x0, 0.0, y0, 2.0]);
impl<T: 'static + Copy, C: Copy + Mul<T, Output = A>, A: Add<Output = A> + AsPrimitive<T>> SplitProcess<T, T, DirectForm1<T>> for Biquad<C>
let mut state = DirectForm1 {
xy: [0.0, 1.0, 2.0, 3.0],
};
let x0 = 4.0;
let y0 = Biquad::<f32>::IDENTITY.process(&mut state, x0);
assert_eq!(y0, x0);
assert_eq!(state.xy, [x0, 0.0, y0, 2.0]);Source§impl<T: Copy + Mul<Output = T> + Add<Output = T>> SplitProcess<T, T, DirectForm2Transposed<T>> for Biquad<T>
use dsp_process::SplitProcess;
use idsp::iir::*;
let biquad = Biquad::<f32>::IDENTITY;
let mut state = DirectForm2Transposed::default();
let x = 3.0f32;
let y = biquad.process(&mut state, x);
assert_eq!(x, y);
impl<T: Copy + Mul<Output = T> + Add<Output = T>> SplitProcess<T, T, DirectForm2Transposed<T>> for Biquad<T>
use dsp_process::SplitProcess;
use idsp::iir::*;
let biquad = Biquad::<f32>::IDENTITY;
let mut state = DirectForm2Transposed::default();
let x = 3.0f32;
let y = biquad.process(&mut state, x);
assert_eq!(x, y);Source§impl<const F: i8> SplitProcess<i32, i32, DirectForm1Dither> for Biquad<Q<i32, i64, F>>
let mut state = DirectForm1Dither {
xy: [1, 2, 3, 4],
e: 5,
};
let x0 = 6;
let y0 = Biquad::<Q32<30>>::IDENTITY.process(&mut state, x0);
assert_eq!(y0, x0);
assert_eq!(state.xy, [x0, 1, y0, 3]);
assert_eq!(state.e, 20);
impl<const F: i8> SplitProcess<i32, i32, DirectForm1Dither> for Biquad<Q<i32, i64, F>>
let mut state = DirectForm1Dither {
xy: [1, 2, 3, 4],
e: 5,
};
let x0 = 6;
let y0 = Biquad::<Q32<30>>::IDENTITY.process(&mut state, x0);
assert_eq!(y0, x0);
assert_eq!(state.xy, [x0, 1, y0, 3]);
assert_eq!(state.e, 20);