1
0
Fork 0

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.
This commit is contained in:
Robbert van der Helm 2022-10-13 01:20:56 +02:00
parent d57003a0e9
commit 727d88c4d7
9 changed files with 484 additions and 225 deletions

View file

@ -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 chronological order. If a new feature did not require any changes to existing
code then it will not be listed here. 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] ## [2022-09-22]
- `nih_plug_egui` has been updated from egui 0.17 to egui 0.19. - `nih_plug_egui` has been updated from egui 0.17 to egui 0.19.

12
Cargo.lock generated
View file

@ -3278,9 +3278,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.39" version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -3319,9 +3319,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -4016,9 +4016,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.96" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -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 that can be serialized with [Serde](https://serde.rs/) to your plugin's
`Params` object and annotating them with `#[persist = "key"]`. `Params` object and annotating them with `#[persist = "key"]`.
- Group your parameters into logical groups by nesting `Params` objects using - 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` - When needed, you can also provide your own implementation for the `Params`
trait to enable dynamically generated parameters and arrays of if mostly trait to enable compile time generated parameters and other bespoke
identical parameter objects. functionality.
- Stateful. Behaves mostly like JUCE, just without all of the boilerplate. - 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 - 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. with utilities and adapters to help with common access patterns.

View file

@ -1,6 +1,5 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use std::collections::HashSet;
use syn::spanned::Spanned; use syn::spanned::Spanned;
pub fn derive_params(input: TokenStream) -> TokenStream { 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 // 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 // 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 // JSON. The `nested` fields should also implement the `Params` trait and their fields will be
// inherited and added to this field's lists. // inherited and added to this field's lists. We'll also enforce that there are no duplicate
let mut param_mapping_self_tokens = Vec::new(); // keys at compile time.
let mut field_serialize_tokens = Vec::new(); // TODO: This duplication check doesn't work for nested fields since we don't know anything
let mut field_deserialize_tokens = Vec::new(); // about the fields on the nested structs
let mut nested_params_field_idents: Vec<syn::Ident> = Vec::new(); let mut params: Vec<Param> = Vec::new();
let mut nested_params_group_names: Vec<String> = Vec::new(); let mut persistent_fields: Vec<PersistentField> = Vec::new();
let mut nested_params: Vec<NestedParams> = 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 { for field in fields.named {
let field_name = match &field.ident { let field_name = match &field.ident {
Some(ident) => ident, Some(ident) => ident,
_ => continue, _ => continue,
}; };
// These two attributes are mutually exclusive // All attributes are mutually exclusive. If we encounter multiple or duplicate attributes,
let mut id_attr: Option<String> = None; // then we'll error out.
let mut persist_attr: Option<String> = None; let mut processed_attribute = false;
// And the `#[nested = "..."]` attribute contains a group name we should use
let mut nested_attr: Option<String> = None;
for attr in &field.attrs { for attr in &field.attrs {
if attr.path.is_ident("id") { if attr.path.is_ident("id") {
match attr.parse_meta() { match attr.parse_meta() {
@ -58,13 +50,33 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
lit: syn::Lit::Str(s), lit: syn::Lit::Str(s),
.. ..
})) => { })) => {
if id_attr.is_none() { if processed_attribute {
id_attr = Some(s.value()); return syn::Error::new(
} else { attr.span(),
return syn::Error::new(attr.span(), "Duplicate id attribute") "Duplicate or incompatible attribute found",
)
.to_compile_error() .to_compile_error()
.into(); .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( return syn::Error::new(
@ -82,13 +94,30 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
lit: syn::Lit::Str(s), lit: syn::Lit::Str(s),
.. ..
})) => { })) => {
if persist_attr.is_none() { if processed_attribute {
persist_attr = Some(s.value()); return syn::Error::new(
} else { attr.span(),
return syn::Error::new(attr.span(), "Duplicate persist attribute") "Duplicate or incompatible attribute found",
)
.to_compile_error() .to_compile_error()
.into(); .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( return syn::Error::new(
@ -101,36 +130,111 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
} }
}; };
} else if attr.path.is_ident("nested") { } 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<syn::LitStr> = None;
let mut nested_group: Option<syn::LitStr> = None;
match attr.parse_meta() { match attr.parse_meta() {
Ok(syn::Meta::NameValue(syn::MetaNameValue { Ok(syn::Meta::List(syn::MetaList {
lit: syn::Lit::Str(s), nested: nested_attrs,
.. ..
})) => { })) => {
let s = s.value(); if processed_attribute {
if s.is_empty() { return syn::Error::new(
return syn::Error::new(attr.span(), "Group names cannot be empty") attr.span(),
"Duplicate or incompatible attribute found",
)
.to_compile_error() .to_compile_error()
.into(); .into();
} else if s.contains('/') { }
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( return syn::Error::new(
attr.span(), attr.span(),
"Group names may not contain slashes", "Group names may not contain slashes",
) )
.to_compile_error() .to_compile_error()
.into(); .into();
} else if nested_attr.is_some() {
return syn::Error::new(attr.span(), "Duplicate nested attribute")
.to_compile_error()
.into();
} else { } else {
nested_attr = Some(s); 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( return syn::Error::new(
attr.span(), attr.span(),
"The nested attribute should be a key-value pair with a string \ "'array' cannot be used together with 'id_prefix'",
argument: #[nested = \"Group Name\"]", )
.to_compile_error()
.into()
}
});
processed_attribute = true;
}
_ => {
return syn::Error::new(
attr.span(),
"The nested attribute should be a list in the following format: \
#[nested([array | id_prefix = \"foo\"], [group = \"group name\"])]",
) )
.to_compile_error() .to_compile_error()
.into() .into()
@ -138,145 +242,198 @@ 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();
} }
// 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 // These are pairs of `(parameter_id, param_ptr, param_group)`. The specific
// parameter types know how to convert themselves into the correct ParamPtr variant. // 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 // Top-level parameters have no group, and we'll prefix the group name specified in
// the `#[nested = "..."]` attribute to fields coming from nested groups // the `#[nested(...)]` attribute to fields coming from nested groups
param_mapping_self_tokens.push( let param_mapping_nested_tokens = nested_params.iter().map(|nested| match nested {
quote! { (String::from(#param_id), self.#field_name.as_ptr(), String::new()) }, // TODO: No idea how to splice this as an `Option<&str>`, so this involves some
); // copy-pasting
} NestedParams::Inline { field, group: Some(group) } => quote! {
(None, Some(persist_key)) => { param_map.extend(self.#field.param_map().into_iter().map(|(param_id, param_ptr, nested_group_name)| {
if !persist_ids.insert(persist_key.clone()) { if nested_group_name.is_empty() {
return syn::Error::new( (param_id, param_ptr, String::from(#group))
field.span(), } else {
"Multiple persisted fields with the same ID found", (param_id, param_ptr, format!("{}/{}", #group, nested_group_name))
)
.to_compile_error()
.into();
} }
}));
},
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);
// We don't know anything about the field types, but because we can generate this if nested_group_name.is_empty() {
// function we get type erasure for free since we only need to worry about byte (param_id, param_ptr, String::from(#group))
// vectors } else {
field_serialize_tokens.push(quote! { (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( match ::nih_plug::param::internals::PersistentField::map(
&self.#field_name, &self.#field,
::nih_plug::param::internals::serialize_field, ::nih_plug::param::internals::serialize_field,
) { ) {
Ok(data) => { Ok(data) => {
serialized.insert(String::from(#persist_key), data); serialized.insert(String::from(#key), data);
} }
Err(err) => { Err(err) => {
::nih_plug::nih_debug_assert_failure!( ::nih_plug::nih_debug_assert_failure!(
"Could not serialize '{}': {}", "Could not serialize '{}': {}",
#persist_key, #key,
err err
) )
} }
}; };
}); },
field_deserialize_tokens.push(quote! { quote! {
#persist_key => { #key => {
match ::nih_plug::param::internals::deserialize_field(&data) { match ::nih_plug::param::internals::deserialize_field(&data) {
Ok(deserialized) => { Ok(deserialized) => {
::nih_plug::param::internals::PersistentField::set( ::nih_plug::param::internals::PersistentField::set(
&self.#field_name, &self.#field,
deserialized, deserialized,
); );
} }
Err(err) => { Err(err) => {
::nih_plug::nih_debug_assert_failure!( ::nih_plug::nih_debug_assert_failure!(
"Could not deserialize '{}': {}", "Could not deserialize '{}': {}",
#persist_key, #key,
err err
) )
} }
}; };
} }
}); },
}
(Some(_), Some(_)) => {
return syn::Error::new(
field.span(),
"The id and persist attributes are mutually exclusive",
) )
.to_compile_error() })
.into(); .unzip();
}
(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);
}
}
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! { quote! {
unsafe impl #impl_generics Params for #struct_name #ty_generics #where_clause { let inlineme = self.#field.serialize_fields();
fn param_map(&self) -> Vec<(String, nih_plug::prelude::ParamPtr, String)> { serialized.extend(inlineme);
// This may not be in scope otherwise, used to call .as_ptr() },
use ::nih_plug::param::Param; quote! { self.#field.deserialize_fields(serialized) },
),
let mut param_map = vec![#(#param_mapping_self_tokens),*]; NestedParams::Array { field, .. } => (
quote! {
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; for field in self.#field.iter() {
let nested_params_groups: &[String] = &[#(String::from(#nested_params_group_names)),*]; serialized.extend(field.serialize_fields());
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)
} }
) },
}); quote! {
for field in self.#field.iter() {
param_map.extend(prefixed_nested_param_map); field.deserialize_fields(serialized);
} }
},
),
})
.unzip();
param_map let serialize_fields_tokens = quote! {
} #[allow(unused_mut)]
fn serialize_fields(&self) -> ::std::collections::BTreeMap<String, String> {
let mut serialized = ::std::collections::BTreeMap::new(); let mut serialized = ::std::collections::BTreeMap::new();
#(#field_serialize_tokens)* #(#serialize_fields_self_tokens);*
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; #(#serialize_fields_nested_tokens);*
for nested_params in nested_params_fields {
serialized.extend(nested_params.serialize_fields());
}
serialized serialized
} };
fn deserialize_fields(&self, serialized: &::std::collections::BTreeMap<String, String>) { let deserialize_fields_tokens = quote! {
for (field_name, data) in serialized { for (field_name, data) in serialized {
match field_name.as_str() { match field_name.as_str() {
#(#field_deserialize_tokens)* #(#deserialize_fields_match_self_tokens)*
_ => ::nih_plug::nih_debug_assert_failure!("Unknown serialized field name: {} (this may not be accurate)", field_name), _ => ::nih_plug::nih_debug_assert_failure!("Unknown serialized field name: {} (this may not be accurate)", field_name),
} }
} }
@ -285,12 +442,69 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
// parameter structs. An easy fix would be to use // parameter structs. An easy fix would be to use
// https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.drain_filter // https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.drain_filter
// once that gets stabilized. // once that gets stabilized.
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*]; #(#deserialize_fields_nested_tokens);*
for nested_params in nested_params_fields { };
nested_params.deserialize_fields(serialized);
(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)> {
#param_map_tokens
} }
fn serialize_fields(&self) -> ::std::collections::BTreeMap<String, String> {
#serialize_fields_tokens
}
fn deserialize_fields(&self, serialized: &::std::collections::BTreeMap<String, String>) {
#deserialize_fields_tokens
} }
} }
} }
.into() .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<syn::LitStr>,
},
/// 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<syn::LitStr>,
},
/// 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<syn::LitStr>,
},
}

View file

@ -27,21 +27,25 @@ struct GainParams {
/// You can also nest parameter structs. These will appear as a separate nested group if your /// You can also nest parameter structs. These will appear as a separate nested group if your
/// DAW displays parameters in a tree structure. /// DAW displays parameters in a tree structure.
#[nested = "Subparameters"] #[nested(group = "Subparameters")]
pub sub_params: SubParams, 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)] #[derive(Params)]
struct SubParams { struct SubParams {
#[id = "thing"] #[id = "thing"]
pub nested_parameter: FloatParam, pub nested_parameter: FloatParam,
#[nested = "Sub-Subparameters"]
pub sub_sub_params: SubSubParams,
} }
#[derive(Params)] #[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"] #[id = "noope"]
pub nope: FloatParam, pub nope: FloatParam,
} }
@ -94,10 +98,14 @@ impl Default for GainParams {
}, },
) )
.with_value_to_string(formatters::v2s_f32_rounded(2)), .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 },
),
}),
} }
} }
} }

View file

@ -154,9 +154,9 @@ pub enum ThresholdMode {
/// Contains the compressor parameters for both the upwards and downwards compressor banks. /// Contains the compressor parameters for both the upwards and downwards compressor banks.
#[derive(Params)] #[derive(Params)]
pub struct CompressorBankParams { pub struct CompressorBankParams {
#[nested = "upwards"] #[nested(group = "upwards")]
pub upwards: Arc<CompressorParams>, pub upwards: Arc<CompressorParams>,
#[nested = "downwards"] #[nested(group = "downwards")]
pub downwards: Arc<CompressorParams>, pub downwards: Arc<CompressorParams>,
} }

View file

@ -83,14 +83,14 @@ pub struct SpectralCompressorParams {
// can use the generic UIs // can use the generic UIs
/// Global parameters. These could just live in this struct but I wanted a separate generic UI /// Global parameters. These could just live in this struct but I wanted a separate generic UI
/// just for these. /// just for these.
#[nested = "global"] #[nested(group = "global")]
pub global: Arc<GlobalParams>, pub global: Arc<GlobalParams>,
/// Parameters controlling the compressor thresholds and curves. /// Parameters controlling the compressor thresholds and curves.
#[nested = "threshold"] #[nested(group = "threshold")]
pub threshold: Arc<compressor_bank::ThresholdParams>, pub threshold: Arc<compressor_bank::ThresholdParams>,
/// Parameters for the upwards and downwards compressors. /// Parameters for the upwards and downwards compressors.
#[nested = "compressors"] #[nested(group = "compressors")]
pub compressors: compressor_bank::CompressorBankParams, pub compressors: compressor_bank::CompressorBankParams,
} }

View file

@ -71,8 +71,8 @@
//! The string `"foobar"` here uniquely identifies the parameter, making it possible to reorder //! 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 //! 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 //! 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 //! documentation for details on all supported features, and also be sure to take a look at the
//! plugins](https://github.com/robbert-vdh/nih-plug/tree/master/plugins). //! [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, //! - 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 //! 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. //! out the [`Smoother`][prelude::Smoother] API for more details.

View file

@ -36,27 +36,52 @@ pub mod serialize_atomic_cell {
/// Describes a struct containing parameters and other persistent fields. /// 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 /// 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 /// parameter fields by adding `#[derive(Params)]`. When deriving this trait, any of those parameter
/// = "stable"]` attribute, where `stable` is an up to 6 character long string (to avoid collisions) /// fields should have the `#[id = "stable"]` attribute, where `stable` is an up to 6 character long
/// that will be used to identify the parameter internally so you can safely move it around and /// string (to avoid collisions) that will be used to identify the parameter internally so you can
/// rename the field without breaking compatibility with old presets. /// 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 /// 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"]` /// preset data. These fields should be [`PersistentField`]s annotated with the `#[persist = "key"]`
/// attribute containing types that can be serialized and deserialized with /// attribute containing types that can be serialized and deserialized with
/// [Serde](https://serde.rs/). /// [Serde](https://serde.rs/).
/// ///
/// And finally when deriving this trait, it is also possible to inherit the parameters from other /// ## `#[nested]`, `#[nested(group_name = "group name")]`
/// `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 /// Finally, the `Params` object may include parameters from other objects. Setting a group name is
/// persisting keys still need to be **unique** when using nested parameter structs. This currently /// optional, but some hosts can use this information to display the parameters in a tree structure.
/// has the following caveats: /// 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. /// - 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. /// - 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 /// # 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 /// 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 /// 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 /// `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 /// 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 /// 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` /// 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 /// structs marked with `#[nested(...)]` while prefixing that group name before the parameter's
/// parameter's original group name. Dereferencing the pointers stored in the values is only /// original group name. Dereferencing the pointers stored in the values is only valid as long
/// valid as long as this object is valid. /// as this object is valid.
/// ///
/// # Note /// # Note
/// ///