Skip to main content

aura_params/
lib.rs

1#![forbid(unsafe_code)]
2
3mod info;
4mod range;
5pub mod sample;
6mod smooth;
7mod types;
8
9pub use info::{MidiSource, ParamFlags, ParamInfo, ParamUnit, ParamValueKind, map_source_to_param};
10pub use range::ParamRange;
11pub use sample::{Float, Sample};
12pub use smooth::{Smoother, SmoothingStyle};
13pub use types::{
14    AudioTap, BoolParam, DEFAULT_TAP_CAPACITY, EnumParam, FloatParam, FloatParamReadF32,
15    FloatParamReadF64, IntParam, MeterSlot, ParamEnum,
16};
17
18/// Implementation detail — not part of the stable public API.
19/// Meter IDs from `#[meter]` start at this base (derive + wrappers).
20#[doc(hidden)]
21pub const METER_ID_BASE: u32 = 1 << 24;
22
23/// Sealing module: external crates cannot implement [`Params`] or
24/// [`ParamEnum`] directly because they can't name `Sealed`. The
25/// `#[derive(Params)]` and `#[derive(ParamEnum)]` macros emit the
26/// `Sealed` impl alongside their trait impls, so derive users are
27/// unaffected.
28#[doc(hidden)]
29pub mod __private {
30    pub trait Sealed {}
31}
32
33/// Format a plain parameter value as a display string based on the parameter's unit.
34///
35/// Used by the `#[derive(Params)]` macro for default `format_value` implementations
36/// on `FloatParam` and `IntParam` fields. `IntParam` is identified by
37/// `ParamValueKind::Int`, set by the derive from the field type - its
38/// value is always integer-valued, so the fractional `{:.1}` / `{:.2}`
39/// formats float-typed params use would render "0.0 st" / "0.00"
40/// instead of "0 st" / "0".
41#[must_use]
42pub fn format_param_value(info: &ParamInfo, value: f64) -> String {
43    let is_int = info.kind == ParamValueKind::Int;
44    // Round to nearest integer before display so a smoothed IntParam
45    // that's mid-transition doesn't briefly render the rounded-down
46    // half-step (e.g. an `i32::from(value)` of -1 when value is -0.5
47    // mid-snap). `IntParam::value_i32` rounds the same way at the
48    // audio-thread read site.
49    #[allow(clippy::cast_possible_truncation)]
50    let int_value = value.round() as i64;
51    match info.unit {
52        ParamUnit::Db => {
53            if is_int {
54                format!("{int_value} dB")
55            } else {
56                format!("{value:.1} dB")
57            }
58        }
59        ParamUnit::Hz => {
60            if value >= 1000.0 {
61                format!("{:.1} kHz", value / 1000.0)
62            } else {
63                format!("{value:.0} Hz")
64            }
65        }
66        ParamUnit::Milliseconds => {
67            if is_int {
68                format!("{int_value} ms")
69            } else {
70                format!("{value:.1} ms")
71            }
72        }
73        ParamUnit::Seconds => {
74            if value >= 1.0 {
75                format!("{value:.2} s")
76            } else {
77                format!("{:.0} ms", value * 1000.0)
78            }
79        }
80        ParamUnit::Percent => format!("{value:.1}%"),
81        ParamUnit::Semitones => {
82            if is_int {
83                format!("{int_value} st")
84            } else {
85                format!("{value:.1} st")
86            }
87        }
88        ParamUnit::Degrees => {
89            if is_int {
90                format!("{int_value}°")
91            } else {
92                format!("{value:.1}°")
93            }
94        }
95        ParamUnit::Pan => {
96            // Convention: pan params are normalized to [-1.0, 1.0]. Round
97            // to nearest integer percent first so the dead-zone test and
98            // L/R label agree (e.g. -0.004 → 0% → "C", -0.006 → -1% → "1L").
99            // Result is bounded by `[-100, 100]` after clamp to `[-1, 1]`.
100            #[allow(clippy::cast_possible_truncation)]
101            let pct = (value * 100.0).round() as i32;
102            match pct.cmp(&0) {
103                std::cmp::Ordering::Equal => "C".to_string(),
104                std::cmp::Ordering::Less => format!("{}L", -pct),
105                std::cmp::Ordering::Greater => format!("{pct}R"),
106            }
107        }
108        ParamUnit::None => {
109            if is_int {
110                format!("{int_value}")
111            } else {
112                format!("{value:.2}")
113            }
114        }
115    }
116}
117
118/// Inverse of [`format_param_value`] for host `text_to_value` round-trips.
119///
120/// Accepts bare numbers and the unit suffixes `format_param_value` emits
121/// (`kHz`/`Hz`, `ms`/`s`, `dB`, `%`, `st`, `°`, pan `C`/`nL`/`nR`). Case-
122/// insensitive on ASCII unit suffixes; whitespace around the number is
123/// ignored. Returns `None` when the text is not a known form for `info.unit`.
124#[must_use]
125pub fn parse_param_value(info: &ParamInfo, text: &str) -> Option<f64> {
126    let t = text.trim();
127    if t.is_empty() {
128        return None;
129    }
130    match info.unit {
131        ParamUnit::Hz => {
132            let lower = t.to_ascii_lowercase();
133            if let Some(rest) = lower.strip_suffix("khz") {
134                return rest.trim().parse::<f64>().ok().map(|v| v * 1000.0);
135            }
136            if let Some(rest) = lower.strip_suffix("hz") {
137                return rest.trim().parse().ok();
138            }
139            t.parse().ok()
140        }
141        ParamUnit::Seconds => {
142            let lower = t.to_ascii_lowercase();
143            if let Some(rest) = lower.strip_suffix("ms") {
144                return rest.trim().parse::<f64>().ok().map(|v| v / 1000.0);
145            }
146            if let Some(rest) = lower.strip_suffix('s') {
147                return rest.trim().parse().ok();
148            }
149            t.parse().ok()
150        }
151        ParamUnit::Milliseconds => parse_strip_suffixes(t, &["ms"]),
152        ParamUnit::Db => parse_strip_suffixes(t, &["db", "dB"]),
153        ParamUnit::Percent => {
154            let rest = t.strip_suffix('%').unwrap_or(t).trim();
155            rest.parse().ok()
156        }
157        ParamUnit::Semitones => parse_strip_suffixes(t, &["st"]),
158        ParamUnit::Degrees => {
159            let rest = t
160                .strip_suffix('°')
161                .or_else(|| t.strip_suffix("deg"))
162                .or_else(|| t.strip_suffix("DEG"))
163                .unwrap_or(t)
164                .trim();
165            rest.parse().ok()
166        }
167        ParamUnit::Pan => parse_pan_text(t),
168        ParamUnit::None => t.parse().ok(),
169    }
170}
171
172fn parse_strip_suffixes(text: &str, suffixes: &[&str]) -> Option<f64> {
173    let lower = text.to_ascii_lowercase();
174    for suf in suffixes {
175        let suf_l = suf.to_ascii_lowercase();
176        if let Some(rest) = lower.strip_suffix(suf_l.as_str()) {
177            return rest.trim().parse().ok();
178        }
179    }
180    text.trim().parse().ok()
181}
182
183fn parse_pan_text(t: &str) -> Option<f64> {
184    let s = t.trim();
185    if s.eq_ignore_ascii_case("c") || s == "0" {
186        return Some(0.0);
187    }
188    // "50L" / "100R" / "50l"
189    if s.len() >= 2 {
190        let (num, side) = s.split_at(s.len() - 1);
191        let pct: f64 = num.parse().ok()?;
192        match side.as_bytes()[0].to_ascii_lowercase() {
193            b'l' => return Some((-pct / 100.0).clamp(-1.0, 1.0)),
194            b'r' => return Some((pct / 100.0).clamp(-1.0, 1.0)),
195            _ => {}
196        }
197    }
198    s.parse().ok()
199}
200
201/// Trait implemented by #[derive(Params)] on a struct.
202/// Format wrappers use this to enumerate, read, and write parameters.
203///
204/// Stays dyn-compatible (every method dispatches through `&self`) so
205/// editors can pass `Arc<dyn Params>` into the screenshot pipeline
206/// without naming the concrete type. Generic code that needs to
207/// *construct* a fresh `Params` value should add a `Default` bound
208/// rather than expecting one on the trait - `#[derive(Params)]` emits
209/// `impl Default` alongside the trait impl, so that bound is free for
210/// derive users.
211pub trait Params: __private::Sealed + Send + Sync + 'static {
212    /// All parameter infos, in declaration order.
213    fn param_infos(&self) -> Vec<ParamInfo>;
214
215    /// Append parameter infos onto an existing buffer. Default impl
216    /// delegates to [`Self::param_infos`] and `extend`s; the derive
217    /// macro overrides for nested structs so deep trees don't pay
218    /// O(depth) intermediate `Vec` allocations per outer call.
219    fn append_param_infos(&self, into: &mut Vec<ParamInfo>) {
220        into.extend(self.param_infos());
221    }
222
223    /// Static parameter metadata without constructing a plugin instance.
224    ///
225    /// Format wrappers use this at register time so they skip DSP/UI
226    /// allocation. Derive overrides with a `LazyLock`-cached info list.
227    /// Default: empty — hand-written impls fall back to the instance path.
228    /// `Self: Sized` keeps `&dyn Params` dyn-compatible.
229    #[must_use]
230    fn param_infos_static() -> Vec<ParamInfo>
231    where
232        Self: Sized,
233    {
234        Vec::new()
235    }
236
237    /// Number of parameters.
238    fn count(&self) -> usize;
239
240    /// IDs of every `#[meter]` slot declared on the params struct
241    /// (including nested subtrees), in declaration order. Default impl
242    /// returns empty - only structs that declare meters need to
243    /// override. The derive macro implements it automatically.
244    ///
245    /// Format wrappers that expose DSP-side meters back to the UI
246    /// (LV2's output control ports, for instance) use this to know
247    /// which IDs to poll each `process()`.
248    fn meter_ids(&self) -> Vec<u32> {
249        Vec::new()
250    }
251
252    /// Get normalized value (0.0–1.0) by ID.
253    fn get_normalized(&self, id: u32) -> Option<f64>;
254
255    /// Set normalized value (0.0–1.0) by ID.
256    ///
257    /// Takes `&self`, not `&mut self` - the per-param storage in
258    /// `FloatParam` / `BoolParam` / `IntParam` / `EnumParam` is built
259    /// on `AtomicU32` / `AtomicU64`, so writes go through interior
260    /// mutability. Format wrappers, GUI editors, and the audio thread
261    /// all hold `&Params` (or `Arc<Params>`) concurrently and write
262    /// without coordination - every implementation must be sound under
263    /// concurrent `&self` writes from multiple threads.
264    fn set_normalized(&self, id: u32, value: f64);
265
266    /// Set normalized value and read back the resulting plain value in
267    /// one call (GUI → host automation after a write). Default:
268    /// `set_normalized` then `get_plain`. Derive overrides for a single
269    /// match-arm walk.
270    fn set_normalized_returning_plain(&self, id: u32, value: f64) -> f64 {
271        self.set_normalized(id, value);
272        self.get_plain(id).unwrap_or(0.0)
273    }
274
275    /// Set normalized value and read back the clamped/stepped normalized
276    /// value in one call (VST3-style host automation). Same override
277    /// contract as [`Self::set_normalized_returning_plain`].
278    fn set_normalized_returning_normalized(&self, id: u32, value: f64) -> f64 {
279        self.set_normalized(id, value);
280        self.get_normalized(id).unwrap_or(0.0)
281    }
282
283    /// Get plain value by ID.
284    fn get_plain(&self, id: u32) -> Option<f64>;
285
286    /// Set plain **base** value by ID (host automation / state).
287    ///
288    /// Same `&self` interior-mutability contract as
289    /// [`Self::set_normalized`]. Does not clear mono modulation.
290    fn set_plain(&self, id: u32, value: f64);
291
292    /// Set mono modulation offset for `id` (CLAP `PARAM_MOD` amount).
293    ///
294    /// Default: no-op (params without mod storage ignore). Derive overrides
295    /// for `FloatParam` fields. Non-modulatable ids may still store amount;
296    /// hosts only send mods for flagged params.
297    fn set_mod(&self, id: u32, amount: f64) {
298        let _ = (id, amount);
299    }
300
301    /// Format a plain value to display string.
302    fn format_value(&self, id: u32, value: f64) -> Option<String>;
303
304    /// Parse a display string to plain value.
305    fn parse_value(&self, id: u32, text: &str) -> Option<f64>;
306
307    /// Reset all smoothers to current values.
308    fn snap_smoothers(&self);
309
310    /// Update smoother sample rates.
311    fn set_sample_rate(&self, sample_rate: f64);
312
313    /// Collect all parameter IDs and their current plain values.
314    fn collect_values(&self) -> (Vec<u32>, Vec<f64>);
315
316    /// Restore parameter values from a list of (id, value) pairs.
317    fn restore_values(&self, values: &[(u32, f64)]);
318
319    /// Serialize this store's `#[persist]` fields into a keyed blob the
320    /// host saves alongside the parameter values. Default: empty (no
321    /// persist fields). The `#[derive(Params)]` macro overrides it when
322    /// any field carries `#[persist]`.
323    #[must_use]
324    fn serialize_persist(&self) -> Vec<u8> {
325        Vec::new()
326    }
327
328    /// Restore this store's `#[persist]` fields from a blob produced by
329    /// [`Self::serialize_persist`]. Unknown / missing keys are skipped,
330    /// leaving those fields at their current value. Default: no-op.
331    fn load_persist(&self, data: &[u8]) {
332        let _ = data;
333    }
334
335    /// Walk every parameter and meter ID reachable from `self`
336    /// (including nested `#[nested]` substructs) and panic on the
337    /// first duplicate.
338    ///
339    /// Why this isn't just a compile-time check: the
340    /// `#[derive(Params)]` collision check at expansion time only
341    /// sees IDs declared in the *current* struct. A parent param
342    /// `id = 5` and a nested-substruct param `id = 5` both compile,
343    /// because the parent derive doesn't see into the nested type.
344    /// At runtime, the `set_plain` / `get_plain` dispatcher matches
345    /// at the outer level first and silently never reaches the
346    /// nested one - preset round-trips would corrupt the nested
347    /// value. This method makes that bug surface as a panic at
348    /// plugin construction instead of as quiet state loss.
349    ///
350    /// Called automatically by the derive-generated `Self::new()`.
351    /// Plugin code shouldn't need to invoke it directly.
352    fn assert_no_id_collisions(&self) {
353        let mut all = self.param_infos();
354        // Borrow the names from the existing infos so the panic
355        // message can identify *which* IDs collided.
356        let mut seen: Vec<(u32, &'static str)> = Vec::with_capacity(all.len());
357        for info in all.drain(..) {
358            for (prev_id, prev_name) in &seen {
359                assert!(
360                    *prev_id != info.id,
361                    "duplicate parameter ID {}: '{}' and '{}' (likely a \
362                     parent / nested-struct collision; the per-struct \
363                     compile-time check can't see across nested types)",
364                    info.id,
365                    prev_name,
366                    info.name,
367                );
368            }
369            seen.push((info.id, info.name));
370        }
371        let mut seen_meters: Vec<u32> = Vec::new();
372        for meter_id in self.meter_ids() {
373            for (prev_id, prev_name) in &seen {
374                assert!(
375                    *prev_id != meter_id,
376                    "meter ID {meter_id} collides with parameter ID for '{prev_name}'",
377                );
378            }
379            // Meter IDs auto-assign per struct from a shared base, so two
380            // `#[nested]` structs that each declare a meter hand back the
381            // same ID and would alias in meter storage. The per-struct
382            // compile-time check can't see across nested types; surface it
383            // as a construction panic instead of silent aliasing.
384            assert!(
385                !seen_meters.contains(&meter_id),
386                "duplicate meter ID {meter_id} (two #[nested] structs each \
387                 declare a meter; nested meters aren't supported - keep \
388                 meters in a single Params struct)",
389            );
390            seen_meters.push(meter_id);
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::range::ParamRange;
399
400    fn pan_info() -> ParamInfo {
401        ParamInfo {
402            id: 0,
403            name: "Pan",
404            short_name: "Pan",
405            group: "",
406            range: ParamRange::Linear {
407                min: -1.0,
408                max: 1.0,
409            },
410            default_plain: 0.0,
411            flags: ParamFlags::empty(),
412            unit: ParamUnit::Pan,
413            kind: ParamValueKind::Float,
414            midi_map: None,
415            midi_channel: None,
416        }
417    }
418
419    #[test]
420    fn pan_centre() {
421        let info = pan_info();
422        assert_eq!(format_param_value(&info, 0.0), "C");
423        assert_eq!(format_param_value(&info, 0.004), "C");
424        assert_eq!(format_param_value(&info, -0.004), "C");
425    }
426
427    #[test]
428    fn pan_left() {
429        let info = pan_info();
430        assert_eq!(format_param_value(&info, -0.5), "50L");
431        assert_eq!(format_param_value(&info, -1.0), "100L");
432        assert_eq!(format_param_value(&info, -0.006), "1L");
433    }
434
435    #[test]
436    fn pan_right() {
437        let info = pan_info();
438        assert_eq!(format_param_value(&info, 0.5), "50R");
439        assert_eq!(format_param_value(&info, 1.0), "100R");
440        assert_eq!(format_param_value(&info, 0.006), "1R");
441    }
442
443    fn int_info(unit: ParamUnit) -> ParamInfo {
444        ParamInfo {
445            id: 0,
446            name: "n",
447            short_name: "n",
448            group: "",
449            range: ParamRange::Discrete { min: -12, max: 12 },
450            default_plain: 0.0,
451            flags: ParamFlags::empty(),
452            unit,
453            kind: ParamValueKind::Int,
454            midi_map: None,
455            midi_channel: None,
456        }
457    }
458
459    #[test]
460    fn int_param_no_fractional_zero() {
461        // IntParam values must render with no decimal places.
462        // A hard-coded `{:.1}` formatter (regardless of param kind)
463        // would render "0.0 st" / "-5.0 st" for semitone values.
464        assert_eq!(
465            format_param_value(&int_info(ParamUnit::Semitones), 0.0),
466            "0 st"
467        );
468        assert_eq!(
469            format_param_value(&int_info(ParamUnit::Semitones), -5.0),
470            "-5 st"
471        );
472        assert_eq!(format_param_value(&int_info(ParamUnit::None), 0.0), "0");
473        assert_eq!(format_param_value(&int_info(ParamUnit::Db), 6.0), "6 dB");
474        assert_eq!(
475            format_param_value(&int_info(ParamUnit::Milliseconds), 50.0),
476            "50 ms"
477        );
478    }
479
480    fn hz_info() -> ParamInfo {
481        ParamInfo {
482            id: 0,
483            name: "Freq",
484            short_name: "f",
485            group: "",
486            range: ParamRange::Logarithmic {
487                min: 2.0,
488                max: 20_000.0,
489            },
490            default_plain: 1000.0,
491            flags: ParamFlags::empty(),
492            unit: ParamUnit::Hz,
493            kind: ParamValueKind::Float,
494            midi_map: None,
495            midi_channel: None,
496        }
497    }
498
499    #[test]
500    fn hz_khz_roundtrip_parse() {
501        let info = hz_info();
502        assert_eq!(format_param_value(&info, 1000.0), "1.0 kHz");
503        assert_eq!(parse_param_value(&info, "1.0 kHz"), Some(1000.0));
504        assert_eq!(parse_param_value(&info, "1.2 kHz"), Some(1200.0));
505        assert_eq!(parse_param_value(&info, "440 Hz"), Some(440.0));
506        assert_eq!(parse_param_value(&info, "880"), Some(880.0));
507    }
508
509    #[test]
510    fn pan_text_roundtrip_parse() {
511        let info = pan_info();
512        assert_eq!(parse_param_value(&info, "C"), Some(0.0));
513        assert_eq!(parse_param_value(&info, "50L"), Some(-0.5));
514        assert_eq!(parse_param_value(&info, "100R"), Some(1.0));
515    }
516}