From be4bbd44006dffac12660c72a70be4d89b5cfe6e Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Thu, 3 Mar 2022 20:37:01 +0100 Subject: [PATCH] Allow nested parameter structs See the Parameters docstring for the caveats. --- nih_plug_derive/src/lib.rs | 74 +++++++++++++++++++++++++++----- nih_plug_egui/src/lib.rs | 3 +- plugins/examples/gain/src/lib.rs | 29 +++++++++++-- src/param/internals.rs | 24 ++++++++--- 4 files changed, 111 insertions(+), 19 deletions(-) diff --git a/nih_plug_derive/src/lib.rs b/nih_plug_derive/src/lib.rs index 6def8050..ce27c155 100644 --- a/nih_plug_derive/src/lib.rs +++ b/nih_plug_derive/src/lib.rs @@ -5,7 +5,7 @@ use quote::quote; use std::collections::HashSet; use syn::spanned::Spanned; -#[proc_macro_derive(Params, attributes(id, persist))] +#[proc_macro_derive(Params, attributes(id, persist, nested))] pub fn derive_params(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as syn::DeriveInput); @@ -25,16 +25,21 @@ pub fn derive_params(input: TokenStream) -> TokenStream { } }; - // We only care about fields with `id` and `persist` attributes. For the `id` fields we'll build - // a mapping function that creates a hashmap containing pointers to those parmaeters. For the - // `persist` function we'll create functions that serialize and deserialize those fields - // individually (so they can be added and removed independently of eachother) using JSON. + // We only care about fields with `id`, `persist`, and `nested` attributes. For the `id` fields + // we'll build a mapping function that creates a hashmap containing pointers to those + // parmaeters. For the `persist` function we'll create functions that serialize and deserialize + // those fields individually (so they can be added and removed independently of eachother) using + // JSON. The `nested` fields should also implement the `Params` trait and their fields will be + // inherited and added to this field's lists. let mut param_mapping_insert_tokens = Vec::new(); let mut param_id_string_tokens = Vec::new(); let mut field_serialize_tokens = Vec::new(); let mut field_deserialize_tokens = Vec::new(); + let mut nested_fields_idents = Vec::new(); // We'll also enforce that there are no duplicate keys at compile time + // TODO: This doesn't work for nested fields since we don't know anything about the fields on + // the nested structs let mut param_ids = HashSet::new(); let mut persist_ids = HashSet::new(); for field in fields.named { @@ -46,6 +51,7 @@ pub fn derive_params(input: TokenStream) -> TokenStream { // These two attributes are mutually exclusive let mut id_attr: Option = None; let mut persist_attr: Option = None; + let mut nested = false; for attr in &field.attrs { if attr.path.is_ident("id") { match attr.parse_meta() { @@ -93,6 +99,26 @@ pub fn derive_params(input: TokenStream) -> TokenStream { .into() } }; + } else if attr.path.is_ident("nested") { + match attr.parse_meta() { + Ok(syn::Meta::Path(_)) => { + if !nested { + nested = true; + } else { + return syn::Error::new(attr.span(), "Duplicate nested attribute") + .to_compile_error() + .into(); + } + } + _ => { + return syn::Error::new( + attr.span(), + "The nested attribute should not have any arguments: #[nested]", + ) + .to_compile_error() + .into(); + } + }; } } @@ -169,6 +195,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream { } (None, None) => (), } + + if nested { + nested_fields_idents.push(field_name.clone()); + } } quote! { @@ -180,21 +210,36 @@ pub fn derive_params(input: TokenStream) -> TokenStream { use ::nih_plug::Param; let mut param_map = std::collections::HashMap::new(); - #(#param_mapping_insert_tokens)* + let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; + for nested_params in nested_fields { + unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) }; + } + param_map } - fn param_ids(self: std::pin::Pin<&Self>) -> &'static [&'static str] { - &[#(#param_id_string_tokens)*] + fn param_ids(self: std::pin::Pin<&Self>) -> Vec<&'static str> { + let mut ids = vec![#(#param_id_string_tokens)*]; + + let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; + for nested_params in nested_fields { + unsafe { ids.append(&mut Pin::new_unchecked(*nested_params).param_ids()) }; + } + + ids } fn serialize_fields(&self) -> ::std::collections::HashMap { let mut serialized = ::std::collections::HashMap::new(); - #(#field_serialize_tokens)* + let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; + for nested_params in nested_fields { + unsafe { serialized.extend(Pin::new_unchecked(*nested_params).serialized_fields()) }; + } + serialized } @@ -202,9 +247,18 @@ pub fn derive_params(input: TokenStream) -> TokenStream { for (field_name, data) in serialized { match field_name.as_str() { #(#field_deserialize_tokens)* - _ => nih_log!("Unknown field name: {}", field_name), + _ => nih_log!("Unknown serialized field name: {} (this may not be accurate)", field_name), } } + + // FIXME: The above warning will course give false postiives when using nested + // parameter structs. An easy fix would be to use + // https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.drain_filter + // once that gets stabilized. + let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; + for nested_params in nested_fields { + unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) }; + } } } } diff --git a/nih_plug_egui/src/lib.rs b/nih_plug_egui/src/lib.rs index b353e040..2253749d 100644 --- a/nih_plug_egui/src/lib.rs +++ b/nih_plug_egui/src/lib.rs @@ -23,7 +23,8 @@ pub mod widgets; /// contains the GUI's intitial size, and this is kept in sync whenever the GUI gets resized. You /// can also use this to know if the GUI is open, so you can avoid performing potentially expensive /// calculations while the GUI is not open. If you want this size to be persisted when restoring a -/// plugin instance, then you can store it in a `#[persist]` field on your parameters struct. +/// plugin instance, then you can store it in a `#[persist = "key"]` field on your parameters +/// struct. /// /// See [EguiState::from_size()]. // diff --git a/plugins/examples/gain/src/lib.rs b/plugins/examples/gain/src/lib.rs index 9cbe58c0..c8c9a3be 100644 --- a/plugins/examples/gain/src/lib.rs +++ b/plugins/examples/gain/src/lib.rs @@ -19,14 +19,25 @@ struct GainParams { #[id = "gain"] pub gain: FloatParam, - #[id = "as_long_as_this_name_stays_constant"] - pub the_field_name_can_change: BoolParam, + #[id = "stable"] + pub but_field_names_can_change: BoolParam, /// This field isn't used in this exampleq, but anything written to the vector would be restored /// together with a preset/state file saved for this plugin. This can be useful for storign /// things like sample data. #[persist = "industry_secrets"] pub random_data: RwLock>, + + /// You can also nest parameter structs. This is only for your own organization: they will still + /// appear as a flat list to the host. + #[nested] + pub sub_params: SubParams, +} + +#[derive(Params)] +struct SubParams { + #[id = "thing"] + pub nested_parameter: FloatParam, } impl Default for Gain { @@ -63,7 +74,7 @@ impl Default for GainParams { // // ..Default::default(), }, // ...or use the builder interface: - the_field_name_can_change: BoolParam::new("Important value", false).with_callback( + but_field_names_can_change: BoolParam::new("Important value", false).with_callback( Arc::new(|_new_value: bool| { // If, for instance, updating this parameter would require other parts of the // plugin's internal state to be updated other values to also be updated, then @@ -73,6 +84,18 @@ impl Default for GainParams { // Persisted fields can be intialized like any other fields, and they'll keep their when // restoring the plugin's state. random_data: RwLock::new(Vec::new()), + sub_params: SubParams { + nested_parameter: FloatParam::new( + "Unused Nested Parameter", + 0.5, + FloatRange::Skewed { + min: 2.0, + max: 2.4, + factor: FloatRange::skew_factor(2.0), + }, + ) + .with_value_to_string(formatters::f32_rounded(2)), + }, } } } diff --git a/src/param/internals.rs b/src/param/internals.rs index 12610537..26963e2e 100644 --- a/src/param/internals.rs +++ b/src/param/internals.rs @@ -15,10 +15,21 @@ pub use serde_json::to_string as serialize_field; /// assigning a unique identifier to each parameter. We can then build a mapping from those /// parameter IDs to the parameters using the [Params::param_map()] function. That way we can have /// easy to work with JUCE-style parameter objects in the plugin without needing to manually -/// register each parameter, like you would in JUCE. +/// register each parameter, like you would in JUCE. When deriving this trait, any of those +/// parameters should have the `#[id = "stable"]` attribute, where `stable` is an up to 6 character +/// (to avoid collisions) string that will be used for the parameter's internal identifier. /// /// The other persistent parameters should be [PersistentField]s containing types that can be -/// serialized and deserialized with Serde. +/// serialized and deserialized with Serde. When deriving this trait, any of those fields should be +/// marked with `#[persist = "key"]`. +/// +/// And finally when deriving this trait, it is also possible to inherit the parameters from other +/// `Params` objects by adding the `#[nested]` attribute to those fields. Parameter IDs and +/// persisting keys still need to be **unique** when usting nested parameter structs. This currently +/// has the following caveats: +/// +/// - Enforcing that parameter IDs and persist keys are unique does not work across nested structs. +/// - Deserializing persisted fields will give false positives about fields not existing. /// /// Take a look at the example gain plugin to see how this should be used. /// @@ -26,8 +37,6 @@ pub use serde_json::to_string as serialize_field; /// /// This implementation is safe when using from the wrapper because the plugin object needs to be /// pinned, and it can never outlive the wrapper. -// -// TODO: Add a `#[nested]` attribute for nested params objects pub trait Params { /// Create a mapping from unique parameter IDs to parameters. This is done for every parameter /// field marked with `#[id = "stable_name"]`. Dereferencing the pointers stored in the values @@ -36,7 +45,12 @@ pub trait Params { /// All parameter IDs from `param_map`, in a stable order. This order will be used to display /// the parameters. - fn param_ids(self: Pin<&Self>) -> &'static [&'static str]; + /// + /// TODO: This used to be a static slice, but now that we supported nested parameter objects + /// that's become a bit more difficult since Rust does not have a convenient way to + /// concatenate an arbitrary number of static slices. There's probably a better way to do + /// this. + fn param_ids(self: Pin<&Self>) -> Vec<&'static str>; /// Serialize all fields marked with `#[persist = "stable_name"]` into a hash map containing /// JSON-representations of those fields so they can be written to the plugin's state and