1
0
Fork 0

Add parameter groups with #[nested = "Group Name"]

This commit is contained in:
Robbert van der Helm 2022-03-16 17:04:38 +01:00
parent c3f717480e
commit 581e5911fc
5 changed files with 113 additions and 38 deletions

View file

@ -62,7 +62,7 @@ for download links.
options. That way you can use regular Rust pattern matching when working options. That way you can use regular Rust pattern matching when working
with these values without having to do any conversions yourself. with these values without having to do any conversions yourself.
- Store additional non-parameter state for your plugin by adding any field - 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"]`. `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 Name"]`attribute.

View file

@ -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 // 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.
let mut param_mapping_insert_tokens = Vec::new(); 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 param_id_string_tokens = Vec::new();
let mut field_serialize_tokens = Vec::new(); let mut field_serialize_tokens = Vec::new();
let mut field_deserialize_tokens = Vec::new(); let mut field_deserialize_tokens = Vec::new();
let mut nested_fields_idents = Vec::new(); let mut nested_params_field_idents: Vec<syn::Ident> = Vec::new();
let mut nested_params_group_names: Vec<String> = Vec::new();
// We'll also enforce that there are no duplicate keys at compile time // 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 // 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 // These two attributes are mutually exclusive
let mut id_attr: Option<String> = None; let mut id_attr: Option<String> = None;
let mut persist_attr: Option<String> = None; let mut persist_attr: Option<String> = None;
let mut nested = 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() {
@ -98,22 +101,34 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
}; };
} else if attr.path.is_ident("nested") { } else if attr.path.is_ident("nested") {
match attr.parse_meta() { match attr.parse_meta() {
Ok(syn::Meta::Path(_)) => { Ok(syn::Meta::NameValue(syn::MetaNameValue {
if !nested { lit: syn::Lit::Str(s),
nested = true; ..
} else { })) => {
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") return syn::Error::new(attr.span(), "Duplicate nested attribute")
.to_compile_error() .to_compile_error()
.into(); .into();
} else {
nested_attr = Some(s);
} }
} }
_ => { _ => {
return syn::Error::new( return syn::Error::new(
attr.span(), 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() .to_compile_error()
.into(); .into()
} }
}; };
} }
@ -134,6 +149,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
// variant // variant
param_mapping_insert_tokens param_mapping_insert_tokens
.push(quote! { param_map.insert(#param_id, self.#field_name.as_ptr()); }); .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, }); param_id_string_tokens.push(quote! { #param_id, });
} }
(None, Some(stable_name)) => { (None, Some(stable_name)) => {
@ -193,8 +212,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
(None, None) => (), (None, None) => (),
} }
if nested { if let Some(nested_group_name) = nested_attr {
nested_fields_idents.push(field_name.clone()); 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( fn param_map(
self: std::pin::Pin<&Self>, self: std::pin::Pin<&Self>,
) -> std::collections::HashMap<&'static str, nih_plug::param::internals::ParamPtr> { ) -> 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; use ::nih_plug::param::Param;
let mut param_map = std::collections::HashMap::new(); let mut param_map = std::collections::HashMap::new();
#(#param_mapping_insert_tokens)* #(#param_mapping_insert_tokens)*
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
for nested_params in nested_fields { for nested_params in nested_params_fields {
unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) }; unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) };
} }
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> { fn param_ids(self: std::pin::Pin<&Self>) -> Vec<&'static str> {
let mut ids = vec![#(#param_id_string_tokens)*]; let mut ids = vec![#(#param_id_string_tokens)*];
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
for nested_params in nested_fields { for nested_params in nested_params_fields {
unsafe { ids.append(&mut Pin::new_unchecked(*nested_params).param_ids()) }; 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(); let mut serialized = ::std::collections::HashMap::new();
#(#field_serialize_tokens)* #(#field_serialize_tokens)*
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
for nested_params in nested_fields { for nested_params in nested_params_fields {
unsafe { serialized.extend(Pin::new_unchecked(*nested_params).serialize_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 // 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_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*]; let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
for nested_params in nested_fields { for nested_params in nested_params_fields {
unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) }; unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) };
} }
} }

View file

@ -21,9 +21,9 @@ struct GainParams {
#[persist = "industry_secrets"] #[persist = "industry_secrets"]
pub random_data: RwLock<Vec<f32>>, pub random_data: RwLock<Vec<f32>>,
/// You can also nest parameter structs. This is only for your own organization: they will still /// You can also nest parameter structs. These will appear as a separate nested group if your
/// appear as a flat list to the host. /// DAW displays parameters in a tree structure.
#[nested] #[nested = "Subparameters"]
pub sub_params: SubParams, pub sub_params: SubParams,
} }
@ -31,6 +31,15 @@ struct GainParams {
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)]
struct SubSubParams {
#[id = "noope"]
pub nope: FloatParam,
} }
impl Default for Gain { impl Default for Gain {
@ -88,6 +97,9 @@ impl Default for GainParams {
}, },
) )
.with_value_to_string(formatters::f32_rounded(2)), .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 }),
},
}, },
} }
} }

View file

@ -11,21 +11,22 @@ pub use serde_json::from_str as deserialize_field;
/// Re-export for use in the [`Params`] proc-macro. /// Re-export for use in the [`Params`] proc-macro.
pub use serde_json::to_string as serialize_field; pub use serde_json::to_string as serialize_field;
/// Describes a struct containing parameters and other persistent fields. The idea is that we can /// Describes a struct containing parameters and other persistent fields.
/// 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.
/// ///
/// The other persistent parameters should be [`PersistentField`]s containing types that can be /// This trait can be derived on a struct containing [`FloatParam`][super::FloatParam] and other
/// serialized and deserialized with Serde. When deriving this trait, any of those fields should be /// parameter fields. When deriving this trait, any of those parameter fields should have the `#[id
/// marked with `#[persist = "key"]`. /// = "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 /// 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 /// persisting keys still need to be **unique** when usting nested parameter structs. This currently
/// has the following caveats: /// has the following caveats:
/// ///
@ -44,8 +45,13 @@ pub trait Params {
/// is only valid as long as this pinned object is valid. /// is only valid as long as this pinned object is valid.
fn param_map(self: Pin<&Self>) -> HashMap<&'static str, ParamPtr>; 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 /// Contains group names for each parameter in [`param_map()`][Self::param_map()]. This is
/// the parameters. /// 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 /// 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 /// that's become a bit more difficult since Rust does not have a convenient way to

View file

@ -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 /// Basic functionality that needs to be implemented by a plugin. The wrappers will use this to
/// expose the plugin in a particular plugin format. /// 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 /// 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: /// implemented. Notable missing features include:
/// ///
/// - Sidechain inputs /// - Sidechain inputs
/// - Multiple output busses /// - Multiple output busses
/// - Special handling for offline processing /// - 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 /// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to
/// interact with it yet /// interact with it yet
/// - Outputting parameter changes from the plugin /// - Outputting parameter changes from the plugin