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.
This commit is contained in:
parent
99fe9e865b
commit
fd3f4c2c48
1 changed files with 102 additions and 18 deletions
|
@ -1,5 +1,5 @@
|
||||||
use atomic_float::AtomicF32;
|
use atomic_float::AtomicF32;
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
|
|
||||||
/// Controls if and how parameters gets smoothed.
|
/// Controls if and how parameters gets smoothed.
|
||||||
pub enum SmoothingStyle {
|
pub enum SmoothingStyle {
|
||||||
|
@ -25,7 +25,10 @@ pub struct Smoother<T> {
|
||||||
/// The kind of snoothing that needs to be applied, if any.
|
/// The kind of snoothing that needs to be applied, if any.
|
||||||
style: SmoothingStyle,
|
style: SmoothingStyle,
|
||||||
/// The number of steps of smoothing left to take.
|
/// 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 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
|
/// the specified tiem frame. This is also a floating point number to keep the smoothing
|
||||||
/// uniform.
|
/// uniform.
|
||||||
|
@ -40,7 +43,7 @@ impl<T: Default> Default for Smoother<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
style: SmoothingStyle::None,
|
style: SmoothingStyle::None,
|
||||||
steps_left: AtomicU32::new(0),
|
steps_left: AtomicI32::new(0),
|
||||||
step_size: Default::default(),
|
step_size: Default::default(),
|
||||||
current: AtomicF32::new(0.0),
|
current: AtomicF32::new(0.0),
|
||||||
target: Default::default(),
|
target: Default::default(),
|
||||||
|
@ -86,7 +89,7 @@ impl Smoother<f32> {
|
||||||
let steps_left = match self.style {
|
let steps_left = match self.style {
|
||||||
SmoothingStyle::None => 1,
|
SmoothingStyle::None => 1,
|
||||||
SmoothingStyle::Linear(time) | SmoothingStyle::Logarithmic(time) => {
|
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);
|
self.steps_left.store(steps_left, Ordering::Relaxed);
|
||||||
|
@ -104,22 +107,36 @@ impl Smoother<f32> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
// Yes, Clippy, like I said, this was intentional
|
||||||
#[allow(clippy::should_implement_trait)]
|
#[allow(clippy::should_implement_trait)]
|
||||||
pub fn next(&self) -> f32 {
|
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 {
|
if self.steps_left.load(Ordering::Relaxed) > 0 {
|
||||||
let current = self.current.load(Ordering::Relaxed);
|
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
|
// The number of steps usually won't fit exactly, so make sure we don't end up with
|
||||||
// with overshoots or undershoots
|
// quantization errors on overshoots or undershoots. We also need to account for the
|
||||||
let old_steps_left = self.steps_left.fetch_sub(1, Ordering::Relaxed);
|
// possibility that we only have `n < steps` steps left.
|
||||||
let new = if old_steps_left == 1 {
|
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
|
self.target
|
||||||
} else {
|
} else {
|
||||||
match &self.style {
|
match &self.style {
|
||||||
SmoothingStyle::None => self.target,
|
SmoothingStyle::None => self.target,
|
||||||
SmoothingStyle::Linear(_) => current + self.step_size,
|
SmoothingStyle::Linear(_) => current + (self.step_size * steps as f32),
|
||||||
SmoothingStyle::Logarithmic(_) => current * self.step_size,
|
SmoothingStyle::Logarithmic(_) => current * (self.step_size.powi(steps as i32)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.current.store(new, Ordering::Relaxed);
|
self.current.store(new, Ordering::Relaxed);
|
||||||
|
@ -145,7 +162,7 @@ impl Smoother<i32> {
|
||||||
let steps_left = match self.style {
|
let steps_left = match self.style {
|
||||||
SmoothingStyle::None => 1,
|
SmoothingStyle::None => 1,
|
||||||
SmoothingStyle::Linear(time) | SmoothingStyle::Logarithmic(time) => {
|
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);
|
self.steps_left.store(steps_left, Ordering::Relaxed);
|
||||||
|
@ -161,21 +178,36 @@ impl Smoother<i32> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
#[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 {
|
if self.steps_left.load(Ordering::Relaxed) > 0 {
|
||||||
let current = self.current.load(Ordering::Relaxed);
|
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
|
// The number of steps usually won't fit exactly, so make sure we don't end up with
|
||||||
// with overshoots or undershoots
|
// quantization errors on overshoots or undershoots. We also need to account for the
|
||||||
let old_steps_left = self.steps_left.fetch_sub(1, Ordering::Relaxed);
|
// possibility that we only have `n < steps` steps left.
|
||||||
let new = if old_steps_left == 1 {
|
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
|
self.target as f32
|
||||||
} else {
|
} else {
|
||||||
match &self.style {
|
match &self.style {
|
||||||
SmoothingStyle::None => self.target as f32,
|
SmoothingStyle::None => self.target as f32,
|
||||||
SmoothingStyle::Linear(_) => current + self.step_size,
|
SmoothingStyle::Linear(_) => current + (self.step_size * steps as f32),
|
||||||
SmoothingStyle::Logarithmic(_) => current * self.step_size,
|
SmoothingStyle::Logarithmic(_) => current * self.step_size.powi(steps as i32),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.current.store(new, Ordering::Relaxed);
|
self.current.store(new, Ordering::Relaxed);
|
||||||
|
@ -252,4 +284,56 @@ mod tests {
|
||||||
assert_ne!(smoother.next(), 20);
|
assert_ne!(smoother.next(), 20);
|
||||||
assert_eq!(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<f32> = 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<i32> = 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<f32> = 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<i32> = 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue