beamer_core/params.rs
1//! Parameter system for audio plugins.
2//!
3//! This module provides traits and types for declaring and managing plugin parameters
4//! in a format-agnostic way. Parameters use normalized values (0.0 to 1.0) for
5//! host communication, with conversion to/from plain values handled by the plugin.
6//!
7//! # Thread Safety
8//!
9//! The [`Parameters`] trait requires `Send + Sync` because parameters may be
10//! accessed from multiple threads:
11//! - Audio thread: reads parameter values during processing
12//! - UI thread: displays and modifies parameter values
13//! - Host thread: automation playback and recording
14//!
15//! Use atomic types (e.g., `AtomicU64` with `to_bits`/`from_bits`) for lock-free access.
16
17use crate::types::{ParamId, ParamValue};
18
19// =============================================================================
20// VST3 Unit System (Parameter Grouping)
21// =============================================================================
22
23/// VST3 Unit ID type.
24///
25/// Units are used to organize parameters into hierarchical groups in the DAW UI.
26/// Each unit has a unique ID and can have a parent unit.
27pub type UnitId = i32;
28
29/// Root unit ID constant (parameters with no group).
30///
31/// The root unit (ID 0) always exists and contains ungrouped parameters.
32pub const ROOT_UNIT_ID: UnitId = 0;
33
34/// Information about a parameter group (VST3 Unit).
35///
36/// Units form a tree structure via parent_id references:
37/// - Root unit (id=0, parent=0) always exists implicitly
38/// - Top-level groups have parent_id=0
39/// - Nested groups reference their parent's unit_id
40#[derive(Debug, Clone)]
41pub struct UnitInfo {
42 /// Unique unit identifier.
43 pub id: UnitId,
44 /// Display name shown in DAW (e.g., "Filter", "Amp Envelope").
45 pub name: &'static str,
46 /// Parent unit ID (ROOT_UNIT_ID for top-level groups).
47 pub parent_id: UnitId,
48}
49
50impl UnitInfo {
51 /// Create a new unit info.
52 pub const fn new(id: UnitId, name: &'static str, parent_id: UnitId) -> Self {
53 Self { id, name, parent_id }
54 }
55
56 /// Create the root unit.
57 pub const fn root() -> Self {
58 Self {
59 id: ROOT_UNIT_ID,
60 name: "",
61 parent_id: ROOT_UNIT_ID,
62 }
63 }
64}
65
66/// Trait for querying VST3 unit hierarchy.
67///
68/// Implemented automatically by `#[derive(Params)]` when nested groups are present.
69/// Provides information about parameter groups for DAW display.
70///
71/// Unit IDs are assigned dynamically at runtime to support deeply nested groups
72/// where the same nested struct type can appear in multiple contexts with
73/// different parent units.
74pub trait Units {
75 /// Total number of units (including root).
76 ///
77 /// Returns 1 if there are no groups (just the root unit).
78 /// For nested groups, this returns 1 + total nested groups (including deeply nested).
79 fn unit_count(&self) -> usize {
80 1 // Default: only root unit
81 }
82
83 /// Get unit info by index.
84 ///
85 /// Index 0 always returns the root unit.
86 /// Returns `UnitInfo` by value to support dynamic construction for nested groups.
87 fn unit_info(&self, index: usize) -> Option<UnitInfo> {
88 if index == 0 {
89 Some(UnitInfo::root())
90 } else {
91 None
92 }
93 }
94
95 /// Find unit ID by name (linear search).
96 fn find_unit_by_name(&self, name: &str) -> Option<UnitId> {
97 for i in 0..self.unit_count() {
98 if let Some(info) = self.unit_info(i) {
99 if info.name == name {
100 return Some(info.id);
101 }
102 }
103 }
104 None
105 }
106}
107
108/// Flags controlling parameter behavior.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct ParamFlags {
111 /// Parameter can be automated by the host.
112 pub can_automate: bool,
113 /// Parameter is read-only (display only).
114 pub is_readonly: bool,
115 /// Parameter is the bypass switch.
116 pub is_bypass: bool,
117 /// Parameter should be displayed as a dropdown list (for enums).
118 /// When true, host shows text labels from getParamStringByValue().
119 pub is_list: bool,
120}
121
122impl Default for ParamFlags {
123 fn default() -> Self {
124 Self {
125 can_automate: true,
126 is_readonly: false,
127 is_bypass: false,
128 is_list: false,
129 }
130 }
131}
132
133/// Metadata describing a single parameter.
134#[derive(Debug, Clone)]
135pub struct ParamInfo {
136 /// Unique parameter identifier.
137 pub id: ParamId,
138 /// Full parameter name (e.g., "Master Volume").
139 pub name: &'static str,
140 /// Short parameter name for constrained UIs (e.g., "Vol").
141 pub short_name: &'static str,
142 /// Unit label (e.g., "dB", "%", "Hz").
143 pub units: &'static str,
144 /// Default value in normalized form (0.0 to 1.0).
145 pub default_normalized: ParamValue,
146 /// Number of discrete steps. 0 = continuous, 1 = toggle, >1 = discrete.
147 pub step_count: i32,
148 /// Behavioral flags.
149 pub flags: ParamFlags,
150 /// VST3 Unit ID (parameter group). ROOT_UNIT_ID (0) for ungrouped parameters.
151 pub unit_id: UnitId,
152}
153
154impl ParamInfo {
155 /// Create a new continuous parameter with default flags.
156 pub const fn new(id: ParamId, name: &'static str) -> Self {
157 Self {
158 id,
159 name,
160 short_name: name,
161 units: "",
162 default_normalized: 0.5,
163 step_count: 0,
164 flags: ParamFlags {
165 can_automate: true,
166 is_readonly: false,
167 is_bypass: false,
168 is_list: false,
169 },
170 unit_id: ROOT_UNIT_ID,
171 }
172 }
173
174 /// Set the short name.
175 pub const fn with_short_name(mut self, short_name: &'static str) -> Self {
176 self.short_name = short_name;
177 self
178 }
179
180 /// Set the unit label.
181 pub const fn with_units(mut self, units: &'static str) -> Self {
182 self.units = units;
183 self
184 }
185
186 /// Set the default normalized value.
187 pub const fn with_default(mut self, default: ParamValue) -> Self {
188 self.default_normalized = default;
189 self
190 }
191
192 /// Set the step count (0 = continuous).
193 pub const fn with_steps(mut self, steps: i32) -> Self {
194 self.step_count = steps;
195 self
196 }
197
198 /// Set parameter flags.
199 pub const fn with_flags(mut self, flags: ParamFlags) -> Self {
200 self.flags = flags;
201 self
202 }
203
204 /// Create a bypass toggle parameter with standard configuration.
205 ///
206 /// This creates a parameter pre-configured as a bypass switch:
207 /// - Toggle (step_count = 1)
208 /// - Automatable
209 /// - Marked with `is_bypass = true` flag
210 /// - Default value = 0.0 (not bypassed)
211 ///
212 /// # Example
213 ///
214 /// ```ignore
215 /// const PARAM_BYPASS: u32 = 0;
216 ///
217 /// struct MyParams {
218 /// bypass: AtomicU64,
219 /// bypass_info: ParamInfo,
220 /// }
221 ///
222 /// impl MyParams {
223 /// fn new() -> Self {
224 /// Self {
225 /// bypass: AtomicU64::new(0.0f64.to_bits()),
226 /// bypass_info: ParamInfo::bypass(PARAM_BYPASS),
227 /// }
228 /// }
229 /// }
230 /// ```
231 pub const fn bypass(id: ParamId) -> Self {
232 Self {
233 id,
234 name: "Bypass",
235 short_name: "Byp",
236 units: "",
237 default_normalized: 0.0,
238 step_count: 1,
239 flags: ParamFlags {
240 can_automate: true,
241 is_readonly: false,
242 is_bypass: true,
243 is_list: false,
244 },
245 unit_id: ROOT_UNIT_ID,
246 }
247 }
248
249 /// Set the unit ID (parameter group).
250 pub const fn with_unit(mut self, unit_id: UnitId) -> Self {
251 self.unit_id = unit_id;
252 self
253 }
254}
255
256/// Trait for plugin parameter collections.
257///
258/// Implement this trait to declare your plugin's parameters. The VST3 wrapper
259/// will use this to communicate parameter information and values to the host.
260///
261/// # Example
262///
263/// ```ignore
264/// use std::sync::atomic::{AtomicU64, Ordering};
265/// use beamer_core::{Parameters, ParamInfo, ParamId, ParamValue};
266///
267/// pub struct MyParams {
268/// gain: AtomicU64,
269/// gain_info: ParamInfo,
270/// }
271///
272/// impl Parameters for MyParams {
273/// fn count(&self) -> usize { 1 }
274///
275/// fn info(&self, index: usize) -> Option<&ParamInfo> {
276/// match index {
277/// 0 => Some(&self.gain_info),
278/// _ => None,
279/// }
280/// }
281///
282/// fn get_normalized(&self, id: ParamId) -> ParamValue {
283/// match id {
284/// 0 => f64::from_bits(self.gain.load(Ordering::Relaxed)),
285/// _ => 0.0,
286/// }
287/// }
288///
289/// fn set_normalized(&self, id: ParamId, value: ParamValue) {
290/// match id {
291/// 0 => self.gain.store(value.to_bits(), Ordering::Relaxed),
292/// _ => {}
293/// }
294/// }
295///
296/// // ... implement other methods
297/// }
298/// ```
299pub trait Parameters: Send + Sync {
300 /// Returns the number of parameters.
301 fn count(&self) -> usize;
302
303 /// Returns parameter info by index (0 to count-1).
304 ///
305 /// Returns `None` if index is out of bounds.
306 fn info(&self, index: usize) -> Option<&ParamInfo>;
307
308 /// Gets the current normalized value (0.0 to 1.0) for a parameter.
309 ///
310 /// This must be lock-free and safe to call from the audio thread.
311 fn get_normalized(&self, id: ParamId) -> ParamValue;
312
313 /// Sets the normalized value (0.0 to 1.0) for a parameter.
314 ///
315 /// This must be lock-free and safe to call from the audio thread.
316 /// Implementations should clamp the value to [0.0, 1.0].
317 fn set_normalized(&self, id: ParamId, value: ParamValue);
318
319 /// Converts a normalized value to a display string.
320 ///
321 /// Used by the host to display parameter values in automation lanes,
322 /// tooltips, etc.
323 fn normalized_to_string(&self, id: ParamId, normalized: ParamValue) -> String;
324
325 /// Parses a display string to a normalized value.
326 ///
327 /// Used when the user types a value directly. Returns `None` if
328 /// the string cannot be parsed.
329 fn string_to_normalized(&self, id: ParamId, string: &str) -> Option<ParamValue>;
330
331 /// Converts a normalized value (0.0-1.0) to a plain/real value.
332 ///
333 /// For example, a frequency parameter might map 0.0-1.0 to 20-20000 Hz.
334 fn normalized_to_plain(&self, id: ParamId, normalized: ParamValue) -> ParamValue;
335
336 /// Converts a plain/real value to a normalized value (0.0-1.0).
337 ///
338 /// Inverse of `normalized_to_plain`.
339 fn plain_to_normalized(&self, id: ParamId, plain: ParamValue) -> ParamValue;
340
341 /// Find parameter info by ID.
342 ///
343 /// Default implementation searches linearly through all parameters.
344 fn info_by_id(&self, id: ParamId) -> Option<&ParamInfo> {
345 (0..self.count()).find_map(|i| {
346 let info = self.info(i)?;
347 if info.id == id {
348 Some(info)
349 } else {
350 None
351 }
352 })
353 }
354}
355
356/// Empty parameter collection for plugins with no parameters.
357#[derive(Debug, Clone, Copy, Default)]
358pub struct NoParams;
359
360impl Units for NoParams {}
361
362impl Parameters for NoParams {
363 fn count(&self) -> usize {
364 0
365 }
366
367 fn info(&self, _index: usize) -> Option<&ParamInfo> {
368 None
369 }
370
371 fn get_normalized(&self, _id: ParamId) -> ParamValue {
372 0.0
373 }
374
375 fn set_normalized(&self, _id: ParamId, _value: ParamValue) {}
376
377 fn normalized_to_string(&self, _id: ParamId, _normalized: ParamValue) -> String {
378 String::new()
379 }
380
381 fn string_to_normalized(&self, _id: ParamId, _string: &str) -> Option<ParamValue> {
382 None
383 }
384
385 fn normalized_to_plain(&self, _id: ParamId, normalized: ParamValue) -> ParamValue {
386 normalized
387 }
388
389 fn plain_to_normalized(&self, _id: ParamId, plain: ParamValue) -> ParamValue {
390 plain
391 }
392}
393
394impl crate::param_types::Params for NoParams {
395 fn count(&self) -> usize {
396 0
397 }
398
399 fn iter(&self) -> Box<dyn Iterator<Item = &dyn crate::param_types::ParamRef> + '_> {
400 Box::new(std::iter::empty())
401 }
402
403 fn by_id(&self, _id: ParamId) -> Option<&dyn crate::param_types::ParamRef> {
404 None
405 }
406}