diff --git a/plugins/crossover/src/biquad.rs b/plugins/crossover/src/biquad.rs new file mode 100644 index 00000000..4fb1e9a9 --- /dev/null +++ b/plugins/crossover/src/biquad.rs @@ -0,0 +1,196 @@ +// Crossover: clean crossovers as a multi-out plugin +// Copyright (C) 2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use nih_plug::debug::*; + +use std::f32::consts; +use std::ops::{Add, Mul, Sub}; +use std::simd::f32x2; + +/// 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 +/// crossovers. +/// +/// Based on . +/// +/// The type parameter T should be either an `f32` or a SIMD type. +#[derive(Clone, Copy, Debug)] +pub struct Biquad { + pub coefficients: BiquadCoefficients, + s1: T, + s2: T, +} + +/// The coefficients `[b0, b1, b2, a1, a2]` for [`Biquad`]. These coefficients are all +/// prenormalized, i.e. they have been divided by `a0`. +/// +/// The type parameter T should be either an `f32` or a SIMD type. +#[derive(Clone, Copy, Debug)] +pub struct BiquadCoefficients { + b0: T, + b1: T, + b2: T, + a1: T, + a2: T, +} + +/// Either an `f32` or some SIMD vector type of `f32`s that can be used with our biquads. +pub trait SimdType: + Mul + Sub + Add + Copy + Sized +{ + fn from_f32(value: f32) -> Self; +} + +impl Default for Biquad { + /// Before setting constants the filter should just act as an identity function. + fn default() -> Self { + Self { + coefficients: BiquadCoefficients::identity(), + s1: T::from_f32(0.0), + s2: T::from_f32(0.0), + } + } +} + +impl Biquad { + /// Process a single sample. + pub fn process(&mut self, sample: T) -> T { + let result = self.coefficients.b0 * sample + self.s1; + + self.s1 = self.coefficients.b1 * sample - self.coefficients.a1 * result + self.s2; + self.s2 = self.coefficients.b2 * sample - self.coefficients.a2 * result; + + result + } + + /// Reset the state to zero, useful after making making large, non-interpolatable changes to the + /// filter coefficients. + pub fn reset(&mut self) { + self.s1 = T::from_f32(0.0); + self.s2 = T::from_f32(0.0); + } +} + +impl BiquadCoefficients { + /// Convert scalar coefficients into the correct vector type. + pub fn from_f32s(scalar: BiquadCoefficients) -> Self { + Self { + b0: T::from_f32(scalar.b0), + b1: T::from_f32(scalar.b1), + b2: T::from_f32(scalar.b2), + a1: T::from_f32(scalar.a1), + a2: T::from_f32(scalar.a2), + } + } + + /// Filter coefficients that would cause the sound to be passed through as is. + pub fn identity() -> Self { + Self::from_f32s(BiquadCoefficients { + b0: 1.0, + b1: 0.0, + b2: 0.0, + a1: 0.0, + a2: 0.0, + }) + } + + /// Compute the coefficients for a low-pass filter. + /// + /// Based on . + pub fn lowpass(sample_rate: f32, frequency: f32, q: f32) -> Self { + nih_debug_assert!(sample_rate > 0.0); + nih_debug_assert!(frequency > 0.0); + nih_debug_assert!(frequency < sample_rate / 2.0); + nih_debug_assert!(q > 0.0); + + let omega0 = consts::TAU * (frequency / sample_rate); + let cos_omega0 = omega0.cos(); + let alpha = omega0.sin() / (2.0 * q); + + // We'll prenormalize everything with a0 + let a0 = 1.0 + alpha; + let b0 = ((1.0 - cos_omega0) / 2.0) / a0; + let b1 = (1.0 - cos_omega0) / a0; + let b2 = ((1.0 - cos_omega0) / 2.0) / a0; + let a1 = (-2.0 * cos_omega0) / a0; + let a2 = (1.0 - alpha) / a0; + + Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) + } + + /// Compute the coefficients for a high-pass filter. + /// + /// Based on . + pub fn highpass(sample_rate: f32, frequency: f32, q: f32) -> Self { + nih_debug_assert!(sample_rate > 0.0); + nih_debug_assert!(frequency > 0.0); + nih_debug_assert!(frequency < sample_rate / 2.0); + nih_debug_assert!(q > 0.0); + + let omega0 = consts::TAU * (frequency / sample_rate); + let cos_omega0 = omega0.cos(); + let alpha = omega0.sin() / (2.0 * q); + + // We'll prenormalize everything with a0 + let a0 = 1.0 + alpha; + let b0 = ((1.0 + cos_omega0) / 2.0) / a0; + let b1 = -(1.0 + cos_omega0) / a0; + let b2 = ((1.0 + cos_omega0) / 2.0) / a0; + let a1 = (-2.0 * cos_omega0) / a0; + let a2 = (1.0 - alpha) / a0; + + Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) + } + + /// Compute the coefficients for an all-pass filter. + /// + /// Based on . + pub fn allpass(sample_rate: f32, frequency: f32, q: f32) -> Self { + nih_debug_assert!(sample_rate > 0.0); + nih_debug_assert!(frequency > 0.0); + nih_debug_assert!(frequency < sample_rate / 2.0); + nih_debug_assert!(q > 0.0); + + let omega0 = consts::TAU * (frequency / sample_rate); + let cos_omega0 = omega0.cos(); + let alpha = omega0.sin() / (2.0 * q); + + // We'll prenormalize everything with a0 + let a0 = 1.0 + alpha; + let b0 = (1.0 - alpha) / a0; + let b1 = (-2.0 * cos_omega0) / a0; + let b2 = (1.0 + alpha) / a0; + let a1 = (-2.0 * cos_omega0) / a0; + let a2 = (1.0 - alpha) / a0; + + Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) + } +} + +impl SimdType for f32 { + #[inline(always)] + fn from_f32(value: f32) -> Self { + value + } +} + +impl SimdType for f32x2 { + #[inline(always)] + fn from_f32(value: f32) -> Self { + f32x2::splat(value) + } +} diff --git a/plugins/crossover/src/crossover/iir.rs b/plugins/crossover/src/crossover/iir.rs index b98fbe0b..36252e7a 100644 --- a/plugins/crossover/src/crossover/iir.rs +++ b/plugins/crossover/src/crossover/iir.rs @@ -16,10 +16,9 @@ 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::biquad::{Biquad, BiquadCoefficients}; use crate::NUM_BANDS; const NEUTRAL_Q: f32 = std::f32::consts::FRAC_1_SQRT_2; @@ -273,178 +272,3 @@ impl AllPassCascade { } } } - -/// 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 -/// crossovers. -/// -/// Based on . -/// -/// The type parameter T should be either an `f32` or a SIMD type. -#[derive(Clone, Copy, Debug)] -pub struct Biquad { - pub coefficients: BiquadCoefficients, - s1: T, - s2: T, -} - -/// The coefficients `[b0, b1, b2, a1, a2]` for [`Biquad`]. These coefficients are all -/// prenormalized, i.e. they have been divided by `a0`. -/// -/// The type parameter T should be either an `f32` or a SIMD type. -#[derive(Clone, Copy, Debug)] -pub struct BiquadCoefficients { - b0: T, - b1: T, - b2: T, - a1: T, - a2: T, -} - -/// Either an `f32` or some SIMD vector type of `f32`s that can be used with our biquads. -pub trait SimdType: - Mul + Sub + Add + Copy + Sized -{ - fn from_f32(value: f32) -> Self; -} - -impl Default for Biquad { - /// Before setting constants the filter should just act as an identity function. - fn default() -> Self { - Self { - coefficients: BiquadCoefficients::identity(), - s1: T::from_f32(0.0), - s2: T::from_f32(0.0), - } - } -} - -impl Biquad { - /// Process a single sample. - pub fn process(&mut self, sample: T) -> T { - let result = self.coefficients.b0 * sample + self.s1; - - self.s1 = self.coefficients.b1 * sample - self.coefficients.a1 * result + self.s2; - self.s2 = self.coefficients.b2 * sample - self.coefficients.a2 * result; - - result - } - - /// Reset the state to zero, useful after making making large, non-interpolatable changes to the - /// filter coefficients. - pub fn reset(&mut self) { - self.s1 = T::from_f32(0.0); - self.s2 = T::from_f32(0.0); - } -} - -impl BiquadCoefficients { - /// Convert scalar coefficients into the correct vector type. - pub fn from_f32s(scalar: BiquadCoefficients) -> Self { - Self { - b0: T::from_f32(scalar.b0), - b1: T::from_f32(scalar.b1), - b2: T::from_f32(scalar.b2), - a1: T::from_f32(scalar.a1), - a2: T::from_f32(scalar.a2), - } - } - - /// Filter coefficients that would cause the sound to be passed through as is. - pub fn identity() -> Self { - Self::from_f32s(BiquadCoefficients { - b0: 1.0, - b1: 0.0, - b2: 0.0, - a1: 0.0, - a2: 0.0, - }) - } - - /// Compute the coefficients for a low-pass filter. - /// - /// Based on . - pub fn lowpass(sample_rate: f32, frequency: f32, q: f32) -> Self { - nih_debug_assert!(sample_rate > 0.0); - nih_debug_assert!(frequency > 0.0); - nih_debug_assert!(frequency < sample_rate / 2.0); - nih_debug_assert!(q > 0.0); - - let omega0 = consts::TAU * (frequency / sample_rate); - let cos_omega0 = omega0.cos(); - let alpha = omega0.sin() / (2.0 * q); - - // We'll prenormalize everything with a0 - let a0 = 1.0 + alpha; - let b0 = ((1.0 - cos_omega0) / 2.0) / a0; - let b1 = (1.0 - cos_omega0) / a0; - let b2 = ((1.0 - cos_omega0) / 2.0) / a0; - let a1 = (-2.0 * cos_omega0) / a0; - let a2 = (1.0 - alpha) / a0; - - Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) - } - - /// Compute the coefficients for a high-pass filter. - /// - /// Based on . - pub fn highpass(sample_rate: f32, frequency: f32, q: f32) -> Self { - nih_debug_assert!(sample_rate > 0.0); - nih_debug_assert!(frequency > 0.0); - nih_debug_assert!(frequency < sample_rate / 2.0); - nih_debug_assert!(q > 0.0); - - let omega0 = consts::TAU * (frequency / sample_rate); - let cos_omega0 = omega0.cos(); - let alpha = omega0.sin() / (2.0 * q); - - // We'll prenormalize everything with a0 - let a0 = 1.0 + alpha; - let b0 = ((1.0 + cos_omega0) / 2.0) / a0; - let b1 = -(1.0 + cos_omega0) / a0; - let b2 = ((1.0 + cos_omega0) / 2.0) / a0; - let a1 = (-2.0 * cos_omega0) / a0; - let a2 = (1.0 - alpha) / a0; - - Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) - } - - /// Compute the coefficients for an all-pass filter. - /// - /// Based on . - pub fn allpass(sample_rate: f32, frequency: f32, q: f32) -> Self { - nih_debug_assert!(sample_rate > 0.0); - nih_debug_assert!(frequency > 0.0); - nih_debug_assert!(frequency < sample_rate / 2.0); - nih_debug_assert!(q > 0.0); - - let omega0 = consts::TAU * (frequency / sample_rate); - let cos_omega0 = omega0.cos(); - let alpha = omega0.sin() / (2.0 * q); - - // We'll prenormalize everything with a0 - let a0 = 1.0 + alpha; - let b0 = (1.0 - alpha) / a0; - let b1 = (-2.0 * cos_omega0) / a0; - let b2 = (1.0 + alpha) / a0; - let a1 = (-2.0 * cos_omega0) / a0; - let a2 = (1.0 - alpha) / a0; - - Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) - } -} - -impl SimdType for f32 { - #[inline(always)] - fn from_f32(value: f32) -> Self { - value - } -} - -impl SimdType for f32x2 { - #[inline(always)] - fn from_f32(value: f32) -> Self { - f32x2::splat(value) - } -} diff --git a/plugins/crossover/src/lib.rs b/plugins/crossover/src/lib.rs index f51d5d58..1e28e132 100644 --- a/plugins/crossover/src/lib.rs +++ b/plugins/crossover/src/lib.rs @@ -24,6 +24,7 @@ use nih_plug::prelude::*; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +mod biquad; mod crossover; /// The number of bands. Not used directly here, but this avoids hardcoding some constants in the