From fd3f4c2c4825995253fa0001e5a46c33c26fb232 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 13 Feb 2022 18:32:59 +0100 Subject: [PATCH] Allow skipping steps in the smoothers I'll need this for Diopser since it's going to have a parameter to control the automation's granularity. --- src/param/smoothing.rs | 120 ++++++++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 18 deletions(-) diff --git a/src/param/smoothing.rs b/src/param/smoothing.rs index e81b5ea5..d9b55f19 100644 --- a/src/param/smoothing.rs +++ b/src/param/smoothing.rs @@ -1,5 +1,5 @@ use atomic_float::AtomicF32; -use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::atomic::{AtomicI32, Ordering}; /// Controls if and how parameters gets smoothed. pub enum SmoothingStyle { @@ -25,7 +25,10 @@ pub struct Smoother { /// The kind of snoothing that needs to be applied, if any. style: SmoothingStyle, /// The number of steps of smoothing left to take. - steps_left: AtomicU32, + /// + // This is a signed integer because we can skip multiple steps, which would otherwise make it + // possible to get an underflow here. + steps_left: AtomicI32, /// The amount we should adjust the current value each sample to be able to reach the target in /// the specified tiem frame. This is also a floating point number to keep the smoothing /// uniform. @@ -40,7 +43,7 @@ impl Default for Smoother { fn default() -> Self { Self { style: SmoothingStyle::None, - steps_left: AtomicU32::new(0), + steps_left: AtomicI32::new(0), step_size: Default::default(), current: AtomicF32::new(0.0), target: Default::default(), @@ -86,7 +89,7 @@ impl Smoother { let steps_left = match self.style { SmoothingStyle::None => 1, SmoothingStyle::Linear(time) | SmoothingStyle::Logarithmic(time) => { - (sample_rate * time / 1000.0).round() as u32 + (sample_rate * time / 1000.0).round() as i32 } }; self.steps_left.store(steps_left, Ordering::Relaxed); @@ -104,22 +107,36 @@ impl Smoother { }; } + /// Get the next value from this smoother. The value will be equal to the previous value once + // the smoothing period is over. This should be called exactly once per sample. // Yes, Clippy, like I said, this was intentional #[allow(clippy::should_implement_trait)] pub fn next(&self) -> f32 { + self.next_step(1) + } + + /// [Self::next()], but with the ability to skip forward in the smoother. [Self::next()] is + /// equivalent to calling this function with a `steps` value of 1. Calling this function with a + /// `steps` value of `n` means will cause you to skip the next `n - 1` values and return the + /// `n`th value. + pub fn next_step(&self, steps: u32) -> f32 { + nih_debug_assert_ne!(steps, 0); + if self.steps_left.load(Ordering::Relaxed) > 0 { let current = self.current.load(Ordering::Relaxed); - // The number of steps usually won't fit exactly, so make sure we don't do weird things - // with overshoots or undershoots - let old_steps_left = self.steps_left.fetch_sub(1, Ordering::Relaxed); - let new = if old_steps_left == 1 { + // The number of steps usually won't fit exactly, so make sure we don't end up with + // quantization errors on overshoots or undershoots. We also need to account for the + // possibility that we only have `n < steps` steps left. + let old_steps_left = self.steps_left.fetch_sub(steps as i32, Ordering::Relaxed); + let new = if old_steps_left <= steps as i32 { + self.steps_left.store(0, Ordering::Relaxed); self.target } else { match &self.style { SmoothingStyle::None => self.target, - SmoothingStyle::Linear(_) => current + self.step_size, - SmoothingStyle::Logarithmic(_) => current * self.step_size, + SmoothingStyle::Linear(_) => current + (self.step_size * steps as f32), + SmoothingStyle::Logarithmic(_) => current * (self.step_size.powi(steps as i32)), } }; self.current.store(new, Ordering::Relaxed); @@ -145,7 +162,7 @@ impl Smoother { let steps_left = match self.style { SmoothingStyle::None => 1, SmoothingStyle::Linear(time) | SmoothingStyle::Logarithmic(time) => { - (sample_rate * time / 1000.0).round() as u32 + (sample_rate * time / 1000.0).round() as i32 } }; self.steps_left.store(steps_left, Ordering::Relaxed); @@ -161,21 +178,36 @@ impl Smoother { }; } + /// Get the next value from this smoother. The value will be equal to the previous value once + // the smoothing period is over. This should be called exactly once per sample. + // Yes, Clippy, like I said, this was intentional #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> i32 { + pub fn next(&self) -> i32 { + self.next_step(1) + } + + /// [Self::next()], but with the ability to skip forward in the smoother. [Self::next()] is + /// equivalent to calling this function with a `steps` value of 1. Calling this function with a + /// `steps` value of `n` means will cause you to skip the next `n - 1` values and return the + /// `n`th value. + pub fn next_step(&self, steps: u32) -> i32 { + nih_debug_assert_ne!(steps, 0); + if self.steps_left.load(Ordering::Relaxed) > 0 { let current = self.current.load(Ordering::Relaxed); - // The number of steps usually won't fit exactly, so make sure we don't do weird things - // with overshoots or undershoots - let old_steps_left = self.steps_left.fetch_sub(1, Ordering::Relaxed); - let new = if old_steps_left == 1 { + // The number of steps usually won't fit exactly, so make sure we don't end up with + // quantization errors on overshoots or undershoots. We also need to account for the + // possibility that we only have `n < steps` steps left. + let old_steps_left = self.steps_left.fetch_sub(steps as i32, Ordering::Relaxed); + let new = if old_steps_left <= steps as i32 { + self.steps_left.store(0, Ordering::Relaxed); self.target as f32 } else { match &self.style { SmoothingStyle::None => self.target as f32, - SmoothingStyle::Linear(_) => current + self.step_size, - SmoothingStyle::Logarithmic(_) => current * self.step_size, + SmoothingStyle::Linear(_) => current + (self.step_size * steps as f32), + SmoothingStyle::Logarithmic(_) => current * self.step_size.powi(steps as i32), } }; self.current.store(new, Ordering::Relaxed); @@ -252,4 +284,56 @@ mod tests { assert_ne!(smoother.next(), 20); assert_eq!(smoother.next(), 20); } + + /// Same as [linear_f32_smoothing], but skipping steps instead. + #[test] + fn skipping_linear_f32_smoothing() { + let mut smoother: Smoother = Smoother::new(SmoothingStyle::Linear(100.0)); + smoother.reset(10.0); + assert_eq!(smoother.next(), 10.0); + + smoother.set_target(100.0, 20.0); + smoother.next_step(8); + assert_ne!(smoother.next(), 20.0); + assert_eq!(smoother.next(), 20.0); + } + + /// Same as [linear_i32_smoothing], but skipping steps instead. + #[test] + fn skipping_linear_i32_smoothing() { + let mut smoother: Smoother = Smoother::new(SmoothingStyle::Linear(100.0)); + smoother.reset(10); + assert_eq!(smoother.next(), 10); + + smoother.set_target(100.0, 20); + smoother.next_step(8); + assert_ne!(smoother.next(), 20); + assert_eq!(smoother.next(), 20); + } + + /// Same as [logarithmic_f32_smoothing], but skipping steps instead. + #[test] + fn skipping_logarithmic_f32_smoothing() { + let mut smoother: Smoother = Smoother::new(SmoothingStyle::Logarithmic(100.0)); + smoother.reset(10.0); + assert_eq!(smoother.next(), 10.0); + + smoother.set_target(100.0, 20.0); + smoother.next_step(8); + assert_ne!(smoother.next(), 20.0); + assert_eq!(smoother.next(), 20.0); + } + + /// Same as [logarithmic_i32_smoothing], but skipping steps instead. + #[test] + fn skipping_logarithmic_i32_smoothing() { + let mut smoother: Smoother = Smoother::new(SmoothingStyle::Logarithmic(100.0)); + smoother.reset(10); + assert_eq!(smoother.next(), 10); + + smoother.set_target(100.0, 20); + smoother.next_step(8); + assert_ne!(smoother.next(), 20); + assert_eq!(smoother.next(), 20); + } }