1
0
Fork 0

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<AtomicF32>` to be
updated from a parameter callback.
This commit is contained in:
Robbert van der Helm 2023-04-05 18:08:22 +02:00
parent 95d7dabcee
commit 8a7100ac3e
4 changed files with 58 additions and 35 deletions

View file

@ -16,6 +16,23 @@ state is to list breaking changes.
- The `nih_debug_assert*!()` macros are now upgraded to regular panicking - The `nih_debug_assert*!()` macros are now upgraded to regular panicking
`debug_assert!()` macros during tests. `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<AtomicF32>` 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<Smoothingstyle>`. 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] ## [2023-04-01]

View file

@ -114,15 +114,15 @@ impl CrossoverParams {
// TODO: More sensible default frequencies // TODO: More sensible default frequencies
crossover_1_freq: FloatParam::new("Crossover 1", 200.0, crossover_range) 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_value_to_string(crossover_value_to_string.clone())
.with_string_to_value(crossover_string_to_value.clone()), .with_string_to_value(crossover_string_to_value.clone()),
crossover_2_freq: FloatParam::new("Crossover 2", 1000.0, crossover_range) 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_value_to_string(crossover_value_to_string.clone())
.with_string_to_value(crossover_string_to_value.clone()), .with_string_to_value(crossover_string_to_value.clone()),
crossover_3_freq: FloatParam::new("Crossover 3", 5000.0, crossover_range) 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_value_to_string(crossover_value_to_string.clone())
.with_string_to_value(crossover_string_to_value.clone()), .with_string_to_value(crossover_string_to_value.clone()),
crossover_4_freq: FloatParam::new("Crossover 4", 10000.0, crossover_range) crossover_4_freq: FloatParam::new("Crossover 4", 10000.0, crossover_range)

View file

@ -1,11 +1,21 @@
//! Utilities to handle smoothing parameter changes over time. //! Utilities to handle smoothing parameter changes over time.
use atomic_float::AtomicF32;
use std::sync::atomic::{AtomicI32, Ordering}; 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. /// Controls if and how parameters gets smoothed.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub enum SmoothingStyle { 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<AtomicF32>` 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<AtomicF32>, Arc<SmoothingStyle>),
/// No smoothing is applied. The parameter's `value` field contains the latest sample value /// No smoothing is applied. The parameter's `value` field contains the latest sample value
/// available for the parameters. /// available for the parameters.
None, None,
@ -64,18 +74,6 @@ pub struct SmootherIter<'a, T: Smoothable> {
} }
impl SmoothingStyle { 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 /// Compute the number of steps to reach the target value based on the sample rate and this
/// smoothing style's duration. /// smoothing style's duration.
#[inline] #[inline]
@ -83,10 +81,12 @@ impl SmoothingStyle {
nih_debug_assert!(sample_rate > 0.0); nih_debug_assert!(sample_rate > 0.0);
match self { match self {
SmoothingStyle::None => 1, Self::OversamplingAware(oversampling_times, style) => {
SmoothingStyle::Linear(time) style.num_steps(sample_rate * oversampling_times.load(Ordering::Relaxed))
| SmoothingStyle::Logarithmic(time) }
| SmoothingStyle::Exponential(time) => {
Self::None => 1,
Self::Linear(time) | Self::Logarithmic(time) | Self::Exponential(time) => {
nih_debug_assert!(*time >= 0.0); nih_debug_assert!(*time >= 0.0);
(sample_rate * time / 1000.0).round() as u32 (sample_rate * time / 1000.0).round() as u32
} }
@ -101,9 +101,11 @@ impl SmoothingStyle {
nih_debug_assert!(num_steps >= 1); nih_debug_assert!(num_steps >= 1);
match self { match self {
SmoothingStyle::None => 0.0, Self::OversamplingAware(_, style) => style.step_size(start, target, num_steps),
SmoothingStyle::Linear(_) => (target - start) / (num_steps as f32),
SmoothingStyle::Logarithmic(_) => { 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` // We need to solve `start * (step_size ^ num_steps) = target` for `step_size`
nih_debug_assert_ne!(start, 0.0); nih_debug_assert_ne!(start, 0.0);
((target / start) as f64).powf((num_steps as f64).recip()) as f32 ((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 // 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 // reaches 99.99% of the target value after `num_steps`. The smoother will snap to the
// target value after that point. // 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] #[inline]
pub fn next(&self, current: f32, target: f32, step_size: f32) -> f32 { pub fn next(&self, current: f32, target: f32, step_size: f32) -> f32 {
match self { match self {
SmoothingStyle::None => target, Self::OversamplingAware(_, style) => style.next(current, target, step_size),
SmoothingStyle::Linear(_) => current + step_size,
SmoothingStyle::Logarithmic(_) => current * step_size, Self::None => target,
SmoothingStyle::Exponential(_) => (current * step_size) + (target * (1.0 - step_size)), 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); nih_debug_assert!(steps >= 1);
match self { match self {
SmoothingStyle::None => target, Self::OversamplingAware(_, style) => style.next_step(current, target, step_size, steps),
SmoothingStyle::Linear(_) => current + (step_size * steps as f32),
SmoothingStyle::Logarithmic(_) => current * (step_size.powi(steps as i32)), Self::None => target,
SmoothingStyle::Exponential(_) => { 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) + // This is the same as calculating `current = (current * step_size) +
// (target * (1 - step_size))` in a loop since the target value won't change // (target * (1 - step_size))` in a loop since the target value won't change
let coefficient = step_size.powi(steps as i32); let coefficient = step_size.powi(steps as i32);
@ -198,7 +204,7 @@ impl<T: Smoothable> Clone for Smoother<T> {
// We can't derive clone because of the atomics, but these atomics are only here to allow // We can't derive clone because of the atomics, but these atomics are only here to allow
// Send+Sync interior mutability // Send+Sync interior mutability
Self { Self {
style: self.style, style: self.style.clone(),
steps_left: AtomicI32::new(self.steps_left.load(Ordering::Relaxed)), steps_left: AtomicI32::new(self.steps_left.load(Ordering::Relaxed)),
step_size: AtomicF32::new(self.step_size.load(Ordering::Relaxed)), step_size: AtomicF32::new(self.step_size.load(Ordering::Relaxed)),
current: AtomicF32::new(self.current.load(Ordering::Relaxed)), current: AtomicF32::new(self.current.load(Ordering::Relaxed)),

View file

@ -27,7 +27,7 @@ pub use crate::midi::{control_change, MidiConfig, NoteEvent, PluginNoteEvent};
pub use crate::params::enums::{Enum, EnumParam}; pub use crate::params::enums::{Enum, EnumParam};
pub use crate::params::internals::ParamPtr; pub use crate::params::internals::ParamPtr;
pub use crate::params::range::{FloatRange, IntRange}; 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::Params;
pub use crate::params::{BoolParam, FloatParam, IntParam, Param, ParamFlags}; pub use crate::params::{BoolParam, FloatParam, IntParam, Param, ParamFlags};
#[cfg(feature = "vst3")] #[cfg(feature = "vst3")]