Skip to content

Commit

Permalink
Smoothen wow depth control
Browse files Browse the repository at this point in the history
Signed-off-by: Petr Horacek <[email protected]>
  • Loading branch information
phoracek committed Jan 21, 2023
1 parent 4dca09f commit c164d08
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions dsp/src/dc_blocker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! Based on <https://ccrma.stanford.edu/~jos/fp/DC_Blocker_Software_Implementations.html>.
// XXX: Assuming 48 kHz sample rate.
const POLE: f32 = 0.995;

#[derive(Default, Debug)]
Expand Down
1 change: 1 addition & 0 deletions dsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions dsp/src/one_pole_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Simple one-pole low-pass filter.
//!
//! Based on <https://www.earlevel.com/main/2012/12/15/a-one-pole-filter/>,
//! 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
}
}
31 changes: 22 additions & 9 deletions dsp/src/wow_flutter/wow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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);
Expand All @@ -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,
}
}

Expand All @@ -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);
}
}

Expand All @@ -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);
Expand Down

0 comments on commit c164d08

Please sign in to comment.