Skip to main content

aether_nodes/
eq.rs

1//! Parametric EQ — 3-band equalizer with adjustable frequency, gain, and Q.
2//!
3//! Param layout:
4//!   0 = low_freq    (Hz, 20 – 500)
5//!   1 = low_gain    (dB, -24 – 24)
6//!   2 = mid_freq    (Hz, 200 – 5000)
7//!   3 = mid_gain    (dB, -24 – 24)
8//!   4 = mid_q       (0.1 – 10.0)
9//!   5 = high_freq   (Hz, 2000 – 20000)
10//!   6 = high_gain   (dB, -24 – 24)
11
12use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
13
14/// Biquad filter coefficients.
15#[derive(Debug, Clone, Copy)]
16struct BiquadCoeffs {
17    b0: f32,
18    b1: f32,
19    b2: f32,
20    a1: f32,
21    a2: f32,
22}
23
24/// Biquad filter state.
25#[derive(Debug, Clone, Copy)]
26struct BiquadState {
27    x1: f32,
28    x2: f32,
29    y1: f32,
30    y2: f32,
31}
32
33impl BiquadState {
34    fn new() -> Self {
35        Self {
36            x1: 0.0,
37            x2: 0.0,
38            y1: 0.0,
39            y2: 0.0,
40        }
41    }
42
43    #[inline(always)]
44    fn process(&mut self, x: f32, coeffs: &BiquadCoeffs) -> f32 {
45        let y = coeffs.b0 * x + coeffs.b1 * self.x1 + coeffs.b2 * self.x2
46            - coeffs.a1 * self.y1
47            - coeffs.a2 * self.y2;
48
49        self.x2 = self.x1;
50        self.x1 = x;
51        self.y2 = self.y1;
52        self.y1 = y;
53
54        y
55    }
56}
57
58impl BiquadCoeffs {
59    /// Peaking EQ filter (bell curve).
60    fn peaking(freq: f32, gain_db: f32, q: f32, sample_rate: f32) -> Self {
61        let a = 10.0f32.powf(gain_db / 40.0);
62        let omega = 2.0 * std::f32::consts::PI * freq / sample_rate;
63        let sin_omega = omega.sin();
64        let cos_omega = omega.cos();
65        let alpha = sin_omega / (2.0 * q);
66
67        let b0 = 1.0 + alpha * a;
68        let b1 = -2.0 * cos_omega;
69        let b2 = 1.0 - alpha * a;
70        let a0 = 1.0 + alpha / a;
71        let a1 = -2.0 * cos_omega;
72        let a2 = 1.0 - alpha / a;
73
74        Self {
75            b0: b0 / a0,
76            b1: b1 / a0,
77            b2: b2 / a0,
78            a1: a1 / a0,
79            a2: a2 / a0,
80        }
81    }
82
83    /// Low shelf filter.
84    fn low_shelf(freq: f32, gain_db: f32, sample_rate: f32) -> Self {
85        let a = 10.0f32.powf(gain_db / 40.0);
86        let omega = 2.0 * std::f32::consts::PI * freq / sample_rate;
87        let sin_omega = omega.sin();
88        let cos_omega = omega.cos();
89        let alpha = sin_omega / 2.0 * ((a + 1.0 / a) * (1.0 / 0.75 - 1.0) + 2.0).sqrt();
90
91        let a_plus_1 = a + 1.0;
92        let a_minus_1 = a - 1.0;
93        let sqrt_a = a.sqrt();
94
95        let b0 = a * (a_plus_1 - a_minus_1 * cos_omega + 2.0 * sqrt_a * alpha);
96        let b1 = 2.0 * a * (a_minus_1 - a_plus_1 * cos_omega);
97        let b2 = a * (a_plus_1 - a_minus_1 * cos_omega - 2.0 * sqrt_a * alpha);
98        let a0 = a_plus_1 + a_minus_1 * cos_omega + 2.0 * sqrt_a * alpha;
99        let a1 = -2.0 * (a_minus_1 + a_plus_1 * cos_omega);
100        let a2 = a_plus_1 + a_minus_1 * cos_omega - 2.0 * sqrt_a * alpha;
101
102        Self {
103            b0: b0 / a0,
104            b1: b1 / a0,
105            b2: b2 / a0,
106            a1: a1 / a0,
107            a2: a2 / a0,
108        }
109    }
110
111    /// High shelf filter.
112    fn high_shelf(freq: f32, gain_db: f32, sample_rate: f32) -> Self {
113        let a = 10.0f32.powf(gain_db / 40.0);
114        let omega = 2.0 * std::f32::consts::PI * freq / sample_rate;
115        let sin_omega = omega.sin();
116        let cos_omega = omega.cos();
117        let alpha = sin_omega / 2.0 * ((a + 1.0 / a) * (1.0 / 0.75 - 1.0) + 2.0).sqrt();
118
119        let a_plus_1 = a + 1.0;
120        let a_minus_1 = a - 1.0;
121        let sqrt_a = a.sqrt();
122
123        let b0 = a * (a_plus_1 + a_minus_1 * cos_omega + 2.0 * sqrt_a * alpha);
124        let b1 = -2.0 * a * (a_minus_1 + a_plus_1 * cos_omega);
125        let b2 = a * (a_plus_1 + a_minus_1 * cos_omega - 2.0 * sqrt_a * alpha);
126        let a0 = a_plus_1 - a_minus_1 * cos_omega + 2.0 * sqrt_a * alpha;
127        let a1 = 2.0 * (a_minus_1 - a_plus_1 * cos_omega);
128        let a2 = a_plus_1 - a_minus_1 * cos_omega - 2.0 * sqrt_a * alpha;
129
130        Self {
131            b0: b0 / a0,
132            b1: b1 / a0,
133            b2: b2 / a0,
134            a1: a1 / a0,
135            a2: a2 / a0,
136        }
137    }
138}
139
140pub struct ParametricEq {
141    low_state: BiquadState,
142    mid_state: BiquadState,
143    high_state: BiquadState,
144    low_coeffs: BiquadCoeffs,
145    mid_coeffs: BiquadCoeffs,
146    high_coeffs: BiquadCoeffs,
147    last_sample_rate: f32,
148}
149
150impl ParametricEq {
151    pub fn new() -> Self {
152        Self {
153            low_state: BiquadState::new(),
154            mid_state: BiquadState::new(),
155            high_state: BiquadState::new(),
156            low_coeffs: BiquadCoeffs::low_shelf(100.0, 0.0, 48000.0),
157            mid_coeffs: BiquadCoeffs::peaking(1000.0, 0.0, 1.0, 48000.0),
158            high_coeffs: BiquadCoeffs::high_shelf(5000.0, 0.0, 48000.0),
159            last_sample_rate: 48000.0,
160        }
161    }
162}
163
164impl Default for ParametricEq {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl DspNode for ParametricEq {
171    fn process(
172        &mut self,
173        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
174        output: &mut [f32; BUFFER_SIZE],
175        params: &mut ParamBlock,
176        sample_rate: f32,
177    ) {
178        let silence = [0.0f32; BUFFER_SIZE];
179        let input = inputs[0].unwrap_or(&silence);
180
181        // Get parameters with validation
182        let low_freq = params.get(0).current.clamp(20.0, 500.0);
183        let low_gain = params.get(1).current.clamp(-24.0, 24.0);
184        let mid_freq = params.get(2).current.clamp(200.0, 5000.0);
185        let mid_gain = params.get(3).current.clamp(-24.0, 24.0);
186        let mid_q = params.get(4).current.clamp(0.1, 10.0);
187        let high_freq = params.get(5).current.clamp(2000.0, 20000.0);
188        let high_gain = params.get(6).current.clamp(-24.0, 24.0);
189
190        // Update coefficients if sample rate changed or at start of buffer
191        if (sample_rate - self.last_sample_rate).abs() > 0.1 {
192            self.last_sample_rate = sample_rate;
193            self.low_coeffs = BiquadCoeffs::low_shelf(low_freq, low_gain, sample_rate);
194            self.mid_coeffs = BiquadCoeffs::peaking(mid_freq, mid_gain, mid_q, sample_rate);
195            self.high_coeffs = BiquadCoeffs::high_shelf(high_freq, high_gain, sample_rate);
196        }
197
198        for i in 0..BUFFER_SIZE {
199            let x = input[i];
200
201            // Process through all three bands
202            let y1 = self.low_state.process(x, &self.low_coeffs);
203            let y2 = self.mid_state.process(y1, &self.mid_coeffs);
204            let y3 = self.high_state.process(y2, &self.high_coeffs);
205
206            output[i] = y3;
207            params.tick_all();
208        }
209    }
210
211    fn type_name(&self) -> &'static str {
212        "ParametricEq"
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_eq_silence_passthrough() {
222        let mut eq = ParametricEq::new();
223        let mut params = ParamBlock::new();
224        // Flat EQ (0dB gain on all bands)
225        for &v in &[100.0f32, 0.0, 1000.0, 0.0, 1.0, 5000.0, 0.0] {
226            params.add(v);
227        }
228        let input = [0.0f32; BUFFER_SIZE];
229        let inputs = [Some(&input); MAX_INPUTS];
230        let mut output = [0.0f32; BUFFER_SIZE];
231        eq.process(&inputs, &mut output, &mut params, 48000.0);
232        for s in &output {
233            assert!(s.abs() < 1e-6, "silence should pass through");
234        }
235    }
236
237    #[test]
238    fn test_eq_processes_signal() {
239        let mut eq = ParametricEq::new();
240        let mut params = ParamBlock::new();
241        // Boost mid frequencies
242        for &v in &[100.0f32, 0.0, 1000.0, 12.0, 1.0, 5000.0, 0.0] {
243            params.add(v);
244        }
245        let input = [0.1f32; BUFFER_SIZE];
246        let inputs = [Some(&input); MAX_INPUTS];
247        let mut output = [0.0f32; BUFFER_SIZE];
248        eq.process(&inputs, &mut output, &mut params, 48000.0);
249        // Output should be non-zero
250        assert!(
251            output[BUFFER_SIZE - 1].abs() > 0.0,
252            "EQ should process signal"
253        );
254    }
255}