From 8f89754ba56e326d146b50d9bc440325b6912686 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 2 Feb 2022 21:08:23 +0100 Subject: [PATCH] Add simple linear parameter smoothing --- plugins/gain/src/lib.rs | 9 ++-- plugins/sine/src/lib.rs | 13 +++-- src/lib.rs | 1 + src/param.rs | 17 ++----- src/param/smoothing.rs | 110 ++++++++++++++++++++++++++++++++++++++++ src/wrapper/vst3.rs | 37 ++++++++++++-- 6 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 src/param/smoothing.rs diff --git a/plugins/gain/src/lib.rs b/plugins/gain/src/lib.rs index e18a52ca..bea48e3a 100644 --- a/plugins/gain/src/lib.rs +++ b/plugins/gain/src/lib.rs @@ -21,7 +21,7 @@ use nih_plug::{ formatters, util, Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus, Vst3Plugin, }; -use nih_plug::{BoolParam, FloatParam, Param, Params, Range}; +use nih_plug::{BoolParam, FloatParam, Param, Params, Range, Smoother, SmoothingStyle}; use parking_lot::RwLock; use std::pin::Pin; @@ -57,6 +57,7 @@ impl Default for GainParams { Self { gain: FloatParam { value: 0.0, + smoothed: Smoother::new(SmoothingStyle::SmoothLinear(2.0)), value_changed: None, // If, for instance, updating this parameter would require other parts of the // plugin's internal state to be updated other values to also be updated, then you @@ -119,9 +120,11 @@ impl Plugin for Gain { fn process(&mut self, buffer: &mut Buffer, _context: &dyn ProcessContext) -> ProcessStatus { // TODO: The wrapper should set FTZ if not yet enabled, mention ths in the process fuctnion for samples in buffer.iter_mut() { + // Smoothing is optionally built into the parameters themselves + let gain = self.params.gain.smoothed.next(); + for sample in samples { - // TODO: Smoothing - *sample *= util::db_to_gain(self.params.gain.value); + *sample *= util::db_to_gain(gain); } } diff --git a/plugins/sine/src/lib.rs b/plugins/sine/src/lib.rs index 116b190c..ac601712 100644 --- a/plugins/sine/src/lib.rs +++ b/plugins/sine/src/lib.rs @@ -21,7 +21,7 @@ use nih_plug::{ formatters, util, Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus, Vst3Plugin, }; -use nih_plug::{FloatParam, Param, Params, Range}; +use nih_plug::{FloatParam, Param, Params, Range, Smoother, SmoothingStyle}; use std::f32::consts; use std::pin::Pin; @@ -60,6 +60,7 @@ impl Default for SineParams { Self { gain: FloatParam { value: -10.0, + smoothed: Smoother::new(SmoothingStyle::SmoothLinear(2.0)), range: Range::Linear { min: -30.0, max: 0.0, @@ -71,6 +72,7 @@ impl Default for SineParams { }, frequency: FloatParam { value: 420.0, + smoothed: Smoother::new(SmoothingStyle::SmoothLinear(10.0)), range: Range::Skewed { min: 1.0, max: 20_000.0, @@ -117,8 +119,12 @@ impl Plugin for Sine { } fn process(&mut self, buffer: &mut Buffer, _context: &dyn ProcessContext) -> ProcessStatus { - let phase_delta = self.params.frequency.value / self.sample_rate; for samples in buffer.iter_mut() { + // Smoothing is optionally built into the parameters themselves + let gain = self.params.gain.smoothed.next(); + let frequency = self.params.frequency.smoothed.next(); + + let phase_delta = frequency / self.sample_rate; let sine = (self.phase * consts::TAU).sin(); self.phase += phase_delta; @@ -127,8 +133,7 @@ impl Plugin for Sine { } for sample in samples { - // TODO: Parameter smoothing - *sample = sine * util::db_to_gain(self.params.gain.value); + *sample = sine * util::db_to_gain(gain); } } diff --git a/src/lib.rs b/src/lib.rs index efa63dd8..8140f96d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,7 @@ pub use buffer::Buffer; pub use context::ProcessContext; pub use param::internals::Params; pub use param::range::Range; +pub use param::smoothing::{Smoother, SmoothingStyle}; pub use param::{BoolParam, FloatParam, IntParam, Param}; pub use plugin::{BufferConfig, BusConfig, Plugin, ProcessStatus, Vst3Plugin}; diff --git a/src/param.rs b/src/param.rs index 5da14e75..9d038db7 100644 --- a/src/param.rs +++ b/src/param.rs @@ -20,9 +20,11 @@ use std::fmt::Display; use std::sync::Arc; use self::range::{NormalizebleRange, Range}; +use self::smoothing::Smoother; pub mod internals; pub mod range; +pub mod smoothing; pub type FloatParam = PlainParam; pub type IntParam = PlainParam; @@ -69,19 +71,7 @@ pub struct PlainParam { /// Storing parameter values like this instead of in a single contiguous array is bad for cache /// locality, but it does allow for a much nicer declarative API. pub value: T, - - // // TODO: Add optional value smoothing using an Enum. This would need to include at least - // // - `Smoothing::None`: Don't do any work, `value` is just the most recent vlaue in the - // // block - // // - `Smoothing::Smooth(f32)`: Automatically smooth to `f32` milliseconds. The host will - // // provide this as an iterator (would probably be much faster than precalculating - // // verything). - // // - `Smoothing::SampleAccurate(f32)`: Same as `Smooth`, but uses sample accurate - // // automation values if the host provides those instead of the last value. - // // - // // And this would need to integrate nicely with the sample buffer iterator adapter when - // // that gets added - // pub smoothed: Smoothing, + pub smoothed: Smoother, /// Optional callback for listening to value changes. The argument passed to this function is /// the parameter's new **plain** value. This should not do anything expensive as it may be /// called multiple times in rapid succession. @@ -132,6 +122,7 @@ where fn default() -> Self { Self { value: T::default(), + smoothed: Smoother::none(), value_changed: None, range: Range::default(), name: "", diff --git a/src/param/smoothing.rs b/src/param/smoothing.rs new file mode 100644 index 00000000..199d5603 --- /dev/null +++ b/src/param/smoothing.rs @@ -0,0 +1,110 @@ +// nih-plug: plugins, but rewritten in Rust +// Copyright (C) 2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/// 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, + /// Smooth parameter changes so the . + SmoothLinear(f32), + // TODO: Sample-accurate modes +} + +/// A smoother, providing a smoothed value for each sample. +pub struct Smoother { + /// The kind of snoothing that needs to be applied, if any. + style: SmoothingStyle, + /// The number of steps of smoothing left to take. + steps_left: u32, + /// 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: f32, + /// The value we're smoothing towards + target: T, +} + +impl Default for Smoother { + fn default() -> Self { + Self { + style: SmoothingStyle::None, + steps_left: 0, + step_size: Default::default(), + current: 0.0, + target: Default::default(), + } + } +} + +impl Smoother { + /// 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() + } +} + +// 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. +// TODO: Also implement for i32 +impl Smoother { + /// Set the target value. + pub fn set_target(&mut self, sample_rate: f32, target: f32) { + self.target = target; + self.steps_left = match self.style { + SmoothingStyle::None => 1, + SmoothingStyle::SmoothLinear(time) => (sample_rate * time / 1000.0).round() as u32, + }; + self.step_size = match self.style { + SmoothingStyle::None => 0.0, + SmoothingStyle::SmoothLinear(_) => { + (self.target - self.current) / self.steps_left as f32 + } + }; + } + + // Yes, Clippy, like I said, this was intentional + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> f32 { + if self.steps_left > 1 { + // The number of steps usually won't fit exactly, so make sure we don't do weird things + // with overshoots or undershoots + self.steps_left -= 1; + if self.steps_left == 0 { + self.current = self.target; + } else { + match &self.style { + SmoothingStyle::None => self.current = self.target, + SmoothingStyle::SmoothLinear(_) => self.current += self.step_size, + }; + } + + self.current + } else { + self.target + } + } +} diff --git a/src/wrapper/vst3.rs b/src/wrapper/vst3.rs index f572f46b..c5b294d2 100644 --- a/src/wrapper/vst3.rs +++ b/src/wrapper/vst3.rs @@ -43,7 +43,7 @@ use widestring::U16CStr; use crate::buffer::Buffer; use crate::context::{EventLoop, MainThreadExecutor, OsEventLoop, ProcessContext}; use crate::param::internals::ParamPtr; -use crate::param::range::Range; +use crate::param::range::{NormalizebleRange, Range}; use crate::param::Param; use crate::plugin::{BufferConfig, BusConfig, Plugin, ProcessStatus, Vst3Plugin}; use crate::wrapper::state::{ParamValue, State}; @@ -259,14 +259,29 @@ impl WrapperInner<'_, P> { wrapper } - unsafe fn set_normalized_value_by_hash(&self, hash: u32, normalized_value: f64) -> tresult { + /// Convenience function for setting a value for a parameter as triggered by a VST3 parameter + /// update. The same rate is for updating parameter smoothing. + unsafe fn set_normalized_value_by_hash( + &self, + hash: u32, + normalized_value: f64, + sample_rate: Option, + ) -> tresult { if hash == *BYPASS_PARAM_HASH { self.bypass_state .store(normalized_value >= 0.5, Ordering::SeqCst); kResultOk } else if let Some(param_ptr) = self.param_by_hash.get(&hash) { - param_ptr.set_normalized_value(normalized_value as f32); + // Also update the parameter's smoothing if applicable + match (param_ptr, sample_rate) { + (ParamPtr::FloatParam(p), Some(sample_rate)) => { + let plain_value = (**p).range.unnormalize(normalized_value as f32); + (**p).set_plain_value(plain_value); + (**p).smoothed.set_target(sample_rate, plain_value); + } + _ => param_ptr.set_normalized_value(normalized_value as f32), + } kResultOk } else { @@ -780,7 +795,13 @@ impl IEditController for Wrapper<'_, P> { return kResultOk; } - self.inner.set_normalized_value_by_hash(id, value) + let sample_rate = self + .inner + .current_buffer_config + .load() + .map(|c| c.sample_rate); + self.inner + .set_normalized_value_by_hash(id, value, sample_rate) } unsafe fn set_component_handler( @@ -939,6 +960,11 @@ impl IAudioProcessor for Wrapper<'_, P> { // We need to handle incoming automation first let data = &*data; + let sample_rate = self + .inner + .current_buffer_config + .load() + .map(|c| c.sample_rate); if let Some(param_changes) = data.input_param_changes.upgrade() { let num_param_queues = param_changes.get_parameter_count(); for change_queue_idx in 0..num_param_queues { @@ -959,7 +985,8 @@ impl IAudioProcessor for Wrapper<'_, P> { &mut value, ) == kResultOk { - self.inner.set_normalized_value_by_hash(param_hash, value); + self.inner + .set_normalized_value_by_hash(param_hash, value, sample_rate); } } }