From 63a7aadb7500db95b2c821e1e3bcbf285d9377a9 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 16 Mar 2022 20:09:38 +0100 Subject: [PATCH] Add an abstraction around VST3's unit shenanigans And the ability to convert group paths to them. --- src/wrapper/vst3.rs | 1 + src/wrapper/vst3/param_units.rs | 139 ++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/wrapper/vst3/param_units.rs diff --git a/src/wrapper/vst3.rs b/src/wrapper/vst3.rs index 1dc9eff0..ffa176fd 100644 --- a/src/wrapper/vst3.rs +++ b/src/wrapper/vst3.rs @@ -4,6 +4,7 @@ mod util; mod context; mod factory; mod inner; +mod param_units; mod view; mod wrapper; diff --git a/src/wrapper/vst3/param_units.rs b/src/wrapper/vst3/param_units.rs new file mode 100644 index 00000000..955a9581 --- /dev/null +++ b/src/wrapper/vst3/param_units.rs @@ -0,0 +1,139 @@ +//! Parameter hierarchies in VST3 requires you to define units, which are linearly indexed logical +//! units that have a name, a parent, and then a whole bunch of other data like note numbers and +//! MIDI program state. We'll need to implement some of that to conver our list of slash-separated +//! parmaeter group paths to units. +//! +//! + +use std::collections::{HashMap, HashSet}; + +use vst3_sys::vst::kRootUnitId; + +/// Transforms a map containing parameter hashes and slash-separated paths to an array of VST3 units +/// and a mapping for each parameter hash to a unit (or to `None` if they belong to the root unit). +/// This is conceptually similar to a prefix tree/trie, but since we don't need any of the lookup +/// properties of those data structures we can brute force it by converting all paths to a set, +/// converting the rightmost component of each unique path to a unit, and then assigning the parent +/// units accordingly. +/// +/// +pub struct ParamUnits { + /// The unique units, with flat indices. + units: Vec, + /// The index of the unit a parameter belongs to, or `None` if it belongs to the root unit. + /// + /// NOTE: The returned unit ID is actually one higher than this index because VST3 uses 0 as a + /// no-unit value + unit_id_by_hash: HashMap, +} + +/// A VST3 'unit'. Repurposed for a bunch of things, but we only care about parameter hierarchies. +/// +/// +pub struct ParamUnit { + /// The name of the unit, without any of the proceeding components. + pub name: String, + /// The ID of the parent unit, or `kRootUnitId`/0 if the parent would be the root node. Because + /// 0 is reserved, these IDs are one higher than the actual index in `ParamUnits::units`. + pub parent_id: i32, +} + +impl ParamUnits { + /// Construct a [`ParamUnits`] object from an iterator over pairs of `(param_hash, param_group)` + /// where `param_hash` is the integer hash used to represent a parameter in the VST3 wrapper and + /// `param_group` is a slash delimited path. + /// + /// Returns an error if the iterator contains nested groups without a matching parent. + pub fn from_param_groups<'a, I>(groups: I) -> Result + where + I: Iterator + Clone, + { + // First we'll build a unit for each unique parameter + let unique_group_names: HashSet<&str> = groups + .clone() + .into_iter() + .map(|(_, group_name)| group_name) + .collect(); + let mut groups_units: Vec<(&str, ParamUnit)> = unique_group_names + .into_iter() + .map(|group_name| { + ( + group_name, + ParamUnit { + name: match group_name.rfind('/') { + Some(sep_pos) => group_name[sep_pos + 1..].to_string(), + None => group_name.to_string(), + }, + parent_id: kRootUnitId, + }, + ) + }) + .collect(); + + // Then we need to assign the correct parent IDs. We'll also sort the units so the order is + // stable. + groups_units.sort_by(|(group_name_l, _), (group_name_r, _)| group_name_l.cmp(group_name_r)); + + // We need to be able to map group names to unit IDs + // NOTE: Now it starts getting complicated because VST3 units are one indexed, so the unit + // IDs are one higher than the index in our vector + let vst3_unit_id_by_group_name: HashMap<&str, i32> = groups_units + .iter() + .enumerate() + // Note the +1 here + .map(|(unit_id, (group_name, _))| (*group_name, unit_id as i32 + 1)) + .collect(); + + for unit_id in 0..groups_units.len() { + // We'll do an index-based loop with some clones because here it doesn't matter much for + // performance anyways and otherwise it's a bit difficult to keep the borrow checker + // happy ehre + let group_name = groups_units[0].0; + + // If the group name does not contain any slashes then the unit's parent should stay at + // the root unit + if let Some(sep_pos) = group_name.rfind('/') { + let parent_group_name = &group_name[..sep_pos]; + let parent_unit_id = *vst3_unit_id_by_group_name + .get(parent_group_name) + .ok_or("Missing parent group")?; + groups_units[unit_id].1.parent_id = parent_unit_id; + } + } + + let unit_id_by_hash: HashMap = groups + .map(|(param_hash, group_name)| { + if group_name.is_empty() { + (param_hash, kRootUnitId) + } else { + (param_hash, vst3_unit_id_by_group_name[group_name]) + } + }) + .collect(); + let units: Vec = groups_units.into_iter().map(|(_, unit)| unit).collect(); + + Ok(Self { + units, + unit_id_by_hash, + }) + } + + /// Get the number of units. + pub fn len(&self) -> usize { + self.units.len() + } + + /// Get the unit ID and the unit's information for a unit with the given 0-indexed index (to + /// make everything more confusing). + pub fn info(&self, index: usize) -> Option<(i32, &ParamUnit)> { + let info = self.units.get(index)?; + + // NOTE: The VST3 unit indices are off by one because 0 is reserved fro the root unit + Some((index as i32 + 1, info)) + } + + /// Get the ID of the unit the paramter belongs to or. `kRootUnitId`/0 indicates the root unit. + pub fn get_vst3_unit_id(&self, param_hash: u32) -> Option { + self.unit_id_by_hash.get(¶m_hash).copied() + } +}