Skip to main content

aether_nodes/
filter.rs

1//! State-variable filter (SVF) — simultaneous LP/HP/BP outputs.
2//!
3//! Param layout:
4//!   0 = cutoff frequency (Hz)
5//!   1 = resonance Q (0.5..20)
6//!   2 = mode (0=LP, 1=HP, 2=BP)
7
8use aether_core::{node::DspNode, param::ParamBlock, state::StateBlob, BUFFER_SIZE, MAX_INPUTS};
9
10#[derive(Clone, Copy, Default)]
11struct SvfState {
12    ic1eq: f32,
13    ic2eq: f32,
14}
15
16pub struct StateVariableFilter {
17    ic1eq: f32,
18    ic2eq: f32,
19}
20
21impl StateVariableFilter {
22    pub fn new() -> Self {
23        Self {
24            ic1eq: 0.0,
25            ic2eq: 0.0,
26        }
27    }
28
29    /// Andy Simper's SVF (Cytomic) — topology-preserving transform.
30    #[inline(always)]
31    fn process_sample(&mut self, input: f32, cutoff: f32, q: f32, sr: f32) -> (f32, f32, f32) {
32        let g = (std::f32::consts::PI * cutoff / sr).tan();
33        let k = 1.0 / q.max(0.1);
34        let a1 = 1.0 / (1.0 + g * (g + k));
35        let a2 = g * a1;
36        let a3 = g * a2;
37
38        let v3 = input - self.ic2eq;
39        let v1 = a1 * self.ic1eq + a2 * v3;
40        let v2 = self.ic2eq + a2 * self.ic1eq + a3 * v3;
41
42        self.ic1eq = 2.0 * v1 - self.ic1eq;
43        self.ic2eq = 2.0 * v2 - self.ic2eq;
44
45        let lp = v2;
46        let bp = v1;
47        let hp = input - k * v1 - v2;
48        (lp, hp, bp)
49    }
50}
51
52impl Default for StateVariableFilter {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl DspNode for StateVariableFilter {
59    fn process(
60        &mut self,
61        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
62        output: &mut [f32; BUFFER_SIZE],
63        params: &mut ParamBlock,
64        sample_rate: f32,
65    ) {
66        let silence = [0.0f32; BUFFER_SIZE];
67        let input = inputs[0].unwrap_or(&silence);
68
69        for (i, out) in output.iter_mut().enumerate() {
70            let cutoff = params.get(0).current.clamp(20.0, sample_rate * 0.49);
71            let q = params.get(1).current.clamp(0.5, 20.0);
72            let mode = params.get(2).current as u32;
73
74            let (lp, hp, bp) = self.process_sample(input[i], cutoff, q, sample_rate);
75            *out = match mode {
76                0 => lp,
77                1 => hp,
78                _ => bp,
79            };
80            params.tick_all();
81        }
82    }
83
84    fn capture_state(&self) -> StateBlob {
85        StateBlob::from_value(&SvfState {
86            ic1eq: self.ic1eq,
87            ic2eq: self.ic2eq,
88        })
89    }
90
91    fn restore_state(&mut self, state: StateBlob) {
92        let s: SvfState = state.to_value();
93        self.ic1eq = s.ic1eq;
94        self.ic2eq = s.ic2eq;
95    }
96
97    fn type_name(&self) -> &'static str {
98        "StateVariableFilter"
99    }
100}