From c164d0848926ab363470eb685e5150aa579b9017 Mon Sep 17 00:00:00 2001 From: Petr Horacek Date: Sat, 21 Jan 2023 12:13:30 +0100 Subject: [PATCH] Smoothen wow depth control Signed-off-by: Petr Horacek --- CHANGELOG.md | 1 + dsp/src/dc_blocker.rs | 1 + dsp/src/lib.rs | 1 + dsp/src/one_pole_filter.rs | 31 +++++++++++++++++++++++++++++++ dsp/src/wow_flutter/wow.rs | 31 ++++++++++++++++++++++--------- 5 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 dsp/src/one_pole_filter.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d821f5..d22957b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ backwards compatibility. * Delay start to prevent pops. * Increase pot buffer to prevent clicks. * Randomize intensity and spacing of flutter. +* Smoothen wow depth control. ## 0.6.0 diff --git a/dsp/src/dc_blocker.rs b/dsp/src/dc_blocker.rs index cd83380..813764f 100644 --- a/dsp/src/dc_blocker.rs +++ b/dsp/src/dc_blocker.rs @@ -2,6 +2,7 @@ //! //! Based on . +// XXX: Assuming 48 kHz sample rate. const POLE: f32 = 0.995; #[derive(Default, Debug)] diff --git a/dsp/src/lib.rs b/dsp/src/lib.rs index fee3c50..e814379 100644 --- a/dsp/src/lib.rs +++ b/dsp/src/lib.rs @@ -30,6 +30,7 @@ mod compressor; mod dc_blocker; mod linkwitz_riley_filter; mod math; +mod one_pole_filter; mod pre_amp; mod ring_buffer; mod state_variable_filter; diff --git a/dsp/src/one_pole_filter.rs b/dsp/src/one_pole_filter.rs new file mode 100644 index 0000000..2792ab9 --- /dev/null +++ b/dsp/src/one_pole_filter.rs @@ -0,0 +1,31 @@ +//! Simple one-pole low-pass filter. +//! +//! Based on , +//! this filter may be useful for attribute smoothening. + +use core::f32::consts::PI; + +use libm::expf; + +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct OnePoleFilter { + y_m1: f32, + a0: f32, + b1: f32, +} + +impl OnePoleFilter { + // Cutoff is normalized frequency. 10.0 / sample_rate would be 10 Hz. + pub fn new(sample_rate: f32, cutoff: f32) -> Self { + let normalized_frequency = cutoff / sample_rate; + let b1 = expf(-2.0 * PI * normalized_frequency); + let a0 = 1.0 - b1; + Self { y_m1: 0.0, a0, b1 } + } + + pub fn tick(&mut self, x: f32) -> f32 { + self.y_m1 = x * self.a0 + self.y_m1 * self.b1; + self.y_m1 + } +} diff --git a/dsp/src/wow_flutter/wow.rs b/dsp/src/wow_flutter/wow.rs index 50e2aa4..7efa4d8 100644 --- a/dsp/src/wow_flutter/wow.rs +++ b/dsp/src/wow_flutter/wow.rs @@ -3,10 +3,16 @@ use micromath::F32Ext as _; use super::ornstein_uhlenbeck::OrnsteinUhlenbeck; use super::wavefolder; +use crate::one_pole_filter::OnePoleFilter; use crate::random::Random; use crate::state_variable_filter::StateVariableFilter; use crate::trigonometry; +// Smoothening of the depth attribute to make sure that wow does not +// scroll to the present too abruptly, causing pops when hitting 0. +const DEPTH_CUTOFF: f32 = 0.1; +const CONTROL_SAMPLE_RATE: f32 = 1000.0; + // These constants were obtained through design in hack/wow.py and // experimentation with sound. const BASE_FREQUENCY: f32 = 0.07; @@ -25,9 +31,10 @@ pub struct Attributes { pub struct Wow { sample_rate: f32, depth: f32, + depth_filter: OnePoleFilter, phase: f32, ornstein_uhlenbeck: OrnsteinUhlenbeck, - filter: StateVariableFilter, + modulation_filter: StateVariableFilter, } impl Wow { @@ -36,10 +43,11 @@ impl Wow { sample_rate > 500, "Wow may be unstable for low sample rates" ); - let filter = { - let mut filter = StateVariableFilter::new(sample_rate); - filter.set_frequency(MODULATION_CUTOFF); - filter + let depth_filter = OnePoleFilter::new(CONTROL_SAMPLE_RATE, DEPTH_CUTOFF); + let modulation_filter = { + let mut modulation_filter = StateVariableFilter::new(sample_rate); + modulation_filter.set_frequency(MODULATION_CUTOFF); + modulation_filter }; let ornstein_uhlenbeck = { let mut ornstein_uhlenbeck = OrnsteinUhlenbeck::new(sample_rate as f32); @@ -50,9 +58,10 @@ impl Wow { Self { sample_rate: sample_rate as f32, depth: 0.0, + depth_filter, phase: 0.5, // Start the offset sine wave on 0.0 ornstein_uhlenbeck, - filter, + modulation_filter, } } @@ -68,11 +77,11 @@ impl Wow { x }; - wavefolder::fold(self.filter.tick(target).low_pass, 0.0, 1000.0) + wavefolder::fold(self.modulation_filter.tick(target).low_pass, 0.0, 1000.0) } pub fn set_attributes(&mut self, attributes: &Attributes) { - self.depth = attributes.depth; + self.depth = self.depth_filter.tick(attributes.depth); } } @@ -96,7 +105,11 @@ mod tests { #[test] fn it_spans_in_expected_range() { let mut wow = Wow::new(SAMPLE_RATE); - wow.set_attributes(&Attributes { depth: 1.0 }); + + // Depth is filtered, let it reach the destination. + for _ in 0..10000 { + wow.set_attributes(&Attributes { depth: 1.0 }); + } let x = wow.pop(&mut TestRandom); let (mut min, mut max) = (x, x);