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
|
// 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 std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
mod filter;
|
||||||
|
|
||||||
/// The length of silence after which the signal should start fading out into silence. This is to
|
/// The length of silence after which the signal should start fading out into silence. This is to
|
||||||
/// avoid outputting a constant DC signal.
|
/// avoid outputting a constant DC signal.
|
||||||
const SILENCE_FADEOUT_START_MS: f32 = 1000.0;
|
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.
|
/// 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;
|
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 {
|
struct LoudnessWarWinner {
|
||||||
params: Arc<LoudnessWarWinnerParams>,
|
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 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
|
/// 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.
|
/// 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.
|
/// The output gain, set to -24 dB by default because oof ouchie.
|
||||||
#[id = "output"]
|
#[id = "output"]
|
||||||
output_gain: FloatParam,
|
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 {
|
impl Default for LoudnessWarWinner {
|
||||||
|
@ -50,6 +69,9 @@ impl Default for LoudnessWarWinner {
|
||||||
Self {
|
Self {
|
||||||
params: Arc::new(LoudnessWarWinnerParams::default()),
|
params: Arc::new(LoudnessWarWinnerParams::default()),
|
||||||
|
|
||||||
|
sample_rate: 1.0,
|
||||||
|
bp_filters: Vec::new(),
|
||||||
|
|
||||||
num_silent_samples: 0,
|
num_silent_samples: 0,
|
||||||
silence_fadeout_start_samples: 0,
|
silence_fadeout_start_samples: 0,
|
||||||
silence_fadeout_end_samples: 0,
|
silence_fadeout_end_samples: 0,
|
||||||
|
@ -74,6 +96,21 @@ impl Default for LoudnessWarWinnerParams {
|
||||||
.with_unit(" dB")
|
.with_unit(" dB")
|
||||||
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
|
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
|
||||||
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
|
.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(
|
fn initialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
_bus_config: &BusConfig,
|
bus_config: &BusConfig,
|
||||||
buffer_config: &BufferConfig,
|
buffer_config: &BufferConfig,
|
||||||
_context: &mut impl ProcessContext,
|
_context: &mut impl ProcessContext,
|
||||||
) -> bool {
|
) -> 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 =
|
self.silence_fadeout_start_samples =
|
||||||
(SILENCE_FADEOUT_START_MS / 1000.0 * buffer_config.sample_rate).round() as u32;
|
(SILENCE_FADEOUT_START_MS / 1000.0 * buffer_config.sample_rate).round() as u32;
|
||||||
self.silence_fadeout_end_samples =
|
self.silence_fadeout_end_samples =
|
||||||
|
@ -115,6 +160,12 @@ impl Plugin for LoudnessWarWinner {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&mut self) {
|
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
|
// Start with silence, so we don't immediately output a DC signal if the plugin is inserted
|
||||||
// on a silent channel
|
// on a silent channel
|
||||||
self.num_silent_samples = self.silence_fadeout_end_samples;
|
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() {
|
for mut channel_samples in buffer.iter_samples() {
|
||||||
let output_gain = self.params.output_gain.smoothed.next();
|
let output_gain = self.params.output_gain.smoothed.next();
|
||||||
|
|
||||||
// TODO: Add a second parameter called "WIN HARDER" that bandpasses the signal around 5
|
// When the `WIN_HARDER` parameter is engaged, we'll band-pass the signal around 5 kHz
|
||||||
// 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;
|
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;
|
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;
|
*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 {
|
impl ClapPlugin for LoudnessWarWinner {
|
||||||
const CLAP_ID: &'static str = "nl.robbertvanderhelm.loudness-war-winner";
|
const CLAP_ID: &'static str = "nl.robbertvanderhelm.loudness-war-winner";
|
||||||
const CLAP_DESCRIPTION: &'static str = "Win the loudness war with ease";
|
const CLAP_DESCRIPTION: &'static str = "Win the loudness war with ease";
|
||||||
|
|
Loading…
Reference in a new issue