From 8a7100ac3e8f61fcfa9c5b510846487dd40e845c Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 5 Apr 2023 18:08:22 +0200 Subject: [PATCH] Add an OversamplingAware smoothing style This can be used to have an ergonomic way to do multi-rate smoothing with variable oversampling amounts that only the `Arc` to be updated from a parameter callback. --- CHANGELOG.md | 17 +++++++++ plugins/crossover/src/lib.rs | 6 ++-- src/params/smoothing.rs | 68 ++++++++++++++++++++---------------- src/prelude.rs | 2 +- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cdde79b..61f95839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,23 @@ state is to list breaking changes. - The `nih_debug_assert*!()` macros are now upgraded to regular panicking `debug_assert!()` macros during tests. +- `SmoothingStyle::for_oversampling_factor()` has been removed in favor of a new + mechanism that allows the smmoothers to be aware of oversampling. A new + `Smoothingstyle::OversamplingAware(oversampling_times, style)` can be used to + wrap another `Smoothingstyle` to make it aware of an oversampling amount that + can change at runtime. The `oversampling_times` is an `Arc` that + indicates the current oversampling amount. This makes it possible to link + multiple parameters to the same oversampling amount, have different sets of + parameters run at different effective sample rates, and automatically update + those oversampling amounts/sample rate multipliers from a parameter callback. +- As a consequence of the above change, `Smoothingstyle` is no longer `Copy` + since the `OversamplingAware` smoothing style contain an + `Arc`. It can still be `Clone`d. + +### Changes + +- The prelude module now also re-exports the `AtomicF32` type since it's needed + to use the new `Smoothingstyle::OversamplingAware`. ## [2023-04-01] diff --git a/plugins/crossover/src/lib.rs b/plugins/crossover/src/lib.rs index f4c11b53..d7f407ac 100644 --- a/plugins/crossover/src/lib.rs +++ b/plugins/crossover/src/lib.rs @@ -114,15 +114,15 @@ impl CrossoverParams { // TODO: More sensible default frequencies crossover_1_freq: FloatParam::new("Crossover 1", 200.0, crossover_range) - .with_smoother(crossover_smoothing_style) + .with_smoother(crossover_smoothing_style.clone()) .with_value_to_string(crossover_value_to_string.clone()) .with_string_to_value(crossover_string_to_value.clone()), crossover_2_freq: FloatParam::new("Crossover 2", 1000.0, crossover_range) - .with_smoother(crossover_smoothing_style) + .with_smoother(crossover_smoothing_style.clone()) .with_value_to_string(crossover_value_to_string.clone()) .with_string_to_value(crossover_string_to_value.clone()), crossover_3_freq: FloatParam::new("Crossover 3", 5000.0, crossover_range) - .with_smoother(crossover_smoothing_style) + .with_smoother(crossover_smoothing_style.clone()) .with_value_to_string(crossover_value_to_string.clone()) .with_string_to_value(crossover_string_to_value.clone()), crossover_4_freq: FloatParam::new("Crossover 4", 10000.0, crossover_range) diff --git a/src/params/smoothing.rs b/src/params/smoothing.rs index b2a6a704..fda4e511 100644 --- a/src/params/smoothing.rs +++ b/src/params/smoothing.rs @@ -1,11 +1,21 @@ //! Utilities to handle smoothing parameter changes over time. -use atomic_float::AtomicF32; use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::Arc; + +// Re-exported here because it's sued in `SmoothingStyle`. +pub use atomic_float::AtomicF32; /// Controls if and how parameters gets smoothed. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum SmoothingStyle { + /// Wraps another smoothing style to create a multi-rate oversampling-aware smoother for a + /// parameter that's used in an oversampled part of the plugin. The `Arc` indicates + /// the oversampling amount, where `1.0` means no oversampling. This value can change at + /// runtime, and it effectively scales the sample rate when computing new smoothing coefficients + /// when the parameter's value changes. + OversamplingAware(Arc, Arc), + /// No smoothing is applied. The parameter's `value` field contains the latest sample value /// available for the parameters. None, @@ -64,18 +74,6 @@ pub struct SmootherIter<'a, T: Smoothable> { } impl SmoothingStyle { - /// Utility function to modify the duration to compensate for an oversampling factor. When using - /// 4x oversampling, the duration needs to be four times as long to compensate for the four - /// times higher effective sample rate. - pub fn for_oversampling_factor(self, factor: f32) -> Self { - match self { - SmoothingStyle::None => SmoothingStyle::None, - SmoothingStyle::Linear(time) => SmoothingStyle::Linear(time * factor), - SmoothingStyle::Logarithmic(time) => SmoothingStyle::Logarithmic(time * factor), - SmoothingStyle::Exponential(time) => SmoothingStyle::Exponential(time * factor), - } - } - /// Compute the number of steps to reach the target value based on the sample rate and this /// smoothing style's duration. #[inline] @@ -83,10 +81,12 @@ impl SmoothingStyle { nih_debug_assert!(sample_rate > 0.0); match self { - SmoothingStyle::None => 1, - SmoothingStyle::Linear(time) - | SmoothingStyle::Logarithmic(time) - | SmoothingStyle::Exponential(time) => { + Self::OversamplingAware(oversampling_times, style) => { + style.num_steps(sample_rate * oversampling_times.load(Ordering::Relaxed)) + } + + Self::None => 1, + Self::Linear(time) | Self::Logarithmic(time) | Self::Exponential(time) => { nih_debug_assert!(*time >= 0.0); (sample_rate * time / 1000.0).round() as u32 } @@ -101,9 +101,11 @@ impl SmoothingStyle { nih_debug_assert!(num_steps >= 1); match self { - SmoothingStyle::None => 0.0, - SmoothingStyle::Linear(_) => (target - start) / (num_steps as f32), - SmoothingStyle::Logarithmic(_) => { + Self::OversamplingAware(_, style) => style.step_size(start, target, num_steps), + + Self::None => 0.0, + Self::Linear(_) => (target - start) / (num_steps as f32), + Self::Logarithmic(_) => { // We need to solve `start * (step_size ^ num_steps) = target` for `step_size` nih_debug_assert_ne!(start, 0.0); ((target / start) as f64).powf((num_steps as f64).recip()) as f32 @@ -112,7 +114,7 @@ impl SmoothingStyle { // multiplied by, while the target value is multiplied by one minus the coefficient. This // reaches 99.99% of the target value after `num_steps`. The smoother will snap to the // target value after that point. - SmoothingStyle::Exponential(_) => 0.0001f64.powf((num_steps as f64).recip()) as f32, + Self::Exponential(_) => 0.0001f64.powf((num_steps as f64).recip()) as f32, } } @@ -125,10 +127,12 @@ impl SmoothingStyle { #[inline] pub fn next(&self, current: f32, target: f32, step_size: f32) -> f32 { match self { - SmoothingStyle::None => target, - SmoothingStyle::Linear(_) => current + step_size, - SmoothingStyle::Logarithmic(_) => current * step_size, - SmoothingStyle::Exponential(_) => (current * step_size) + (target * (1.0 - step_size)), + Self::OversamplingAware(_, style) => style.next(current, target, step_size), + + Self::None => target, + Self::Linear(_) => current + step_size, + Self::Logarithmic(_) => current * step_size, + Self::Exponential(_) => (current * step_size) + (target * (1.0 - step_size)), } } @@ -143,10 +147,12 @@ impl SmoothingStyle { nih_debug_assert!(steps >= 1); match self { - SmoothingStyle::None => target, - SmoothingStyle::Linear(_) => current + (step_size * steps as f32), - SmoothingStyle::Logarithmic(_) => current * (step_size.powi(steps as i32)), - SmoothingStyle::Exponential(_) => { + Self::OversamplingAware(_, style) => style.next_step(current, target, step_size, steps), + + Self::None => target, + Self::Linear(_) => current + (step_size * steps as f32), + Self::Logarithmic(_) => current * (step_size.powi(steps as i32)), + Self::Exponential(_) => { // This is the same as calculating `current = (current * step_size) + // (target * (1 - step_size))` in a loop since the target value won't change let coefficient = step_size.powi(steps as i32); @@ -198,7 +204,7 @@ impl Clone for Smoother { // We can't derive clone because of the atomics, but these atomics are only here to allow // Send+Sync interior mutability Self { - style: self.style, + style: self.style.clone(), steps_left: AtomicI32::new(self.steps_left.load(Ordering::Relaxed)), step_size: AtomicF32::new(self.step_size.load(Ordering::Relaxed)), current: AtomicF32::new(self.current.load(Ordering::Relaxed)), diff --git a/src/prelude.rs b/src/prelude.rs index 83a61a51..c314fd55 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -27,7 +27,7 @@ pub use crate::midi::{control_change, MidiConfig, NoteEvent, PluginNoteEvent}; pub use crate::params::enums::{Enum, EnumParam}; pub use crate::params::internals::ParamPtr; pub use crate::params::range::{FloatRange, IntRange}; -pub use crate::params::smoothing::{Smoothable, Smoother, SmoothingStyle}; +pub use crate::params::smoothing::{AtomicF32, Smoothable, Smoother, SmoothingStyle}; pub use crate::params::Params; pub use crate::params::{BoolParam, FloatParam, IntParam, Param, ParamFlags}; #[cfg(feature = "vst3")]