From ebe2b241460225b3b8bc4e81af8c6eb553a1f090 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 29 May 2022 16:21:36 +0200 Subject: [PATCH] Add the actual crossovers to the Crossover plugin That feeling when you write a whole bunch of garbage in one without testing it go and it actually works. --- plugins/crossover/src/crossover/iir.rs | 152 +++++++++++++++++++++++++ plugins/crossover/src/lib.rs | 120 +++++++++++++++++-- 2 files changed, 262 insertions(+), 10 deletions(-) diff --git a/plugins/crossover/src/crossover/iir.rs b/plugins/crossover/src/crossover/iir.rs index cd804b5a..06e78312 100644 --- a/plugins/crossover/src/crossover/iir.rs +++ b/plugins/crossover/src/crossover/iir.rs @@ -14,11 +14,163 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use nih_plug::buffer::ChannelSamples; use nih_plug::debug::*; use std::f32::consts; use std::ops::{Add, Mul, Sub}; use std::simd::f32x2; +use crate::NUM_BANDS; + +#[derive(Debug)] +pub struct IirCrossover { + /// The kind of crossover to use. `.update_filters()` must be called after changing this. + mode: IirCrossoverType, + + /// The crossovers. Depending on the number of bands argument passed to `.process()` one to four + /// of these may be used. + crossovers: [Crossover; NUM_BANDS - 1], +} + +/// The type of IIR crossover to use. +#[derive(Debug, Clone, Copy)] +pub enum IirCrossoverType { + /// Clean crossover with 24 dB/octave slopes and one period of delay in the power band. Stacks + /// two Butterworth-style (i.e. $q = \frac{\sqrt{2}}{2}$) filters per crossover. + LinkwitzRiley24, +} + +/// A single crossover using multiple biquads in series to get steeper slopes. This can do both the +/// low-pass and the high-pass parts of the crossover. +#[derive(Debug, Clone, Default)] +struct Crossover { + /// Filters for the low-pass section of the crossover. Not all filters may be used dependign on + /// the crossover type. + lp_filters: [Biquad; 2], + /// Filters for the high-pass section of the crossover. Not all filters may be used dependign on + /// the crossover type. + hp_filters: [Biquad; 2], +} + +impl IirCrossover { + /// Create a new multiband crossover processor. All filters will be configured to pass audio + /// through as it. `.update()` needs to be called first to set up the filters, and `.reset()` + /// can be called whenever the filter state must be cleared. + pub fn new(mode: IirCrossoverType) -> Self { + Self { + mode, + crossovers: Default::default(), + } + } + + /// Split the signal into bands using the crossovers previously configured through `.update()`. + /// The split bands will be written to `band_outputs`. `main_io` is not written to, and should + /// be cleared separately. + pub fn process( + &mut self, + num_bands: usize, + main_io: &ChannelSamples, + mut band_outputs: [ChannelSamples; NUM_BANDS], + ) { + nih_debug_assert!(num_bands >= 2); + nih_debug_assert!(num_bands <= NUM_BANDS); + // Required for the SIMD, so we'll just do a hard assert or the unchecked conversions will + // be unsound + assert!(main_io.len() == 2); + + let mut samples: f32x2 = unsafe { main_io.to_simd_unchecked() }; + match self.mode { + IirCrossoverType::LinkwitzRiley24 => { + for (crossover, band_channel_samples) in self + .crossovers + .iter_mut() + .zip(band_outputs.iter_mut()) + .take(num_bands as usize - 1) + { + let (lp_samples, hp_samples) = crossover.process_lr24(samples); + + unsafe { band_channel_samples.from_simd_unchecked(lp_samples) }; + samples = hp_samples; + } + + // And the final high-passed result should be written to the last band + unsafe { band_outputs[num_bands - 1].from_simd_unchecked(samples) }; + } + } + } + + /// Update the crossover frequencies for all filters. If the frequencies are not monotonic then + /// this function will ensure that they are. + pub fn update(&mut self, sample_rate: f32, mut frequencies: [f32; NUM_BANDS - 1]) { + // Make sure the frequencies are monotonic + for frequency_idx in 1..NUM_BANDS - 1 { + if frequencies[frequency_idx] < frequencies[frequency_idx - 1] { + frequencies[frequency_idx] = frequencies[frequency_idx - 1]; + } + } + + match self.mode { + IirCrossoverType::LinkwitzRiley24 => { + const Q: f32 = std::f32::consts::FRAC_1_SQRT_2; + for (crossover, frequency) in self.crossovers.iter_mut().zip(frequencies) { + let lp_coefs = BiquadCoefficients::lowpass(sample_rate, frequency, Q); + let hp_coefs = BiquadCoefficients::highpass(sample_rate, frequency, Q); + crossover.update_coefficients(lp_coefs, hp_coefs); + } + } + } + } + + /// Reset the internal filter state for all crossovers. + pub fn reset(&mut self) { + for crossover in &mut self.crossovers { + crossover.reset(); + } + } +} + +impl Crossover { + /// Process left and right audio samples through two low-pass and two high-pass filter stages. + /// The resulting tuple contains the low-passed and the high-passed samples. Used for the + /// Linkwitz-Riley 24 dB/octave crossover. + pub fn process_lr24(&mut self, samples: f32x2) -> (f32x2, f32x2) { + let mut low_passed = samples; + for filter in &mut self.lp_filters[..2] { + low_passed = filter.process(low_passed) + } + let mut high_passed = samples; + for filter in &mut self.hp_filters[..2] { + high_passed = filter.process(high_passed) + } + + (low_passed, high_passed) + } + + /// Update the coefficients for all filters in the crossover. + pub fn update_coefficients( + &mut self, + lp_coefs: BiquadCoefficients, + hp_coefs: BiquadCoefficients, + ) { + for filter in &mut self.lp_filters { + filter.coefficients = lp_coefs; + } + for filter in &mut self.hp_filters { + filter.coefficients = hp_coefs; + } + } + + /// Reset the internal filter state. + pub fn reset(&mut self) { + for filter in &mut self.lp_filters { + filter.reset(); + } + for filter in &mut self.hp_filters { + filter.reset(); + } + } +} + /// A simple biquad filter with functions for generating coefficients for second order low-pass and /// high-pass filters. Since these filters have 3 dB of attenuation at the center frequency, we'll /// two of them in series to get 6 dB of attenutation at the crossover point for the LR24 diff --git a/plugins/crossover/src/lib.rs b/plugins/crossover/src/lib.rs index 49f9348b..65d3a2fd 100644 --- a/plugins/crossover/src/lib.rs +++ b/plugins/crossover/src/lib.rs @@ -19,16 +19,26 @@ #[cfg(not(feature = "simd"))] compile_error!("Compiling without SIMD support is currently not supported"); +use crossover::iir::{IirCrossover, IirCrossoverType}; use nih_plug::prelude::*; use std::sync::Arc; mod crossover; +/// The number of bands. Not used directly here, but this avoids hardcoding some constants in the +/// crossover implementations. +pub const NUM_BANDS: usize = 5; + const MIN_CROSSOVER_FREQUENCY: f32 = 40.0; const MAX_CROSSOVER_FREQUENCY: f32 = 20_000.0; struct Crossover { params: Arc, + + buffer_config: BufferConfig, + + /// Provides the LR24 crossover. + iir_crossover: IirCrossover, } // TODO: Add multiple crossover types. Haven't added the control for that yet because the current @@ -65,7 +75,14 @@ impl Default for CrossoverParams { let crossover_string_to_value = formatters::s2v_f32_hz_then_khz(); Self { - num_bands: IntParam::new("Band Count", 2, IntRange::Linear { min: 2, max: 5 }), + num_bands: IntParam::new( + "Band Count", + 2, + IntRange::Linear { + min: 2, + max: NUM_BANDS as i32, + }, + ), // TODO: More sensible default frequencies crossover_1_freq: FloatParam::new("Crossover 1", 200.0, crossover_range) .with_smoother(crossover_smoothing_style) @@ -91,6 +108,15 @@ impl Default for Crossover { fn default() -> Self { Crossover { params: Arc::new(CrossoverParams::default()), + + buffer_config: BufferConfig { + sample_rate: 1.0, + min_buffer_size: None, + max_buffer_size: 0, + process_mode: ProcessMode::Realtime, + }, + + iir_crossover: IirCrossover::new(IirCrossoverType::LinkwitzRiley24), } } } @@ -134,35 +160,109 @@ impl Plugin for Crossover { fn initialize( &mut self, _bus_config: &BusConfig, - _buffer_config: &BufferConfig, + buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { - // TODO: Setup filters + self.buffer_config = *buffer_config; + + // Make sure the filter states match the current parameters + self.update_filters(); + true } fn reset(&mut self) { - // TODO: Reset filters + self.iir_crossover.reset(); } fn process( &mut self, buffer: &mut Buffer, - _aux: &mut AuxiliaryBuffers, + aux: &mut AuxiliaryBuffers, _context: &mut impl ProcessContext, ) -> ProcessStatus { - // TODO: Do the splitty thing + let aux_outputs = &mut aux.outputs; + let (band_1_buffer, aux_outputs) = aux_outputs.split_first_mut().unwrap(); + let (band_2_buffer, aux_outputs) = aux_outputs.split_first_mut().unwrap(); + let (band_3_buffer, aux_outputs) = aux_outputs.split_first_mut().unwrap(); + let (band_4_buffer, aux_outputs) = aux_outputs.split_first_mut().unwrap(); + let (band_5_buffer, _) = aux_outputs.split_first_mut().unwrap(); - // The main output should be silent as the signal is already evenly split over the other - // bands - for channel_slice in buffer.as_slice() { - channel_slice.fill(0.0); + // Snoclists for days + for ( + ( + ( + ((main_channel_samples, band_1_channel_samples), band_2_channel_samples), + band_3_channel_samples, + ), + band_4_channel_samples, + ), + band_5_channel_samples, + ) in buffer + .iter_samples() + .zip(band_1_buffer.iter_samples()) + .zip(band_2_buffer.iter_samples()) + .zip(band_3_buffer.iter_samples()) + .zip(band_4_buffer.iter_samples()) + .zip(band_5_buffer.iter_samples()) + { + // We can avoid a lot of hardcoding and conditionals by restoring the original array structure + let bands = [ + band_1_channel_samples, + band_2_channel_samples, + band_3_channel_samples, + band_4_channel_samples, + band_5_channel_samples, + ]; + + // Only update the filters when needed + self.maybe_update_filters(); + + self.iir_crossover.process( + self.params.num_bands.value as usize, + &main_channel_samples, + bands, + ); + + // The main output should be silent as the signal is already evenly split over the other + // bands + for sample in main_channel_samples { + *sample = 0.0; + } } ProcessStatus::Normal } } +impl Crossover { + /// Update the filter coefficients for the crossovers, but only if it's needed. + fn maybe_update_filters(&mut self) { + if self.params.crossover_1_freq.smoothed.is_smoothing() + || self.params.crossover_2_freq.smoothed.is_smoothing() + || self.params.crossover_3_freq.smoothed.is_smoothing() + || self.params.crossover_4_freq.smoothed.is_smoothing() + { + self.update_filters(); + } + } + + /// Update the filter coefficients for the crossovers. + fn update_filters(&mut self) { + // This function will take care of non-monotonic crossover frequencies for us, e.g. + // crossover 2 being lower than crossover 1 + self.iir_crossover.update( + self.buffer_config.sample_rate, + [ + self.params.crossover_1_freq.smoothed.next(), + self.params.crossover_2_freq.smoothed.next(), + self.params.crossover_3_freq.smoothed.next(), + self.params.crossover_4_freq.smoothed.next(), + ], + ) + } +} + impl ClapPlugin for Crossover { const CLAP_ID: &'static str = "nl.robbertvanderhelm.crossover"; const CLAP_DESCRIPTION: &'static str = "Cleanly split a signal into multiple bands";