1
0
Fork 0
nih-plug/src/param/smoothing.rs

256 lines
9 KiB
Rust
Raw Normal View History

use atomic_float::AtomicF32;
use std::sync::atomic::{AtomicU32, Ordering};
2022-02-03 07:08:23 +11:00
/// Controls if and how parameters gets smoothed.
pub enum SmoothingStyle {
/// No smoothing is applied. The parameter's `value` field contains the latest sample value
/// available for the parameters.
None,
2022-02-03 09:00:17 +11:00
/// Smooth parameter changes so the current value approaches the target value at a constant
/// rate.
Linear(f32),
2022-02-03 09:00:17 +11:00
/// Smooth parameter changes such that the rate matches the curve of a logarithmic function.
/// This is useful for smoothing things like frequencies and decibel gain value. **The caveat is
/// that the value may never reach 0**, or you will end up multiplying and dividing things by
/// zero. Make sure your value ranges don't include 0.
Logarithmic(f32),
2022-02-03 07:08:23 +11:00
// TODO: Sample-accurate modes
}
/// A smoother, providing a smoothed value for each sample.
//
// TODO: We need to use atomics here so we can share the params object with the GUI. Is there a
// better alternative to allow the process function to mutate these smoothers?
2022-02-03 07:08:23 +11:00
pub struct Smoother<T> {
/// 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,
2022-02-03 07:08:23 +11:00
/// 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.
step_size: f32,
/// The value for the current sample. Always stored as floating point for obvious reasons.
current: AtomicF32,
2022-02-03 07:08:23 +11:00
/// The value we're smoothing towards
target: T,
}
impl<T: Default> Default for Smoother<T> {
fn default() -> Self {
Self {
style: SmoothingStyle::None,
steps_left: AtomicU32::new(0),
2022-02-03 07:08:23 +11:00
step_size: Default::default(),
current: AtomicF32::new(0.0),
2022-02-03 07:08:23 +11:00
target: Default::default(),
}
}
}
impl<T: Default> Smoother<T> {
/// Use the specified style for the smoothing.
pub fn new(style: SmoothingStyle) -> Self {
Self {
style,
..Default::default()
}
}
/// Convenience function for not applying any smoothing at all. Same as `Smoother::default`.
pub fn none() -> Self {
Default::default()
}
2022-02-04 02:51:35 +11:00
2022-02-05 22:56:03 +11:00
/// Whether calling [Self::next()] will yield a new value or an old value. Useful if you need to
2022-02-04 02:51:35 +11:00
/// recompute something wheenver this parameter changes.
pub fn is_smoothing(&self) -> bool {
self.steps_left.load(Ordering::Relaxed) > 0
2022-02-04 02:51:35 +11:00
}
2022-02-03 07:08:23 +11:00
}
// These are not iterators for the sole reason that this will always yield a value, and needing to
// unwrap all of those options is not going to be very fun.
impl Smoother<f32> {
/// Reset the smoother the specified value.
pub fn reset(&mut self, value: f32) {
self.target = value;
self.current.store(value, Ordering::Relaxed);
self.steps_left.store(0, Ordering::Relaxed);
}
2022-02-03 07:08:23 +11:00
/// Set the target value.
pub fn set_target(&mut self, sample_rate: f32, target: f32) {
2022-02-03 07:08:23 +11:00
self.target = target;
let steps_left = match self.style {
SmoothingStyle::None => 1,
SmoothingStyle::Linear(time) | SmoothingStyle::Logarithmic(time) => {
(sample_rate * time / 1000.0).round() as u32
}
};
self.steps_left.store(steps_left, Ordering::Relaxed);
let current = self.current.load(Ordering::Relaxed);
self.step_size = match self.style {
SmoothingStyle::None => 0.0,
SmoothingStyle::Linear(_) => (self.target - current) / steps_left as f32,
SmoothingStyle::Logarithmic(_) => {
// We need to solve `current * (step_size ^ steps_left) = target` for
// `step_size`
nih_debug_assert_ne!(current, 0.0);
(self.target / current).powf((steps_left as f32).recip())
}
};
2022-02-03 07:08:23 +11:00
}
// Yes, Clippy, like I said, this was intentional
#[allow(clippy::should_implement_trait)]
pub fn next(&self) -> f32 {
2022-02-13 06:54:03 +11:00
if self.steps_left.load(Ordering::Relaxed) > 0 {
let current = self.current.load(Ordering::Relaxed);
2022-02-03 07:08:23 +11:00
// 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 {
self.target
2022-02-03 07:08:23 +11:00
} else {
match &self.style {
SmoothingStyle::None => self.target,
SmoothingStyle::Linear(_) => current + self.step_size,
SmoothingStyle::Logarithmic(_) => current * self.step_size,
}
};
self.current.store(new, Ordering::Relaxed);
new
2022-02-03 07:08:23 +11:00
} else {
self.target
}
}
}
2022-02-03 07:26:20 +11:00
impl Smoother<i32> {
/// Reset the smoother the specified value.
pub fn reset(&mut self, value: i32) {
self.target = value;
self.current.store(value as f32, Ordering::Relaxed);
self.steps_left.store(0, Ordering::Relaxed);
}
pub fn set_target(&mut self, sample_rate: f32, target: i32) {
2022-02-03 07:26:20 +11:00
self.target = target;
let steps_left = match self.style {
SmoothingStyle::None => 1,
SmoothingStyle::Linear(time) | SmoothingStyle::Logarithmic(time) => {
(sample_rate * time / 1000.0).round() as u32
}
};
self.steps_left.store(steps_left, Ordering::Relaxed);
let current = self.current.load(Ordering::Relaxed);
self.step_size = match self.style {
SmoothingStyle::None => 0.0,
SmoothingStyle::Linear(_) => (self.target as f32 - current) / steps_left as f32,
SmoothingStyle::Logarithmic(_) => {
nih_debug_assert_ne!(current, 0.0);
(self.target as f32 / current).powf((steps_left as f32).recip())
}
};
2022-02-03 07:26:20 +11:00
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> i32 {
2022-02-13 06:54:03 +11:00
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 {
self.target as f32
2022-02-03 07:26:20 +11:00
} else {
match &self.style {
SmoothingStyle::None => self.target as f32,
SmoothingStyle::Linear(_) => current + self.step_size,
SmoothingStyle::Logarithmic(_) => current * self.step_size,
}
};
self.current.store(new, Ordering::Relaxed);
new.round() as i32
2022-02-03 07:26:20 +11:00
} else {
self.target
}
}
}
2022-02-03 08:34:29 +11:00
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_f32_smoothing() {
let mut smoother: Smoother<f32> = Smoother::new(SmoothingStyle::Linear(100.0));
smoother.reset(10.0);
2022-02-03 08:34:29 +11:00
assert_eq!(smoother.next(), 10.0);
// Instead of testing the actual values, we'll make sure that we reach the target values at
// the expected time.
smoother.set_target(100.0, 20.0);
2022-02-03 08:34:29 +11:00
for _ in 0..(10 - 2) {
2022-02-11 09:40:18 +11:00
smoother.next();
2022-02-03 08:34:29 +11:00
}
assert_ne!(smoother.next(), 20.0);
assert_eq!(smoother.next(), 20.0);
}
#[test]
fn linear_i32_smoothing() {
let mut smoother: Smoother<i32> = Smoother::new(SmoothingStyle::Linear(100.0));
smoother.reset(10);
2022-02-03 08:34:29 +11:00
assert_eq!(smoother.next(), 10);
// Integers are rounded, but with these values we can still test this
smoother.set_target(100.0, 20);
2022-02-03 08:34:29 +11:00
for _ in 0..(10 - 2) {
2022-02-11 09:40:18 +11:00
smoother.next();
2022-02-03 08:34:29 +11:00
}
assert_ne!(smoother.next(), 20);
assert_eq!(smoother.next(), 20);
}
2022-02-03 09:00:17 +11:00
#[test]
fn logarithmic_f32_smoothing() {
let mut smoother: Smoother<f32> = Smoother::new(SmoothingStyle::Logarithmic(100.0));
smoother.reset(10.0);
2022-02-03 09:00:17 +11:00
assert_eq!(smoother.next(), 10.0);
// Instead of testing the actual values, we'll make sure that we reach the target values at
// the expected time.
smoother.set_target(100.0, 20.0);
2022-02-03 09:00:17 +11:00
for _ in 0..(10 - 2) {
2022-02-11 09:40:18 +11:00
smoother.next();
2022-02-03 09:00:17 +11:00
}
assert_ne!(smoother.next(), 20.0);
assert_eq!(smoother.next(), 20.0);
}
#[test]
fn logarithmic_i32_smoothing() {
let mut smoother: Smoother<i32> = Smoother::new(SmoothingStyle::Logarithmic(100.0));
smoother.reset(10);
2022-02-03 09:00:17 +11:00
assert_eq!(smoother.next(), 10);
// Integers are rounded, but with these values we can still test this
smoother.set_target(100.0, 20);
2022-02-03 09:00:17 +11:00
for _ in 0..(10 - 2) {
2022-02-11 09:40:18 +11:00
smoother.next();
2022-02-03 09:00:17 +11:00
}
assert_ne!(smoother.next(), 20);
assert_eq!(smoother.next(), 20);
}
2022-02-03 08:34:29 +11:00
}