Skip to main content

sim_lib_audio_dsp/
filter.rs

1use std::f32::consts::PI;
2
3use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
4
5use crate::common::{clamp_cutoff, db_to_gain, input_sample, output_channels, prepare_channels};
6
7/// Mode of a [`OnePoleFilter`].
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum OnePoleMode {
10    /// One-pole low-pass response.
11    LowPass,
12    /// One-pole high-pass response.
13    HighPass,
14}
15
16#[derive(Clone, Copy, Debug, Default, PartialEq)]
17struct OnePoleState {
18    z1: f32,
19}
20
21/// A per-channel one-pole low-/high-pass filter [`Processor`].
22#[derive(Clone, Debug, PartialEq)]
23pub struct OnePoleFilter {
24    mode: OnePoleMode,
25    cutoff_hz: f32,
26    sample_rate_hz: f32,
27    states: Vec<OnePoleState>,
28}
29
30impl OnePoleFilter {
31    /// Creates a low-pass one-pole filter at the given cutoff.
32    pub fn low_pass(cutoff_hz: f32) -> Self {
33        Self::new(OnePoleMode::LowPass, cutoff_hz)
34    }
35
36    /// Creates a high-pass one-pole filter at the given cutoff.
37    pub fn high_pass(cutoff_hz: f32) -> Self {
38        Self::new(OnePoleMode::HighPass, cutoff_hz)
39    }
40
41    /// Creates a one-pole filter with the given mode and cutoff in hertz.
42    pub fn new(mode: OnePoleMode, cutoff_hz: f32) -> Self {
43        Self {
44            mode,
45            cutoff_hz,
46            sample_rate_hz: 48_000.0,
47            states: Vec::new(),
48        }
49    }
50
51    fn alpha(&self) -> f32 {
52        let cutoff = clamp_cutoff(self.cutoff_hz, self.sample_rate_hz);
53        1.0 - (-2.0 * PI * cutoff / self.sample_rate_hz).exp()
54    }
55}
56
57impl Processor for OnePoleFilter {
58    fn prepare(&mut self, cfg: PrepareConfig) {
59        self.sample_rate_hz = cfg.sample_rate_hz as f32;
60        prepare_channels(
61            &mut self.states,
62            cfg.out_channels as usize,
63            OnePoleState::default(),
64        );
65    }
66
67    fn reset(&mut self) {
68        self.states.fill(OnePoleState::default());
69    }
70
71    fn process(&mut self, block: &mut ProcessBlock<'_>) {
72        // The audio callback never allocates: `prepare` sized the per-channel
73        // state, so clamp to it rather than grow a wider block in place.
74        let prepared = self.states.len();
75        debug_assert!(
76            output_channels(block) <= prepared,
77            "OnePoleFilter::process received more channels than prepare configured"
78        );
79        let channels = output_channels(block).min(prepared);
80        let alpha = self.alpha();
81        let frames = block.frames as usize;
82        for channel in 0..channels {
83            let state = &mut self.states[channel];
84            for frame in 0..frames {
85                let input = input_sample(block, channel, frame);
86                state.z1 += alpha * (input - state.z1);
87                block.out_audio[channel][frame] = match self.mode {
88                    OnePoleMode::LowPass => state.z1,
89                    OnePoleMode::HighPass => input - state.z1,
90                };
91            }
92        }
93    }
94}
95
96/// Response type of a [`BiquadFilter`].
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub enum BiquadKind {
99    /// Low-pass response.
100    LowPass,
101    /// High-pass response.
102    HighPass,
103    /// Band-pass response.
104    BandPass,
105    /// Band-reject (notch) response.
106    Notch,
107    /// Peaking EQ with the given gain.
108    Peaking {
109        /// Peak gain in decibels.
110        gain_db: f32,
111    },
112}
113
114#[derive(Clone, Copy, Debug, PartialEq)]
115struct Coefficients {
116    b0: f32,
117    b1: f32,
118    b2: f32,
119    a1: f32,
120    a2: f32,
121}
122
123impl Default for Coefficients {
124    fn default() -> Self {
125        Self {
126            b0: 1.0,
127            b1: 0.0,
128            b2: 0.0,
129            a1: 0.0,
130            a2: 0.0,
131        }
132    }
133}
134
135#[derive(Clone, Copy, Debug, Default, PartialEq)]
136struct BiquadState {
137    z1: f32,
138    z2: f32,
139}
140
141/// A per-channel RBJ biquad filter [`Processor`].
142#[derive(Clone, Debug, PartialEq)]
143pub struct BiquadFilter {
144    kind: BiquadKind,
145    frequency_hz: f32,
146    q: f32,
147    sample_rate_hz: f32,
148    coefficients: Coefficients,
149    states: Vec<BiquadState>,
150}
151
152impl BiquadFilter {
153    /// Creates a biquad filter of the given kind, frequency, and Q (clamped to
154    /// `>= 0.05`).
155    pub fn new(kind: BiquadKind, frequency_hz: f32, q: f32) -> Self {
156        let mut filter = Self {
157            kind,
158            frequency_hz,
159            q: q.max(0.05),
160            sample_rate_hz: 48_000.0,
161            coefficients: Coefficients::default(),
162            states: Vec::new(),
163        };
164        filter.update_coefficients();
165        filter
166    }
167
168    /// Creates a low-pass biquad at the given frequency and Q.
169    pub fn low_pass(frequency_hz: f32, q: f32) -> Self {
170        Self::new(BiquadKind::LowPass, frequency_hz, q)
171    }
172
173    /// Creates a high-pass biquad at the given frequency and Q.
174    pub fn high_pass(frequency_hz: f32, q: f32) -> Self {
175        Self::new(BiquadKind::HighPass, frequency_hz, q)
176    }
177
178    /// Creates a band-pass biquad at the given frequency and Q.
179    pub fn band_pass(frequency_hz: f32, q: f32) -> Self {
180        Self::new(BiquadKind::BandPass, frequency_hz, q)
181    }
182
183    /// Creates a notch biquad at the given frequency and Q.
184    pub fn notch(frequency_hz: f32, q: f32) -> Self {
185        Self::new(BiquadKind::Notch, frequency_hz, q)
186    }
187
188    fn update_coefficients(&mut self) {
189        let frequency = clamp_cutoff(self.frequency_hz, self.sample_rate_hz);
190        let omega = 2.0 * PI * frequency / self.sample_rate_hz;
191        let sin = omega.sin();
192        let cos = omega.cos();
193        let alpha = sin / (2.0 * self.q.max(0.05));
194        let (b0, b1, b2, a0, a1, a2) = match self.kind {
195            BiquadKind::LowPass => (
196                (1.0 - cos) * 0.5,
197                1.0 - cos,
198                (1.0 - cos) * 0.5,
199                1.0 + alpha,
200                -2.0 * cos,
201                1.0 - alpha,
202            ),
203            BiquadKind::HighPass => (
204                (1.0 + cos) * 0.5,
205                -(1.0 + cos),
206                (1.0 + cos) * 0.5,
207                1.0 + alpha,
208                -2.0 * cos,
209                1.0 - alpha,
210            ),
211            BiquadKind::BandPass => (alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * cos, 1.0 - alpha),
212            BiquadKind::Notch => (1.0, -2.0 * cos, 1.0, 1.0 + alpha, -2.0 * cos, 1.0 - alpha),
213            BiquadKind::Peaking { gain_db } => {
214                let amp = db_to_gain(gain_db).sqrt();
215                (
216                    1.0 + alpha * amp,
217                    -2.0 * cos,
218                    1.0 - alpha * amp,
219                    1.0 + alpha / amp,
220                    -2.0 * cos,
221                    1.0 - alpha / amp,
222                )
223            }
224        };
225        self.coefficients = Coefficients {
226            b0: b0 / a0,
227            b1: b1 / a0,
228            b2: b2 / a0,
229            a1: a1 / a0,
230            a2: a2 / a0,
231        };
232    }
233}
234
235impl Processor for BiquadFilter {
236    fn prepare(&mut self, cfg: PrepareConfig) {
237        self.sample_rate_hz = cfg.sample_rate_hz as f32;
238        self.update_coefficients();
239        prepare_channels(
240            &mut self.states,
241            cfg.out_channels as usize,
242            BiquadState::default(),
243        );
244    }
245
246    fn reset(&mut self) {
247        self.states.fill(BiquadState::default());
248    }
249
250    fn process(&mut self, block: &mut ProcessBlock<'_>) {
251        // The audio callback never allocates: `prepare` sized the per-channel
252        // state, so clamp to it rather than grow a wider block in place.
253        let prepared = self.states.len();
254        debug_assert!(
255            output_channels(block) <= prepared,
256            "BiquadFilter::process received more channels than prepare configured"
257        );
258        let channels = output_channels(block).min(prepared);
259        let c = self.coefficients;
260        let frames = block.frames as usize;
261        for channel in 0..channels {
262            let state = &mut self.states[channel];
263            for frame in 0..frames {
264                let input = input_sample(block, channel, frame);
265                let output = c.b0 * input + state.z1;
266                state.z1 = c.b1 * input - c.a1 * output + state.z2;
267                state.z2 = c.b2 * input - c.a2 * output;
268                block.out_audio[channel][frame] = output;
269            }
270        }
271    }
272}
273
274/// Output tap selected from a [`StateVariableFilter`].
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub enum StateVariableMode {
277    /// Low-pass output.
278    LowPass,
279    /// High-pass output.
280    HighPass,
281    /// Band-pass output.
282    BandPass,
283    /// Notch (low + high) output.
284    Notch,
285}
286
287#[derive(Clone, Copy, Debug, Default, PartialEq)]
288struct SvfState {
289    ic1eq: f32,
290    ic2eq: f32,
291}
292
293/// A per-channel zero-delay-feedback state-variable filter [`Processor`].
294#[derive(Clone, Debug, PartialEq)]
295pub struct StateVariableFilter {
296    mode: StateVariableMode,
297    frequency_hz: f32,
298    q: f32,
299    sample_rate_hz: f32,
300    states: Vec<SvfState>,
301}
302
303impl StateVariableFilter {
304    /// Creates a state-variable filter with the given output mode, frequency,
305    /// and Q (clamped to `>= 0.05`).
306    pub fn new(mode: StateVariableMode, frequency_hz: f32, q: f32) -> Self {
307        Self {
308            mode,
309            frequency_hz,
310            q: q.max(0.05),
311            sample_rate_hz: 48_000.0,
312            states: Vec::new(),
313        }
314    }
315
316    fn process_sample(&self, state: &mut SvfState, input: f32) -> f32 {
317        let frequency = clamp_cutoff(self.frequency_hz, self.sample_rate_hz);
318        let g = (PI * frequency / self.sample_rate_hz).tan();
319        let k = 1.0 / self.q.max(0.05);
320        let a1 = 1.0 / (1.0 + g * (g + k));
321        let a2 = g * a1;
322        let a3 = g * a2;
323        let v3 = input - state.ic2eq;
324        let v1 = a1 * state.ic1eq + a2 * v3;
325        let v2 = state.ic2eq + a2 * state.ic1eq + a3 * v3;
326        state.ic1eq = 2.0 * v1 - state.ic1eq;
327        state.ic2eq = 2.0 * v2 - state.ic2eq;
328        let low = v2;
329        let high = input - k * v1 - v2;
330        match self.mode {
331            StateVariableMode::LowPass => low,
332            StateVariableMode::HighPass => high,
333            StateVariableMode::BandPass => v1,
334            StateVariableMode::Notch => low + high,
335        }
336    }
337}
338
339impl Processor for StateVariableFilter {
340    fn prepare(&mut self, cfg: PrepareConfig) {
341        self.sample_rate_hz = cfg.sample_rate_hz as f32;
342        prepare_channels(
343            &mut self.states,
344            cfg.out_channels as usize,
345            SvfState::default(),
346        );
347    }
348
349    fn reset(&mut self) {
350        self.states.fill(SvfState::default());
351    }
352
353    fn process(&mut self, block: &mut ProcessBlock<'_>) {
354        // The audio callback never allocates: `prepare` sized the per-channel
355        // state, so clamp to it rather than grow a wider block in place.
356        let prepared = self.states.len();
357        debug_assert!(
358            output_channels(block) <= prepared,
359            "StateVariableFilter::process received more channels than prepare configured"
360        );
361        let channels = output_channels(block).min(prepared);
362        let frames = block.frames as usize;
363        for channel in 0..channels {
364            for frame in 0..frames {
365                let input = input_sample(block, channel, frame);
366                let mut state = self.states[channel];
367                let output = self.process_sample(&mut state, input);
368                self.states[channel] = state;
369                block.out_audio[channel][frame] = output;
370            }
371        }
372    }
373}