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,
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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};
|
||||||
|
|
||||||
|
|
17
src/param.rs
17
src/param.rs
|
@ -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
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::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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue