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
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.

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
// 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<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
// 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<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 {
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) };
}
}

View file

@ -21,9 +21,9 @@ struct GainParams {
#[persist = "industry_secrets"]
pub random_data: RwLock<Vec<f32>>,
/// 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 }),
},
},
}
}

View file

@ -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

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
/// 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