Skip to main content

aura_params/
info.rs

1use crate::range::ParamRange;
2
3/// Metadata for a single parameter, used by format wrappers.
4///
5/// `Copy` because every field is POD (`&'static str`, scalars,
6/// bitflags, the [`ParamRange`] / [`ParamUnit`] enums). Lets the
7/// audio path pass `param_infos[i]` by value without `clone()` noise.
8#[derive(Clone, Copy, Debug)]
9pub struct ParamInfo {
10    pub id: u32,
11    pub name: &'static str,
12    pub short_name: &'static str,
13    pub group: &'static str,
14    pub range: ParamRange,
15    pub default_plain: f64,
16    pub flags: ParamFlags,
17    pub unit: ParamUnit,
18    /// Which `*Param` type backs this entry. Drives display rounding
19    /// (`IntParam` skips fractional digits) and `value_text` parsing,
20    /// independently of [`ParamRange`] - a `FloatParam` declared with
21    /// `range = "discrete(...)"` should still format as a float, so
22    /// inferring kind from range alone is wrong.
23    pub kind: ParamValueKind,
24    /// Optional MIDI-learn **hint** (`#[param(midi_cc = …)]` /
25    /// `midi_source` / `midi_channel`). Stored on the info list for
26    /// tooling / future use. **Not consumed** by CLAP / VST3 / LV2
27    /// wrappers today — hosts own MIDI mapping.
28    pub midi_map: Option<MidiSource>,
29    /// Optional channel scope for [`Self::midi_map`], wire channel
30    /// `0..=15`. `None` = any channel.
31    pub midi_channel: Option<u8>,
32}
33
34/// MIDI message kind for [`ParamInfo::midi_map`] hints.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum MidiSource {
37    /// Control change, `0..=127`.
38    Cc(u8),
39    /// Pitch bend.
40    PitchBend,
41    /// Channel pressure (mono aftertouch).
42    ChannelPressure,
43    /// Program change.
44    ProgramChange,
45}
46
47/// Resolve which parameter a MIDI `source` on `channel` is bound to
48/// from a param-info list (first match; derive rejects overlaps).
49/// Helper for tooling — format wrappers do not call this.
50#[must_use]
51pub fn map_source_to_param(infos: &[ParamInfo], channel: u8, source: MidiSource) -> Option<u32> {
52    infos
53        .iter()
54        .find(|p| p.midi_map == Some(source) && p.midi_channel.is_none_or(|ch| ch == channel))
55        .map(|p| p.id)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    fn info(id: u32, map: Option<MidiSource>, channel: Option<u8>) -> ParamInfo {
63        ParamInfo {
64            id,
65            name: "p",
66            short_name: "p",
67            group: "",
68            range: ParamRange::Linear { min: 0.0, max: 1.0 },
69            default_plain: 0.0,
70            flags: ParamFlags::AUTOMATABLE,
71            unit: ParamUnit::None,
72            kind: ParamValueKind::Float,
73            midi_map: map,
74            midi_channel: channel,
75        }
76    }
77
78    #[test]
79    fn resolves_cc_any_and_scoped_channel() {
80        let infos = [
81            info(1, Some(MidiSource::Cc(74)), None),    // any channel
82            info(2, Some(MidiSource::Cc(71)), Some(0)), // channel 1 (wire 0)
83            info(3, Some(MidiSource::PitchBend), None),
84        ];
85        // Any-channel CC matches whatever channel.
86        assert_eq!(map_source_to_param(&infos, 5, MidiSource::Cc(74)), Some(1));
87        // Scoped CC matches only its channel.
88        assert_eq!(map_source_to_param(&infos, 0, MidiSource::Cc(71)), Some(2));
89        assert_eq!(map_source_to_param(&infos, 1, MidiSource::Cc(71)), None);
90        // Non-CC source resolves by kind.
91        assert_eq!(
92            map_source_to_param(&infos, 9, MidiSource::PitchBend),
93            Some(3)
94        );
95        // Unmapped source/CC returns None.
96        assert_eq!(map_source_to_param(&infos, 0, MidiSource::Cc(7)), None);
97    }
98}
99
100/// Which strongly-typed `*Param` constructor produced this
101/// [`ParamInfo`]. The `#[derive(Params)]` macro sets it from the
102/// field type so format-side code can branch on the original
103/// typing without re-deriving it from `range` / `unit`.
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum ParamValueKind {
106    Float,
107    Int,
108    Bool,
109    Enum,
110}
111
112bitflags::bitflags! {
113    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
114    pub struct ParamFlags: u32 {
115        const AUTOMATABLE = 0b0_0001;
116        const HIDDEN      = 0b0_0010;
117        const READONLY    = 0b0_0100;
118        const IS_BYPASS   = 0b0_1000;
119        /// Sample-accurate sub-block chunking: a host automation or
120        /// mono-mod event targeting this param splits the audio block
121        /// at its sample offset (see `aura_core::chunked_process`).
122        /// Defaults on; clear with `#[param(chunk = false)]` for
123        /// expensive-to-retarget params (FFT size, lookahead, …).
124        const CHUNKED     = 0b1_0000;
125        /// Host may modulate this parameter (CLAP
126        /// `CLAP_PARAM_IS_MODULATABLE` + `CLAP_EVENT_PARAM_MOD`).
127        /// Opt-in via `#[param(flags = "modulatable")]`. Effective DSP
128        /// value is `clamp(base + mod)`; host UI still shows base.
129        const MODULATABLE = 0b10_0000;
130        /// Per-note-id (polyphonic) modulation: implies
131        /// [`Self::MODULATABLE`] and maps to
132        /// `CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID`. Mono `PARAM_MOD`
133        /// (`note_id < 0`) still hits [`crate::Params::set_mod`];
134        /// per-note mods arrive on `ProcessContext.notes`.
135        const MODULATABLE_PER_NOTE = 0b100_0000;
136    }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum ParamUnit {
141    None,
142    Db,
143    Hz,
144    Milliseconds,
145    Seconds,
146    Percent,
147    Semitones,
148    Pan,
149    Degrees,
150}
151
152impl ParamUnit {
153    /// Format-agnostic unit string for host display.
154    #[must_use]
155    pub fn as_str(&self) -> &'static str {
156        match self {
157            Self::Db => "dB",
158            Self::Hz => "Hz",
159            Self::Milliseconds => "ms",
160            Self::Seconds => "s",
161            Self::Percent => "%",
162            Self::Semitones => "st",
163            Self::Degrees => "°",
164            Self::Pan | Self::None => "",
165        }
166    }
167}