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 /// Parameter is hidden from the DAW's parameter list.
121 /// Used for internal parameters like MIDI CC emulation.
122 pub is_hidden: bool,
123}
124
125impl Default for ParamFlags {
126 fn default() -> Self {
127 Self {
128 can_automate: true,
129 is_readonly: false,
130 is_bypass: false,
131 is_list: false,
132 is_hidden: false,
133 }
134 }
135}
136
137/// Metadata describing a single parameter.
138#[derive(Debug, Clone)]
139pub struct ParamInfo {
140 /// Unique parameter identifier.
141 pub id: ParamId,
142 /// Full parameter name (e.g., "Master Volume").
143 pub name: &'static str,
144 /// Short parameter name for constrained UIs (e.g., "Vol").
145 pub short_name: &'static str,
146 /// Unit label (e.g., "dB", "%", "Hz").
147 pub units: &'static str,
148 /// Default value in normalized form (0.0 to 1.0).
149 pub default_normalized: ParamValue,
150 /// Number of discrete steps. 0 = continuous, 1 = toggle, >1 = discrete.
151 pub step_count: i32,
152 /// Behavioral flags.
153 pub flags: ParamFlags,
154 /// VST3 Unit ID (parameter group). ROOT_UNIT_ID (0) for ungrouped parameters.
155 pub unit_id: UnitId,
156}
157
158impl ParamInfo {
159 /// Create a new continuous parameter with default flags.
160 pub const fn new(id: ParamId, name: &'static str) -> Self {
161 Self {
162 id,
163 name,
164 short_name: name,
165 units: "",
166 default_normalized: 0.5,
167 step_count: 0,
168 flags: ParamFlags {
169 can_automate: true,
170 is_readonly: false,
171 is_bypass: false,
172 is_list: false,
173 is_hidden: false,
174 },
175 unit_id: ROOT_UNIT_ID,
176 }
177 }
178
179 /// Set the short name.
180 pub const fn with_short_name(mut self, short_name: &'static str) -> Self {
181 self.short_name = short_name;
182 self
183 }
184
185 /// Set the unit label.
186 pub const fn with_units(mut self, units: &'static str) -> Self {
187 self.units = units;
188 self
189 }
190
191 /// Set the default normalized value.
192 pub const fn with_default(mut self, default: ParamValue) -> Self {
193 self.default_normalized = default;
194 self
195 }
196
197 /// Set the step count (0 = continuous).
198 pub const fn with_steps(mut self, steps: i32) -> Self {
199 self.step_count = steps;
200 self
201 }
202
203 /// Set parameter flags.
204 pub const fn with_flags(mut self, flags: ParamFlags) -> Self {
205 self.flags = flags;
206 self
207 }
208
209 /// Create a bypass toggle parameter with standard configuration.
210 ///
211 /// This creates a parameter pre-configured as a bypass switch:
212 /// - Toggle (step_count = 1)
213 /// - Automatable
214 /// - Marked with `is_bypass = true` flag
215 /// - Default value = 0.0 (not bypassed)
216 ///
217 /// # Example
218 ///
219 /// ```ignore
220 /// const PARAM_BYPASS: u32 = 0;
221 ///
222 /// struct MyParams {
223 /// bypass: AtomicU64,
224 /// bypass_info: ParamInfo,
225 /// }
226 ///
227 /// impl MyParams {
228 /// fn new() -> Self {
229 /// Self {
230 /// bypass: AtomicU64::new(0.0f64.to_bits()),
231 /// bypass_info: ParamInfo::bypass(PARAM_BYPASS),
232 /// }
233 /// }
234 /// }
235 /// ```
236 pub const fn bypass(id: ParamId) -> Self {
237 Self {
238 id,
239 name: "Bypass",
240 short_name: "Byp",
241 units: "",
242 default_normalized: 0.0,
243 step_count: 1,
244 flags: ParamFlags {
245 can_automate: true,
246 is_readonly: false,
247 is_bypass: true,
248 is_list: false,
249 is_hidden: false,
250 },
251 unit_id: ROOT_UNIT_ID,
252 }
253 }
254
255 /// Set the unit ID (parameter group).
256 pub const fn with_unit(mut self, unit_id: UnitId) -> Self {
257 self.unit_id = unit_id;
258 self
259 }
260}
261
262/// Trait for plugin parameter collections.
263///
264/// Implement this trait to declare your plugin's parameters. The VST3 wrapper
265/// will use this to communicate parameter information and values to the host.
266///
267/// # Example
268///
269/// ```ignore
270/// use std::sync::atomic::{AtomicU64, Ordering};
271/// use beamer_core::{Parameters, ParamInfo, ParamId, ParamValue};
272///
273/// pub struct MyParams {
274/// gain: AtomicU64,
275/// gain_info: ParamInfo,
276/// }
277///
278/// impl Parameters for MyParams {
279/// fn count(&self) -> usize { 1 }
280///
281/// fn info(&self, index: usize) -> Option<&ParamInfo> {
282/// match index {
283/// 0 => Some(&self.gain_info),
284/// _ => None,
285/// }
286/// }
287///
288/// fn get_normalized(&self, id: ParamId) -> ParamValue {
289/// match id {
290/// 0 => f64::from_bits(self.gain.load(Ordering::Relaxed)),
291/// _ => 0.0,
292/// }
293/// }
294///
295/// fn set_normalized(&self, id: ParamId, value: ParamValue) {
296/// match id {
297/// 0 => self.gain.store(value.to_bits(), Ordering::Relaxed),
298/// _ => {}
299/// }
300/// }
301///
302/// // ... implement other methods
303/// }
304/// ```
305pub trait Parameters: Send + Sync {
306 /// Returns the number of parameters.
307 fn count(&self) -> usize;
308
309 /// Returns parameter info by index (0 to count-1).
310 ///
311 /// Returns `None` if index is out of bounds.
312 fn info(&self, index: usize) -> Option<&ParamInfo>;
313
314 /// Gets the current normalized value (0.0 to 1.0) for a parameter.
315 ///
316 /// This must be lock-free and safe to call from the audio thread.
317 fn get_normalized(&self, id: ParamId) -> ParamValue;
318
319 /// Sets the normalized value (0.0 to 1.0) for a parameter.
320 ///
321 /// This must be lock-free and safe to call from the audio thread.
322 /// Implementations should clamp the value to [0.0, 1.0].
323 fn set_normalized(&self, id: ParamId, value: ParamValue);
324
325 /// Converts a normalized value to a display string.
326 ///
327 /// Used by the host to display parameter values in automation lanes,
328 /// tooltips, etc.
329 fn normalized_to_string(&self, id: ParamId, normalized: ParamValue) -> String;
330
331 /// Parses a display string to a normalized value.
332 ///
333 /// Used when the user types a value directly. Returns `None` if
334 /// the string cannot be parsed.
335 fn string_to_normalized(&self, id: ParamId, string: &str) -> Option<ParamValue>;
336
337 /// Converts a normalized value (0.0-1.0) to a plain/real value.
338 ///
339 /// For example, a frequency parameter might map 0.0-1.0 to 20-20000 Hz.
340 fn normalized_to_plain(&self, id: ParamId, normalized: ParamValue) -> ParamValue;
341
342 /// Converts a plain/real value to a normalized value (0.0-1.0).
343 ///
344 /// Inverse of `normalized_to_plain`.
345 fn plain_to_normalized(&self, id: ParamId, plain: ParamValue) -> ParamValue;
346
347 /// Find parameter info by ID.
348 ///
349 /// Default implementation searches linearly through all parameters.
350 fn info_by_id(&self, id: ParamId) -> Option<&ParamInfo> {
351 (0..self.count()).find_map(|i| {
352 let info = self.info(i)?;
353 if info.id == id {
354 Some(info)
355 } else {
356 None
357 }
358 })
359 }
360}
361
362/// Empty parameter collection for plugins with no parameters.
363#[derive(Debug, Clone, Copy, Default)]
364pub struct NoParams;
365
366impl Units for NoParams {}
367
368impl Parameters for NoParams {
369 fn count(&self) -> usize {
370 0
371 }
372
373 fn info(&self, _index: usize) -> Option<&ParamInfo> {
374 None
375 }
376
377 fn get_normalized(&self, _id: ParamId) -> ParamValue {
378 0.0
379 }
380
381 fn set_normalized(&self, _id: ParamId, _value: ParamValue) {}
382
383 fn normalized_to_string(&self, _id: ParamId, _normalized: ParamValue) -> String {
384 String::new()
385 }
386
387 fn string_to_normalized(&self, _id: ParamId, _string: &str) -> Option<ParamValue> {
388 None
389 }
390
391 fn normalized_to_plain(&self, _id: ParamId, normalized: ParamValue) -> ParamValue {
392 normalized
393 }
394
395 fn plain_to_normalized(&self, _id: ParamId, plain: ParamValue) -> ParamValue {
396 plain
397 }
398}
399
400impl crate::param_types::Params for NoParams {
401 fn count(&self) -> usize {
402 0
403 }
404
405 fn iter(&self) -> Box<dyn Iterator<Item = &dyn crate::param_types::ParamRef> + '_> {
406 Box::new(std::iter::empty())
407 }
408
409 fn by_id(&self, _id: ParamId) -> Option<&dyn crate::param_types::ParamRef> {
410 None
411 }
412}