From 43980ec459e2819dbe8fe7369521d3b21e4dd422 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 7 Jun 2022 00:39:18 +0200 Subject: [PATCH] Add the function to design the FIR band filters Using the previously added biquad->linear phase FIR conversion function. --- plugins/crossover/src/crossover/fir.rs | 86 +++++++++++++++++++++++++- plugins/crossover/src/crossover/iir.rs | 3 +- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/plugins/crossover/src/crossover/fir.rs b/plugins/crossover/src/crossover/fir.rs index 08b090e6..6e3a4a55 100644 --- a/plugins/crossover/src/crossover/fir.rs +++ b/plugins/crossover/src/crossover/fir.rs @@ -19,7 +19,7 @@ use nih_plug::debug::*; use std::f32; use std::simd::{f32x2, StdFloat}; -use crate::biquad::{Biquad, BiquadCoefficients}; +use crate::biquad::{Biquad, BiquadCoefficients, NEUTRAL_Q}; use crate::NUM_BANDS; // TODO: These filters would be more efficient when processing four samples at a time instead of @@ -145,7 +145,8 @@ impl FirCrossover { } } - /// Update the crossover frequencies for all filters. + /// Update the crossover frequencies for all filters. `num_bands` is assumed to be in `[2, + /// NUM_BANDS]`. pub fn update( &mut self, sample_rate: f32, @@ -153,7 +154,86 @@ impl FirCrossover { frequencies: [f32; NUM_BANDS - 1], ) { match self.mode { - FirCrossoverType::LinkwitzRiley24LinearPhase => todo!(), + FirCrossoverType::LinkwitzRiley24LinearPhase => { + // The goal here is to design 2-5 filters with the same frequency response + // magnitudes as the split bands in the IIR LR24 crossover version with the same + // center frequencies would have. The algorithm works in two stages. First, the IIR + // low-pass filters for the 1-4 crossovers used in the equivalent IIR LR24 version + // are computed and converted to equivalent linear-phase FIR filters using the + // algorithm described below in `FirCoefficients`. Then these are used to build the + // coefficients for the 2-5 bands: + // + // - The first band is always simply the first band's + // low-pass filter. + // - The middle bands are band-pass filters. These are created by taking the next + // crossover's low-pass filter and subtracting the accumulated band impulse + // response up to that point. The accumulated band impulse response is initialized + // with the first band's low-pass filter, and the band-pass filter for every band + // after that gets added to it. + // - The final band is a high-pass filter that's computed through spectral inversion + // from the accumulated band impulse response. + + // As explained above, we'll start with the low-pass band + nih_debug_assert!(num_bands >= 2); + let iir_coefs = BiquadCoefficients::lowpass(sample_rate, frequencies[0], NEUTRAL_Q); + let lp_fir_coefs = + FirCoefficients::design_fourth_order_linear_phase_low_pass_from_biquad( + iir_coefs, + ); + self.band_filters[0].coefficients = lp_fir_coefs; + + // For the band-pass filters and the final high-pass filter, we need to keep track + // of the accumulated impulse response + let mut accumulated_ir = self.band_filters[0].coefficients.clone(); + for (split_frequency, band_filter) in frequencies + .iter() + .zip(self.band_filters.iter_mut()) + // There are `num_bands` bands, so there are `num_bands - 1` crossovers. The + // last band is formed from the accumulated impulse response. + .take(num_bands - 1) + // And the first band is already taken care of + .skip(1) + { + let iir_coefs = + BiquadCoefficients::lowpass(sample_rate, *split_frequency, NEUTRAL_Q); + let lp_fir_coefs = + FirCoefficients::design_fourth_order_linear_phase_low_pass_from_biquad( + iir_coefs, + ); + + // We want the band between the accumulated frequency response and the next + // crossover's low-pass filter + let mut fir_bp_coefs = lp_fir_coefs; + for (bp_coef, accumulated_coef) in + fir_bp_coefs.0.iter_mut().zip(accumulated_ir.0.iter_mut()) + { + // At this poing `bp_coef` is the low-pass filter + *bp_coef -= *accumulated_coef; + + // And the accumulated coefficients for the next band/for the high-pass + // filter should contain this band-pass filter. This becomes a bit weirder + // to read when it's a single loop, but essentially this is what's going on + // here: + // + // fir_bp_coefs = fir_lp_coefs - accumulated_ir + // accumulated_ir += fir_bp_coefs + + *accumulated_coef += *bp_coef; + } + + band_filter.coefficients = fir_bp_coefs; + } + + // And finally we can do a spectral inversion of the accumulated IR to the the last + // band's high-pass filter + let mut fir_hp_coefs = accumulated_ir; + for coef in fir_hp_coefs.0.iter_mut() { + *coef = -*coef; + } + fir_hp_coefs.0[FILTER_SIZE / 2] += f32x2::splat(1.0); + + self.band_filters[num_bands].coefficients = fir_hp_coefs; + } } } diff --git a/plugins/crossover/src/crossover/iir.rs b/plugins/crossover/src/crossover/iir.rs index f668f17e..5c9c90cd 100644 --- a/plugins/crossover/src/crossover/iir.rs +++ b/plugins/crossover/src/crossover/iir.rs @@ -126,7 +126,8 @@ impl IirCrossover { } } - /// Update the crossover frequencies for all filters. + /// Update the crossover frequencies for all filters. `num_bands` is assumed to be in `[2, + /// NUM_BANDS]`. pub fn update( &mut self, sample_rate: f32,