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:
parent
33120ecfe7
commit
ebe2b24146
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue