diff --git a/plugins/loudness_war_winner/src/filter.rs b/plugins/loudness_war_winner/src/filter.rs new file mode 100644 index 00000000..92e510a6 --- /dev/null +++ b/plugins/loudness_war_winner/src/filter.rs @@ -0,0 +1,144 @@ +// Loudness War Winner: Because negative LUFS are boring +// 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 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 . +/// +/// 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 band-pass filter. + /// + /// Based on . + pub fn bandpass(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 = alpha / a0; + let b1 = 0.0 / a0; + let b2 = -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 + } +} + +// TODO: Add SIMD +// impl SimdType for f32x2 { +// #[inline(always)] +// fn from_f32(value: f32) -> Self { +// f32x2::splat(value) +// } +// } diff --git a/plugins/loudness_war_winner/src/lib.rs b/plugins/loudness_war_winner/src/lib.rs index 10271d55..17408c5d 100644 --- a/plugins/loudness_war_winner/src/lib.rs +++ b/plugins/loudness_war_winner/src/lib.rs @@ -14,18 +14,31 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#[macro_use] +extern crate nih_plug; + use nih_plug::prelude::*; use std::sync::Arc; +mod filter; + /// The length of silence after which the signal should start fading out into silence. This is to /// avoid outputting a constant DC signal. const SILENCE_FADEOUT_START_MS: f32 = 1000.0; /// The time it takes after `SILENCE_FADEOUT_START_MS` to fade from a full scale DC signal to silence. const SILENCE_FADEOUT_END_MS: f32 = SILENCE_FADEOUT_START_MS + 1000.0; +/// The center frequency for our optional bandpass filter, in Hertz. +const BP_FREQUENCY: f32 = 5500.0; + struct LoudnessWarWinner { params: Arc, + sample_rate: f32, + /// To win even harder we'll band-pass the signal around 5.5 kHz when the `WIN HARDER` parameter + /// is enabled. And we'll cascade four of these filters while we're at it. + bp_filters: Vec<[filter::Biquad; 4]>, + /// The number of samples since the last non-zero sample. This is used to fade into silence when /// the input has also been silent for a while instead of outputting a constant DC signal. All /// channels need to be silent for a signal to be considered silent. @@ -43,6 +56,12 @@ struct LoudnessWarWinnerParams { /// The output gain, set to -24 dB by default because oof ouchie. #[id = "output"] output_gain: FloatParam, + + /// When non-zero, this engages a bandpass filter around 5.5 kHz to help with the LUFS + /// K-Weighting. This is a fraction in `[0, 1]`. [`LoudnessWarWinner::update_bp_filters()`] + /// calculates the filter's Q value basedo n this. + #[id = "powah"] + win_harder_factor: FloatParam, } impl Default for LoudnessWarWinner { @@ -50,6 +69,9 @@ impl Default for LoudnessWarWinner { Self { params: Arc::new(LoudnessWarWinnerParams::default()), + sample_rate: 1.0, + bp_filters: Vec::new(), + num_silent_samples: 0, silence_fadeout_start_samples: 0, silence_fadeout_end_samples: 0, @@ -74,6 +96,21 @@ impl Default for LoudnessWarWinnerParams { .with_unit(" dB") .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) .with_string_to_value(formatters::s2v_f32_gain_to_db()), + win_harder_factor: FloatParam::new( + "WIN HARDER", + 0.0, + // This ramps up hard, so we'll make sure the 'usable' (for a lack of a better word) + // value range is larger + FloatRange::Skewed { + min: 0.0, + max: 1.0, + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_smoother(SmoothingStyle::Linear(30.0)) + .with_unit("%") + .with_value_to_string(formatters::v2s_f32_percentage(0)) + .with_string_to_value(formatters::s2v_f32_percentage()), } } } @@ -100,10 +137,18 @@ impl Plugin for LoudnessWarWinner { fn initialize( &mut self, - _bus_config: &BusConfig, + bus_config: &BusConfig, buffer_config: &BufferConfig, _context: &mut impl ProcessContext, ) -> bool { + self.sample_rate = buffer_config.sample_rate; + + self.bp_filters.resize( + bus_config.num_output_channels as usize, + [filter::Biquad::default(); 4], + ); + self.update_bp_filters(); + self.silence_fadeout_start_samples = (SILENCE_FADEOUT_START_MS / 1000.0 * buffer_config.sample_rate).round() as u32; self.silence_fadeout_end_samples = @@ -115,6 +160,12 @@ impl Plugin for LoudnessWarWinner { } fn reset(&mut self) { + for filters in &mut self.bp_filters { + for filter in filters { + filter.reset(); + } + } + // Start with silence, so we don't immediately output a DC signal if the plugin is inserted // on a silent channel self.num_silent_samples = self.silence_fadeout_end_samples; @@ -128,11 +179,24 @@ impl Plugin for LoudnessWarWinner { for mut channel_samples in buffer.iter_samples() { let output_gain = self.params.output_gain.smoothed.next(); - // TODO: Add a second parameter called "WIN HARDER" that bandpasses the signal around 5 - // kHz + // When the `WIN_HARDER` parameter is engaged, we'll band-pass the signal around 5 kHz + if self.params.win_harder_factor.smoothed.is_smoothing() { + self.update_bp_filters(); + } + let apply_bp_filters = self.params.win_harder_factor.smoothed.previous_value() > 0.0; + let mut is_silent = true; - for sample in channel_samples.iter_mut() { + for (sample, bp_filters) in channel_samples.iter_mut().zip(&mut self.bp_filters) { is_silent &= *sample == 0.0; + + // For better performance we can move this conditional to an outer loop, but right + // now it shouldn't be too bad + if apply_bp_filters { + for filter in bp_filters { + *sample = filter.process(*sample); + } + } + *sample = if *sample >= 0.0 { 1.0 } else { -1.0 } * output_gain; } @@ -163,6 +227,22 @@ impl Plugin for LoudnessWarWinner { } } +impl LoudnessWarWinner { + /// Update the band-pass filters. This should only be called during processing if + /// `self.params.win_harder_factor.smoothed.is_smoothing()`. + fn update_bp_filters(&mut self) { + let q = 0.00001 + (self.params.win_harder_factor.smoothed.next() * 30.0); + + let biquad_coefficients = + filter::BiquadCoefficients::bandpass(self.sample_rate, BP_FREQUENCY, q); + for filters in &mut self.bp_filters { + for filter in filters { + filter.coefficients = biquad_coefficients; + } + } + } +} + impl ClapPlugin for LoudnessWarWinner { const CLAP_ID: &'static str = "nl.robbertvanderhelm.loudness-war-winner"; const CLAP_DESCRIPTION: &'static str = "Win the loudness war with ease";