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
2 changed files with 262 additions and 10 deletions
|
@ -14,11 +14,163 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use nih_plug::buffer::ChannelSamples;
|
||||||
use nih_plug::debug::*;
|
use nih_plug::debug::*;
|
||||||
use std::f32::consts;
|
use std::f32::consts;
|
||||||
use std::ops::{Add, Mul, Sub};
|
use std::ops::{Add, Mul, Sub};
|
||||||
use std::simd::f32x2;
|
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
|
/// 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
|
/// 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
|
/// 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"))]
|
#[cfg(not(feature = "simd"))]
|
||||||
compile_error!("Compiling without SIMD support is currently not supported");
|
compile_error!("Compiling without SIMD support is currently not supported");
|
||||||
|
|
||||||
|
use crossover::iir::{IirCrossover, IirCrossoverType};
|
||||||
use nih_plug::prelude::*;
|
use nih_plug::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod crossover;
|
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 MIN_CROSSOVER_FREQUENCY: f32 = 40.0;
|
||||||
const MAX_CROSSOVER_FREQUENCY: f32 = 20_000.0;
|
const MAX_CROSSOVER_FREQUENCY: f32 = 20_000.0;
|
||||||
|
|
||||||
struct Crossover {
|
struct Crossover {
|
||||||
params: Arc<CrossoverParams>,
|
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
|
// 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();
|
let crossover_string_to_value = formatters::s2v_f32_hz_then_khz();
|
||||||
|
|
||||||
Self {
|
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
|
// TODO: More sensible default frequencies
|
||||||
crossover_1_freq: FloatParam::new("Crossover 1", 200.0, crossover_range)
|
crossover_1_freq: FloatParam::new("Crossover 1", 200.0, crossover_range)
|
||||||
.with_smoother(crossover_smoothing_style)
|
.with_smoother(crossover_smoothing_style)
|
||||||
|
@ -91,6 +108,15 @@ impl Default for Crossover {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Crossover {
|
Crossover {
|
||||||
params: Arc::new(CrossoverParams::default()),
|
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(
|
fn initialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
_bus_config: &BusConfig,
|
_bus_config: &BusConfig,
|
||||||
_buffer_config: &BufferConfig,
|
buffer_config: &BufferConfig,
|
||||||
_context: &mut impl InitContext,
|
_context: &mut impl InitContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// TODO: Setup filters
|
self.buffer_config = *buffer_config;
|
||||||
|
|
||||||
|
// Make sure the filter states match the current parameters
|
||||||
|
self.update_filters();
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
// TODO: Reset filters
|
self.iir_crossover.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(
|
fn process(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &mut Buffer,
|
buffer: &mut Buffer,
|
||||||
_aux: &mut AuxiliaryBuffers,
|
aux: &mut AuxiliaryBuffers,
|
||||||
_context: &mut impl ProcessContext,
|
_context: &mut impl ProcessContext,
|
||||||
) -> ProcessStatus {
|
) -> 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
|
// Snoclists for days
|
||||||
// bands
|
for (
|
||||||
for channel_slice in buffer.as_slice() {
|
(
|
||||||
channel_slice.fill(0.0);
|
(
|
||||||
|
((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
|
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 {
|
impl ClapPlugin for Crossover {
|
||||||
const CLAP_ID: &'static str = "nl.robbertvanderhelm.crossover";
|
const CLAP_ID: &'static str = "nl.robbertvanderhelm.crossover";
|
||||||
const CLAP_DESCRIPTION: &'static str = "Cleanly split a signal into multiple bands";
|
const CLAP_DESCRIPTION: &'static str = "Cleanly split a signal into multiple bands";
|
||||||
|
|
Loading…
Add table
Reference in a new issue