1
0
Fork 0

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.
This commit is contained in:
Robbert van der Helm 2022-05-29 16:21:36 +02:00
parent 33120ecfe7
commit ebe2b24146
2 changed files with 262 additions and 10 deletions

View file

@ -14,11 +14,163 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<f32x2>; 2],
/// Filters for the high-pass section of the crossover. Not all filters may be used dependign on
/// the crossover type.
hp_filters: [Biquad<f32x2>; 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<f32x2>,
hp_coefs: BiquadCoefficients<f32x2>,
) {
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

View file

@ -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<CrossoverParams>,
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";