From 727d88c4d77e3fe376683aeb113ff05a6f2072d0 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Thu, 13 Oct 2022 01:20:56 +0200 Subject: [PATCH] Add ID renaming superpowers to #[nested(...)] This can now be used for most common use cases where you previously had to do a manual `Params` implementation, like arrays of parameter objects and duplicate parameter objects. --- BREAKING_CHANGES.md | 10 + Cargo.lock | 12 +- README.md | 8 +- nih_plug_derive/src/params.rs | 588 ++++++++++++------ plugins/examples/gain/src/lib.rs | 24 +- .../src/compressor_bank.rs | 4 +- plugins/spectral_compressor/src/lib.rs | 6 +- src/lib.rs | 4 +- src/param/internals.rs | 53 +- 9 files changed, 484 insertions(+), 225 deletions(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index d4c40c22..43c9b3f6 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -6,6 +6,16 @@ new and what's changed, this document lists all breaking changes in reverse chronological order. If a new feature did not require any changes to existing code then it will not be listed here. +## [2022-10-13] + +- The `#[nested]` parameter attribute has gained super powers and has its syntax + changed. It can now automatically handle many situations that previously + required custom `Params` implementations to have multiple almost identical + copies of a parameter struct. The current version supports both fields with + unique parameter ID prefixes, and arrays of parameter objects. See the + [`Params`](https://nih-plug.robbertvanderhelm.nl/nih_plug/param/internals/trait.Params.html) + trait for more information on the new syntax. + ## [2022-09-22] - `nih_plug_egui` has been updated from egui 0.17 to egui 0.19. diff --git a/Cargo.lock b/Cargo.lock index 54b069c2..7b0d347d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3278,9 +3278,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -3319,9 +3319,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -4016,9 +4016,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.96" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index e0a70973..aa7962c3 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,12 @@ Scroll down for more information on the underlying plugin framework. 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. + the `#[nested(group = "...")]`attribute. + - The `#[nested]` attribute also enables you to use multiple copies of the + same parameter, either as regular object fields or through arrays. - When needed, you can also provide your own implementation for the `Params` - trait to enable dynamically generated parameters and arrays of if mostly - identical parameter objects. + trait to enable compile time generated parameters and other bespoke + functionality. - Stateful. Behaves mostly like JUCE, just without all of the boilerplate. - Does not make any assumptions on how you want to process audio, but does come with utilities and adapters to help with common access patterns. diff --git a/nih_plug_derive/src/params.rs b/nih_plug_derive/src/params.rs index e77ef0fc..e7a2168a 100644 --- a/nih_plug_derive/src/params.rs +++ b/nih_plug_derive/src/params.rs @@ -1,6 +1,5 @@ use proc_macro::TokenStream; use quote::quote; -use std::collections::HashSet; use syn::spanned::Spanned; pub fn derive_params(input: TokenStream) -> TokenStream { @@ -28,29 +27,22 @@ pub fn derive_params(input: TokenStream) -> TokenStream { // parameters. 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_self_tokens = Vec::new(); - let mut field_serialize_tokens = Vec::new(); - let mut field_deserialize_tokens = 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 - // the nested structs - let mut param_ids = HashSet::new(); - let mut persist_ids = HashSet::new(); + // inherited and added to this field's lists. We'll also enforce that there are no duplicate + // keys at compile time. + // TODO: This duplication check doesn't work for nested fields since we don't know anything + // about the fields on the nested structs + let mut params: Vec = Vec::new(); + let mut persistent_fields: Vec = Vec::new(); + let mut nested_params: Vec = Vec::new(); for field in fields.named { let field_name = match &field.ident { Some(ident) => ident, _ => continue, }; - // These two attributes are mutually exclusive - let mut id_attr: Option = None; - let mut persist_attr: Option = None; - // And the `#[nested = "..."]` attribute contains a group name we should use - let mut nested_attr: Option = None; + // All attributes are mutually exclusive. If we encounter multiple or duplicate attributes, + // then we'll error out. + let mut processed_attribute = false; for attr in &field.attrs { if attr.path.is_ident("id") { match attr.parse_meta() { @@ -58,13 +50,33 @@ pub fn derive_params(input: TokenStream) -> TokenStream { lit: syn::Lit::Str(s), .. })) => { - if id_attr.is_none() { - id_attr = Some(s.value()); - } else { - return syn::Error::new(attr.span(), "Duplicate id attribute") - .to_compile_error() - .into(); + if processed_attribute { + return syn::Error::new( + attr.span(), + "Duplicate or incompatible attribute found", + ) + .to_compile_error() + .into(); } + + // This is a vector since we want to preserve the order. If structs get + // large enough to the point where a linear search starts being expensive, + // then the plugin should probably start splitting up their parameters. + if params.iter().any(|p| p.id == s) { + return syn::Error::new( + field.span(), + "Multiple parameters with the same ID found", + ) + .to_compile_error() + .into(); + } + + params.push(Param { + id: s, + field: field_name.clone(), + }); + + processed_attribute = true; } _ => { return syn::Error::new( @@ -82,13 +94,30 @@ pub fn derive_params(input: TokenStream) -> TokenStream { lit: syn::Lit::Str(s), .. })) => { - if persist_attr.is_none() { - persist_attr = Some(s.value()); - } else { - return syn::Error::new(attr.span(), "Duplicate persist attribute") - .to_compile_error() - .into(); + if processed_attribute { + return syn::Error::new( + attr.span(), + "Duplicate or incompatible attribute found", + ) + .to_compile_error() + .into(); } + + if persistent_fields.iter().any(|p| p.key == s) { + return syn::Error::new( + field.span(), + "Multiple persistent fields with the same key found", + ) + .to_compile_error() + .into(); + } + + persistent_fields.push(PersistentField { + key: s, + field: field_name.clone(), + }); + + processed_attribute = true; } _ => { return syn::Error::new( @@ -101,36 +130,111 @@ pub fn derive_params(input: TokenStream) -> TokenStream { } }; } else if attr.path.is_ident("nested") { + // This one is more complicated. Support an `array` attribute, an `id_prefix = + // "foo"` attribute, and a `group = "group name"` attribute. All are optional, and + // the first two are mutually exclusive. + let mut nested_array = false; + let mut nested_id_prefix: Option = None; + let mut nested_group: Option = None; match attr.parse_meta() { - Ok(syn::Meta::NameValue(syn::MetaNameValue { - lit: syn::Lit::Str(s), + Ok(syn::Meta::List(syn::MetaList { + nested: nested_attrs, .. })) => { - 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('/') { + if processed_attribute { return syn::Error::new( attr.span(), - "Group names may not contain slashes", + "Duplicate or incompatible attribute found", ) .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); } + + for nested_attr in nested_attrs { + match nested_attr { + syn::NestedMeta::Meta(syn::Meta::Path(p)) + if p.is_ident("array") => + { + nested_array = true; + } + syn::NestedMeta::Meta(syn::Meta::NameValue( + syn::MetaNameValue { + path, + lit: syn::Lit::Str(s), + .. + }, + )) if path.is_ident("id_prefix") => { + nested_id_prefix = Some(s.clone()); + } + syn::NestedMeta::Meta(syn::Meta::NameValue( + syn::MetaNameValue { + path, + lit: syn::Lit::Str(s), + .. + }, + )) if path.is_ident("group") => { + let group_name = s.value(); + if group_name.is_empty() { + return syn::Error::new( + attr.span(), + "Group names cannot be empty", + ) + .to_compile_error() + .into(); + } else if group_name.contains('/') { + return syn::Error::new( + attr.span(), + "Group names may not contain slashes", + ) + .to_compile_error() + .into(); + } else { + nested_group = Some(s.clone()); + } + } + _ => { + return syn::Error::new( + nested_attr.span(), + "Unknown attribute. See the Params trait documentation \ + for more information.", + ) + .to_compile_error() + .into() + } + } + } + + nested_params.push(match (nested_array, nested_id_prefix) { + (true, None) => NestedParams::Array { + field: field_name.clone(), + group: nested_group, + }, + (false, Some(id_prefix)) => NestedParams::Prefixed { + field: field_name.clone(), + id_prefix, + group: nested_group, + }, + (false, None) => NestedParams::Inline { + field: field_name.clone(), + group: nested_group, + }, + (true, Some(_)) => { + return syn::Error::new( + attr.span(), + "'array' cannot be used together with 'id_prefix'", + ) + .to_compile_error() + .into() + } + }); + + processed_attribute = true; } _ => { return syn::Error::new( attr.span(), - "The nested attribute should be a key-value pair with a string \ - argument: #[nested = \"Group Name\"]", + "The nested attribute should be a list in the following format: \ + #[nested([array | id_prefix = \"foo\"], [group = \"group name\"])]", ) .to_compile_error() .into() @@ -138,159 +242,269 @@ pub fn derive_params(input: TokenStream) -> TokenStream { }; } } - - match (id_attr, persist_attr) { - (Some(param_id), None) => { - if !param_ids.insert(param_id.clone()) { - return syn::Error::new( - field.span(), - "Multiple fields with the same parameter ID found", - ) - .to_compile_error() - .into(); - } - - // These are pairs of `(parameter_id, param_ptr, param_group)`. The specific - // parameter types know how to convert themselves into the correct ParamPtr variant. - // 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_mapping_self_tokens.push( - quote! { (String::from(#param_id), self.#field_name.as_ptr(), String::new()) }, - ); - } - (None, Some(persist_key)) => { - if !persist_ids.insert(persist_key.clone()) { - return syn::Error::new( - field.span(), - "Multiple persisted fields with the same ID found", - ) - .to_compile_error() - .into(); - } - - // We don't know anything about the field types, but because we can generate this - // function we get type erasure for free since we only need to worry about byte - // vectors - field_serialize_tokens.push(quote! { - match ::nih_plug::param::internals::PersistentField::map( - &self.#field_name, - ::nih_plug::param::internals::serialize_field, - ) { - Ok(data) => { - serialized.insert(String::from(#persist_key), data); - } - Err(err) => { - ::nih_plug::nih_debug_assert_failure!( - "Could not serialize '{}': {}", - #persist_key, - err - ) - } - }; - }); - field_deserialize_tokens.push(quote! { - #persist_key => { - match ::nih_plug::param::internals::deserialize_field(&data) { - Ok(deserialized) => { - ::nih_plug::param::internals::PersistentField::set( - &self.#field_name, - deserialized, - ); - } - Err(err) => { - ::nih_plug::nih_debug_assert_failure!( - "Could not deserialize '{}': {}", - #persist_key, - err - ) - } - }; - } - }); - } - (Some(_), Some(_)) => { - return syn::Error::new( - field.span(), - "The id and persist attributes are mutually exclusive", - ) - .to_compile_error() - .into(); - } - (None, None) => (), - } - - 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); - } } + // The next step is build the gathered information into tokens that can be spliced into a + // `Params` implementation + let param_map_tokens = { + // `param_map` adds the parameters from this struct, and then handles the nested tokens. + let param_mapping_self_tokens = params.into_iter().map( + |Param {field, id}| quote! { (String::from(#id), self.#field.as_ptr(), String::new()) }, + ); + + // How nested parameters are handled depends on the `NestedParams` variant. + // These are pairs of `(parameter_id, param_ptr, param_group)`. The specific + // parameter types know how to convert themselves into the correct ParamPtr variant. + // Top-level parameters have no group, and we'll prefix the group name specified in + // the `#[nested(...)]` attribute to fields coming from nested groups + let param_mapping_nested_tokens = nested_params.iter().map(|nested| match nested { + // TODO: No idea how to splice this as an `Option<&str>`, so this involves some + // copy-pasting + NestedParams::Inline { field, group: Some(group) } => quote! { + param_map.extend(self.#field.param_map().into_iter().map(|(param_id, param_ptr, nested_group_name)| { + if nested_group_name.is_empty() { + (param_id, param_ptr, String::from(#group)) + } else { + (param_id, param_ptr, format!("{}/{}", #group, nested_group_name)) + } + })); + }, + NestedParams::Inline { field, group: None } => quote! { + param_map.extend(self.#field.param_map()); + }, + NestedParams::Prefixed { + field, + id_prefix, + group: Some(group), + } => quote! { + param_map.extend(self.#field.param_map().into_iter().map(|(param_id, param_ptr, nested_group_name)| { + let param_id = format!("{}_{}", #id_prefix, param_id); + + if nested_group_name.is_empty() { + (param_id, param_ptr, String::from(#group)) + } else { + (param_id, param_ptr, format!("{}/{}", #group, nested_group_name)) + } + })); + }, + NestedParams::Prefixed { + field, + id_prefix, + group: None, + } => quote! { + param_map.extend(self.#field.param_map().into_iter().map(|(param_id, param_ptr, nested_group_name)| { + let param_id = format!("{}_{}", #id_prefix, param_id); + + (param_id, param_ptr, nested_group_name) + })); + }, + // We'll start at index 1 for display purposes. Both the group and the parameter ID get + // a suffix matching the array index. + NestedParams::Array { field, group: Some(group) } => quote! { + param_map.extend(self.#field.iter().enumerate().flat_map(|(idx, params)| { + let idx = idx + 1; + + params.param_map().into_iter().map(move |(param_id, param_ptr, nested_group_name)| { + let param_id = format!("{}_{}", param_id, idx); + let group = format!("{} {}", #group, idx); + + // Note that this is different from the other variants + if nested_group_name.is_empty() { + (param_id, param_ptr, group) + } else { + (param_id, param_ptr, format!("{}/{}", group, nested_group_name)) + } + }) + })); + }, + NestedParams::Array { field, group: None } => quote! { + param_map.extend(self.#field.iter().enumerate().flat_map(|(idx, params)| { + let idx = idx + 1; + + params.param_map().into_iter().map(move |(param_id, param_ptr, nested_group_name)| { + let param_id = format!("{}_{}", param_id, idx); + + (param_id, param_ptr, nested_group_name) + }) + })); + }, + }); + + quote! { + // This may not be in scope otherwise, used to call .as_ptr() + use ::nih_plug::param::Param; + + #[allow(unused_mut)] + let mut param_map = vec![#(#param_mapping_self_tokens),*]; + + #(#param_mapping_nested_tokens);* + + param_map + } + }; + + let (serialize_fields_tokens, deserialize_fields_tokens) = { + // Like with `param_map()`, we'll try to do the serialization for this struct and then + // recursively call the child parameter structs. We don't know anything about the actual + // field types, but because we can generate this function we can get type erasure for free + // since we only need to worry about byte vectors. + let (serialize_fields_self_tokens, deserialize_fields_match_self_tokens): (Vec<_>, Vec<_>) = + persistent_fields + .into_iter() + .map(|PersistentField { field, key }| { + ( + quote! { + match ::nih_plug::param::internals::PersistentField::map( + &self.#field, + ::nih_plug::param::internals::serialize_field, + ) { + Ok(data) => { + serialized.insert(String::from(#key), data); + } + Err(err) => { + ::nih_plug::nih_debug_assert_failure!( + "Could not serialize '{}': {}", + #key, + err + ) + } + }; + }, + quote! { + #key => { + match ::nih_plug::param::internals::deserialize_field(&data) { + Ok(deserialized) => { + ::nih_plug::param::internals::PersistentField::set( + &self.#field, + deserialized, + ); + } + Err(err) => { + ::nih_plug::nih_debug_assert_failure!( + "Could not deserialize '{}': {}", + #key, + err + ) + } + }; + } + }, + ) + }) + .unzip(); + + let (serialize_fields_nested_tokens, deserialize_fields_nested_tokens): (Vec<_>, Vec<_>) = + nested_params + .iter() + .map(|nested| match nested { + NestedParams::Inline { field, .. } | NestedParams::Prefixed { field, .. } => ( + // TODO: For some reason the macro won't parse correctly if you inline this + quote! { + let inlineme = self.#field.serialize_fields(); + serialized.extend(inlineme); + }, + quote! { self.#field.deserialize_fields(serialized) }, + ), + NestedParams::Array { field, .. } => ( + quote! { + for field in self.#field.iter() { + serialized.extend(field.serialize_fields()); + } + }, + quote! { + for field in self.#field.iter() { + field.deserialize_fields(serialized); + } + }, + ), + }) + .unzip(); + + let serialize_fields_tokens = quote! { + #[allow(unused_mut)] + let mut serialized = ::std::collections::BTreeMap::new(); + #(#serialize_fields_self_tokens);* + + #(#serialize_fields_nested_tokens);* + + serialized + }; + + let deserialize_fields_tokens = quote! { + for (field_name, data) in serialized { + match field_name.as_str() { + #(#deserialize_fields_match_self_tokens)* + _ => ::nih_plug::nih_debug_assert_failure!("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. + #(#deserialize_fields_nested_tokens);* + }; + + (serialize_fields_tokens, deserialize_fields_tokens) + }; + quote! { unsafe impl #impl_generics Params for #struct_name #ty_generics #where_clause { fn param_map(&self) -> Vec<(String, nih_plug::prelude::ParamPtr, String)> { - // This may not be in scope otherwise, used to call .as_ptr() - use ::nih_plug::param::Param; - - let mut param_map = vec![#(#param_mapping_self_tokens),*]; - - let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; - let nested_params_groups: &[String] = &[#(String::from(#nested_params_group_names)),*]; - for (nested_params, group_name) in - nested_params_fields.into_iter().zip(nested_params_groups) - { - let nested_param_map = nested_params.param_map(); - let prefixed_nested_param_map = - nested_param_map - .into_iter() - .map(|(param_id, param_ptr, nested_group_name)| { - ( - param_id, - param_ptr, - if nested_group_name.is_empty() { - group_name.to_string() - } else { - format!("{}/{}", group_name, nested_group_name) - } - ) - }); - - param_map.extend(prefixed_nested_param_map); - } - - param_map + #param_map_tokens } fn serialize_fields(&self) -> ::std::collections::BTreeMap { - let mut serialized = ::std::collections::BTreeMap::new(); - #(#field_serialize_tokens)* - - let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; - for nested_params in nested_params_fields { - serialized.extend(nested_params.serialize_fields()); - } - - serialized + #serialize_fields_tokens } fn deserialize_fields(&self, serialized: &::std::collections::BTreeMap) { - for (field_name, data) in serialized { - match field_name.as_str() { - #(#field_deserialize_tokens)* - _ => ::nih_plug::nih_debug_assert_failure!("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_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; - for nested_params in nested_params_fields { - nested_params.deserialize_fields(serialized); - } + #deserialize_fields_tokens } } } .into() } + +/// A parameter that should be added to the parameter map. +#[derive(Debug)] +struct Param { + /// The name of the parameter's field on the struct. + field: syn::Ident, + /// The parameter's unique ID. + id: syn::LitStr, +} + +/// A field containing data that must be stored in the plugin's state. +#[derive(Debug)] +struct PersistentField { + /// The name of the field on the struct. + field: syn::Ident, + /// The field's unique key. + key: syn::LitStr, +} + +/// A field containing another object whose parameters and persistent fields should be added to this +/// struct's. +#[derive(Debug)] +enum NestedParams { + /// The nested struct's parameters are taken as is. + Inline { + field: syn::Ident, + group: Option, + }, + /// The nested struct's parameters will get an ID prefix. The original parmaeter with ID `foo` + /// will become `{id_prefix}_foo`. + Prefixed { + field: syn::Ident, + id_prefix: syn::LitStr, + group: Option, + }, + /// This field is an array-like data structure containing nested parameter structs. The + /// parameter `foo` will get the new parameter ID `foo_{array_idx + 1}`, and if the group name + /// is set then the group will be `{group_name} {array_idx + 1}`. + Array { + field: syn::Ident, + group: Option, + }, +} diff --git a/plugins/examples/gain/src/lib.rs b/plugins/examples/gain/src/lib.rs index 710a8df1..32a35569 100644 --- a/plugins/examples/gain/src/lib.rs +++ b/plugins/examples/gain/src/lib.rs @@ -27,21 +27,25 @@ struct GainParams { /// 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"] + #[nested(group = "Subparameters")] pub sub_params: SubParams, + + /// Nested parameters also support some advanced functionality for reusing the same parameter + /// struct multiple times. + #[nested(array, group = "Array Parameters")] + pub array_params: [ArrayParams; 3], } #[derive(Params)] struct SubParams { #[id = "thing"] pub nested_parameter: FloatParam, - - #[nested = "Sub-Subparameters"] - pub sub_sub_params: SubSubParams, } #[derive(Params)] -struct SubSubParams { +struct ArrayParams { + /// This parameter's ID will get a `_1`, `_2`, and a `_3` suffix because of how it's used in + /// `array_params` above. #[id = "noope"] pub nope: FloatParam, } @@ -94,10 +98,14 @@ impl Default for GainParams { }, ) .with_value_to_string(formatters::v2s_f32_rounded(2)), - sub_sub_params: SubSubParams { - nope: FloatParam::new("Nope", 0.5, FloatRange::Linear { min: 1.0, max: 2.0 }), - }, }, + array_params: [1, 2, 3].map(|index| ArrayParams { + nope: FloatParam::new( + format!("Nope {index}"), + 0.5, + FloatRange::Linear { min: 1.0, max: 2.0 }, + ), + }), } } } diff --git a/plugins/spectral_compressor/src/compressor_bank.rs b/plugins/spectral_compressor/src/compressor_bank.rs index b6b71fe3..0fff1b3e 100644 --- a/plugins/spectral_compressor/src/compressor_bank.rs +++ b/plugins/spectral_compressor/src/compressor_bank.rs @@ -154,9 +154,9 @@ pub enum ThresholdMode { /// Contains the compressor parameters for both the upwards and downwards compressor banks. #[derive(Params)] pub struct CompressorBankParams { - #[nested = "upwards"] + #[nested(group = "upwards")] pub upwards: Arc, - #[nested = "downwards"] + #[nested(group = "downwards")] pub downwards: Arc, } diff --git a/plugins/spectral_compressor/src/lib.rs b/plugins/spectral_compressor/src/lib.rs index 27bd7c8d..e6a71ff2 100644 --- a/plugins/spectral_compressor/src/lib.rs +++ b/plugins/spectral_compressor/src/lib.rs @@ -83,14 +83,14 @@ pub struct SpectralCompressorParams { // can use the generic UIs /// Global parameters. These could just live in this struct but I wanted a separate generic UI /// just for these. - #[nested = "global"] + #[nested(group = "global")] pub global: Arc, /// Parameters controlling the compressor thresholds and curves. - #[nested = "threshold"] + #[nested(group = "threshold")] pub threshold: Arc, /// Parameters for the upwards and downwards compressors. - #[nested = "compressors"] + #[nested(group = "compressors")] pub compressors: compressor_bank::CompressorBankParams, } diff --git a/src/lib.rs b/src/lib.rs index 119e5d7e..d67eed78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,8 +71,8 @@ //! The string `"foobar"` here uniquely identifies the parameter, making it possible to reorder //! and rename parameters as long as this string stays constant. You can also store persistent //! non-parameter data and other parameter objects in a `Params` struct. Check out the trait's -//! documentation for more details, and also be sure to take a look at the [example -//! plugins](https://github.com/robbert-vdh/nih-plug/tree/master/plugins). +//! documentation for details on all supported features, and also be sure to take a look at the +//! [example plugins](https://github.com/robbert-vdh/nih-plug/tree/master/plugins). //! - After calling `.with_smoother()` during an integer or floating point parameter's creation, //! you can use `param.smoothed` to access smoothed values for that parameter. Be sure to check //! out the [`Smoother`][prelude::Smoother] API for more details. diff --git a/src/param/internals.rs b/src/param/internals.rs index a31ceace..69a1bde1 100644 --- a/src/param/internals.rs +++ b/src/param/internals.rs @@ -36,27 +36,52 @@ pub mod serialize_atomic_cell { /// Describes a struct containing parameters and other persistent fields. /// +/// # Deriving `Params` and `#[id = "stable"]` +/// /// 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 internally so you can safely move it around and -/// rename the field without breaking compatibility with old presets. +/// parameter fields by adding `#[derive(Params)]`. 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 internally so you can +/// safely move it around and rename the field without breaking compatibility with old presets. +/// +/// ## `#[persist = "key"]` /// /// 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 = "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 using nested parameter structs. This currently -/// has the following caveats: +/// ## `#[nested]`, `#[nested(group_name = "group name")]` +/// +/// Finally, the `Params` object may include parameters from other objects. Setting a group name is +/// optional, but some hosts can use this information to display the parameters in a tree structure. +/// Parameter IDs and persisting keys still need to be **unique** when using 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. +/// Take a look at the example gain example plugin to see how this is used. +/// +/// ## `#[nested(id_prefix = "foo", group_name = "Foo")]` +/// +/// Adding this attribute to a `Params` sub-object works similarly to the regular `#[nested]` +/// attribute, but it also adds an ID to all parameters from the nested object. If a parameter in +/// the nested nested object normally has parameter ID `bar`, the parameter's ID will now be renamed +/// to `foo_bar`. _This makes it possible to reuse same parameter struct with different names and +/// parameter indices._ +/// +/// This does **not** support persistent fields. +/// +/// ## `#[nested(array, group_name = "Foo")]` +/// +/// This can be applied to an array-like data structure and it works similar to a `nested` attribute +/// with an `id_name`, except that it will iterate over the array and create unique indices for all +/// nested parameters. If the nested parameters object has a parameter called `bar`, then that +/// parameter will belong to the group `Foo {array_index + 1}`, and it will have the renamed +/// parameter ID `bar_{array_index + 1}`. +/// +/// This does **not** support persistent fields. /// /// # Safety /// @@ -66,13 +91,13 @@ pub unsafe trait Params: 'static + Send + Sync { /// Create a mapping from unique parameter IDs to parameter pointers along with the name of the /// group/unit/module they are in, as a `(param_id, param_ptr, group)` triple. The order of the /// `Vec` determines the display order in the (host's) generic UI. The group name is either an - /// empty string for top level parameters, or a slash/delimited `"Group Name 1/Group Name 2"` if + /// empty string for top level parameters, or a slash/delimited `"group name 1/Group Name 2"` if /// this `Params` object contains nested child objects. All components of a group path must /// exist or you may encounter panics. The derive macro does this for every parameter field /// marked with `#[id = "stable"]`, and it also inlines all fields from nested child `Params` - /// structs marked with `#[nested = "Group Name"]` while prefixing that group name before the - /// parameter's original group name. Dereferencing the pointers stored in the values is only - /// valid as long as this object is valid. + /// structs marked with `#[nested(...)]` while prefixing that group name before the parameter's + /// original group name. Dereferencing the pointers stored in the values is only valid as long + /// as this object is valid. /// /// # Note ///