SvfCoef::allpass has the phase of a Linkwitz-Riley split at the same frequency, so a crossover tree can keep its lower bands in phase with the upper ones. The test checks it against a real LR4 split sample by sample and for flat magnitude.
122 lines
3.2 KiB
Rust
122 lines
3.2 KiB
Rust
//! Trapezoidal state-variable filter (Simper/Cytomic). Stable and click-free under fast
|
||
//! retuning, so it can follow a gliding note. Coefficients are separate from state, so one
|
||
//! set can drive both channels.
|
||
use std::f32::consts::PI;
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
pub struct SvfCoef {
|
||
a1: f32,
|
||
a2: f32,
|
||
a3: f32,
|
||
m0: f32,
|
||
m1: f32,
|
||
m2: f32,
|
||
}
|
||
impl Default for SvfCoef {
|
||
/// Identity: passes the input unchanged.
|
||
fn default() -> Self {
|
||
Self::IDENTITY
|
||
}
|
||
}
|
||
impl SvfCoef {
|
||
pub const IDENTITY: Self = Self {
|
||
a1: 1.,
|
||
a2: 0.,
|
||
a3: 0.,
|
||
m0: 1.,
|
||
m1: 0.,
|
||
m2: 0.,
|
||
};
|
||
fn base(rate: f32, hz: f32, k: f32, warp: f32) -> (f32, f32, f32) {
|
||
let g = (PI * (hz / rate).clamp(1e-6, 0.49)).tan() * warp;
|
||
let a1 = 1. / (1. + g * (g + k));
|
||
let a2 = g * a1;
|
||
(a1, a2, g * a2)
|
||
}
|
||
/// Peaking filter. At 0 dB it is an exact identity.
|
||
pub fn bell(rate: f32, hz: f32, q: f32, gain_db: f32) -> Self {
|
||
let a = 10f32.powf(gain_db / 40.);
|
||
let k = 1. / (q.max(1e-3) * a);
|
||
let (a1, a2, a3) = Self::base(rate, hz, k, 1.);
|
||
Self {
|
||
a1,
|
||
a2,
|
||
a3,
|
||
m0: 1.,
|
||
m1: k * (a * a - 1.),
|
||
m2: 0.,
|
||
}
|
||
}
|
||
pub fn lowpass(rate: f32, hz: f32, q: f32) -> Self {
|
||
let (a1, a2, a3) = Self::base(rate, hz, 1. / q.max(1e-3), 1.);
|
||
Self {
|
||
a1,
|
||
a2,
|
||
a3,
|
||
m0: 0.,
|
||
m1: 0.,
|
||
m2: 1.,
|
||
}
|
||
}
|
||
pub fn highpass(rate: f32, hz: f32, q: f32) -> Self {
|
||
let k = 1. / q.max(1e-3);
|
||
let (a1, a2, a3) = Self::base(rate, hz, k, 1.);
|
||
Self {
|
||
a1,
|
||
a2,
|
||
a3,
|
||
m0: 1.,
|
||
m1: -k,
|
||
m2: -1.,
|
||
}
|
||
}
|
||
/// Second-order allpass. With `q` = 1/√2 it has the phase of a Linkwitz–Riley crossover
|
||
/// built from two lowpass and two highpass stages at the same frequency, so it keeps
|
||
/// the bands of a crossover tree in phase.
|
||
pub fn allpass(rate: f32, hz: f32, q: f32) -> Self {
|
||
let k = 1. / q.max(1e-3);
|
||
let (a1, a2, a3) = Self::base(rate, hz, k, 1.);
|
||
Self {
|
||
a1,
|
||
a2,
|
||
a3,
|
||
m0: 1.,
|
||
m1: -2. * k,
|
||
m2: 0.,
|
||
}
|
||
}
|
||
pub fn high_shelf(rate: f32, hz: f32, q: f32, gain_db: f32) -> Self {
|
||
let a = 10f32.powf(gain_db / 40.);
|
||
let k = 1. / q.max(1e-3);
|
||
let (a1, a2, a3) = Self::base(rate, hz, k, a.sqrt());
|
||
Self {
|
||
a1,
|
||
a2,
|
||
a3,
|
||
m0: a * a,
|
||
m1: k * (1. - a) * a,
|
||
m2: 1. - a * a,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
pub struct Svf {
|
||
ic1: f32,
|
||
ic2: f32,
|
||
}
|
||
impl Svf {
|
||
#[inline]
|
||
pub fn process(&mut self, c: &SvfCoef, v0: f32) -> f32 {
|
||
let v3 = v0 - self.ic2;
|
||
let v1 = c.a1 * self.ic1 + c.a2 * v3;
|
||
let v2 = self.ic2 + c.a2 * self.ic1 + c.a3 * v3;
|
||
self.ic1 = 2. * v1 - self.ic1;
|
||
self.ic2 = 2. * v2 - self.ic2;
|
||
c.m0 * v0 + c.m1 * v1 + c.m2 * v2
|
||
}
|
||
pub fn reset(&mut self) {
|
||
*self = Self::default();
|
||
}
|
||
}
|