Add simple linear parameter smoothing
This commit is contained in:
parent
fced4001c0
commit
8f89754ba5
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
17
src/param.rs
17
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<f32>;
|
||||
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
|
||||
/// 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<T>,
|
||||
pub smoothed: Smoother<T>,
|
||||
/// 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: "",
|
||||
|
|
110
src/param/smoothing.rs
Normal file
110
src/param/smoothing.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<P: Plugin> 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<f32>,
|
||||
) -> 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<P: Plugin> 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<P: Plugin> 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<P: Plugin> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue