1mod output;
2mod target;
3
4pub use output::{
5 MidiColorSource, MidiEffectParameter, MidiMeterOutput, MidiNamedSceneColorMapping,
6 MidiOutputBinding, MidiOutputBindingKind, MidiOutputBindingSelector, MidiSceneColorMessage,
7 MidiSceneColorMessageMap, MidiSceneColorValueOutput, MidiValueOutput, MidiValueSource,
8 MidiValueSourceKind,
9};
10pub use target::{MidiEffectTarget, MidiEffectTargetKind, MidiEncoder, MidiRelativeEncoding};
11
12use super::AssetTrait;
13use serde::{Deserialize, Serialize};
14use strum::{Display, EnumDiscriminants, EnumIter};
15
16#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
17pub struct MidiController {
18 pub mapping: MidiControllerMapping,
19}
20
21impl AssetTrait for MidiController {
22 const DIR_NAME: &'static str = "midi_controllers";
23 const NAME: &'static str = "MIDI Controller";
24 const SHOW_NAME_IF_SELECTED: bool = true;
25}
26
27#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
28pub struct MidiControllerMapping {
29 pub input_bindings: Vec<MidiInputBinding>,
30 pub output_bindings: Vec<MidiOutputBinding>,
31 pub color_mappings: Vec<MidiNamedSceneColorMapping>,
32}
33
34#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
35pub struct MidiInputBinding {
36 pub name: String,
37 pub trigger: MidiTrigger,
38 pub action: MidiInputAction,
39}
40
41impl Default for MidiInputBinding {
42 fn default() -> Self {
43 Self {
44 name: "Input".to_owned(),
45 trigger: MidiTrigger::default(),
46 action: MidiInputAction::Tap,
47 }
48 }
49}
50
51#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
52pub struct MidiTrigger {
53 pub status: u8,
54 pub data1: u8,
55 pub match_data1: bool,
56}
57
58impl Default for MidiTrigger {
59 fn default() -> Self {
60 Self {
61 status: 0,
62 data1: 0,
63 match_data1: true,
64 }
65 }
66}
67
68#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, EnumDiscriminants)]
71#[strum_discriminants(
72 name(MidiInputActionKind),
73 derive(Display, EnumIter),
74 strum(serialize_all = "title_case")
75)]
76pub enum MidiInputAction {
77 Tap,
78 SetMainDimmer,
79 #[strum_discriminants(strum(serialize = "Set Main Dimmer (Relative)"))]
81 SetMainDimmerRelative {
82 encoder: MidiEncoder,
83 },
84 SetBlackout,
85 ToggleBlackout,
87 SetSpeedAdd,
89 SetSpeedMultiply,
90 #[strum_discriminants(strum(serialize = "Set Speed (Relative)"))]
92 SetSpeedRelative {
93 encoder: MidiEncoder,
94 },
95 #[strum_discriminants(strum(serialize = "Select Scene (Distributed 0-127)"))]
96 SelectSceneDistributed,
97 #[strum_discriminants(strum(serialize = "Select Scene (Relative)"))]
99 SelectSceneRelative {
100 encoder: MidiEncoder,
101 },
102 SelectScene {
103 target: MidiSceneTarget,
104 },
105 #[strum_discriminants(strum(serialize = "Select Effect"))]
107 SelectSceneEffect {
108 index: u8,
109 },
110 #[strum_discriminants(strum(serialize = "Select Effect (Relative)"))]
112 SelectSceneEffectRelative {
113 encoder: MidiEncoder,
114 },
115 #[strum_discriminants(strum(serialize = "Select Effect (Distributed 0-127)"))]
117 SelectSceneEffectDistributed,
118 ToggleSceneActive {
119 target: MidiSceneTarget,
120 },
121 SetSceneActive {
122 target: MidiSceneTarget,
123 },
124 FlashScene {
125 target: MidiSceneTarget,
126 },
127 SetSceneOpacity {
128 target: MidiSceneTarget,
129 },
130 SetSceneInputDimmer {
131 target: MidiSceneTarget,
132 },
133 SetSceneBeatOffset {
134 target: MidiSceneTarget,
135 },
136 SetSceneIgnoreMainDimmer {
137 target: MidiSceneTarget,
138 },
139 SetSceneSetOffsetOnFlash {
140 target: MidiSceneTarget,
141 },
142 #[strum_discriminants(strum(serialize = "Set Effect Setting (n,n) f32"))]
143 SetSceneEffectSettingF32 {
144 target: MidiSceneTarget,
145 #[serde(rename = "effect_index")]
146 effect: MidiEffectTarget,
147 setting_index: u8,
148 },
149 #[strum_discriminants(strum(serialize = "Set Effect Opacity"))]
150 SetSceneEffectOpacity {
151 target: MidiSceneTarget,
152 #[serde(rename = "effect_index")]
153 effect: MidiEffectTarget,
154 },
155 #[strum_discriminants(strum(serialize = "Set Effect Color Shift"))]
156 SetSceneEffectColorShift {
157 target: MidiSceneTarget,
158 #[serde(rename = "effect_index")]
159 effect: MidiEffectTarget,
160 },
161 #[strum_discriminants(strum(serialize = "Set Effect Beat Progression"))]
162 SetSceneEffectBeatProgression {
163 target: MidiSceneTarget,
164 #[serde(rename = "effect_index")]
165 effect: MidiEffectTarget,
166 },
167 #[strum_discriminants(strum(serialize = "Set Effect Beat Offset"))]
168 SetSceneEffectBeatOffset {
169 target: MidiSceneTarget,
170 #[serde(rename = "effect_index")]
171 effect: MidiEffectTarget,
172 },
173 #[strum_discriminants(strum(serialize = "Set Effect Speed Exponent"))]
174 SetSceneEffectSpeedExponent {
175 target: MidiSceneTarget,
176 #[serde(rename = "effect_index")]
177 effect: MidiEffectTarget,
178 },
179 #[serde(alias = "SetSceneEffectGroupIndex")]
183 #[strum_discriminants(strum(serialize = "Set Effect Layout"))]
184 SetSceneEffectLayout {
185 target: MidiSceneTarget,
186 #[serde(rename = "effect_index")]
187 effect: MidiEffectTarget,
188 },
189}
190
191#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default, EnumDiscriminants)]
194#[strum_discriminants(
195 name(MidiSceneTargetKind),
196 derive(Display, EnumIter),
197 strum(serialize_all = "title_case")
198)]
199pub enum MidiSceneTarget {
200 #[default]
201 #[strum_discriminants(strum(serialize = "Selected Scene"))]
202 Selected,
203 #[strum_discriminants(strum(serialize = "Quick Scene"))]
204 Quick { index: u8 },
205 #[strum_discriminants(strum(serialize = "Grid Scene"))]
206 Grid { row: u8, col: u8 },
207}
208pub fn normalize_controller_key(port_name: &str) -> String {
214 let trimmed = strip_alsa_address(port_name.trim());
215 if let Some(prefix) = trimmed.strip_suffix(']')
216 && let Some((base, suffix)) = prefix.rsplit_once(" [")
217 && !base.is_empty()
218 && !suffix.is_empty()
219 && suffix.chars().all(|ch| ch.is_ascii_digit())
220 {
221 return base.to_owned();
222 }
223
224 trimmed.to_owned()
225}
226
227fn strip_alsa_address(name: &str) -> &str {
229 let Some((base, address)) = name.rsplit_once(' ') else {
230 return name;
231 };
232 let is_number = |part: &str| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit());
233 match address.split_once(':') {
234 Some((client, port)) if !base.is_empty() && is_number(client) && is_number(port) => {
235 base.trim_end()
236 }
237 _ => name,
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn a_full_meter_lights_all_twelve_leds_of_its_strip() {
247 let meter = MidiMeterOutput {
248 strip: 2,
249 source: MidiValueSource::default(),
250 };
251 assert_eq!(meter.message(1.0), [0xD0, 0x2C]);
252 assert_eq!(meter.message(0.0), [0xD0, 0x20]);
253 assert_eq!(meter.message(2.0), [0xD0, 0x2C]);
254 }
255
256 #[test]
257 fn alsa_addresses_are_not_part_of_the_controller_key() {
258 assert_eq!(
259 normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 32:0"),
260 "iCON P1-X1:iCON P1-X1 MIDI 1"
261 );
262 assert_eq!(
263 normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 36:0"),
264 normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 32:0")
265 );
266 }
267
268 #[test]
269 fn numbered_port_names_keep_their_number() {
270 assert_eq!(normalize_controller_key("APC40 [2]"), "APC40");
271 assert_eq!(normalize_controller_key("Port 2"), "Port 2");
272 }
273
274 #[test]
277 fn group_index_bindings_saved_before_the_patch_load_as_layout_bindings() {
278 let action: MidiInputAction = serde_json::from_str(
279 r#"{"SetSceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
280 )
281 .expect("an old group index action must load");
282 assert_eq!(
283 action,
284 MidiInputAction::SetSceneEffectLayout {
285 target: MidiSceneTarget::Selected,
286 effect: MidiEffectTarget::Index { index: 1 },
287 }
288 );
289
290 let source: MidiValueSource = serde_json::from_str(
291 r#"{"SceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
292 )
293 .expect("an old group index feedback must load");
294 assert_eq!(
295 source,
296 MidiValueSource::SceneEffectLayout {
297 target: MidiSceneTarget::Selected,
298 effect: MidiEffectTarget::Index { index: 1 },
299 }
300 );
301 }
302
303 #[test]
306 fn kinds_read_as_the_controller_window_labels() {
307 use strum::IntoEnumIterator;
308 let actions: Vec<String> = MidiInputActionKind::iter().map(|k| k.to_string()).collect();
309 assert_eq!(
310 actions,
311 [
312 "Tap",
313 "Set Main Dimmer",
314 "Set Main Dimmer (Relative)",
315 "Set Blackout",
316 "Toggle Blackout",
317 "Set Speed Add",
318 "Set Speed Multiply",
319 "Set Speed (Relative)",
320 "Select Scene (Distributed 0-127)",
321 "Select Scene (Relative)",
322 "Select Scene",
323 "Select Effect",
324 "Select Effect (Relative)",
325 "Select Effect (Distributed 0-127)",
326 "Toggle Scene Active",
327 "Set Scene Active",
328 "Flash Scene",
329 "Set Scene Opacity",
330 "Set Scene Input Dimmer",
331 "Set Scene Beat Offset",
332 "Set Scene Ignore Main Dimmer",
333 "Set Scene Set Offset On Flash",
334 "Set Effect Setting (n,n) f32",
335 "Set Effect Opacity",
336 "Set Effect Color Shift",
337 "Set Effect Beat Progression",
338 "Set Effect Beat Offset",
339 "Set Effect Speed Exponent",
340 "Set Effect Layout",
341 ]
342 );
343 let sources: Vec<String> = MidiValueSourceKind::iter().map(|k| k.to_string()).collect();
344 assert_eq!(
345 sources,
346 [
347 "Selected Scene Opacity",
348 "Selected Scene (Distributed 0-127)",
349 "Selected Effect (Distributed 0-127)",
350 "Selected Scene Opacity Live",
351 "Effect Live Value",
352 "Effect Selected",
353 "Selected Scene Input Dimmer",
354 "Selected Scene Beat Offset",
355 "Selected Scene Ignore Main Dimmer",
356 "Selected Scene Set Offset On Flash",
357 "Main Dimmer",
358 "Beat Flank",
359 "Blackout",
360 "Scene Opacity",
361 "Scene Input Dimmer",
362 "Scene Beat Offset",
363 "Scene Ignore Main Dimmer",
364 "Scene Set Offset On Flash",
365 "Scene Active",
366 "Scene Flashed",
367 "Scene Effect Setting (n,n) f32",
368 "Effect Opacity",
369 "Effect Color Shift",
370 "Effect Beat Progression",
371 "Effect Beat Offset",
372 "Effect Speed Exponent",
373 "Effect Layout",
374 ]
375 );
376 let targets: Vec<String> = MidiSceneTargetKind::iter().map(|k| k.to_string()).collect();
377 assert_eq!(targets, ["Selected Scene", "Quick Scene", "Grid Scene"]);
378 let effects: Vec<String> = MidiEffectTargetKind::iter()
379 .map(|k| k.to_string())
380 .collect();
381 assert_eq!(effects, ["Selected Effect", "Effect Index"]);
382 let outputs: Vec<String> = MidiOutputBindingSelector::iter()
383 .map(|k| k.to_string())
384 .collect();
385 assert_eq!(
386 outputs,
387 ["Single Value", "Scene Color Mapping", "Mackie Meter"]
388 );
389 assert_eq!(MidiColorSource::default().to_string(), "Scene Color");
390 }
391}