1
0
Fork 0

Add simple linear parameter smoothing

This commit is contained in:
Robbert van der Helm 2022-02-02 21:08:23 +01:00
parent fced4001c0
commit 8f89754ba5
6 changed files with 162 additions and 25 deletions

View file

@ -21,7 +21,7 @@ use nih_plug::{
formatters, util, Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus, formatters, util, Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus,
Vst3Plugin, Vst3Plugin,
}; };
use nih_plug::{BoolParam, FloatParam, Param, Params, Range}; use nih_plug::{BoolParam, FloatParam, Param, Params, Range, Smoother, SmoothingStyle};
use parking_lot::RwLock; use parking_lot::RwLock;
use std::pin::Pin; use std::pin::Pin;
@ -57,6 +57,7 @@ impl Default for GainParams {
Self { Self {
gain: FloatParam { gain: FloatParam {
value: 0.0, value: 0.0,
smoothed: Smoother::new(SmoothingStyle::SmoothLinear(2.0)),
value_changed: None, value_changed: None,
// If, for instance, updating this parameter would require other parts of the // 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 // 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 { 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 // TODO: The wrapper should set FTZ if not yet enabled, mention ths in the process fuctnion
for samples in buffer.iter_mut() { 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 { for sample in samples {
// TODO: Smoothing *sample *= util::db_to_gain(gain);
*sample *= util::db_to_gain(self.params.gain.value);
} }
} }

View file

@ -21,7 +21,7 @@ use nih_plug::{
formatters, util, Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus, formatters, util, Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus,
Vst3Plugin, Vst3Plugin,
}; };
use nih_plug::{FloatParam, Param, Params, Range}; use nih_plug::{FloatParam, Param, Params, Range, Smoother, SmoothingStyle};
use std::f32::consts; use std::f32::consts;
use std::pin::Pin; use std::pin::Pin;
@ -60,6 +60,7 @@ impl Default for SineParams {
Self { Self {
gain: FloatParam { gain: FloatParam {
value: -10.0, value: -10.0,
smoothed: Smoother::new(SmoothingStyle::SmoothLinear(2.0)),
range: Range::Linear { range: Range::Linear {
min: -30.0, min: -30.0,
max: 0.0, max: 0.0,
@ -71,6 +72,7 @@ impl Default for SineParams {
}, },
frequency: FloatParam { frequency: FloatParam {
value: 420.0, value: 420.0,
smoothed: Smoother::new(SmoothingStyle::SmoothLinear(10.0)),
range: Range::Skewed { range: Range::Skewed {
min: 1.0, min: 1.0,
max: 20_000.0, max: 20_000.0,
@ -117,8 +119,12 @@ impl Plugin for Sine {
} }
fn process(&mut self, buffer: &mut Buffer, _context: &dyn ProcessContext) -> ProcessStatus { 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() { 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(); let sine = (self.phase * consts::TAU).sin();
self.phase += phase_delta; self.phase += phase_delta;
@ -127,8 +133,7 @@ impl Plugin for Sine {
} }
for sample in samples { for sample in samples {
// TODO: Parameter smoothing *sample = sine * util::db_to_gain(gain);
*sample = sine * util::db_to_gain(self.params.gain.value);
} }
} }

View file

@ -30,6 +30,7 @@ pub use buffer::Buffer;
pub use context::ProcessContext; pub use context::ProcessContext;
pub use param::internals::Params; pub use param::internals::Params;
pub use param::range::Range; pub use param::range::Range;
pub use param::smoothing::{Smoother, SmoothingStyle};
pub use param::{BoolParam, FloatParam, IntParam, Param}; pub use param::{BoolParam, FloatParam, IntParam, Param};
pub use plugin::{BufferConfig, BusConfig, Plugin, ProcessStatus, Vst3Plugin}; pub use plugin::{BufferConfig, BusConfig, Plugin, ProcessStatus, Vst3Plugin};

View file

@ -20,9 +20,11 @@ use std::fmt::Display;
use std::sync::Arc; use std::sync::Arc;
use self::range::{NormalizebleRange, Range}; use self::range::{NormalizebleRange, Range};
use self::smoothing::Smoother;
pub mod internals; pub mod internals;
pub mod range; pub mod range;
pub mod smoothing;
pub type FloatParam = PlainParam<f32>; pub type FloatParam = PlainParam<f32>;
pub type IntParam = PlainParam<i32>; pub type IntParam = PlainParam<i32>;
@ -69,19 +71,7 @@ pub struct PlainParam<T> {
/// Storing parameter values like this instead of in a single contiguous array is bad for cache /// 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. /// locality, but it does allow for a much nicer declarative API.
pub value: T, pub value: T,
pub smoothed: Smoother<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<T>,
/// Optional callback for listening to value changes. The argument passed to this function is /// 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 /// the parameter's new **plain** value. This should not do anything expensive as it may be
/// called multiple times in rapid succession. /// called multiple times in rapid succession.
@ -132,6 +122,7 @@ where
fn default() -> Self { fn default() -> Self {
Self { Self {
value: T::default(), value: T::default(),
smoothed: Smoother::none(),
value_changed: None, value_changed: None,
range: Range::default(), range: Range::default(),
name: "", name: "",

110
src/param/smoothing.rs Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
/// 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<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: 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<T: Default> Default for Smoother<T> {
fn default() -> Self {
Self {
style: SmoothingStyle::None,
steps_left: 0,
step_size: Default::default(),
current: 0.0,
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()
}
}
// 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<f32> {
/// 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
}
}
}

View file

@ -43,7 +43,7 @@ use widestring::U16CStr;
use crate::buffer::Buffer; use crate::buffer::Buffer;
use crate::context::{EventLoop, MainThreadExecutor, OsEventLoop, ProcessContext}; use crate::context::{EventLoop, MainThreadExecutor, OsEventLoop, ProcessContext};
use crate::param::internals::ParamPtr; use crate::param::internals::ParamPtr;
use crate::param::range::Range; use crate::param::range::{NormalizebleRange, Range};
use crate::param::Param; use crate::param::Param;
use crate::plugin::{BufferConfig, BusConfig, Plugin, ProcessStatus, Vst3Plugin}; use crate::plugin::{BufferConfig, BusConfig, Plugin, ProcessStatus, Vst3Plugin};
use crate::wrapper::state::{ParamValue, State}; use crate::wrapper::state::{ParamValue, State};
@ -259,14 +259,29 @@ impl<P: Plugin> WrapperInner<'_, P> {
wrapper 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<f32>,
) -> tresult {
if hash == *BYPASS_PARAM_HASH { if hash == *BYPASS_PARAM_HASH {
self.bypass_state self.bypass_state
.store(normalized_value >= 0.5, Ordering::SeqCst); .store(normalized_value >= 0.5, Ordering::SeqCst);
kResultOk kResultOk
} else if let Some(param_ptr) = self.param_by_hash.get(&hash) { } 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 kResultOk
} else { } else {
@ -780,7 +795,13 @@ impl<P: Plugin> IEditController for Wrapper<'_, P> {
return kResultOk; 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( unsafe fn set_component_handler(
@ -939,6 +960,11 @@ impl<P: Plugin> IAudioProcessor for Wrapper<'_, P> {
// We need to handle incoming automation first // We need to handle incoming automation first
let data = &*data; 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() { if let Some(param_changes) = data.input_param_changes.upgrade() {
let num_param_queues = param_changes.get_parameter_count(); let num_param_queues = param_changes.get_parameter_count();
for change_queue_idx in 0..num_param_queues { for change_queue_idx in 0..num_param_queues {
@ -959,7 +985,8 @@ impl<P: Plugin> IAudioProcessor for Wrapper<'_, P> {
&mut value, &mut value,
) == kResultOk ) == kResultOk
{ {
self.inner.set_normalized_value_by_hash(param_hash, value); self.inner
.set_normalized_value_by_hash(param_hash, value, sample_rate);
} }
} }
} }