beamer_core/
params.rs

1//! Low-level parameter system for VST3 host communication.
2//!
3//! This module provides the [`Parameters`] trait for direct VST3 host communication.
4//! It exposes the raw normalized value interface that VST3 expects.
5//!
6//! # Choosing Between `Params` and `Parameters`
7//!
8//! Beamer provides two parameter traits that work together:
9//!
10//! - **[`Params`](crate::param_types::Params)** (recommended): High-level trait with
11//!   type-erased iteration, automatic state serialization, and support for parameter
12//!   types like `FloatParam`, `IntParam`, and `BoolParam`. Use `#[derive(Params)]`
13//!   for automatic implementation.
14//!
15//! - **[`Parameters`]**: Low-level trait for direct VST3 host communication. Provides
16//!   raw access to normalized values and parameter metadata. Useful when you need
17//!   fine-grained control over parameter handling or are building custom parameter
18//!   systems.
19//!
20//! For most plugins, use `#[derive(Params)]` which automatically implements both traits.
21//! The `Params` trait builds on top of `Parameters` to provide a more ergonomic API.
22//!
23//! # Thread Safety
24//!
25//! The [`Parameters`] trait requires `Send + Sync` because parameters may be
26//! accessed from multiple threads:
27//! - Audio thread: reads parameter values during processing
28//! - UI thread: displays and modifies parameter values
29//! - Host thread: automation playback and recording
30//!
31//! Use atomic types (e.g., `AtomicU64` with `to_bits`/`from_bits`) for lock-free access.
32
33use crate::types::{ParamId, ParamValue};
34
35// =============================================================================
36// VST3 Unit System (Parameter Grouping)
37// =============================================================================
38
39/// VST3 Unit ID type.
40///
41/// Units are used to organize parameters into hierarchical groups in the DAW UI.
42/// Each unit has a unique ID and can have a parent unit.
43pub type UnitId = i32;
44
45/// Root unit ID constant (parameters with no group).
46///
47/// The root unit (ID 0) always exists and contains ungrouped parameters.
48pub const ROOT_UNIT_ID: UnitId = 0;
49
50/// Information about a parameter group (VST3 Unit).
51///
52/// Units form a tree structure via parent_id references:
53/// - Root unit (id=0, parent=0) always exists implicitly
54/// - Top-level groups have parent_id=0
55/// - Nested groups reference their parent's unit_id
56#[derive(Debug, Clone)]
57pub struct UnitInfo {
58    /// Unique unit identifier.
59    pub id: UnitId,
60    /// Display name shown in DAW (e.g., "Filter", "Amp Envelope").
61    pub name: &'static str,
62    /// Parent unit ID (ROOT_UNIT_ID for top-level groups).
63    pub parent_id: UnitId,
64}
65
66impl UnitInfo {
67    /// Create a new unit info.
68    pub const fn new(id: UnitId, name: &'static str, parent_id: UnitId) -> Self {
69        Self { id, name, parent_id }
70    }
71
72    /// Create the root unit.
73    pub const fn root() -> Self {
74        Self {
75            id: ROOT_UNIT_ID,
76            name: "",
77            parent_id: ROOT_UNIT_ID,
78        }
79    }
80}
81
82/// Trait for querying VST3 unit hierarchy.
83///
84/// Implemented automatically by `#[derive(Params)]` when nested groups are present.
85/// Provides information about parameter groups for DAW display.
86///
87/// Unit IDs are assigned dynamically at runtime to support deeply nested groups
88/// where the same nested struct type can appear in multiple contexts with
89/// different parent units.
90pub trait Units {
91    /// Total number of units (including root).
92    ///
93    /// Returns 1 if there are no groups (just the root unit).
94    /// For nested groups, this returns 1 + total nested groups (including deeply nested).
95    fn unit_count(&self) -> usize {
96        1 // Default: only root unit
97    }
98
99    /// Get unit info by index.
100    ///
101    /// Index 0 always returns the root unit.
102    /// Returns `UnitInfo` by value to support dynamic construction for nested groups.
103    fn unit_info(&self, index: usize) -> Option<UnitInfo> {
104        if index == 0 {
105            Some(UnitInfo::root())
106        } else {
107            None
108        }
109    }
110
111    /// Find unit ID by name (linear search).
112    fn find_unit_by_name(&self, name: &str) -> Option<UnitId> {
113        for i in 0..self.unit_count() {
114            if let Some(info) = self.unit_info(i) {
115                if info.name == name {
116                    return Some(info.id);
117                }
118            }
119        }
120        None
121    }
122}
123
124/// Flags controlling parameter behavior.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct ParamFlags {
127    /// Parameter can be automated by the host.
128    pub can_automate: bool,
129    /// Parameter is read-only (display only).
130    pub is_readonly: bool,
131    /// Parameter is the bypass switch.
132    pub is_bypass: bool,
133    /// Parameter should be displayed as a dropdown list (for enums).
134    /// When true, host shows text labels from getParamStringByValue().
135    pub is_list: bool,
136    /// Parameter is hidden from the DAW's parameter list.
137    /// Used for internal parameters like MIDI CC emulation.
138    pub is_hidden: bool,
139}
140
141impl Default for ParamFlags {
142    fn default() -> Self {
143        Self {
144            can_automate: true,
145            is_readonly: false,
146            is_bypass: false,
147            is_list: false,
148            is_hidden: false,
149        }
150    }
151}
152
153/// Metadata describing a single parameter.
154#[derive(Debug, Clone)]
155pub struct ParamInfo {
156    /// Unique parameter identifier.
157    pub id: ParamId,
158    /// Full parameter name (e.g., "Master Volume").
159    pub name: &'static str,
160    /// Short parameter name for constrained UIs (e.g., "Vol").
161    pub short_name: &'static str,
162    /// Unit label (e.g., "dB", "%", "Hz").
163    pub units: &'static str,
164    /// Default value in normalized form (0.0 to 1.0).
165    pub default_normalized: ParamValue,
166    /// Number of discrete steps. 0 = continuous, 1 = toggle, >1 = discrete.
167    pub step_count: i32,
168    /// Behavioral flags.
169    pub flags: ParamFlags,
170    /// VST3 Unit ID (parameter group). ROOT_UNIT_ID (0) for ungrouped parameters.
171    pub unit_id: UnitId,
172}
173
174impl ParamInfo {
175    /// Create a new continuous parameter with default flags.
176    pub const fn new(id: ParamId, name: &'static str) -> Self {
177        Self {
178            id,
179            name,
180            short_name: name,
181            units: "",
182            default_normalized: 0.5,
183            step_count: 0,
184            flags: ParamFlags {
185                can_automate: true,
186                is_readonly: false,
187                is_bypass: false,
188                is_list: false,
189                is_hidden: false,
190            },
191            unit_id: ROOT_UNIT_ID,
192        }
193    }
194
195    /// Set the short name.
196    pub const fn with_short_name(mut self, short_name: &'static str) -> Self {
197        self.short_name = short_name;
198        self
199    }
200
201    /// Set the unit label.
202    pub const fn with_units(mut self, units: &'static str) -> Self {
203        self.units = units;
204        self
205    }
206
207    /// Set the default normalized value.
208    pub const fn with_default(mut self, default: ParamValue) -> Self {
209        self.default_normalized = default;
210        self
211    }
212
213    /// Set the step count (0 = continuous).
214    pub const fn with_steps(mut self, steps: i32) -> Self {
215        self.step_count = steps;
216        self
217    }
218
219    /// Set parameter flags.
220    pub const fn with_flags(mut self, flags: ParamFlags) -> Self {
221        self.flags = flags;
222        self
223    }
224
225    /// Create a bypass toggle parameter with standard configuration.
226    ///
227    /// This creates a parameter pre-configured as a bypass switch:
228    /// - Toggle (step_count = 1)
229    /// - Automatable
230    /// - Marked with `is_bypass = true` flag
231    /// - Default value = 0.0 (not bypassed)
232    ///
233    /// # Example
234    ///
235    /// ```ignore
236    /// const PARAM_BYPASS: u32 = 0;
237    ///
238    /// struct MyParams {
239    ///     bypass: AtomicU64,
240    ///     bypass_info: ParamInfo,
241    /// }
242    ///
243    /// impl MyParams {
244    ///     fn new() -> Self {
245    ///         Self {
246    ///             bypass: AtomicU64::new(0.0f64.to_bits()),
247    ///             bypass_info: ParamInfo::bypass(PARAM_BYPASS),
248    ///         }
249    ///     }
250    /// }
251    /// ```
252    pub const fn bypass(id: ParamId) -> Self {
253        Self {
254            id,
255            name: "Bypass",
256            short_name: "Byp",
257            units: "",
258            default_normalized: 0.0,
259            step_count: 1,
260            flags: ParamFlags {
261                can_automate: true,
262                is_readonly: false,
263                is_bypass: true,
264                is_list: false,
265                is_hidden: false,
266            },
267            unit_id: ROOT_UNIT_ID,
268        }
269    }
270
271    /// Set the unit ID (parameter group).
272    pub const fn with_unit(mut self, unit_id: UnitId) -> Self {
273        self.unit_id = unit_id;
274        self
275    }
276}
277
278/// Trait for plugin parameter collections.
279///
280/// Implement this trait to declare your plugin's parameters. The VST3 wrapper
281/// will use this to communicate parameter information and values to the host.
282///
283/// # Example
284///
285/// ```ignore
286/// use std::sync::atomic::{AtomicU64, Ordering};
287/// use beamer_core::{Parameters, ParamInfo, ParamId, ParamValue};
288///
289/// pub struct MyParams {
290///     gain: AtomicU64,
291///     gain_info: ParamInfo,
292/// }
293///
294/// impl Parameters for MyParams {
295///     fn count(&self) -> usize { 1 }
296///
297///     fn info(&self, index: usize) -> Option<&ParamInfo> {
298///         match index {
299///             0 => Some(&self.gain_info),
300///             _ => None,
301///         }
302///     }
303///
304///     fn get_normalized(&self, id: ParamId) -> ParamValue {
305///         match id {
306///             0 => f64::from_bits(self.gain.load(Ordering::Relaxed)),
307///             _ => 0.0,
308///         }
309///     }
310///
311///     fn set_normalized(&self, id: ParamId, value: ParamValue) {
312///         match id {
313///             0 => self.gain.store(value.to_bits(), Ordering::Relaxed),
314///             _ => {}
315///         }
316///     }
317///
318///     // ... implement other methods
319/// }
320/// ```
321pub trait Parameters: Send + Sync {
322    /// Returns the number of parameters.
323    fn count(&self) -> usize;
324
325    /// Returns parameter info by index (0 to count-1).
326    ///
327    /// Returns `None` if index is out of bounds.
328    fn info(&self, index: usize) -> Option<&ParamInfo>;
329
330    /// Gets the current normalized value (0.0 to 1.0) for a parameter.
331    ///
332    /// This must be lock-free and safe to call from the audio thread.
333    fn get_normalized(&self, id: ParamId) -> ParamValue;
334
335    /// Sets the normalized value (0.0 to 1.0) for a parameter.
336    ///
337    /// This must be lock-free and safe to call from the audio thread.
338    /// Implementations should clamp the value to [0.0, 1.0].
339    fn set_normalized(&self, id: ParamId, value: ParamValue);
340
341    /// Converts a normalized value to a display string.
342    ///
343    /// Used by the host to display parameter values in automation lanes,
344    /// tooltips, etc.
345    fn normalized_to_string(&self, id: ParamId, normalized: ParamValue) -> String;
346
347    /// Parses a display string to a normalized value.
348    ///
349    /// Used when the user types a value directly. Returns `None` if
350    /// the string cannot be parsed.
351    fn string_to_normalized(&self, id: ParamId, string: &str) -> Option<ParamValue>;
352
353    /// Converts a normalized value (0.0-1.0) to a plain/real value.
354    ///
355    /// For example, a frequency parameter might map 0.0-1.0 to 20-20000 Hz.
356    fn normalized_to_plain(&self, id: ParamId, normalized: ParamValue) -> ParamValue;
357
358    /// Converts a plain/real value to a normalized value (0.0-1.0).
359    ///
360    /// Inverse of `normalized_to_plain`.
361    fn plain_to_normalized(&self, id: ParamId, plain: ParamValue) -> ParamValue;
362
363    /// Find parameter info by ID.
364    ///
365    /// Default implementation searches linearly through all parameters.
366    fn info_by_id(&self, id: ParamId) -> Option<&ParamInfo> {
367        (0..self.count()).find_map(|i| {
368            let info = self.info(i)?;
369            if info.id == id {
370                Some(info)
371            } else {
372                None
373            }
374        })
375    }
376}
377
378/// Empty parameter collection for plugins with no parameters.
379#[derive(Debug, Clone, Copy, Default)]
380pub struct NoParams;
381
382impl Units for NoParams {}
383
384impl Parameters for NoParams {
385    fn count(&self) -> usize {
386        0
387    }
388
389    fn info(&self, _index: usize) -> Option<&ParamInfo> {
390        None
391    }
392
393    fn get_normalized(&self, _id: ParamId) -> ParamValue {
394        0.0
395    }
396
397    fn set_normalized(&self, _id: ParamId, _value: ParamValue) {}
398
399    fn normalized_to_string(&self, _id: ParamId, _normalized: ParamValue) -> String {
400        String::new()
401    }
402
403    fn string_to_normalized(&self, _id: ParamId, _string: &str) -> Option<ParamValue> {
404        None
405    }
406
407    fn normalized_to_plain(&self, _id: ParamId, normalized: ParamValue) -> ParamValue {
408        normalized
409    }
410
411    fn plain_to_normalized(&self, _id: ParamId, plain: ParamValue) -> ParamValue {
412        plain
413    }
414}
415
416impl crate::param_types::Params for NoParams {
417    fn count(&self) -> usize {
418        0
419    }
420
421    fn iter(&self) -> Box<dyn Iterator<Item = &dyn crate::param_types::ParamRef> + '_> {
422        Box::new(std::iter::empty())
423    }
424
425    fn by_id(&self, _id: ParamId) -> Option<&dyn crate::param_types::ParamRef> {
426        None
427    }
428}