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}