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}