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}