truce_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 /// Default host MIDI-learn binding: the MIDI message that should
25 /// drive this parameter. `None` (the common case) means the host
26 /// maps it itself with no plugin hint. Set by `#[param(midi_cc =
27 /// …)]` / `#[param(midi_source = …)]`; read by the VST3
28 /// `IMidiMapping`, AU parameter-MIDI-mapping, and LV2 `midi:binding`
29 /// paths. Ignored by CLAP / VST2 / AAX (the host owns the mapping).
30 pub midi_map: Option<MidiSource>,
31 /// Optional channel scope for [`Self::midi_map`], as the wire
32 /// channel `0..=15`. `None` matches any channel.
33 pub midi_channel: Option<u8>,
34}
35
36/// The MIDI message a parameter binds to for host MIDI-learn. CCs cover
37/// the common case; the non-CC per-channel messages each map onto a
38/// VST3 `ControllerNumbers` value, an AU status byte, and an LV2
39/// `midi:binding` class.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum MidiSource {
42 /// Control change, `0..=127`.
43 Cc(u8),
44 /// Pitch bend.
45 PitchBend,
46 /// Channel pressure (mono aftertouch).
47 ChannelPressure,
48 /// Program change.
49 ProgramChange,
50}
51
52/// Resolve which parameter a MIDI `source` on `channel` is bound to,
53/// from a param-info list. The first param whose binding matches the
54/// source and whose channel scope is unset (any) or equal wins;
55/// `#[derive(Params)]` rejects ambiguous overlaps at compile time, so
56/// at most one matches. Used by the format wrappers' mapping paths.
57#[must_use]
58pub fn map_source_to_param(infos: &[ParamInfo], channel: u8, source: MidiSource) -> Option<u32> {
59 infos
60 .iter()
61 .find(|p| p.midi_map == Some(source) && p.midi_channel.is_none_or(|ch| ch == channel))
62 .map(|p| p.id)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 fn info(id: u32, map: Option<MidiSource>, channel: Option<u8>) -> ParamInfo {
70 ParamInfo {
71 id,
72 name: "p",
73 short_name: "p",
74 group: "",
75 range: ParamRange::Linear { min: 0.0, max: 1.0 },
76 default_plain: 0.0,
77 flags: ParamFlags::AUTOMATABLE,
78 unit: ParamUnit::None,
79 kind: ParamValueKind::Float,
80 midi_map: map,
81 midi_channel: channel,
82 }
83 }
84
85 #[test]
86 fn resolves_cc_any_and_scoped_channel() {
87 let infos = [
88 info(1, Some(MidiSource::Cc(74)), None), // any channel
89 info(2, Some(MidiSource::Cc(71)), Some(0)), // channel 1 (wire 0)
90 info(3, Some(MidiSource::PitchBend), None),
91 ];
92 // Any-channel CC matches whatever channel.
93 assert_eq!(map_source_to_param(&infos, 5, MidiSource::Cc(74)), Some(1));
94 // Scoped CC matches only its channel.
95 assert_eq!(map_source_to_param(&infos, 0, MidiSource::Cc(71)), Some(2));
96 assert_eq!(map_source_to_param(&infos, 1, MidiSource::Cc(71)), None);
97 // Non-CC source resolves by kind.
98 assert_eq!(
99 map_source_to_param(&infos, 9, MidiSource::PitchBend),
100 Some(3)
101 );
102 // Unmapped source/CC returns None.
103 assert_eq!(map_source_to_param(&infos, 0, MidiSource::Cc(7)), None);
104 }
105}
106
107/// Which strongly-typed `*Param` constructor produced this
108/// [`ParamInfo`]. The `#[derive(Params)]` macro sets it from the
109/// field type so format-side code can branch on the original
110/// typing without re-deriving it from `range` / `unit`.
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum ParamValueKind {
113 Float,
114 Int,
115 Bool,
116 Enum,
117}
118
119bitflags::bitflags! {
120 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
121 pub struct ParamFlags: u32 {
122 const AUTOMATABLE = 0b0_0001;
123 const HIDDEN = 0b0_0010;
124 const READONLY = 0b0_0100;
125 const IS_BYPASS = 0b0_1000;
126 /// This parameter participates in sample-accurate sub-block
127 /// chunking: a `ParamChange` event targeting it splits the
128 /// audio block at its `sample_offset`. Defaults on; cleared
129 /// by `#[param(chunk = false)]` on expensive-to-retarget
130 /// params (FFT sizes, lookahead, etc.) where the per-event
131 /// fixed cost of subdividing the block outweighs the
132 /// sample-accuracy win. Read by
133 /// `truce_core::chunked_process::is_split_event`.
134 const CHUNKED = 0b1_0000;
135 }
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum ParamUnit {
140 None,
141 Db,
142 Hz,
143 Milliseconds,
144 Seconds,
145 Percent,
146 Semitones,
147 Pan,
148 Degrees,
149}
150
151impl ParamUnit {
152 /// Format-agnostic unit string for host display.
153 #[must_use]
154 pub fn as_str(&self) -> &'static str {
155 match self {
156 Self::Db => "dB",
157 Self::Hz => "Hz",
158 Self::Milliseconds => "ms",
159 Self::Seconds => "s",
160 Self::Percent => "%",
161 Self::Semitones => "st",
162 Self::Degrees => "°",
163 Self::Pan | Self::None => "",
164 }
165 }
166}