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";