1
0
Fork 0

Win the loudness war harder with band-pass filters

This commit is contained in:
Robbert van der Helm 2022-04-27 17:59:54 +02:00
parent b89b4dfbb2
commit b3d2b79284
2 changed files with 228 additions and 4 deletions

View 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)
// }
// }

View file

@ -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";