Win the loudness war harder with band-pass filters
This commit is contained in:
parent
b89b4dfbb2
commit
b3d2b79284
144
plugins/loudness_war_winner/src/filter.rs
Normal file
144
plugins/loudness_war_winner/src/filter.rs
Normal file
|
@ -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 <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 with 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 band-pass filter.
|
||||
///
|
||||
/// Based on <http://shepazu.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html>.
|
||||
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)
|
||||
// }
|
||||
// }
|
|
@ -14,18 +14,31 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#[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<LoudnessWarWinnerParams>,
|
||||
|
||||
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<f32>; 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";
|
||||
|
|
Loading…
Reference in a new issue