From 76369ad1e1d619e4dcabe69b411a9a40ca02d52a Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Thu, 3 Mar 2022 19:24:40 +0100 Subject: [PATCH] :boom: Rework FloatParam and IntParam They are now two separate types with slightly different options. I had these merged initially because they're 95% the same, and I thought it would be fun to have weird distributions for integer parameters, but that doesn't really work because hosts and the plugin APIs expect the steps to be linear. And if you're going to have an unstepped integer parameter, might as well use FloatParam with rounding. Because non-linear ranges are no longer possible with IntParam, the types have been split up to make everything much more readable instead of adding a parameterizing the range type with another type family. --- plugins/diopser/src/lib.rs | 18 +- plugins/examples/gain-gui/src/lib.rs | 18 +- plugins/examples/gain/src/lib.rs | 4 +- plugins/examples/sine/src/lib.rs | 8 +- src/lib.rs | 2 +- src/param.rs | 6 +- src/param/enums.rs | 6 +- src/param/{plain.rs => float.rs} | 287 ++++++++++++--------------- src/param/integer.rs | 236 ++++++++++++++++++++++ src/param/range.rs | 263 ++++++------------------ 10 files changed, 459 insertions(+), 389 deletions(-) rename src/param/{plain.rs => float.rs} (51%) create mode 100644 src/param/integer.rs diff --git a/plugins/diopser/src/lib.rs b/plugins/diopser/src/lib.rs index 15289108..d3b2c88f 100644 --- a/plugins/diopser/src/lib.rs +++ b/plugins/diopser/src/lib.rs @@ -23,7 +23,7 @@ use nih_plug::{ formatters, Buffer, BufferConfig, BusConfig, ClapPlugin, Plugin, ProcessContext, ProcessStatus, Vst3Plugin, }; -use nih_plug::{BoolParam, FloatParam, IntParam, Params, Range, SmoothingStyle}; +use nih_plug::{BoolParam, FloatParam, FloatRange, IntParam, IntRange, Params, SmoothingStyle}; use nih_plug::{Enum, EnumParam}; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; @@ -140,7 +140,7 @@ impl DiopserParams { filter_stages: IntParam::new( "Filter Stages", 0, - Range::Linear { + IntRange::Linear { min: 0, max: MAX_NUM_FILTERS as i32, }, @@ -155,10 +155,10 @@ impl DiopserParams { filter_frequency: FloatParam::new( "Filter Frequency", 200.0, - Range::Skewed { + FloatRange::Skewed { min: 5.0, // This must never reach 0 max: 20_000.0, - factor: Range::skew_factor(-2.5), + factor: FloatRange::skew_factor(-2.5), }, ) // This needs quite a bit of smoothing to avoid artifacts @@ -170,10 +170,10 @@ impl DiopserParams { // The actual default neutral Q-value would be `sqrt(2) / 2`, but this value // produces slightly less ringing. 0.5, - Range::Skewed { + FloatRange::Skewed { min: 0.01, // This must also never reach 0 max: 30.0, - factor: Range::skew_factor(-2.5), + factor: FloatRange::skew_factor(-2.5), }, ) .with_smoother(SmoothingStyle::Logarithmic(100.0)) @@ -181,10 +181,10 @@ impl DiopserParams { filter_spread_octaves: FloatParam::new( "Filter Spread Octaves", 0.0, - Range::SymmetricalSkewed { + FloatRange::SymmetricalSkewed { min: -5.0, max: 5.0, - factor: Range::skew_factor(-1.0), + factor: FloatRange::skew_factor(-1.0), center: 0.0, }, ) @@ -202,7 +202,7 @@ impl DiopserParams { automation_precision: FloatParam::new( "Automation precision", normalize_automation_precision(128), - Range::Linear { min: 0.0, max: 1.0 }, + FloatRange::Linear { min: 0.0, max: 1.0 }, ) .with_unit("%") .with_value_to_string(Arc::new(|value| format!("{:.0}", value * 100.0))), diff --git a/plugins/examples/gain-gui/src/lib.rs b/plugins/examples/gain-gui/src/lib.rs index e5bb1825..117489aa 100644 --- a/plugins/examples/gain-gui/src/lib.rs +++ b/plugins/examples/gain-gui/src/lib.rs @@ -3,10 +3,10 @@ extern crate nih_plug; use atomic_float::AtomicF32; use nih_plug::{ - util, Buffer, BufferConfig, BusConfig, ClapPlugin, Editor, IntParam, Plugin, ProcessContext, + util, Buffer, BufferConfig, BusConfig, ClapPlugin, Editor, Plugin, ProcessContext, ProcessStatus, Vst3Plugin, }; -use nih_plug::{FloatParam, Params, Range, SmoothingStyle}; +use nih_plug::{FloatParam, FloatRange, IntParam, IntRange, Params, SmoothingStyle}; use nih_plug_egui::{create_egui_editor, egui, widgets, EguiState}; use std::pin::Pin; use std::sync::Arc; @@ -54,7 +54,7 @@ impl Default for GainParams { gain: FloatParam::new( "Gain", 0.0, - Range::Linear { + FloatRange::Linear { min: -30.0, max: 30.0, }, @@ -62,15 +62,7 @@ impl Default for GainParams { .with_smoother(SmoothingStyle::Linear(50.0)) .with_step_size(0.01) .with_unit(" dB"), - some_int: IntParam::new( - "Something", - 3, - Range::Skewed { - min: 0, - max: 3, - factor: Range::skew_factor(1.0), - }, - ), + some_int: IntParam::new("Something", 3, IntRange::Linear { min: 0, max: 3 }), } } } @@ -103,7 +95,7 @@ impl Plugin for Gain { // This is a fancy widget that can get all the information it needs to properly // display and modify the parameter from the parametr itself // It's not yet fully implemented, as the text is missing. - ui.label("Some random wierdly distributed integer"); + ui.label("Some random integer"); ui.add(widgets::ParamSlider::for_param(¶ms.some_int, setter)); ui.label("Gain"); diff --git a/plugins/examples/gain/src/lib.rs b/plugins/examples/gain/src/lib.rs index 506e20e7..9cbe58c0 100644 --- a/plugins/examples/gain/src/lib.rs +++ b/plugins/examples/gain/src/lib.rs @@ -5,7 +5,7 @@ use nih_plug::{ formatters, util, Buffer, BufferConfig, BusConfig, ClapPlugin, Plugin, ProcessContext, ProcessStatus, Vst3Plugin, }; -use nih_plug::{BoolParam, FloatParam, Params, Range, Smoother, SmoothingStyle}; +use nih_plug::{BoolParam, FloatParam, FloatRange, Params, Smoother, SmoothingStyle}; use parking_lot::RwLock; use std::pin::Pin; use std::sync::Arc; @@ -47,7 +47,7 @@ impl Default for GainParams { value: 0.0, smoothed: Smoother::new(SmoothingStyle::Linear(50.0)), value_changed: None, - range: Range::Linear { + range: FloatRange::Linear { min: -30.0, max: 30.0, }, diff --git a/plugins/examples/sine/src/lib.rs b/plugins/examples/sine/src/lib.rs index 0861e3e1..3ef7d6fd 100644 --- a/plugins/examples/sine/src/lib.rs +++ b/plugins/examples/sine/src/lib.rs @@ -5,7 +5,7 @@ use nih_plug::{ formatters, util, Buffer, BufferConfig, BusConfig, ClapPlugin, Plugin, ProcessContext, ProcessStatus, Vst3Plugin, }; -use nih_plug::{BoolParam, FloatParam, Params, Range, Smoother, SmoothingStyle}; +use nih_plug::{BoolParam, FloatParam, FloatRange, Params, Smoother, SmoothingStyle}; use std::f32::consts; use std::pin::Pin; @@ -58,7 +58,7 @@ impl Default for SineParams { gain: FloatParam::new( "Gain", -10.0, - Range::Linear { + FloatRange::Linear { min: -30.0, max: 0.0, }, @@ -69,10 +69,10 @@ impl Default for SineParams { frequency: FloatParam::new( "Frequency", 420.0, - Range::Skewed { + FloatRange::Skewed { min: 1.0, max: 20_000.0, - factor: Range::skew_factor(-2.0), + factor: FloatRange::skew_factor(-2.0), }, ) .with_smoother(SmoothingStyle::Linear(10.0)) diff --git a/src/lib.rs b/src/lib.rs index e22a982c..f3726bde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ pub use buffer::Buffer; pub use context::{GuiContext, ParamSetter, ProcessContext}; pub use param::enums::{Enum, EnumParam}; pub use param::internals::Params; -pub use param::range::Range; +pub use param::range::{FloatRange, IntRange}; pub use param::smoothing::{Smoother, SmoothingStyle}; pub use param::{BoolParam, FloatParam, IntParam, Param}; pub use plugin::{ diff --git a/src/param.rs b/src/param.rs index 6ca2255f..2c7b9122 100644 --- a/src/param.rs +++ b/src/param.rs @@ -7,7 +7,8 @@ use std::fmt::Display; // Parameter types mod boolean; pub mod enums; -mod plain; +mod float; +mod integer; pub mod internals; pub mod range; @@ -15,7 +16,8 @@ pub mod smoothing; pub use boolean::BoolParam; pub use enums::EnumParam; -pub use plain::{FloatParam, IntParam}; +pub use float::FloatParam; +pub use integer::IntParam; /// Describes a single parameter of any type. pub trait Param: Display { diff --git a/src/param/enums.rs b/src/param/enums.rs index 8b6c5cc2..f5ec6a66 100644 --- a/src/param/enums.rs +++ b/src/param/enums.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use std::sync::Arc; use super::internals::ParamPtr; -use super::range::Range; +use super::range::IntRange; use super::{IntParam, Param}; // Re-export the derive macro @@ -71,7 +71,7 @@ impl Default for EnumParam { inner: EnumParamInner { inner: IntParam { value: T::default().to_index() as i32, - range: Range::Linear { + range: IntRange::Linear { min: 0, max: variants.len() as i32 - 1, }, @@ -245,7 +245,7 @@ impl EnumParam { inner: EnumParamInner { inner: IntParam { value: T::to_index(default) as i32, - range: Range::Linear { + range: IntRange::Linear { min: 0, max: variants.len() as i32 - 1, }, diff --git a/src/param/plain.rs b/src/param/float.rs similarity index 51% rename from src/param/plain.rs rename to src/param/float.rs index f2ef437b..08af21ba 100644 --- a/src/param/plain.rs +++ b/src/param/float.rs @@ -1,17 +1,14 @@ -//! Simple number-backed parameters. +//! Continuous (or discrete, with a step size) floating point parameters. use std::fmt::Display; use std::sync::Arc; use super::internals::ParamPtr; -use super::range::{NormalizebleRange, Range}; +use super::range::FloatRange; use super::smoothing::{Smoother, SmoothingStyle}; use super::Param; -pub type FloatParam = PlainParam; -pub type IntParam = PlainParam; - -/// A numerical parameter that's stored unnormalized. The range is used for the normalization +/// A floating point parameter that's stored unnormalized. The range is used for the normalization /// process. /// /// You can either initialize the struct directly, using `..Default::default()` to fill in the @@ -26,14 +23,14 @@ pub type IntParam = PlainParam; // writes to naturally aligned values up to word size are atomic, so there's no risk of reading // a partially written to value here. We should probably reconsider this at some point though. #[repr(C, align(4))] -pub struct PlainParam { +pub struct FloatParam { /// The field's current plain, unnormalized value. Should be initialized with the default value. /// 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, + pub value: f32, /// An optional smoother that will automatically interpolate between the new automation values /// set by the host. - pub smoothed: Smoother, + 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. @@ -42,13 +39,13 @@ pub struct PlainParam { /// parmaeters struct, move a clone of that `Arc` into this closure, and then modify that. /// /// TODO: We probably also want to pass the old value to this function. - pub value_changed: Option>, + pub value_changed: Option>, /// The distribution of the parameter's values. - pub range: Range, - /// The distance between steps of a [FloatParam]. Ignored for [IntParam]. Mostly useful for - /// quantizing GUI input. If this is set and if [Self::value_to_string] is not set, then this is - /// also used when formatting the parameter. This must be a positive, nonzero number. + pub range: FloatRange, + /// The distance between discrete steps in this parameter. Mostly useful for quantizing GUI + /// input. If this is set and if [Self::value_to_string] is not set, then this is also used when + /// formatting the parameter. This must be a positive, nonzero number. pub step_size: Option, /// The parameter's human readable display name. pub name: &'static str, @@ -56,24 +53,20 @@ pub struct PlainParam { /// automatically add a space before the unit. pub unit: &'static str, /// Optional custom conversion function from a plain **unnormalized** value to a string. - pub value_to_string: Option String + Send + Sync>>, + pub value_to_string: Option String + Send + Sync>>, /// Optional custom conversion function from a string to a plain **unnormalized** value. If the /// string cannot be parsed, then this should return a `None`. If this happens while the /// parameter is being updated then the update will be canceled. - pub string_to_value: Option Option + Send + Sync>>, + pub string_to_value: Option Option + Send + Sync>>, } -impl Default for PlainParam -where - T: Default, - Range: Default, -{ +impl Default for FloatParam { fn default() -> Self { Self { - value: T::default(), + value: 0.0, smoothed: Smoother::none(), value_changed: None, - range: Range::default(), + range: FloatRange::default(), step_size: None, name: "", unit: "", @@ -83,7 +76,7 @@ where } } -impl Display for PlainParam { +impl Display for FloatParam { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match (&self.value_to_string, &self.step_size) { (Some(func), _) => write!(f, "{}{}", func(self.value), self.unit), @@ -96,129 +89,117 @@ impl Display for PlainParam { } } -macro_rules! impl_plainparam { - ($ty:ident, $plain:ty) => { - impl Param for $ty { - type Plain = $plain; +impl Param for FloatParam { + type Plain = f32; - fn name(&self) -> &'static str { - self.name - } + fn name(&self) -> &'static str { + self.name + } - fn unit(&self) -> &'static str { - self.unit - } + fn unit(&self) -> &'static str { + self.unit + } - fn step_count(&self) -> Option { - self.range.step_count() - } + fn step_count(&self) -> Option { + None + } - fn plain_value(&self) -> Self::Plain { - self.value - } + fn plain_value(&self) -> Self::Plain { + self.value + } - fn set_plain_value(&mut self, plain: Self::Plain) { - self.value = plain; - if let Some(f) = &self.value_changed { - f(plain); - } - } - - fn normalized_value(&self) -> f32 { - self.preview_normalized(self.value) - } - - fn set_normalized_value(&mut self, normalized: f32) { - self.set_plain_value(self.preview_plain(normalized)); - } - - fn normalized_value_to_string(&self, normalized: f32, include_unit: bool) -> String { - let value = self.preview_plain(normalized); - match (&self.value_to_string, &self.step_size, include_unit) { - (Some(f), _, true) => format!("{}{}", f(value), self.unit), - (Some(f), _, false) => format!("{}", f(value)), - (None, Some(step_size), true) => { - let num_digits = decimals_from_step_size(*step_size); - format!("{:.num_digits$}{}", value, self.unit) - } - (None, Some(step_size), false) => { - let num_digits = decimals_from_step_size(*step_size); - format!("{:.num_digits$}", value) - } - (None, None, true) => format!("{}{}", value, self.unit), - (None, None, false) => format!("{}", value), - } - } - - fn string_to_normalized_value(&self, string: &str) -> Option { - let value = match &self.string_to_value { - Some(f) => f(string), - // TODO: Check how Rust's parse function handles trailing garbage - None => string.parse().ok(), - }?; - - Some(self.preview_normalized(value)) - } - - fn preview_normalized(&self, plain: Self::Plain) -> f32 { - self.range.normalize(plain) - } - - fn preview_plain(&self, normalized: f32) -> Self::Plain { - let value = self.range.unnormalize(normalized); - match &self.step_size { - // Step size snapping is not defined for [IntParam], so this cast is here just - // so we can keep everything in this macro - Some(step_size) => self.range.snap_to_step(value, *step_size as Self::Plain), - None => value, - } - } - - fn set_from_string(&mut self, string: &str) -> bool { - let value = match &self.string_to_value { - Some(f) => f(string), - // TODO: Check how Rust's parse function handles trailing garbage - None => string.parse().ok(), - }; - - match value { - Some(plain) => { - self.set_plain_value(plain); - true - } - None => false, - } - } - - fn update_smoother(&mut self, sample_rate: f32, reset: bool) { - if reset { - self.smoothed.reset(self.value); - } else { - self.smoothed.set_target(sample_rate, self.value); - } - } - - fn initialize_block_smoother(&mut self, max_block_size: usize) { - self.smoothed.initialize_block_smoother(max_block_size); - } - - fn as_ptr(&self) -> ParamPtr { - ParamPtr::$ty(self as *const $ty as *mut $ty) - } + fn set_plain_value(&mut self, plain: Self::Plain) { + self.value = plain; + if let Some(f) = &self.value_changed { + f(plain); } - }; + } + + fn normalized_value(&self) -> f32 { + self.preview_normalized(self.value) + } + + fn set_normalized_value(&mut self, normalized: f32) { + self.set_plain_value(self.preview_plain(normalized)); + } + + fn normalized_value_to_string(&self, normalized: f32, include_unit: bool) -> String { + let value = self.preview_plain(normalized); + match (&self.value_to_string, &self.step_size, include_unit) { + (Some(f), _, true) => format!("{}{}", f(value), self.unit), + (Some(f), _, false) => f(value), + (None, Some(step_size), true) => { + let num_digits = decimals_from_step_size(*step_size); + format!("{:.num_digits$}{}", value, self.unit) + } + (None, Some(step_size), false) => { + let num_digits = decimals_from_step_size(*step_size); + format!("{:.num_digits$}", value) + } + (None, None, true) => format!("{}{}", value, self.unit), + (None, None, false) => format!("{}", value), + } + } + + fn string_to_normalized_value(&self, string: &str) -> Option { + let value = match &self.string_to_value { + Some(f) => f(string), + // TODO: Check how Rust's parse function handles trailing garbage + None => string.parse().ok(), + }?; + + Some(self.preview_normalized(value)) + } + + fn preview_normalized(&self, plain: Self::Plain) -> f32 { + self.range.normalize(plain) + } + + fn preview_plain(&self, normalized: f32) -> Self::Plain { + let value = self.range.unnormalize(normalized); + match &self.step_size { + Some(step_size) => self.range.snap_to_step(value, *step_size as Self::Plain), + None => value, + } + } + + fn set_from_string(&mut self, string: &str) -> bool { + let value = match &self.string_to_value { + Some(f) => f(string), + // TODO: Check how Rust's parse function handles trailing garbage + None => string.parse().ok(), + }; + + match value { + Some(plain) => { + self.set_plain_value(plain); + true + } + None => false, + } + } + + fn update_smoother(&mut self, sample_rate: f32, reset: bool) { + if reset { + self.smoothed.reset(self.value); + } else { + self.smoothed.set_target(sample_rate, self.value); + } + } + + fn initialize_block_smoother(&mut self, max_block_size: usize) { + self.smoothed.initialize_block_smoother(max_block_size); + } + + fn as_ptr(&self) -> ParamPtr { + ParamPtr::FloatParam(self as *const _ as *mut _) + } } -impl_plainparam!(FloatParam, f32); -impl_plainparam!(IntParam, i32); - -impl PlainParam -where - Range: Default, -{ +impl FloatParam { /// Build a new [Self]. Use the other associated functions to modify the behavior of the /// parameter. - pub fn new(name: &'static str, default: T, range: Range) -> Self { + pub fn new(name: &'static str, default: f32, range: FloatRange) -> Self { Self { value: default, range, @@ -227,10 +208,8 @@ where } } - /// Run a callback whenever this parameter's value changes. The argument passed to this function - /// is the parameter's new value. This should not do anything expensive as it may be called - /// multiple times in rapid succession, and it can be run from both the GUI and the audio - /// thread. + /// Set up a smoother that can gradually interpolate changes made to this parameter, preventing + /// clicks and zipper noises. pub fn with_smoother(mut self, style: SmoothingStyle) -> Self { self.smoothed = Smoother::new(style); self @@ -240,7 +219,7 @@ where /// is the parameter's new value. This should not do anything expensive as it may be called /// multiple times in rapid succession, and it can be run from both the GUI and the audio /// thread. - pub fn with_callback(mut self, callback: Arc) -> Self { + pub fn with_callback(mut self, callback: Arc) -> Self { self.value_changed = Some(callback); self } @@ -253,40 +232,36 @@ where self } + /// Set the distance between steps of a [FloatParam]. Mostly useful for quantizing GUI input. If + /// this is set and if [Self::value_to_string] is not set, then this is also used when + /// formatting the parameter. This must be a positive, nonzero number. + pub fn with_step_size(mut self, step_size: f32) -> Self { + self.step_size = Some(step_size); + self + } + /// Use a custom conversion function to convert the plain, unnormalized value to a /// string. pub fn with_value_to_string( mut self, - callback: Arc String + Send + Sync>, + callback: Arc String + Send + Sync>, ) -> Self { self.value_to_string = Some(callback); self } - // `with_step_size` is only implemented for the f32 version - /// Use a custom conversion function to convert from a string to a plain, unnormalized /// value. If the string cannot be parsed, then this should return a `None`. If this /// happens while the parameter is being updated then the update will be canceled. pub fn with_string_to_value( mut self, - callback: Arc Option + Send + Sync>, + callback: Arc Option + Send + Sync>, ) -> Self { self.string_to_value = Some(callback); self } } -impl PlainParam { - /// Set the distance between steps of a [FloatParam]. Mostly useful for quantizing GUI input. If - /// this is set and if [Self::value_to_string] is not set, then this is also used when - /// formatting the parameter. This must be a positive, nonzero number. - pub fn with_step_size(mut self, step_size: f32) -> Self { - self.step_size = Some(step_size); - self - } -} - /// Caldculate how many decimals to round to when displaying a floating point value with a specific /// step size. We'll perform some rounding to ignore spurious extra precision caused by the floating /// point quantization. diff --git a/src/param/integer.rs b/src/param/integer.rs new file mode 100644 index 00000000..29bf2f73 --- /dev/null +++ b/src/param/integer.rs @@ -0,0 +1,236 @@ +//! Stepped integer parameters. + +use std::fmt::Display; +use std::sync::Arc; + +use super::internals::ParamPtr; +use super::range::IntRange; +use super::smoothing::{Smoother, SmoothingStyle}; +use super::Param; + +/// A discrete integer parameter that's stored unnormalized. The range is used for the normalization +/// process. +/// +/// You can either initialize the struct directly, using `..Default::default()` to fill in the +/// unused fields, or you can use the builder interface with [Self::new()]. +// +// XXX: To keep the API simple and to allow the optimizer to do its thing, the values are stored as +// plain primitive values that are modified through the `*mut` pointers from the plugin's +// `Params` object. Technically modifying these while the GUI is open is unsound. We could +// remedy this by changing `value` to be an atomic type and adding a function also called +// `value()` to load that value, but in practice that should not be necessary if we don't do +// anything crazy other than modifying this value. On both AArch64 and x86(_64) reads and +// writes to naturally aligned values up to word size are atomic, so there's no risk of reading +// a partially written to value here. We should probably reconsider this at some point though. +#[repr(C, align(4))] +pub struct IntParam { + /// The field's current plain, unnormalized value. Should be initialized with the default value. + /// 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: i32, + /// An optional smoother that will automatically interpolate between the new automation values + /// set by the host. + 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. + /// + /// To use this, you'll probably want to store an `Arc` alongside the parmater in the + /// parmaeters struct, move a clone of that `Arc` into this closure, and then modify that. + /// + /// TODO: We probably also want to pass the old value to this function. + pub value_changed: Option>, + + /// The distribution of the parameter's values. + pub range: IntRange, + /// The parameter's human readable display name. + pub name: &'static str, + /// The parameter value's unit, added after `value_to_string` if that is set. NIH-plug will not + /// automatically add a space before the unit. + pub unit: &'static str, + /// Optional custom conversion function from a plain **unnormalized** value to a string. + pub value_to_string: Option String + Send + Sync>>, + /// Optional custom conversion function from a string to a plain **unnormalized** value. If the + /// string cannot be parsed, then this should return a `None`. If this happens while the + /// parameter is being updated then the update will be canceled. + pub string_to_value: Option Option + Send + Sync>>, +} + +impl Default for IntParam { + fn default() -> Self { + Self { + value: 0, + smoothed: Smoother::none(), + value_changed: None, + range: IntRange::default(), + name: "", + unit: "", + value_to_string: None, + string_to_value: None, + } + } +} + +impl Display for IntParam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.value_to_string { + Some(func) => write!(f, "{}{}", func(self.value), self.unit), + _ => write!(f, "{}{}", self.value, self.unit), + } + } +} + +impl Param for IntParam { + type Plain = i32; + + fn name(&self) -> &'static str { + self.name + } + + fn unit(&self) -> &'static str { + self.unit + } + + fn step_count(&self) -> Option { + self.range.step_count() + } + + fn plain_value(&self) -> Self::Plain { + self.value + } + + fn set_plain_value(&mut self, plain: Self::Plain) { + self.value = plain; + if let Some(f) = &self.value_changed { + f(plain); + } + } + + fn normalized_value(&self) -> f32 { + self.preview_normalized(self.value) + } + + fn set_normalized_value(&mut self, normalized: f32) { + self.set_plain_value(self.preview_plain(normalized)); + } + + fn normalized_value_to_string(&self, normalized: f32, include_unit: bool) -> String { + let value = self.preview_plain(normalized); + match (&self.value_to_string, include_unit) { + (Some(f), true) => format!("{}{}", f(value), self.unit), + (Some(f), false) => format!("{}", f(value)), + (None, true) => format!("{}{}", value, self.unit), + (None, false) => format!("{}", value), + } + } + + fn string_to_normalized_value(&self, string: &str) -> Option { + let value = match &self.string_to_value { + Some(f) => f(string), + // TODO: Check how Rust's parse function handles trailing garbage + None => string.parse().ok(), + }?; + + Some(self.preview_normalized(value)) + } + + fn preview_normalized(&self, plain: Self::Plain) -> f32 { + self.range.normalize(plain) + } + + fn preview_plain(&self, normalized: f32) -> Self::Plain { + self.range.unnormalize(normalized) + } + + fn set_from_string(&mut self, string: &str) -> bool { + let value = match &self.string_to_value { + Some(f) => f(string), + // TODO: Check how Rust's parse function handles trailing garbage + None => string.parse().ok(), + }; + + match value { + Some(plain) => { + self.set_plain_value(plain); + true + } + None => false, + } + } + + fn update_smoother(&mut self, sample_rate: f32, reset: bool) { + if reset { + self.smoothed.reset(self.value); + } else { + self.smoothed.set_target(sample_rate, self.value); + } + } + + fn initialize_block_smoother(&mut self, max_block_size: usize) { + self.smoothed.initialize_block_smoother(max_block_size); + } + + fn as_ptr(&self) -> ParamPtr { + ParamPtr::IntParam(self as *const _ as *mut _) + } +} + +impl IntParam { + /// Build a new [Self]. Use the other associated functions to modify the behavior of the + /// parameter. + pub fn new(name: &'static str, default: i32, range: IntRange) -> Self { + Self { + value: default, + range, + name, + ..Default::default() + } + } + + /// Set up a smoother that can gradually interpolate changes made to this parameter, preventing + /// clicks and zipper noises. + pub fn with_smoother(mut self, style: SmoothingStyle) -> Self { + self.smoothed = Smoother::new(style); + self + } + + /// Run a callback whenever this parameter's value changes. The argument passed to this function + /// is the parameter's new value. This should not do anything expensive as it may be called + /// multiple times in rapid succession, and it can be run from both the GUI and the audio + /// thread. + pub fn with_callback(mut self, callback: Arc) -> Self { + self.value_changed = Some(callback); + self + } + + /// Display a unit when rendering this parameter to a string. Appended after the + /// [Self::value_to_string] function if that is also set. NIH-plug will not + /// automatically add a space before the unit. + pub fn with_unit(mut self, unit: &'static str) -> Self { + self.unit = unit; + self + } + + /// Use a custom conversion function to convert the plain, unnormalized value to a + /// string. + pub fn with_value_to_string( + mut self, + callback: Arc String + Send + Sync>, + ) -> Self { + self.value_to_string = Some(callback); + self + } + + // `with_step_size` is only implemented for the f32 version + + /// Use a custom conversion function to convert from a string to a plain, unnormalized + /// value. If the string cannot be parsed, then this should return a `None`. If this + /// happens while the parameter is being updated then the update will be canceled. + pub fn with_string_to_value( + mut self, + callback: Arc Option + Send + Sync>, + ) -> Self { + self.string_to_value = Some(callback); + self + } +} diff --git a/src/param/range.rs b/src/param/range.rs index 19c82f9e..bf06766d 100644 --- a/src/param/range.rs +++ b/src/param/range.rs @@ -1,78 +1,61 @@ //! Different ranges for numeric parameters. -/// A distribution for a parameter's range. All range endpoints are inclusive. -/// -/// TODO: Hosts will do weird things when using skewed ranges for integers because of the steps. -/// Perhaps it would be best to just only allow linear ranges for integers. Bitwig for -/// instance will send a parameter change rounding the value down when restoring a plugin -/// patch. +/// A distribution for a floating point parameter's range. All range endpoints are inclusive. #[derive(Debug)] -pub enum Range { +pub enum FloatRange { /// The values are uniformly distributed between `min` and `max`. - Linear { min: T, max: T }, + Linear { min: f32, max: f32 }, /// The range is skewed by a factor. Values above 1.0 will make the end of the range wider, /// while values between 0 and 1 will skew the range towards the start. Use [Range::skew_factor()] /// for a more intuitively way to calculate the skew factor where positive values skew the range /// towards the end while negative values skew the range toward the start. - Skewed { min: T, max: T, factor: f32 }, + Skewed { min: f32, max: f32, factor: f32 }, /// The same as [Range::Skewed], but with the skewing happening from a central point. This /// central point is rescaled to be at 50% of the parameter's range for convenience of use. Git /// blame this comment to find a version that doesn't do this. SymmetricalSkewed { - min: T, - max: T, + min: f32, + max: f32, factor: f32, - center: T, + center: f32, }, } -impl Range<()> { - /// Calculate a skew factor for [Range::Skewed] and [Range::SymmetricalSkewed]. Positive values - /// make the end of the range wider while negative make the start of the range wider. - pub fn skew_factor(factor: f32) -> f32 { - 2.0f32.powf(factor) - } +/// A distribution for an integer parameter's range. All range endpoints are inclusive. Only linear +/// ranges are supported for integers since hosts expect discrete parameters to have a fixed step +/// size. +#[derive(Debug)] +pub enum IntRange { + /// The values are uniformly distributed between `min` and `max`. + Linear { min: i32, max: i32 }, } -/// A normalizable range for type `T`, where `self` is expected to be a type `R`. Higher kinded -/// types would have made this trait definition a lot clearer. -/// -/// Floating point rounding to a step size is always done in the conversion from normalized to -/// plain, inside [super::PlainParam::preview_plain]. -pub(crate) trait NormalizebleRange { - /// Normalize a plain, unnormalized value. Will be clamped to the bounds of the range if the - /// normalized value exceeds `[0, 1]`. - fn normalize(&self, plain: T) -> f32; - - /// Unnormalize a normalized value. Will be clamped to `[0, 1]` if the plain, unnormalized value - /// would exceed that range. - fn unnormalize(&self, normalized: f32) -> T; - - /// Snap a vlue to a step size, clamping to the minimum and maximum value of the range. - fn snap_to_step(&self, value: T, step_size: T) -> T; - - /// The number of steps in this range, if it is stepped. Used for the host's generic UI. - fn step_count(&self) -> Option; -} - -impl Default for Range { +impl Default for FloatRange { fn default() -> Self { Self::Linear { min: 0.0, max: 1.0 } } } -impl Default for Range { +impl Default for IntRange { fn default() -> Self { Self::Linear { min: 0, max: 1 } } } -impl NormalizebleRange for Range { - fn normalize(&self, plain: f32) -> f32 { +impl FloatRange { + /// Calculate a skew factor for [Range::Skewed] and [Range::SymmetricalSkewed]. Positive values + /// make the end of the range wider while negative make the start of the range wider. + pub fn skew_factor(factor: f32) -> f32 { + 2.0f32.powf(factor) + } + + /// Normalize a plain, unnormalized value. Will be clamped to the bounds of the range if the + /// normalized value exceeds `[0, 1]`. + pub fn normalize(&self, plain: f32) -> f32 { match &self { - Range::Linear { min, max } => (plain - min) / (max - min), - Range::Skewed { min, max, factor } => ((plain - min) / (max - min)).powf(*factor), - Range::SymmetricalSkewed { + FloatRange::Linear { min, max } => (plain - min) / (max - min), + FloatRange::Skewed { min, max, factor } => ((plain - min) / (max - min)).powf(*factor), + FloatRange::SymmetricalSkewed { min, max, factor, @@ -102,14 +85,16 @@ impl NormalizebleRange for Range { .clamp(0.0, 1.0) } - fn unnormalize(&self, normalized: f32) -> f32 { + /// Unnormalize a normalized value. Will be clamped to `[0, 1]` if the plain, unnormalized value + /// would exceed that range. + pub fn unnormalize(&self, normalized: f32) -> f32 { let normalized = normalized.clamp(0.0, 1.0); match &self { - Range::Linear { min, max } => (normalized * (max - min)) + min, - Range::Skewed { min, max, factor } => { + FloatRange::Linear { min, max } => (normalized * (max - min)) + min, + FloatRange::Skewed { min, max, factor } => { (normalized.powf(factor.recip()) * (max - min)) + min } - Range::SymmetricalSkewed { + FloatRange::SymmetricalSkewed { min, max, factor, @@ -131,90 +116,41 @@ impl NormalizebleRange for Range { } } - fn snap_to_step(&self, value: f32, step_size: f32) -> f32 { + /// Snap a vlue to a step size, clamping to the minimum and maximum value of the range. + pub fn snap_to_step(&self, value: f32, step_size: f32) -> f32 { let (min, max) = match &self { - Range::Linear { min, max } => (min, max), - Range::Skewed { min, max, .. } => (min, max), - Range::SymmetricalSkewed { min, max, .. } => (min, max), + FloatRange::Linear { min, max } => (min, max), + FloatRange::Skewed { min, max, .. } => (min, max), + FloatRange::SymmetricalSkewed { min, max, .. } => (min, max), }; ((value / step_size).round() * step_size).clamp(*min, *max) } - - fn step_count(&self) -> Option { - None - } } -impl NormalizebleRange for Range { - fn normalize(&self, plain: i32) -> f32 { +impl IntRange { + /// Normalize a plain, unnormalized value. Will be clamped to the bounds of the range if the + /// normalized value exceeds `[0, 1]`. + pub fn normalize(&self, plain: i32) -> f32 { match &self { - Range::Linear { min, max } => (plain - min) as f32 / (max - min) as f32, - Range::Skewed { min, max, factor } => { - ((plain - min) as f32 / (max - min) as f32).powf(*factor) - } - Range::SymmetricalSkewed { - min, - max, - factor, - center, - } => { - // See the comments in the float version - let unscaled_proportion = (plain - min) as f32 / (max - min) as f32; - let center_proportion = (center - min) as f32 / (max - min) as f32; - if unscaled_proportion > center_proportion { - let scaled_proportion = (unscaled_proportion - center_proportion) - * (1.0 - center_proportion).recip(); - (scaled_proportion.powf(*factor) * 0.5) + 0.5 - } else { - let inverted_scaled_proportion = - (center_proportion - unscaled_proportion) * (center_proportion).recip(); - (1.0 - inverted_scaled_proportion.powf(*factor)) * 0.5 - } - } + IntRange::Linear { min, max } => (plain - min) as f32 / (max - min) as f32, } .clamp(0.0, 1.0) } - fn unnormalize(&self, normalized: f32) -> i32 { + /// Unnormalize a normalized value. Will be clamped to `[0, 1]` if the plain, unnormalized value + /// would exceed that range. + pub fn unnormalize(&self, normalized: f32) -> i32 { let normalized = normalized.clamp(0.0, 1.0); match &self { - Range::Linear { min, max } => (normalized * (max - min) as f32).round() as i32 + min, - Range::Skewed { min, max, factor } => { - (normalized.powf(factor.recip()) * (max - min) as f32).round() as i32 + min - } - Range::SymmetricalSkewed { - min, - max, - factor, - center, - } => { - let center_proportion = (center - min) as f32 / (max - min) as f32; - let skewed_proportion = if normalized > 0.5 { - let scaled_proportion = (normalized - 0.5) * 2.0; - (scaled_proportion.powf(factor.recip()) * (1.0 - center_proportion)) - + center_proportion - } else { - let inverted_scaled_proportion = (0.5 - normalized) * 2.0; - (1.0 - inverted_scaled_proportion.powf(factor.recip())) * center_proportion - }; - - (skewed_proportion * (max - min) as f32).round() as i32 + min - } + IntRange::Linear { min, max } => (normalized * (max - min) as f32).round() as i32 + min, } } - fn snap_to_step(&self, value: i32, _step_size: i32) -> i32 { - // Integers are already discrete, and we don't allow setting step sizes on them through the - // builder interface - value - } - - fn step_count(&self) -> Option { + /// The number of steps in this range, if it is stepped. Used for the host's generic UI. + pub fn step_count(&self) -> Option { match self { - Range::Linear { min, max } => Some((max - min) as usize), - Range::Skewed { min, max, .. } => Some((max - min) as usize), - Range::SymmetricalSkewed { min, max, .. } => Some((max - min) as usize), + IntRange::Linear { min, max } => Some((max - min) as usize), } } } @@ -223,35 +159,27 @@ impl NormalizebleRange for Range { mod tests { use super::*; - fn make_linear_float_range() -> Range { - Range::Linear { + fn make_linear_float_range() -> FloatRange { + FloatRange::Linear { min: 10.0, max: 20.0, } } - fn make_linear_int_range() -> Range { - Range::Linear { min: -10, max: 10 } + fn make_linear_int_range() -> IntRange { + IntRange::Linear { min: -10, max: 10 } } - fn make_skewed_float_range(factor: f32) -> Range { - Range::Skewed { + fn make_skewed_float_range(factor: f32) -> FloatRange { + FloatRange::Skewed { min: 10.0, max: 20.0, factor, } } - fn make_skewed_int_range(factor: f32) -> Range { - Range::Skewed { - min: -10, - max: 10, - factor, - } - } - - fn make_symmetrical_skewed_float_range(factor: f32) -> Range { - Range::SymmetricalSkewed { + fn make_symmetrical_skewed_float_range(factor: f32) -> FloatRange { + FloatRange::SymmetricalSkewed { min: 10.0, max: 20.0, factor, @@ -259,15 +187,6 @@ mod tests { } } - fn make_symmetrical_skewed_int_range(factor: f32) -> Range { - Range::SymmetricalSkewed { - min: -10, - max: 10, - factor, - center: -3, - } - } - #[test] fn step_size() { // These are weird step sizes, but if it works here then it will work for anything @@ -284,7 +203,6 @@ mod tests { } mod linear { - use super::super::*; use super::*; #[test] @@ -319,33 +237,20 @@ mod tests { } mod skewed { - use super::super::*; use super::*; #[test] fn range_normalize_float() { - let range = make_skewed_float_range(Range::skew_factor(-2.0)); + let range = make_skewed_float_range(FloatRange::skew_factor(-2.0)); assert_eq!(range.normalize(17.5), 0.9306049); } - #[test] - fn range_normalize_int() { - let range = make_skewed_int_range(Range::skew_factor(-2.0)); - assert_eq!(range.normalize(-5), 0.70710677); - } - #[test] fn range_unnormalize_float() { - let range = make_skewed_float_range(Range::skew_factor(-2.0)); + let range = make_skewed_float_range(FloatRange::skew_factor(-2.0)); assert_eq!(range.unnormalize(0.9306049), 17.5); } - #[test] - fn range_unnormalize_int() { - let range = make_skewed_int_range(Range::skew_factor(-2.0)); - assert_eq!(range.unnormalize(0.70710677), -5); - } - #[test] fn range_normalize_linear_equiv_float() { let linear_range = make_linear_float_range(); @@ -353,13 +258,6 @@ mod tests { assert_eq!(linear_range.normalize(17.5), skewed_range.normalize(17.5)); } - #[test] - fn range_normalize_linear_equiv_int() { - let linear_range = make_linear_int_range(); - let skewed_range = make_skewed_int_range(1.0); - assert_eq!(linear_range.normalize(-5), skewed_range.normalize(-5)); - } - #[test] fn range_unnormalize_linear_equiv_float() { let linear_range = make_linear_float_range(); @@ -369,54 +267,21 @@ mod tests { skewed_range.unnormalize(0.25) ); } - - #[test] - fn range_unnormalize_linear_equiv_int() { - let linear_range = make_linear_int_range(); - let skewed_range = make_skewed_int_range(1.0); - assert_eq!( - linear_range.unnormalize(0.25), - skewed_range.unnormalize(0.25) - ); - } - - #[test] - fn range_unnormalize_linear_equiv_int_rounding() { - let linear_range = make_linear_int_range(); - let skewed_range = make_skewed_int_range(1.0); - assert_eq!( - linear_range.unnormalize(0.73), - skewed_range.unnormalize(0.73) - ); - } } mod symmetrical_skewed { - use super::super::*; use super::*; #[test] fn range_normalize_float() { - let range = make_symmetrical_skewed_float_range(Range::skew_factor(-2.0)); + let range = make_symmetrical_skewed_float_range(FloatRange::skew_factor(-2.0)); assert_eq!(range.normalize(17.5), 0.951801); } - #[test] - fn range_normalize_int() { - let range = make_symmetrical_skewed_int_range(Range::skew_factor(-2.0)); - assert_eq!(range.normalize(-5), 0.13444477); - } - #[test] fn range_unnormalize_float() { - let range = make_symmetrical_skewed_float_range(Range::skew_factor(-2.0)); + let range = make_symmetrical_skewed_float_range(FloatRange::skew_factor(-2.0)); assert_eq!(range.unnormalize(0.951801), 17.5); } - - #[test] - fn range_unnormalize_int() { - let range = make_symmetrical_skewed_int_range(Range::skew_factor(-2.0)); - assert_eq!(range.unnormalize(0.13444477), -5); - } } }