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}