1
0
Fork 0

💥 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.
This commit is contained in:
Robbert van der Helm 2022-03-03 19:24:40 +01:00
parent 006dcde313
commit 76369ad1e1
10 changed files with 459 additions and 389 deletions

View file

@ -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))),

View file

@ -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(&params.some_int, setter));
ui.label("Gain");

View file

@ -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,
},

View file

@ -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))

View file

@ -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::{

View file

@ -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 {

View file

@ -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<T: Enum + Default> Default for EnumParam<T> {
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<T: Enum + 'static> EnumParam<T> {
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,
},

View file

@ -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<f32>;
pub type IntParam = PlainParam<i32>;
/// 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<i32>;
// 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<T> {
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<T>,
pub smoothed: Smoother<f32>,
/// 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<T> {
/// 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<Arc<dyn Fn(T) + Send + Sync>>,
pub value_changed: Option<Arc<dyn Fn(f32) + Send + Sync>>,
/// The distribution of the parameter's values.
pub range: Range<T>,
/// 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<f32>,
/// The parameter's human readable display name.
pub name: &'static str,
@ -56,24 +53,20 @@ pub struct PlainParam<T> {
/// 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<Arc<dyn Fn(T) -> String + Send + Sync>>,
pub value_to_string: Option<Arc<dyn Fn(f32) -> 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<Arc<dyn Fn(&str) -> Option<T> + Send + Sync>>,
pub string_to_value: Option<Arc<dyn Fn(&str) -> Option<f32> + Send + Sync>>,
}
impl<T> Default for PlainParam<T>
where
T: Default,
Range<T>: 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<T: Display + Copy> Display for PlainParam<T> {
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,10 +89,8 @@ impl<T: Display + Copy> Display for PlainParam<T> {
}
}
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
@ -110,7 +101,7 @@ macro_rules! impl_plainparam {
}
fn step_count(&self) -> Option<usize> {
self.range.step_count()
None
}
fn plain_value(&self) -> Self::Plain {
@ -136,7 +127,7 @@ macro_rules! impl_plainparam {
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)),
(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)
@ -167,8 +158,6 @@ macro_rules! impl_plainparam {
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,
}
@ -203,22 +192,14 @@ macro_rules! impl_plainparam {
}
fn as_ptr(&self) -> ParamPtr {
ParamPtr::$ty(self as *const $ty as *mut $ty)
ParamPtr::FloatParam(self as *const _ as *mut _)
}
}
};
}
impl_plainparam!(FloatParam, f32);
impl_plainparam!(IntParam, i32);
impl<T: Default> PlainParam<T>
where
Range<T>: 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<T>) -> 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<dyn Fn(T) + Send + Sync>) -> Self {
pub fn with_callback(mut self, callback: Arc<dyn Fn(f32) + Send + Sync>) -> 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<dyn Fn(T) -> String + Send + Sync>,
callback: Arc<dyn Fn(f32) -> 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<F>(
mut self,
callback: Arc<dyn Fn(&str) -> Option<T> + Send + Sync>,
callback: Arc<dyn Fn(&str) -> Option<f32> + Send + Sync>,
) -> Self {
self.string_to_value = Some(callback);
self
}
}
impl PlainParam<f32> {
/// 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.

236
src/param/integer.rs Normal file
View file

@ -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<i32>,
/// 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<Atomic*>` 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<Arc<dyn Fn(i32) + Send + Sync>>,
/// 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<Arc<dyn Fn(i32) -> 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<Arc<dyn Fn(&str) -> Option<i32> + 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<usize> {
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<f32> {
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<dyn Fn(i32) + Send + Sync>) -> 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<dyn Fn(i32) -> 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<F>(
mut self,
callback: Arc<dyn Fn(&str) -> Option<i32> + Send + Sync>,
) -> Self {
self.string_to_value = Some(callback);
self
}
}

View file

@ -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<T> {
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<T>`. 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<T> {
/// 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<usize>;
}
impl Default for Range<f32> {
impl Default for FloatRange {
fn default() -> Self {
Self::Linear { min: 0.0, max: 1.0 }
}
}
impl Default for Range<i32> {
impl Default for IntRange {
fn default() -> Self {
Self::Linear { min: 0, max: 1 }
}
}
impl NormalizebleRange<f32> for Range<f32> {
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<f32> for Range<f32> {
.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<f32> for Range<f32> {
}
}
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<usize> {
None
}
}
impl NormalizebleRange<i32> for Range<i32> {
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<usize> {
/// The number of steps in this range, if it is stepped. Used for the host's generic UI.
pub fn step_count(&self) -> Option<usize> {
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<i32> for Range<i32> {
mod tests {
use super::*;
fn make_linear_float_range() -> Range<f32> {
Range::Linear {
fn make_linear_float_range() -> FloatRange {
FloatRange::Linear {
min: 10.0,
max: 20.0,
}
}
fn make_linear_int_range() -> Range<i32> {
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<f32> {
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<i32> {
Range::Skewed {
min: -10,
max: 10,
factor,
}
}
fn make_symmetrical_skewed_float_range(factor: f32) -> Range<f32> {
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<i32> {
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);
}
}
}