Skip to main content

sva_formula/
filter.rs

1// Concern: the eight shapes' H(s) as a Rational in s | Non-concern: the biquad recurrence (sva-samples) | IO: (Shape, cutoff, q, gain_db) -> Rational
2
3use std::f64::consts::TAU;
4
5use crate::closed_form::Rational;
6use crate::complex::C64;
7
8/// `OnePole` is the 6 dB/oct filter.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Shape {
11    OnePole,
12    Lowpass,
13    Highpass,
14    Bandpass,
15    Notch,
16    Peaking,
17    Lowshelf,
18    Highshelf,
19}
20
21/// In the order sva-ast's `FILTERS` names them.
22pub const ALL_SHAPES: [Shape; 8] = [
23    Shape::OnePole,
24    Shape::Lowpass,
25    Shape::Highpass,
26    Shape::Bandpass,
27    Shape::Notch,
28    Shape::Peaking,
29    Shape::Lowshelf,
30    Shape::Highshelf,
31];
32
33impl Shape {
34    pub fn default_q(self) -> f64 {
35        match self {
36            Shape::Bandpass | Shape::Notch | Shape::Peaking => 1.0,
37            _ => std::f64::consts::FRAC_1_SQRT_2,
38        }
39    }
40
41    pub fn takes_gain(self) -> bool {
42        matches!(self, Shape::Peaking | Shape::Lowshelf | Shape::Highshelf)
43    }
44}
45
46/// The RBJ Audio EQ Cookbook's analog prototypes, before any bilinear transform.
47pub fn design(shape: Shape, cutoff: f64, q: f64, gain_db: f64) -> Rational {
48    let w0 = TAU * cutoff;
49    let a = 10f64.powf(gain_db / 40.0);
50    let root_a = a.sqrt();
51    match shape {
52        Shape::OnePole => Rational {
53            zeros: Vec::new(),
54            poles: vec![C64::real(-w0)],
55            gain: C64::real(w0),
56        },
57        Shape::Lowpass => Rational {
58            zeros: Vec::new(),
59            poles: quadratic(w0 / q, w0 * w0),
60            gain: C64::real(w0 * w0),
61        },
62        Shape::Highpass => Rational {
63            zeros: vec![C64::ZERO, C64::ZERO],
64            poles: quadratic(w0 / q, w0 * w0),
65            gain: C64::ONE,
66        },
67        Shape::Bandpass => Rational {
68            zeros: vec![C64::ZERO],
69            poles: quadratic(w0 / q, w0 * w0),
70            gain: C64::real(w0 / q),
71        },
72        Shape::Notch => Rational {
73            zeros: vec![C64::new(0.0, w0), C64::new(0.0, -w0)],
74            poles: quadratic(w0 / q, w0 * w0),
75            gain: C64::ONE,
76        },
77        Shape::Peaking => Rational {
78            zeros: quadratic(w0 * a / q, w0 * w0),
79            poles: quadratic(w0 / (q * a), w0 * w0),
80            gain: C64::ONE,
81        },
82        Shape::Lowshelf => Rational {
83            zeros: quadratic(root_a * w0 / q, a * w0 * w0),
84            poles: quadratic(root_a * w0 / (q * a), w0 * w0 / a),
85            gain: C64::ONE,
86        },
87        Shape::Highshelf => Rational {
88            zeros: quadratic(root_a * w0 / (q * a), w0 * w0 / a),
89            poles: quadratic(root_a * w0 / q, a * w0 * w0),
90            gain: C64::real(a * a),
91        },
92    }
93}
94
95/// The two roots of `s^2 + b*s + c`, real or conjugate.
96fn quadratic(b: f64, c: f64) -> Vec<C64> {
97    let discriminant = b * b - 4.0 * c;
98    let half = C64::real(-b / 2.0);
99    let spread = if discriminant >= 0.0 {
100        C64::real(discriminant.sqrt() / 2.0)
101    } else {
102        C64::new(0.0, (-discriminant).sqrt() / 2.0)
103    };
104    vec![half + spread, half - spread]
105}