140 lines
6 KiB
Rust
140 lines
6 KiB
Rust
|
//! 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.
|
||
|
//!
|
||
|
//! <https://steinbergmedia.github.io/vst3_doc/vstinterfaces/classSteinberg_1_1Vst_1_1IUnitInfo.html>
|
||
|
|
||
|
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.
|
||
|
///
|
||
|
/// <https://steinbergmedia.github.io/vst3_doc/vstinterfaces/classSteinberg_1_1Vst_1_1IUnitInfo.html>
|
||
|
pub struct ParamUnits {
|
||
|
/// The unique units, with flat indices.
|
||
|
units: Vec<ParamUnit>,
|
||
|
/// 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<u32, i32>,
|
||
|
}
|
||
|
|
||
|
/// A VST3 'unit'. Repurposed for a bunch of things, but we only care about parameter hierarchies.
|
||
|
///
|
||
|
/// <https://steinbergmedia.github.io/vst3_doc/vstinterfaces/structSteinberg_1_1Vst_1_1UnitInfo.html>
|
||
|
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<Self, &'static str>
|
||
|
where
|
||
|
I: Iterator<Item = (u32, &'a str)> + 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<u32, i32> = 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<ParamUnit> = 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<i32> {
|
||
|
self.unit_id_by_hash.get(¶m_hash).copied()
|
||
|
}
|
||
|
}
|