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}