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