1
0
Fork 0

Add a biquad for low- and high-pass filters

This commit is contained in:
Robbert van der Helm 2022-03-09 13:24:57 +01:00
parent a08624209c
commit bda8ecfb23
3 changed files with 173 additions and 3 deletions

166
plugins/crisp/src/filter.rs Normal file
View file

@ -0,0 +1,166 @@
// Crisp: a distortion plugin but not quite
// 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 <https://www.gnu.org/licenses/>.
use std::f32::consts;
use std::ops::{Add, Mul, Sub};
/// A simple biquad filter with functions for generating coefficients for second order low-pass and
/// high-pass filters.
///
/// Based on <https://en.wikipedia.org/wiki/Digital_biquad_filter#Transposed_direct_forms>.
///
/// The type parameter T should be either an `f32` or a SIMD type.
#[derive(Clone, Copy, Debug)]
pub struct Biquad<T> {
pub coefficients: BiquadCoefficients<T>,
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<T> {
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 iwth our biquads.
pub trait SimdType:
Mul<Output = Self> + Sub<Output = Self> + Add<Output = Self> + Copy + Sized
{
fn from_f32(value: f32) -> Self;
}
impl<T: SimdType> Default for Biquad<T> {
/// 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<T: SimdType> Biquad<T> {
/// 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<T: SimdType> BiquadCoefficients<T> {
/// Convert scalar coefficients into the correct vector type.
pub fn from_f32s(scalar: BiquadCoefficients<f32>) -> 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 <http://shepazu.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html>.
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 <http://shepazu.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html>.
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 })
}
}
impl SimdType for f32 {
fn from_f32(value: f32) -> Self {
value
}
}
// TODO: Add SIMD
// impl SimdType for f32x2 {
// fn from_f32(value: f32) -> Self {
// f32x2::splat(value)
// }
// }

View file

@ -14,11 +14,15 @@
// 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/>.
#[macro_use]
extern crate nih_plug;
use nih_plug::prelude::*; use nih_plug::prelude::*;
use pcg::Pcg32iState; use pcg::Pcg32iState;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
mod filter;
mod pcg; mod pcg;
/// These seeds being fixed makes bouncing deterministic. /// These seeds being fixed makes bouncing deterministic.
@ -28,7 +32,7 @@ const INITIAL_PRNG_SEED: Pcg32iState = Pcg32iState::new(69, 420);
const AMOUNT_GAIN_MULTIPLIER: f32 = 2.0; const AMOUNT_GAIN_MULTIPLIER: f32 = 2.0;
/// This plugin essentially layers the sound with another copy of the signal ring modulated with /// This plugin essentially layers the sound with another copy of the signal ring modulated with
/// white (or filtered) noise. That other copy of the sound may have a low pass filter applied to it /// white (or filtered) noise. That other copy of the sound may have a low-pass filter applied to it
/// since this effect just turns into literal noise at high frequencies. /// since this effect just turns into literal noise at high frequencies.
struct Crisp { struct Crisp {
params: Pin<Box<CrispParams>>, params: Pin<Box<CrispParams>>,

View file

@ -16,7 +16,7 @@ struct Stft {
/// A Hann window function, passed to the overlap-add helper. /// A Hann window function, passed to the overlap-add helper.
window_function: Vec<f32>, window_function: Vec<f32>,
/// The FFT of a simple low pass FIR filter. /// The FFT of a simple low-pass FIR filter.
lp_filter_kernel: Vec<c32>, lp_filter_kernel: Vec<c32>,
/// The algorithms for the FFT and IFFT operations. /// The algorithms for the FFT and IFFT operations.
@ -47,7 +47,7 @@ impl Default for Stft {
let mut real_fft_scratch_buffer: AlignedVec<f32> = AlignedVec::new(WINDOW_SIZE); let mut real_fft_scratch_buffer: AlignedVec<f32> = AlignedVec::new(WINDOW_SIZE);
let mut complex_fft_scratch_buffer: AlignedVec<c32> = AlignedVec::new(WINDOW_SIZE / 2 + 1); let mut complex_fft_scratch_buffer: AlignedVec<c32> = AlignedVec::new(WINDOW_SIZE / 2 + 1);
// Build a super simple low pass filter from one of the built in window function // Build a super simple low-pass filter from one of the built in window function
const FILTER_WINDOW_SIZE: usize = 33; const FILTER_WINDOW_SIZE: usize = 33;
let filter_window = util::window::hann(FILTER_WINDOW_SIZE); let filter_window = util::window::hann(FILTER_WINDOW_SIZE);
real_fft_scratch_buffer[0..FILTER_WINDOW_SIZE].copy_from_slice(&filter_window); real_fft_scratch_buffer[0..FILTER_WINDOW_SIZE].copy_from_slice(&filter_window);