1
0
Fork 0

Rework and optimize block smoothing API

You now need to bring your own buffer instead of the smoother having a
built in vector you would need to pre-allocate. This makes the API
simpler, and also much more flexible when doing polyphonic modulation.

In addition, the new API is much more efficient when there is no
smoothing going on anymore.
This commit is contained in:
Robbert van der Helm 2022-07-06 14:26:43 +02:00
parent 89b2d0a66c
commit 68cf0455ee
11 changed files with 39 additions and 94 deletions

View file

@ -8,6 +8,12 @@ code then it will not be listed here.
## [2022-07-06] ## [2022-07-06]
- The block smoothing API has been reworked. Instead of `Smoother`s having their
own built in block buffer, you now need to provide your own mutable slice for
the smoother to fill. This makes the API easier to understand, more flexible,
and it allows cloning smoothers without worrying about allocations for use
with polyphonic modulation. In addition, the new implementation is much more
efficient when the smoothing period has ended before or during the block.
- There are new `NoteEvent::PolyModulation` and `NoteEvent::MonoAutomation` as - There are new `NoteEvent::PolyModulation` and `NoteEvent::MonoAutomation` as
part of polyphonic modulation support for CLAP plugins. part of polyphonic modulation support for CLAP plugins.

View file

@ -83,10 +83,7 @@ impl<'a> Buffer<'a> {
/// SIMD. /// SIMD.
/// ///
/// The parameter smoothers can also produce smoothed values for an entire block using /// The parameter smoothers can also produce smoothed values for an entire block using
/// [`Smoother::next_block()`][crate::prelude::Smoother::next_block()]. Before using this, you /// [`Smoother::next_block()`][crate::prelude::Smoother::next_block()].
/// will need to call
/// [`Plugin::initialize_block_smoothers()`][crate::prelude::Plugin::initialize_block_smoothers()]
/// with the same `max_block_size` in your initialization function first.
/// ///
/// You can use this to obtain block-slices from a buffer so you can pass them to a library: /// You can use this to obtain block-slices from a buffer so you can pass them to a library:
/// ///

View file

@ -75,9 +75,7 @@
//! plugins](https://github.com/robbert-vdh/nih-plug/tree/master/plugins). //! plugins](https://github.com/robbert-vdh/nih-plug/tree/master/plugins).
//! - After calling `.with_smoother()` during an integer or floating point parameter's creation, //! - After calling `.with_smoother()` during an integer or floating point parameter's creation,
//! you can use `param.smoothed` to access smoothed values for that parameter. Be sure to check //! you can use `param.smoothed` to access smoothed values for that parameter. Be sure to check
//! out the [`Smoother`][prelude::Smoother] API for more details. If you want to generate entire //! out the [`Smoother`][prelude::Smoother] API for more details.
//! blocks of smoothed values, be sure to call the predefined
//! `[Plugin::initialize_block_smoothers()]` method from your plugin's `initialize()` function.
//! //!
//! There's a whole lot more to discuss, but once you understand the above you should be able to //! There's a whole lot more to discuss, but once you understand the above you should be able to
//! figure out the rest by reading through the examples and the API documetnation. Good luck! //! figure out the rest by reading through the examples and the API documetnation. Good luck!

View file

@ -147,11 +147,6 @@ pub trait Param: Display {
/// wrappers. This **does** snap to step sizes for continuous parameters (i.e. [`FloatParam`]). /// wrappers. This **does** snap to step sizes for continuous parameters (i.e. [`FloatParam`]).
fn preview_plain(&self, normalized: f32) -> Self::Plain; fn preview_plain(&self, normalized: f32) -> Self::Plain;
/// Allocate memory for block-based smoothing. The
/// [`Plugin::initialize_block_smoothers()`][crate::prelude::Plugin::initialize_block_smoothers()] method
/// will do this for every smoother.
fn initialize_block_smoother(&mut self, max_block_size: usize);
/// Flags to control the parameter's behavior. See [`ParamFlags`]. /// Flags to control the parameter's behavior. See [`ParamFlags`].
fn flags(&self) -> ParamFlags; fn flags(&self) -> ParamFlags;

View file

@ -143,8 +143,6 @@ impl Param for BoolParam {
normalized > 0.5 normalized > 0.5
} }
fn initialize_block_smoother(&mut self, _max_block_size: usize) {}
fn flags(&self) -> ParamFlags { fn flags(&self) -> ParamFlags {
self.flags self.flags
} }

View file

@ -175,10 +175,6 @@ impl<T: Enum + PartialEq> Param for EnumParam<T> {
T::from_index(self.inner.preview_plain(normalized) as usize) T::from_index(self.inner.preview_plain(normalized) as usize)
} }
fn initialize_block_smoother(&mut self, max_block_size: usize) {
self.inner.initialize_block_smoother(max_block_size)
}
fn flags(&self) -> ParamFlags { fn flags(&self) -> ParamFlags {
self.inner.flags() self.inner.flags()
} }
@ -261,10 +257,6 @@ impl Param for EnumParamInner {
self.inner.preview_plain(normalized) self.inner.preview_plain(normalized)
} }
fn initialize_block_smoother(&mut self, max_block_size: usize) {
self.inner.initialize_block_smoother(max_block_size)
}
fn flags(&self) -> ParamFlags { fn flags(&self) -> ParamFlags {
self.inner.flags() self.inner.flags()
} }

View file

@ -201,10 +201,6 @@ impl Param for FloatParam {
} }
} }
fn initialize_block_smoother(&mut self, max_block_size: usize) {
self.smoothed.initialize_block_smoother(max_block_size);
}
fn flags(&self) -> ParamFlags { fn flags(&self) -> ParamFlags {
self.flags self.flags
} }

View file

@ -166,10 +166,6 @@ impl Param for IntParam {
self.range.unnormalize(normalized) self.range.unnormalize(normalized)
} }
fn initialize_block_smoother(&mut self, max_block_size: usize) {
self.smoothed.initialize_block_smoother(max_block_size);
}
fn flags(&self) -> ParamFlags { fn flags(&self) -> ParamFlags {
self.flags self.flags
} }

View file

@ -161,7 +161,6 @@ impl ParamPtr {
param_ptr_forward!(pub unsafe fn step_count(&self) -> Option<usize>); param_ptr_forward!(pub unsafe fn step_count(&self) -> Option<usize>);
param_ptr_forward!(pub unsafe fn previous_normalized_step(&self, from: f32) -> f32); param_ptr_forward!(pub unsafe fn previous_normalized_step(&self, from: f32) -> f32);
param_ptr_forward!(pub unsafe fn next_normalized_step(&self, from: f32) -> f32); param_ptr_forward!(pub unsafe fn next_normalized_step(&self, from: f32) -> f32);
param_ptr_forward!(pub unsafe fn initialize_block_smoother(&mut self, max_block_size: usize));
param_ptr_forward!(pub unsafe fn normalized_value_to_string(&self, normalized: f32, include_unit: bool) -> String); param_ptr_forward!(pub unsafe fn normalized_value_to_string(&self, normalized: f32, include_unit: bool) -> String);
param_ptr_forward!(pub unsafe fn string_to_normalized_value(&self, string: &str) -> Option<f32>); param_ptr_forward!(pub unsafe fn string_to_normalized_value(&self, string: &str) -> Option<f32>);
param_ptr_forward!(pub unsafe fn flags(&self) -> ParamFlags); param_ptr_forward!(pub unsafe fn flags(&self) -> ParamFlags);

View file

@ -1,11 +1,8 @@
//! Utilities to handle smoothing parameter changes over time. //! Utilities to handle smoothing parameter changes over time.
use atomic_float::AtomicF32; use atomic_float::AtomicF32;
use atomic_refcell::{AtomicRefCell, AtomicRefMut};
use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::atomic::{AtomicI32, Ordering};
use crate::buffer::Block;
/// Controls if and how parameters gets smoothed. /// Controls if and how parameters gets smoothed.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum SmoothingStyle { pub enum SmoothingStyle {
@ -55,15 +52,10 @@ pub struct Smoother<T> {
current: AtomicF32, current: AtomicF32,
/// The value we're smoothing towards /// The value we're smoothing towards
target: T, target: T,
/// A dense buffer containing smoothed values for an entire block of audio. Useful when using
/// [`Buffer::iter_blocks()`][crate::prelude::Buffer::iter_blocks()] to process small blocks of audio
/// multiple times.
block_values: AtomicRefCell<Vec<T>>,
} }
/// An iterator that continuously produces smoothed values. Can be used as an alternative to the /// An iterator that continuously produces smoothed values. Can be used as an alternative to the
/// built-in block-based smoothing API. Since the iterator itself is infinite, you can use /// block-based smoothing API. Since the iterator itself is infinite, you can use
/// [`Smoother::is_smoothing()`] and [`Smoother::steps_left()`] to get information on the current /// [`Smoother::is_smoothing()`] and [`Smoother::steps_left()`] to get information on the current
/// smoothing status. /// smoothing status.
pub struct SmootherIter<'a, T> { pub struct SmootherIter<'a, T> {
@ -85,8 +77,6 @@ impl<T: Smoothable> Default for Smoother<T> {
step_size: Default::default(), step_size: Default::default(),
current: AtomicF32::new(0.0), current: AtomicF32::new(0.0),
target: Default::default(), target: Default::default(),
block_values: AtomicRefCell::new(Vec::new()),
} }
} }
} }
@ -143,15 +133,6 @@ impl<T: Smoothable> Smoother<T> {
SmootherIter { smoother: self } SmootherIter { smoother: self }
} }
/// Allocate memory to store smoothed values for an entire block of audio. Call this in
/// [`Plugin::initialize()`][crate::prelude::Plugin::initialize()] with the same max block size you are
/// going to pass to [`Buffer::iter_blocks()`][crate::prelude::Buffer::iter_blocks()].
pub fn initialize_block_smoother(&mut self, max_block_size: usize) {
self.block_values
.borrow_mut()
.resize_with(max_block_size, || T::default());
}
/// Reset the smoother the specified value. /// Reset the smoother the specified value.
pub fn reset(&mut self, value: T) { pub fn reset(&mut self, value: T) {
self.target = value; self.target = value;
@ -248,43 +229,44 @@ impl<T: Smoothable> Smoother<T> {
T::from_f32(self.current.load(Ordering::Relaxed)) T::from_f32(self.current.load(Ordering::Relaxed))
} }
/// Produce smoothed values for an entire block of audio. Used in conjunction with /// Produce smoothed values for an entire block of audio. This is useful when iterating the same
/// [`Buffer::iter_blocks()`][crate::prelude::Buffer::iter_blocks()]. Make sure to call /// block of audio multiple times. For instance when summing voices for a synthesizer.
/// [`Plugin::initialize_block_smoothers()`][crate::prelude::Plugin::initialize_block_smoothers()] with /// `block_values[..block_len]` will be filled with the smoothed values. This is simply a
/// the same maximum buffer block size as the one passed to `iter_blocks()` in your /// convenient function for [`next_block_exact()`][Self::next_block_exact()] when iterating over
/// [`Plugin::initialize()`][crate::prelude::Plugin::initialize()] function first to allocate memory for /// variable length blocks with a known maximum size.
/// the block smoothing.
///
/// Returns a `None` value if the block length exceed's the allocated capacity.
/// ///
/// # Panics /// # Panics
/// ///
/// Panics if this function is called again while another block value slice is still alive. /// Panics if `block_len > block_values.len()`.
pub fn next_block(&self, block: &Block) -> Option<AtomicRefMut<[T]>> { pub fn next_block(&self, block_values: &mut [T], block_len: usize) {
self.next_block_mapped(block, |x| x) self.next_block_exact_mapped(&mut block_values[..block_len], |x| x)
}
/// The same as [`next_block()`][Self::next_block()], but filling the entire slice.
pub fn next_block_exact(&self, block_values: &mut [T]) {
self.next_block_exact_mapped(block_values, |x| x)
} }
/// The same as [`next_block()`][Self::next_block()], but with a function applied to each /// The same as [`next_block()`][Self::next_block()], but with a function applied to each
/// produced value. Useful when applying modulation to a smoothed parameter. /// produced value. Useful when applying modulation to a smoothed parameter.
pub fn next_block_mapped( pub fn next_block_mapped(&self, block_values: &mut [T], block_len: usize, f: impl Fn(T) -> T) {
&self, self.next_block_exact_mapped(&mut block_values[..block_len], f)
block: &Block,
mut f: impl FnMut(T) -> T,
) -> Option<AtomicRefMut<[T]>> {
let mut block_values = self.block_values.borrow_mut();
if block_values.len() < block.len() {
return None;
} }
// TODO: As a small optimization we could split this up into two loops for the smoothed and /// The same as [`next_block_exact()`][Self::next_block()], but with a function applied to each
// unsmoothed parts. Another worthwhile optimization would be to remember if the /// produced value. Useful when applying modulation to a smoothed parameter.
// buffer is already filled with the target value and [Self::is_smoothing()] is false. pub fn next_block_exact_mapped(&self, block_values: &mut [T], f: impl Fn(T) -> T) {
// In that case we wouldn't need to do anything ehre. // `self.next()` will yield the current value if the parameter is no longer smoothing, but
(&mut block_values[..block.len()]).fill_with(|| f(self.next())); // it's a bit of a waste to continuesly call that if only the first couple or none of the
// values in `block_values` would require smoothing and the rest don't. Instead, we'll just
// smooth the values as necessary, and then reuse the target value for the rest of the
// block.
let num_smoothed_values = block_values
.len()
.min(self.steps_left.load(Ordering::Relaxed) as usize);
Some(AtomicRefMut::map(block_values, |values| { block_values[..num_smoothed_values].fill_with(|| f(self.next()));
&mut values[..block.len()] block_values[num_smoothed_values..].fill(self.target);
}))
} }
} }

View file

@ -152,10 +152,7 @@ pub trait Plugin: Default + Send + Sync + 'static {
/// per-sample SIMD or excessive branching. The parameter smoothers can also work in both modes: /// per-sample SIMD or excessive branching. The parameter smoothers can also work in both modes:
/// use [`Smoother::next()`][crate::prelude::Smoother::next()] for per-sample processing, and /// use [`Smoother::next()`][crate::prelude::Smoother::next()] for per-sample processing, and
/// [`Smoother::next_block()`][crate::prelude::Smoother::next_block()] for block-based /// [`Smoother::next_block()`][crate::prelude::Smoother::next_block()] for block-based
/// processing. In order to use block-based smoothing, you will need to call /// processing.
/// [`initialize_block_smoothers()`][Self::initialize_block_smoothers()] in your
/// [`initialize()`][Self::initialize()] function first to reserve enough capacity in the
/// smoothers.
/// ///
/// The `context` object contains context information as well as callbacks for working with note /// The `context` object contains context information as well as callbacks for working with note
/// events. The [`AuxiliaryBuffers`] contain the plugin's sidechain input buffers and /// events. The [`AuxiliaryBuffers`] contain the plugin's sidechain input buffers and
@ -179,17 +176,6 @@ pub trait Plugin: Default + Send + Sync + 'static {
/// `initialize()` may be called more than once before `deactivate()` is called, for instance /// `initialize()` may be called more than once before `deactivate()` is called, for instance
/// when restoring state while the plugin is still activate. /// when restoring state while the plugin is still activate.
fn deactivate(&mut self) {} fn deactivate(&mut self) {}
/// Convenience function provided to allocate memory for block-based smoothing for this plugin.
/// Since this allocates memory, this should be called in [`initialize()`][Self::initialize()].
/// If you are going to use [`Buffer::iter_blocks()`] and want to use parameter smoothing in
/// those blocks, then call this function with the same maximum block size first before calling
/// [`Smoother::next_block()`][crate::prelude::Smoother::next_block()].
fn initialize_block_smoothers(&mut self, max_block_size: usize) {
for (_, mut param, _) in self.params().param_map() {
unsafe { param.initialize_block_smoother(max_block_size) };
}
}
} }
/// Provides auxiliary metadata needed for a CLAP plugin. /// Provides auxiliary metadata needed for a CLAP plugin.