Skip to main content

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}