diff --git a/Cargo.lock b/Cargo.lock index 7b75e3d4..15d5cfda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,6 +276,7 @@ name = "diopser" version = "0.1.0" dependencies = [ "nih_plug", + "strum", ] [[package]] @@ -399,6 +400,15 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "itoa" version = "1.0.1" @@ -519,6 +529,7 @@ dependencies = [ "raw-window-handle", "serde", "serde_json", + "strum", "vst3-sys", "widestring", "windows", @@ -693,6 +704,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + [[package]] name = "ryu" version = "1.0.9" @@ -783,6 +800,28 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "1.0.86" @@ -800,6 +839,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + [[package]] name = "unicode-xid" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 7ae9ebef..22693e2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ parking_lot = "0.12" raw-window-handle = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +strum = { version = "0.23", features = ["derive"] } # This contains a number of fixes for the reference counting, cross compilation # support, and an incorrect return type vst3-sys = { git = "https://github.com/robbert-vdh/vst3-sys.git", branch = "fix/atomic-reference-count" } diff --git a/src/lib.rs b/src/lib.rs index 96e50d5f..bb4fedba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,8 @@ pub use param::internals::Params; pub use param::range::Range; pub use param::smoothing::{Smoother, SmoothingStyle}; pub use param::{BoolParam, FloatParam, IntParam, Param}; +// TODO: Consider re-exporting these from another module so you can import them all at once +pub use param::{Display, EnumIter, EnumParam}; pub use plugin::{ BufferConfig, BusConfig, Editor, NoteEvent, ParentWindowHandle, Plugin, ProcessStatus, Vst3Plugin, diff --git a/src/param.rs b/src/param.rs index e6e79482..67c98445 100644 --- a/src/param.rs +++ b/src/param.rs @@ -5,13 +5,17 @@ use std::fmt::Display; use std::sync::Arc; -use self::range::{NormalizebleRange, Range}; -use self::smoothing::{Smoother, SmoothingStyle}; +// Re-export for the [EnumParam] +// TODO: Consider re-exporting this from a non-root module to make it a bit less spammy:w +pub use strum::{Display, EnumIter, IntoEnumIterator as EnumIter}; pub mod internals; pub mod range; pub mod smoothing; +use self::range::{NormalizebleRange, Range}; +use self::smoothing::{Smoother, SmoothingStyle}; + pub type FloatParam = PlainParam; pub type IntParam = PlainParam; @@ -144,6 +148,21 @@ pub struct BoolParam { pub string_to_value: Option Option + Send + Sync>>, } +/// An [IntParam]-backed categorical parameter that allows convenient conversion to and from a +/// simple enum. This enum must derive the re-exported [EnumIter], [EnumString] and [Display] +/// traits. +// +// TODO: Figure out a more sound way to get the same interface +pub struct EnumParam { + /// The integer parameter backing this enum parameter. + pub inner: IntParam, + /// An associative list of the variants converted to an i32 and their names. We need this + /// because we're doing some nasty type erasure things with [ParamPtr::Enum], so we can't + /// directly query the associated functions on `T` after the parameter when handling function + /// calls from the wrapper. + variants: Vec<(T, String)>, +} + impl Default for PlainParam where T: Default, @@ -177,6 +196,27 @@ impl Default for BoolParam { } } +impl Default for EnumParam { + fn default() -> Self { + let variants: Vec<_> = T::iter().map(|v| (v, v.to_string())).collect(); + let default = T::default(); + + Self { + inner: IntParam { + value: T::iter() + .position(|v| v == default) + .expect("Invalid variant in init") as i32, + range: Range::Linear { + min: 0, + max: variants.len() as i32 - 1, + }, + ..Default::default() + }, + variants, + } + } +} + macro_rules! impl_plainparam { ($ty:ident, $plain:ty) => { impl Param for $ty { @@ -353,6 +393,67 @@ impl Param for BoolParam { } } +impl Param for EnumParam { + type Plain = T; + + fn update_smoother(&mut self, sample_rate: f32, reset: bool) { + self.inner.update_smoother(sample_rate, reset) + } + + fn set_from_string(&mut self, string: &str) -> bool { + match self.variants.iter().find(|(_, repr)| repr == string) { + Some((variant, _)) => { + self.inner.set_plain_value(self.to_index(*variant)); + true + } + None => false, + } + } + + fn plain_value(&self) -> Self::Plain { + self.from_index(self.inner.plain_value()) + } + + fn set_plain_value(&mut self, plain: Self::Plain) { + self.inner.set_plain_value(self.to_index(plain)) + } + + fn normalized_value(&self) -> f32 { + self.inner.normalized_value() + } + + fn set_normalized_value(&mut self, normalized: f32) { + self.inner.set_normalized_value(normalized) + } + + fn normalized_value_to_string(&self, normalized: f32, _include_unit: bool) -> String { + // XXX: As mentioned below, our type punning would cause `.to_string()` to print the + // incorect value. Because of that, we already stored the string representations for + // variants values in this struct. + let plain = self.preview_plain(normalized); + let index = self.to_index(plain); + self.variants[index as usize].1.clone() + } + + fn string_to_normalized_value(&self, string: &str) -> Option { + self.inner.string_to_normalized_value(string) + } + + fn preview_normalized(&self, plain: Self::Plain) -> f32 { + self.inner.preview_normalized(self.to_index(plain)) + } + + fn preview_plain(&self, normalized: f32) -> Self::Plain { + self.from_index(self.inner.preview_plain(normalized)) + } + + fn as_ptr(&self) -> internals::ParamPtr { + internals::ParamPtr::EnumParam( + self as *const EnumParam as *mut EnumParam as *mut EnumParam, + ) + } +} + impl Display for PlainParam { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match (&self.value_to_string, &self.step_size) { @@ -376,6 +477,12 @@ impl Display for BoolParam { } } +impl Display for EnumParam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.variants[self.inner.plain_value() as usize].1) + } +} + impl PlainParam where Range: Default, @@ -492,6 +599,77 @@ impl BoolParam { } } +impl EnumParam { + /// Build a new [Self]. Use the other associated functions to modify the behavior of the + /// parameter. + pub fn new(name: &'static str, default: T) -> Self { + let variants: Vec<_> = T::iter().map(|v| (v, v.to_string())).collect(); + + Self { + inner: IntParam { + value: variants + .iter() + .position(|(v, _)| v == &default) + .expect("Invalid variant in init") as i32, + range: Range::Linear { + min: 0, + max: variants.len() as i32 - 1, + }, + name, + ..Default::default() + }, + variants, + } + } + + // We currently don't implement callbacks here. If we want to do that, then we'll need to add + // the IntParam fields to the parameter itself. + // TODO: Do exactly that +} + +impl EnumParam { + // TODO: There doesn't seem to be a single enum crate that gives you a dense [0, n_variatns) + // mapping between integers and enum variants. So far linear search over this variants has + // been the best approach. We should probably replace this with our own macro at some + // point. + + /// The number of variants for this parameter + // + // This is part of the magic sauce that lets [ParamPtr::Enum] work. The type parmaeter there is + // a dummy type, acting as a somewhat unsound way to do type erasure. Because all data is stored + // in the struct after initialization (i.e. we no longer rely on T's specifics) and AnyParam is + // represented by an i32 this EnumParam behaves correctly even when casted between Ts. + // + // TODO: Come up with a sounder way to do this. + #[allow(clippy::len_without_is_empty)] + #[inline(never)] + pub fn len(&self) -> usize { + self.variants.len() + } + + /// Get the index associated to an enum variant. + #[inline(never)] + fn to_index(&self, variant: T) -> i32 { + self.variants + .iter() + // This is somewhat shady, as `T` is going to be `AnyEnum` when this is indirectly + // called from the wrapper. + .position(|(v, _)| v == &variant) + .expect("Invalid enum variant") as i32 + } + + /// Get a variant from a index. + /// + /// # Panics + /// + /// indices `>= Self::len()` will trigger a panic. + #[allow(clippy::wrong_self_convention)] + #[inline(never)] + fn from_index(&self, index: i32) -> T { + self.variants[index as usize].0 + } +} + /// 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/internals.rs b/src/param/internals.rs index 4084e353..7179276a 100644 --- a/src/param/internals.rs +++ b/src/param/internals.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::pin::Pin; -use super::Param; +use super::{Display, EnumIter, Param}; /// Re-export for use in the [Params] proc-macro. pub use serde_json::from_str as deserialize_field; @@ -48,12 +48,24 @@ pub trait Params { fn deserialize_fields(&self, serialized: &HashMap); } +/// Dummy enum for in [ParamPtr]. This type needs an explicit representation size so we can compare +/// the discriminants. +#[derive(Display, Clone, Copy, PartialEq, Eq, EnumIter)] +#[repr(i32)] +pub enum AnyEnum { + Foo, + Bar, +} + /// Internal pointers to parameters. This is an implementation detail used by the wrappers. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum ParamPtr { FloatParam(*mut super::FloatParam), IntParam(*mut super::IntParam), BoolParam(*mut super::BoolParam), + /// The enum type parameter is used only as a phantom type, so we can safely cast between these + /// pointers. + EnumParam(*mut super::EnumParam), } // These pointers only point to fields on pinned structs, and the caller always needs to make sure @@ -88,6 +100,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).name, ParamPtr::IntParam(p) => (**p).name, ParamPtr::BoolParam(p) => (**p).name, + ParamPtr::EnumParam(p) => (**p).inner.name, } } @@ -102,6 +115,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).unit, ParamPtr::IntParam(p) => (**p).unit, ParamPtr::BoolParam(_) => "", + ParamPtr::EnumParam(_) => "", } } @@ -118,6 +132,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).update_smoother(sample_rate, reset), ParamPtr::IntParam(p) => (**p).update_smoother(sample_rate, reset), ParamPtr::BoolParam(p) => (**p).update_smoother(sample_rate, reset), + ParamPtr::EnumParam(p) => (**p).update_smoother(sample_rate, reset), } } @@ -133,6 +148,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).set_from_string(string), ParamPtr::IntParam(p) => (**p).set_from_string(string), ParamPtr::BoolParam(p) => (**p).set_from_string(string), + ParamPtr::EnumParam(p) => (**p).set_from_string(string), } } @@ -147,6 +163,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).normalized_value(), ParamPtr::IntParam(p) => (**p).normalized_value(), ParamPtr::BoolParam(p) => (**p).normalized_value(), + ParamPtr::EnumParam(p) => (**p).normalized_value(), } } @@ -163,6 +180,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).set_normalized_value(normalized), ParamPtr::IntParam(p) => (**p).set_normalized_value(normalized), ParamPtr::BoolParam(p) => (**p).set_normalized_value(normalized), + ParamPtr::EnumParam(p) => (**p).set_normalized_value(normalized), } } @@ -178,6 +196,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).preview_normalized(plain), ParamPtr::IntParam(p) => (**p).preview_normalized(plain as i32), ParamPtr::BoolParam(_) => plain, + ParamPtr::EnumParam(p) => (**p).inner.preview_normalized(plain as i32), } } @@ -193,6 +212,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).preview_plain(normalized), ParamPtr::IntParam(p) => (**p).preview_plain(normalized) as f32, ParamPtr::BoolParam(_) => normalized, + ParamPtr::EnumParam(p) => (**p).inner.preview_plain(normalized) as f32, } } @@ -209,6 +229,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).normalized_value_to_string(normalized, include_unit), ParamPtr::IntParam(p) => (**p).normalized_value_to_string(normalized, include_unit), ParamPtr::BoolParam(p) => (**p).normalized_value_to_string(normalized, include_unit), + ParamPtr::EnumParam(p) => (**p).normalized_value_to_string(normalized, include_unit), } } @@ -223,6 +244,7 @@ impl ParamPtr { ParamPtr::FloatParam(p) => (**p).string_to_normalized_value(string), ParamPtr::IntParam(p) => (**p).string_to_normalized_value(string), ParamPtr::BoolParam(p) => (**p).string_to_normalized_value(string), + ParamPtr::EnumParam(p) => (**p).string_to_normalized_value(string), } } } diff --git a/src/wrapper/state.rs b/src/wrapper/state.rs index 852ee165..46e5157e 100644 --- a/src/wrapper/state.rs +++ b/src/wrapper/state.rs @@ -10,6 +10,7 @@ pub(crate) enum ParamValue { F32(f32), I32(i32), Bool(bool), + EnumVariant(String), } /// A plugin's state so it can be restored at a later point. diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index 18a87200..0c3da385 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -265,6 +265,16 @@ impl IComponent for Wrapper

{ (ParamPtr::FloatParam(p), ParamValue::F32(v)) => (**p).set_plain_value(v), (ParamPtr::IntParam(p), ParamValue::I32(v)) => (**p).set_plain_value(v), (ParamPtr::BoolParam(p), ParamValue::Bool(v)) => (**p).set_plain_value(v), + (ParamPtr::EnumParam(p), ParamValue::EnumVariant(s)) => { + if !(**p).set_from_string(&s) { + nih_debug_assert_failure!( + "Invalid stored value '{}' for parameter \"{}\" ({:?})", + s, + param_id_str, + param_ptr, + ); + } + } (param_ptr, param_value) => { nih_debug_assert_failure!( "Invalid serialized value {:?} for parameter \"{}\" ({:?})", @@ -329,6 +339,13 @@ impl IComponent for Wrapper

{ param_id_str.to_string(), ParamValue::Bool((*p).plain_value()), ), + ParamPtr::EnumParam(p) => ( + param_id_str.to_string(), + // XXX: This works, but it's a bit of a roundabout conversion + ParamValue::EnumVariant( + (*p).normalized_value_to_string((*p).normalized_value(), false), + ), + ), }) .collect(); @@ -430,6 +447,7 @@ impl IEditController for Wrapper

{ Range::SymmetricalSkewed { min, max, .. } => max - min, }, ParamPtr::BoolParam(_) => 1, + ParamPtr::EnumParam(p) => (**p).len() as i32 - 1, }; info.default_normalized_value = *default_value as f64; info.unit_id = vst3_sys::vst::kRootUnitId;