Skip to main content

rustriff_lib/services/processors/tone_stack/
biquad.rs

1use std::f32::consts::PI;
2
3/// Types of shelf filters supported by the biquad filter.
4pub enum ShelfType {
5    /// Low shelf filter, boosts or cuts frequencies below the cutoff.
6    Low,
7    /// High shelf filter, boosts or cuts frequencies above the cutoff.
8    High,
9    /// Peak filter, boosts or cuts a narrow band of frequencies around the center.
10    Peak,
11}
12
13/// A biquad filter implementation for audio equalization.
14///
15/// This struct implements a second-order IIR filter using the biquad algorithm.
16/// It maintains internal state (x1, x2, y1, y2) for processing continuous audio streams.
17/// The filter coefficients are calculated based on the RBJ Audio EQ Cookbook formulas.
18///
19/// The filter supports shelf and peak responses for tone stack equalization.
20pub struct Biquad {
21    /// Feedforward coefficient b0.
22    b0: f32,
23    /// Feedforward coefficient b1.
24    b1: f32,
25    /// Feedforward coefficient b2.
26    b2: f32,
27    /// Feedback coefficient a1.
28    a1: f32,
29    /// Feedback coefficient a2.
30    a2: f32,
31    /// Previous input sample x[n-1].
32    x1: f32,
33    /// Previous previous input sample x[n-2].
34    x2: f32,
35    /// Previous output sample y[n-1].
36    y1: f32,
37    /// Previous previous output sample y[n-2].
38    y2: f32,
39    /// Sample rate of the audio signal.
40    sample_rate: f32,
41    /// Center/cutoff frequency of the filter.
42    freq: f32,
43    /// Type of shelf filter.
44    shelf_type: ShelfType,
45}
46
47// Biquad shelf filter implementation based on RBJ Audio EQ Cookbook
48// Reference: https://www.w3.org/2011/audio/audio-eq-cookbook.html
49impl Biquad {
50    /// Creates a new biquad shelf filter with the specified parameters.
51    ///
52    /// # Arguments
53    ///
54    /// * `shelf` - The type of shelf filter (Low, High, or Peak).
55    /// * `sample_rate` - The sample rate of the audio signal (e.g., 44100.0).
56    /// * `freq` - The center/cutoff frequency in Hz.
57    /// * `gain_db` - The gain in decibels (positive for boost, negative for cut).
58    ///
59    /// # Returns
60    ///
61    /// A new `Biquad` instance configured with the calculated coefficients.
62    pub fn new_shelf(shelf: ShelfType, sample_rate: f32, freq: f32, gain_db: f32) -> Self {
63        let (b0, b1, b2, _a0, a1, a2) =
64            Self::calculate_coefficients(&shelf, sample_rate, freq, gain_db);
65
66        Self {
67            b0,
68            b1,
69            b2,
70            a1,
71            a2,
72            x1: 0.0,
73            x2: 0.0,
74            y1: 0.0,
75            y2: 0.0,
76            sample_rate,
77            freq,
78            shelf_type: shelf,
79        }
80    }
81
82    /// Processes a single audio sample through the biquad filter.
83    ///
84    /// This method applies the filter to the input sample and updates the internal state
85    /// for the next sample. It should be called for each sample in sequence.
86    ///
87    /// # Arguments
88    ///
89    /// * `x` - The input audio sample.
90    ///
91    /// # Returns
92    ///
93    /// The filtered output sample.
94    pub fn process(&mut self, x: f32) -> f32 {
95        let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2
96            - self.a1 * self.y1
97            - self.a2 * self.y2;
98        self.x2 = self.x1;
99        self.x1 = x;
100        self.y2 = self.y1;
101        self.y1 = y;
102        y
103    }
104
105    /// Updates the gain of the biquad filter.
106    ///
107    /// Recalculates the filter coefficients based on the new gain value while keeping
108    /// the same frequency and shelf type. The internal state is preserved.
109    ///
110    /// # Arguments
111    ///
112    /// * `gain_db` - The new gain in decibels.
113    pub fn set_gain_db(&mut self, gain_db: f32) {
114        let (b0, b1, b2, _a0, a1, a2) =
115            Self::calculate_coefficients(&self.shelf_type, self.sample_rate, self.freq, gain_db);
116
117        self.b0 = b0;
118        self.b1 = b1;
119        self.b2 = b2;
120        self.a1 = a1;
121        self.a2 = a2;
122        /*
123        println!(
124            "Updated Biquad Coefficients: b0: {:.6}\t  b1: {:.6}\t  b2: {:.6}\t  a1: {:.6}\t  a2: {:.6}\t  a0: {:.6} (should always be 1.0) || Gain (dB): {:.2}",
125            self.b0, self.b1, self.b2, self.a1, self.a2, a0, gain_db
126        );
127         */
128    }
129
130    /// Calculates the biquad filter coefficients based on the RBJ Audio EQ Cookbook.
131    ///
132    /// This function computes the feedforward (b0, b1, b2) and feedback (a0, a1, a2) coefficients
133    /// for the specified filter type, sample rate, frequency, and gain. The coefficients are
134    /// normalized so that a0 = 1.0.
135    ///
136    /// # Arguments
137    ///
138    /// * `shelf` - The type of shelf filter.
139    /// * `sample_rate` - The sample rate of the audio signal.
140    /// * `freq` - The center/cutoff frequency in Hz.
141    /// * `gain_db` - The gain in decibels.
142    ///
143    /// # Returns
144    ///
145    /// A tuple of normalized coefficients: (b0, b1, b2, a0, a1, a2) where a0 is always 1.0.
146    /// Reference: https://www.w3.org/2011/audio/audio-eq-cookbook.html
147    fn calculate_coefficients(
148        shelf: &ShelfType,
149        sample_rate: f32,
150        freq: f32,
151        gain_db: f32,
152    ) -> (f32, f32, f32, f32, f32, f32) {
153        let a = 10.0_f32.powf(gain_db / 40.0);
154        let w0 = 2.0 * PI * freq / sample_rate;
155        let cos = w0.cos();
156        let sin = w0.sin();
157
158        let (b0, b1, b2, a0, a1, a2) = match shelf {
159            ShelfType::Low | ShelfType::High => {
160                let alpha = sin / 16.0 * (2.0 * (a + 1.0 / a)).sqrt();
161                let sqrt_a = a.sqrt();
162                if matches!(shelf, ShelfType::Low) {
163                    (
164                        a * ((a + 1.0) - (a - 1.0) * cos + 2.0 * sqrt_a * alpha),
165                        2.0 * a * ((a - 1.0) - (a + 1.0) * cos),
166                        a * ((a + 1.0) - (a - 1.0) * cos - 2.0 * sqrt_a * alpha),
167                        (a + 1.0) + (a - 1.0) * cos + 2.0 * sqrt_a * alpha,
168                        -2.0 * ((a - 1.0) + (a + 1.0) * cos),
169                        (a + 1.0) + (a - 1.0) * cos - 2.0 * sqrt_a * alpha,
170                    )
171                } else {
172                    (
173                        a * ((a + 1.0) + (a - 1.0) * cos + 2.0 * sqrt_a * alpha),
174                        -2.0 * a * ((a - 1.0) + (a + 1.0) * cos),
175                        a * ((a + 1.0) + (a - 1.0) * cos - 2.0 * sqrt_a * alpha),
176                        (a + 1.0) - (a - 1.0) * cos + 2.0 * sqrt_a * alpha,
177                        2.0 * ((a - 1.0) - (a + 1.0) * cos),
178                        (a + 1.0) - (a - 1.0) * cos - 2.0 * sqrt_a * alpha,
179                    )
180                }
181            }
182            ShelfType::Peak => {
183                let alpha = sin / 8.0;
184                (
185                    1.0 + alpha * a,
186                    -2.0 * cos,
187                    1.0 - alpha * a,
188                    1.0 + alpha / a,
189                    -2.0 * cos,
190                    1.0 - alpha / a,
191                )
192            }
193        };
194        //Normalize Coefficients so that a0 is 1
195        (b0 / a0, b1 / a0, b2 / a0, 1.0, a1 / a0, a2 / a0)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[cfg(test)]
204    mod success_path {
205        use super::*;
206
207        #[test]
208        fn test_process_single_sample() {
209            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 0.0);
210            let input = 0.5;
211            let output = biquad.process(input);
212            assert!(!output.is_nan());
213            assert!(!output.is_infinite());
214        }
215
216        #[test]
217        fn test_process_multiple_samples() {
218            let mut biquad = Biquad::new_shelf(ShelfType::Low, 44100.0, 200.0, 6.0);
219            let samples = vec![0.1, 0.2, 0.3, -0.1, -0.2];
220
221            for sample in samples {
222                let output = biquad.process(sample);
223                assert!(!output.is_nan());
224                assert!(!output.is_infinite());
225                // Verify state is being updated
226                assert_eq!(biquad.x1, sample);
227            }
228        }
229
230        #[test]
231        fn test_set_gain_db() {
232            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 0.0);
233            let initial_b0 = biquad.b0;
234
235            biquad.set_gain_db(6.0);
236            assert_ne!(biquad.b0, initial_b0);
237            assert!(!biquad.b0.is_nan());
238            assert!(!biquad.b0.is_infinite());
239        }
240
241        #[test]
242        fn test_gain_zero_db_peak_filter() {
243            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 0.0);
244            let input = 1.0;
245            let output = biquad.process(input);
246            // With 0dB gain on first sample, should be close to input
247            assert!(output.abs() < 1.5);
248        }
249
250        #[test]
251        fn test_positive_gain_modification() {
252            let mut biquad = Biquad::new_shelf(ShelfType::Low, 44100.0, 100.0, 0.0);
253            biquad.set_gain_db(12.0);
254            let input = 1.0;
255
256            let output = biquad.process(input);
257            assert!(!output.is_nan());
258            assert!(!output.is_infinite());
259            assert!(output.abs() > input);
260        }
261
262        #[test]
263        fn test_negative_gain_modification() {
264            let mut biquad = Biquad::new_shelf(ShelfType::High, 44100.0, 8000.0, 0.0);
265            biquad.set_gain_db(-12.0);
266            let input = 1.0;
267
268            let output = biquad.process(input);
269            assert!(!output.is_nan());
270            assert!(!output.is_infinite());
271            assert!(output.abs() < input);
272        }
273
274        #[test]
275        fn test_state_update_after_process() {
276            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 6.0);
277
278            biquad.process(0.5);
279            assert_eq!(biquad.x1, 0.5);
280            assert_eq!(biquad.x2, 0.0);
281
282            biquad.process(0.3);
283            assert_eq!(biquad.x1, 0.3);
284            assert_eq!(biquad.x2, 0.5);
285        }
286
287        #[test]
288        fn test_different_sample_rates() {
289            let sample_rates = vec![22050.0, 44100.0, 48000.0, 96000.0];
290
291            for sr in sample_rates {
292                let biquad = Biquad::new_shelf(ShelfType::Peak, sr, 1000.0, 6.0);
293                assert_eq!(biquad.sample_rate, sr);
294            }
295        }
296
297        #[test]
298        fn test_different_frequencies() {
299            let frequencies = vec![20.0, 100.0, 1000.0, 10000.0, 20000.0];
300
301            for freq in frequencies {
302                let biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, freq, 6.0);
303                assert_eq!(biquad.freq, freq);
304            }
305        }
306    }
307    #[cfg(test)]
308    mod failure_path {
309        use super::*;
310
311        #[test]
312        fn test_extreme_frequency_high() {
313            let biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 22000.0, 6.0);
314            assert!(!biquad.b0.is_nan());
315            assert!(!biquad.b0.is_infinite());
316        }
317
318        #[test]
319        fn test_extreme_frequency_low() {
320            let biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1.0, 6.0);
321            assert!(!biquad.b0.is_nan());
322            assert!(!biquad.b0.is_infinite());
323        }
324
325        #[test]
326        fn test_very_high_gain() {
327            let biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 48.0);
328            assert!(!biquad.b0.is_nan());
329            assert!(!biquad.b0.is_infinite());
330        }
331
332        #[test]
333        fn test_very_negative_gain() {
334            let biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, -48.0);
335            assert!(!biquad.b0.is_nan());
336            assert!(!biquad.b0.is_infinite());
337        }
338
339        #[test]
340        fn test_process_zero_input() {
341            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 6.0);
342            let output = biquad.process(0.0);
343            assert!(!output.is_nan());
344            assert!(!output.is_infinite());
345        }
346
347        #[test]
348        fn test_process_very_small_input() {
349            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 6.0);
350            let output = biquad.process(1e-6);
351            assert!(!output.is_nan());
352            assert!(!output.is_infinite());
353        }
354
355        #[test]
356        fn test_process_large_input() {
357            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 6.0);
358            let output = biquad.process(100.0);
359            assert!(!output.is_nan());
360            assert!(!output.is_infinite());
361        }
362
363        #[test]
364        fn test_all_shelf_types_with_extreme_values() {
365            let shelf_types = vec![ShelfType::Low, ShelfType::High, ShelfType::Peak];
366
367            for shelf_type in shelf_types {
368                let biquad = Biquad::new_shelf(shelf_type, 44100.0, 5000.0, 24.0);
369                assert!(!biquad.b0.is_nan());
370                assert!(!biquad.b1.is_nan());
371                assert!(!biquad.b2.is_nan());
372                assert!(!biquad.a1.is_nan());
373                assert!(!biquad.a2.is_nan());
374            }
375        }
376
377        #[test]
378        fn test_set_gain_with_extreme_values() {
379            let mut biquad = Biquad::new_shelf(ShelfType::Peak, 44100.0, 1000.0, 0.0);
380
381            biquad.set_gain_db(36.0);
382            assert!(!biquad.b0.is_nan());
383
384            biquad.set_gain_db(-36.0);
385            assert!(!biquad.b0.is_nan());
386        }
387
388        #[test]
389        fn test_process_after_multiple_gain_changes() {
390            let mut biquad = Biquad::new_shelf(ShelfType::Low, 44100.0, 100.0, 0.0);
391
392            biquad.set_gain_db(6.0);
393            let out1 = biquad.process(0.1);
394
395            biquad.set_gain_db(-6.0);
396            let out2 = biquad.process(0.1);
397
398            biquad.set_gain_db(12.0);
399            let out3 = biquad.process(0.1);
400
401            assert!(!out1.is_nan() && !out2.is_nan() && !out3.is_nan());
402        }
403
404        #[test]
405        fn test_low_sample_rate() {
406            let biquad = Biquad::new_shelf(ShelfType::Peak, 8000.0, 500.0, 6.0);
407            assert!(!biquad.b0.is_nan());
408            assert!(!biquad.b0.is_infinite());
409        }
410
411        #[test]
412        fn test_high_sample_rate() {
413            let biquad = Biquad::new_shelf(ShelfType::Peak, 192000.0, 50000.0, 6.0);
414            assert!(!biquad.b0.is_nan());
415            assert!(!biquad.b0.is_infinite());
416        }
417    }
418}