diff --git a/README.md b/README.md index dd92f2b6..2d6af73b 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ for download links. options. That way you can use regular Rust pattern matching when working with these values without having to do any conversions yourself. - Store additional non-parameter state for your plugin by adding any field - that can be serialized with [serde](https://serde.rs/) to your plugin's + that can be serialized with [Serde](https://serde.rs/) to your plugin's `Params` object and annotating them with `#[persist = "key"]`. - Group your parameters into logical groups by nesting `Params` objects using the `#[nested = "Group Name"]`attribute. diff --git a/nih_plug_derive/src/params.rs b/nih_plug_derive/src/params.rs index 72793473..a1c4eb27 100644 --- a/nih_plug_derive/src/params.rs +++ b/nih_plug_derive/src/params.rs @@ -29,10 +29,12 @@ pub fn derive_params(input: TokenStream) -> TokenStream { // 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_groups_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(); + let mut nested_params_field_idents: Vec = Vec::new(); + let mut nested_params_group_names: Vec = 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 @@ -48,7 +50,8 @@ 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; + // And the `#[nested = "..."]` attribute contains a group name we should use + let mut nested_attr: Option = None; for attr in &field.attrs { if attr.path.is_ident("id") { match attr.parse_meta() { @@ -98,22 +101,34 @@ pub fn derive_params(input: TokenStream) -> TokenStream { }; } else if attr.path.is_ident("nested") { match attr.parse_meta() { - Ok(syn::Meta::Path(_)) => { - if !nested { - nested = true; - } else { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Str(s), + .. + })) => { + let s = s.value(); + if s.is_empty() { + return syn::Error::new(attr.span(), "Group names cannot be empty") + .to_compile_error() + .into(); + } else if s.contains('/') { + return syn::Error::new(attr.span(), "Group names may not contain slashes") + .to_compile_error() + .into(); + } else if nested_attr.is_some() { return syn::Error::new(attr.span(), "Duplicate nested attribute") .to_compile_error() .into(); + } else { + nested_attr = Some(s); } } _ => { return syn::Error::new( attr.span(), - "The nested attribute should not have any arguments: #[nested]", + "The nested attribute should be a key-value pair with a string argument: #[nested = \"Group Name\"]", ) .to_compile_error() - .into(); + .into() } }; } @@ -134,6 +149,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream { // variant param_mapping_insert_tokens .push(quote! { param_map.insert(#param_id, self.#field_name.as_ptr()); }); + // Top-level parameters have no group, and we'll prefix the group name specified in + // the `#[nested = "..."]` attribute to fields coming from nested groups + param_groups_insert_tokens + .push(quote! { param_groups.insert(#param_id, String::new()); }); param_id_string_tokens.push(quote! { #param_id, }); } (None, Some(stable_name)) => { @@ -193,8 +212,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream { (None, None) => (), } - if nested { - nested_fields_idents.push(field_name.clone()); + if let Some(nested_group_name) = nested_attr { + nested_params_field_idents.push(field_name.clone()); + // FIXME: Generate the insertion code here + nested_params_group_names.push(nested_group_name); } } @@ -203,25 +224,59 @@ pub fn derive_params(input: TokenStream) -> TokenStream { fn param_map( self: std::pin::Pin<&Self>, ) -> std::collections::HashMap<&'static str, nih_plug::param::internals::ParamPtr> { - // This may not be in scope otherwise + // This may not be in scope otherwise, used to call .as_ptr() use ::nih_plug::param::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 { + let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; + for nested_params in nested_params_fields { unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) }; } param_map } + fn param_groups( + self: std::pin::Pin<&Self>, + ) -> std::collections::HashMap<&'static str, String> { + let mut param_groups = std::collections::HashMap::new(); + #(#param_groups_insert_tokens)* + + let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; + let nested_params_groups: &[&'static str] = &[#(#nested_params_group_names),*]; + for (nested_params, group_name) in + nested_params_fields.into_iter().zip(nested_params_groups) + { + let nested_param_groups = + unsafe { std::pin::Pin::new_unchecked(*nested_params).param_groups() }; + let prefixed_nested_param_groups = + nested_param_groups + .into_iter() + .map(|(param_id, nested_group_name)| { + ( + param_id, + if nested_group_name.is_empty() { + group_name.to_string() + } else { + format!("{}/{}", group_name, nested_group_name) + } + ) + }); + + param_groups.extend(prefixed_nested_param_groups); + } + + param_groups + } + + 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 { + let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; + for nested_params in nested_params_fields { unsafe { ids.append(&mut Pin::new_unchecked(*nested_params).param_ids()) }; } @@ -232,8 +287,8 @@ pub fn derive_params(input: TokenStream) -> TokenStream { 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 { + let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; + for nested_params in nested_params_fields { unsafe { serialized.extend(Pin::new_unchecked(*nested_params).serialize_fields()) }; } @@ -252,8 +307,8 @@ pub fn derive_params(input: TokenStream) -> TokenStream { // 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 { + let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; + for nested_params in nested_params_fields { unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) }; } } diff --git a/plugins/examples/gain/src/lib.rs b/plugins/examples/gain/src/lib.rs index 2d76a336..6a8d76af 100644 --- a/plugins/examples/gain/src/lib.rs +++ b/plugins/examples/gain/src/lib.rs @@ -21,9 +21,9 @@ struct GainParams { #[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] + /// You can also nest parameter structs. These will appear as a separate nested group if your + /// DAW displays parameters in a tree structure. + #[nested = "Subparameters"] pub sub_params: SubParams, } @@ -31,6 +31,15 @@ struct GainParams { struct SubParams { #[id = "thing"] pub nested_parameter: FloatParam, + + #[nested = "Sub-Subparameters"] + pub sub_sub_params: SubSubParams, +} + +#[derive(Params)] +struct SubSubParams { + #[id = "noope"] + pub nope: FloatParam, } impl Default for Gain { @@ -88,6 +97,9 @@ impl Default for GainParams { }, ) .with_value_to_string(formatters::f32_rounded(2)), + sub_sub_params: SubSubParams { + nope: FloatParam::new("Nope", 0.5, FloatRange::Linear { min: 1.0, max: 2.0 }), + }, }, } } diff --git a/src/param/internals.rs b/src/param/internals.rs index e324732a..c306962d 100644 --- a/src/param/internals.rs +++ b/src/param/internals.rs @@ -11,21 +11,22 @@ pub use serde_json::from_str as deserialize_field; /// Re-export for use in the [`Params`] proc-macro. pub use serde_json::to_string as serialize_field; -/// Describes a struct containing parameters and other persistent fields. The idea is that we can -/// have a normal struct containing [`FloatParam`][super::FloatParam] and other parameter types with -/// attributes 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. 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. +/// Describes a struct containing parameters and other persistent fields. /// -/// The other persistent parameters should be [`PersistentField`]s containing types that can be -/// serialized and deserialized with Serde. When deriving this trait, any of those fields should be -/// marked with `#[persist = "key"]`. +/// This trait can be derived on a struct containing [`FloatParam`][super::FloatParam] and other +/// parameter fields. When deriving this trait, any of those parameter fields should have the `#[id +/// = "stable"]` attribute, where `stable` is an up to 6 character long string (to avoid collisions) +/// that will be used to identify the parameter internall so you can safely move it around and +/// rename the field without breaking compatibility with old presets. +/// +/// The struct can also contain other fields that should be persisted along with the rest of the +/// preset data. These fields should be [`PersistentField`]s annotated with the `#[persist = "key"]` +/// attribute containing types that can be serialized and deserialized with +/// [Serde](https://serde.rs/). /// /// 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 +/// `Params` objects by adding the `#[nested = "Group Name"]` attribute to those fields. These +/// groups will be displayed as a tree-like structure if your DAW supports it. Parameter IDs and /// persisting keys still need to be **unique** when usting nested parameter structs. This currently /// has the following caveats: /// @@ -44,8 +45,13 @@ pub trait Params { /// is only valid as long as this pinned object is valid. fn param_map(self: Pin<&Self>) -> HashMap<&'static str, ParamPtr>; - /// All parameter IDs from `param_map`, in a stable order. This order will be used to display - /// the parameters. + /// Contains group names for each parameter in [`param_map()`][Self::param_map()]. This is + /// either an empty string for top level parameters, or a slash/delimited `"Group Name 1/Group + /// Name 2"` string for parameters that belong to `#[nested = "Name"]` parameter objects. + fn param_groups(self: Pin<&Self>) -> HashMap<&'static str, String>; + + /// All parameter IDs from [`param_map()`][Self::param_map()], in a stable order. This order + /// will be used to display the parameters. /// /// 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 diff --git a/src/plugin.rs b/src/plugin.rs index 2a873dec..cab739c3 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -12,13 +12,15 @@ use crate::param::internals::Params; /// Basic functionality that needs to be implemented by a plugin. The wrappers will use this to /// expose the plugin in a particular plugin format. /// +/// The main thing you need to do is define a `[Params]` struct containing all of your parmaeters. +/// See the trait's documentation for more information on how to do that, or check out the examples. +/// /// This is super basic, and lots of things I didn't need or want to use yet haven't been /// implemented. Notable missing features include: /// /// - Sidechain inputs /// - Multiple output busses /// - Special handling for offline processing -/// - Parameter hierarchies/groups /// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to /// interact with it yet /// - Outputting parameter changes from the plugin