// Biquad filter structures rewritten from RBJ's Audio EQ Cookbook // I wanted to rewrite it myself to understand it better and make things clearer // Adapted to rust by Ardura // I'm not using everything here in this Actuate version #![allow(dead_code)] use nih_plug::params::enums::Enum; // This is for my sanity const LEFT: usize = 0; const RIGHT: usize = 1; // These are the filter types implemented #[derive(Clone, Copy, Enum, PartialEq)] pub(crate) enum FilterType { Off, LowPass, HighPass, BandPass, Notch, Peak, LowShelf, HighShelf, } // I wanted these separate from the main struct for readability #[derive(Clone, Copy)] struct BiquadCoefficients { b0: f32, b1: f32, b2: f32, a0: f32, a1: f32, a2: f32, } // This assigns our coefficients when passed the intermediate variables // Nothing to mention here, RBJ has done all the work impl BiquadCoefficients { pub fn new(biquad_type: FilterType, alpha: f32, omega: f32, peak_gain: f32) -> Self { let b0: f32; let b1: f32; let b2: f32; let a0: f32; let a1: f32; let a2: f32; let cos_omega = omega.cos(); let sin_omega = omega.sin(); match biquad_type { FilterType::Off => { b0 = 0.0; b1 = 0.0; b2 = 0.0; a0 = 0.0; a1 = 0.0; a2 = 0.0; } FilterType::LowPass => { b0 = (1.0 - cos_omega) / 2.0; b1 = 1.0 - cos_omega; b2 = (1.0 - cos_omega) / 2.0; a0 = 1.0 + alpha; a1 = -2.0 * cos_omega; a2 = 1.0 - alpha; } FilterType::HighPass => { b0 = (1.0 + cos_omega) / 2.0; b1 = -(1.0 + cos_omega); b2 = (1.0 + cos_omega) / 2.0; a0 = 1.0 + alpha; a1 = -2.0 * cos_omega; a2 = 1.0 - alpha; } FilterType::BandPass => { b0 = sin_omega / 2.0; b1 = 0.0; b2 = -sin_omega / 2.0; a0 = 1.0 + alpha; a1 = -2.0 * cos_omega; a2 = 1.0 - alpha; } FilterType::Notch => { b0 = 1.0; b1 = -2.0 * cos_omega; b2 = 1.0; a0 = 1.0 + alpha; a1 = -2.0 * cos_omega; a2 = 1.0 - alpha; } FilterType::Peak => { let A = (10.0_f32.powf(peak_gain / 40.0)).sqrt(); b0 = 1.0 + alpha * A; b1 = -2.0 * cos_omega; b2 = 1.0 - alpha * A; a0 = 1.0 + alpha / A; a1 = -2.0 * cos_omega; a2 = 1.0 - alpha / A; } FilterType::LowShelf => { let A = (10.0_f32.powf(peak_gain / 40.0)).sqrt(); let sqrt_a_2_alpha = 2.0 * (A).sqrt() * alpha; b0 = A * ((A + 1.0) - (A - 1.0) * cos_omega + sqrt_a_2_alpha); b1 = 2.0 * A * ((A - 1.0) - (A + 1.0) * cos_omega); b2 = A * ((A + 1.0) - (A - 1.0) * cos_omega - sqrt_a_2_alpha); a0 = (A + 1.0) + (A - 1.0) * cos_omega + sqrt_a_2_alpha; a1 = -2.0 * ((A - 1.0) + (A + 1.0) * cos_omega); a2 = (A + 1.0) + (A - 1.0) * cos_omega - sqrt_a_2_alpha; } FilterType::HighShelf => { let A = (10.0_f32.powf(peak_gain / 40.0)).sqrt(); let sqrt_a_2_alpha = 2.0 * (A).sqrt() * alpha; b0 = A * ((A + 1.0) + (A - 1.0) * cos_omega + sqrt_a_2_alpha); b1 = -2.0 * A * ((A - 1.0) + (A + 1.0) * cos_omega); b2 = A * ((A + 1.0) + (A - 1.0) * cos_omega - sqrt_a_2_alpha); a0 = (A + 1.0) - (A - 1.0) * cos_omega + sqrt_a_2_alpha; a1 = 2.0 * ((A - 1.0) - (A + 1.0) * cos_omega); a2 = (A + 1.0) - (A - 1.0) * cos_omega - sqrt_a_2_alpha; } } BiquadCoefficients { b0: b0, b1: b1, b2: b2, a0: a0, a1: a1, a2: a2, } } } // This is the main Biquad struct, once more trying to make things clearer #[derive(Clone, Copy)] pub(crate) struct Biquad { // Main controls for the filter biquad_type: FilterType, sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32, // Tracks previous outputs input_history: [[f32; 2]; 2], output_history: [[f32; 2]; 2], // Coefficients coeffs: BiquadCoefficients, } // This is for interleaving biquad structs - Airwindows inspired // 10 interleave max is just my decision #[derive(Clone, Copy)] pub(crate) struct InterleavedBiquad { interleaves: usize, current_index: usize, biquad_array: [Biquad; 10], } impl Biquad { pub fn new( sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32, biquad_type: FilterType, ) -> Self { let omega = 2.0 * std::f32::consts::PI * center_freq / sample_rate; let alpha = (omega.sin()) / (2.0 * q_factor); Biquad { biquad_type: biquad_type, sample_rate, center_freq, gain_db, q_factor, input_history: [[0.0, 0.0]; 2], output_history: [[0.0, 0.0]; 2], coeffs: BiquadCoefficients::new(biquad_type, alpha, omega, gain_db), } } // This is meant to only recalculate when there's an actual update as this method runs often pub fn update(&mut self, sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32) { let mut recalc = false; if self.sample_rate != sample_rate { self.sample_rate = sample_rate; recalc = true; } if self.center_freq != center_freq { self.center_freq = center_freq; recalc = true; } if self.gain_db != gain_db { self.gain_db = gain_db; recalc = true; } if self.q_factor != q_factor { self.q_factor = q_factor; recalc = true; } if recalc { // Calculate our intermediate variables from our new info and create new coefficients let omega = 2.0 * std::f32::consts::PI * center_freq / sample_rate; let alpha = (omega.sin()) / (2.0 * q_factor); self.coeffs = BiquadCoefficients::new(self.biquad_type, alpha, omega, self.gain_db); } } pub fn set_type(&mut self, biquad_type: FilterType) { if self.biquad_type != biquad_type { self.biquad_type = biquad_type; // Calculate our intermediate variables from our new info and create new coefficients let omega = 2.0 * std::f32::consts::PI * self.center_freq / self.sample_rate; let alpha = (omega.sin()) / (2.0 * self.q_factor); self.coeffs = BiquadCoefficients::new(self.biquad_type, alpha, omega, self.gain_db); } } // I'll handle the oversampling/ordering from the calling thread, I'm trying to K.I.S.S. pub fn process_sample(&mut self, input_l: f32, input_r: f32) -> (f32, f32) { if self.biquad_type == FilterType::Off { return (input_l, input_r); } // Using RBJ's Direct Form I straight from the cookbook let output_l; let output_r; // Calculate our current output for the left side output_l = (self.coeffs.b0 / self.coeffs.a0) * input_l + (self.coeffs.b1 / self.coeffs.a0) * self.input_history[0][LEFT] + (self.coeffs.b2 / self.coeffs.a0) * self.input_history[1][LEFT] - (self.coeffs.a1 / self.coeffs.a0) * self.output_history[0][LEFT] - (self.coeffs.a2 / self.coeffs.a0) * self.output_history[1][LEFT]; // Reassign the history variables self.input_history[1][LEFT] = self.input_history[0][LEFT]; self.input_history[0][LEFT] = input_l; self.output_history[1][LEFT] = self.output_history[0][LEFT]; self.output_history[0][LEFT] = output_l; // Calculate our current output for the right side output_r = (self.coeffs.b0 / self.coeffs.a0) * input_r + (self.coeffs.b1 / self.coeffs.a0) * self.input_history[0][RIGHT] + (self.coeffs.b2 / self.coeffs.a0) * self.input_history[1][RIGHT] - (self.coeffs.a1 / self.coeffs.a0) * self.output_history[0][RIGHT] - (self.coeffs.a2 / self.coeffs.a0) * self.output_history[1][RIGHT]; // Reassign the history variables self.input_history[1][RIGHT] = self.input_history[0][RIGHT]; self.input_history[0][RIGHT] = input_r; self.output_history[1][RIGHT] = self.output_history[0][RIGHT]; self.output_history[0][RIGHT] = output_r; (output_l, output_r) } } impl InterleavedBiquad { pub fn new( sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32, biquad_type: FilterType, new_interleave: usize, ) -> Self { InterleavedBiquad { interleaves: new_interleave, current_index: 0, biquad_array: [Biquad::new(sample_rate, center_freq, gain_db, q_factor, biquad_type); 10], } } pub fn update(&mut self, sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32) { for biquad in self.biquad_array.iter_mut() { biquad.update(sample_rate, center_freq, gain_db, q_factor); } } pub fn set_type(&mut self, biquad_type: FilterType) { for biquad in self.biquad_array.iter_mut() { biquad.set_type(biquad_type); } } pub fn set_interleave(&mut self, new_interleave: usize) { self.interleaves = new_interleave.clamp(2, 10); } pub fn increment_index(&mut self) { // Increment our index self.current_index += 1; if self.current_index >= self.interleaves { self.current_index = 0; } } pub fn process_sample(&mut self, input_l: f32, input_r: f32) -> (f32, f32) { let output_l; let output_r; (output_l, output_r) = self.biquad_array[self.current_index].process_sample(input_l, input_r); // Return (output_l, output_r) } }