Skip to main content

gled_model/asset/midi_controller/
target.rs

1//! Which effect a binding works on, and how an endless encoder reports a
2//! turn.
3
4use serde::{Deserialize, Serialize};
5use strum::{Display, EnumDiscriminants, EnumIter};
6
7/// The effect of a scene a binding works on: the one selected in the
8/// interface, or a fixed position in the scene's effect stack.
9///
10/// A fixed position is saved as the plain index it always was, so
11/// controllers saved before the selected effect existed load unchanged.
12#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default, EnumDiscriminants)]
13#[serde(from = "MidiEffectTargetRepr", into = "MidiEffectTargetRepr")]
14#[strum_discriminants(name(MidiEffectTargetKind), derive(Display, EnumIter))]
15pub enum MidiEffectTarget {
16    #[default]
17    #[strum_discriminants(strum(serialize = "Selected Effect"))]
18    Selected,
19    #[strum_discriminants(strum(serialize = "Effect Index"))]
20    Index { index: u8 },
21}
22
23impl MidiEffectTarget {
24    /// The effect's position in the stack, given the selected one.
25    pub fn resolve(&self, selected_effect: usize) -> usize {
26        match self {
27            Self::Selected => selected_effect,
28            Self::Index { index } => usize::from(*index),
29        }
30    }
31}
32
33/// The saved form: a bare number for an index, `"Selected"` otherwise.
34#[derive(Serialize, Deserialize)]
35#[serde(untagged)]
36enum MidiEffectTargetRepr {
37    Index(u8),
38    Named(MidiEffectTargetName),
39}
40
41#[derive(Serialize, Deserialize)]
42enum MidiEffectTargetName {
43    Selected,
44}
45
46impl From<MidiEffectTargetRepr> for MidiEffectTarget {
47    fn from(repr: MidiEffectTargetRepr) -> Self {
48        match repr {
49            MidiEffectTargetRepr::Index(index) => Self::Index { index },
50            MidiEffectTargetRepr::Named(MidiEffectTargetName::Selected) => Self::Selected,
51        }
52    }
53}
54
55impl From<MidiEffectTarget> for MidiEffectTargetRepr {
56    fn from(target: MidiEffectTarget) -> Self {
57        match target {
58            MidiEffectTarget::Selected => Self::Named(MidiEffectTargetName::Selected),
59            MidiEffectTarget::Index { index } => Self::Index(index),
60        }
61    }
62}
63
64/// An endless encoder: how it encodes a turn, and how many of its steps
65/// make one unit of what it changes (one scene, one effect, one BPM, 1/127
66/// of the dimmer).
67#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
68pub struct MidiEncoder {
69    pub encoding: MidiRelativeEncoding,
70    pub steps_per_unit: u8,
71}
72
73impl Default for MidiEncoder {
74    fn default() -> Self {
75        Self {
76            encoding: MidiRelativeEncoding::default(),
77            steps_per_unit: 1,
78        }
79    }
80}
81
82/// How an endless encoder encodes a turn in the value byte. Both send
83/// 1–63 for a clockwise turn, by that many steps; they differ in the
84/// counter-clockwise direction.
85#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default, Display, EnumIter)]
86pub enum MidiRelativeEncoding {
87    /// 127 is one step counter-clockwise, 126 two, down to 65 (APC40 and
88    /// similar).
89    #[default]
90    #[strum(serialize = "Two's Complement")]
91    TwosComplement,
92    /// 65 is one step counter-clockwise, 66 two, up to 127 (Mackie Control
93    /// encoders, such as the iCON P1-X).
94    #[strum(serialize = "Sign Magnitude")]
95    SignMagnitude,
96}
97
98impl MidiRelativeEncoding {
99    /// The steps a value byte stands for, clockwise positive.
100    pub fn steps(self, value: u8) -> i32 {
101        let value = i32::from(value);
102        if value < 64 {
103            return value;
104        }
105        match self {
106            Self::TwosComplement => value - 128,
107            Self::SignMagnitude => 64 - value,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn a_fixed_index_is_saved_as_the_plain_number_it_was() {
118        let target = MidiEffectTarget::Index { index: 3 };
119        assert_eq!(serde_json::to_string(&target).expect("serializes"), "3");
120        assert_eq!(
121            serde_json::from_str::<MidiEffectTarget>("3").expect("loads"),
122            target
123        );
124    }
125
126    #[test]
127    fn the_selected_effect_is_saved_by_name() {
128        assert_eq!(
129            serde_json::to_string(&MidiEffectTarget::Selected).expect("serializes"),
130            "\"Selected\""
131        );
132        assert_eq!(
133            serde_json::from_str::<MidiEffectTarget>("\"Selected\"").expect("loads"),
134            MidiEffectTarget::Selected
135        );
136    }
137
138    #[test]
139    fn encodings_agree_on_clockwise_turns() {
140        for encoding in [
141            MidiRelativeEncoding::TwosComplement,
142            MidiRelativeEncoding::SignMagnitude,
143        ] {
144            assert_eq!(encoding.steps(0), 0, "{encoding}");
145            assert_eq!(encoding.steps(1), 1, "{encoding}");
146            assert_eq!(encoding.steps(7), 7, "{encoding}");
147        }
148    }
149
150    #[test]
151    fn twos_complement_counts_down_from_127() {
152        assert_eq!(MidiRelativeEncoding::TwosComplement.steps(127), -1);
153        assert_eq!(MidiRelativeEncoding::TwosComplement.steps(125), -3);
154    }
155
156    #[test]
157    fn sign_magnitude_counts_up_from_65() {
158        assert_eq!(MidiRelativeEncoding::SignMagnitude.steps(65), -1);
159        assert_eq!(MidiRelativeEncoding::SignMagnitude.steps(67), -3);
160    }
161}