1mod control;
2mod mode;
3mod output;
4mod target;
5
6pub use control::MidiControl;
7pub use mode::{MidiMode, MidiModeHighlight, binding_in_mode};
8pub use output::{
9 MidiColorSource, MidiEffectParameter, MidiMeterOutput, MidiNamedSceneColorMapping,
10 MidiOutputBinding, MidiOutputBindingKind, MidiOutputBindingSelector, MidiSceneColorMessage,
11 MidiSceneColorMessageMap, MidiSceneColorValueOutput, MidiValueOutput, MidiValueSource,
12 MidiValueSourceKind,
13};
14pub use target::{MidiEffectTarget, MidiEffectTargetKind, MidiEncoder, MidiRelativeEncoding};
15
16use super::AssetTrait;
17use serde::{Deserialize, Serialize};
18use strum::{Display, EnumDiscriminants, EnumIter};
19
20#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
21pub struct MidiController {
22 pub mapping: MidiControllerMapping,
23 #[serde(default)]
26 pub default_for_ports: Vec<String>,
27}
28
29impl AssetTrait for MidiController {
30 const DIR_NAME: &'static str = "midi_controllers";
31 const NAME: &'static str = "MIDI Controller";
32 const SHOW_NAME_IF_SELECTED: bool = true;
33}
34
35#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
36pub struct MidiControllerMapping {
37 pub input_bindings: Vec<MidiInputBinding>,
38 pub output_bindings: Vec<MidiOutputBinding>,
39 pub color_mappings: Vec<MidiNamedSceneColorMapping>,
40 #[serde(default)]
44 pub controls: Vec<MidiControl>,
45 #[serde(default)]
48 pub modes: Vec<MidiMode>,
49}
50
51#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
52pub struct MidiInputBinding {
53 pub name: String,
54 pub trigger: MidiTrigger,
55 pub action: MidiInputAction,
56 #[serde(default)]
58 pub mode: Option<u8>,
59 #[serde(default)]
63 pub touch: Option<MidiTrigger>,
64}
65
66impl Default for MidiInputBinding {
67 fn default() -> Self {
68 Self {
69 name: "Input".to_owned(),
70 trigger: MidiTrigger::default(),
71 action: MidiInputAction::Tap,
72 mode: None,
73 touch: None,
74 }
75 }
76}
77
78#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
79pub struct MidiTrigger {
80 pub status: u8,
81 pub data1: u8,
82 pub match_data1: bool,
83}
84
85impl Default for MidiTrigger {
86 fn default() -> Self {
87 Self {
88 status: 0,
89 data1: 0,
90 match_data1: true,
91 }
92 }
93}
94
95#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, EnumDiscriminants)]
98#[strum_discriminants(
99 name(MidiInputActionKind),
100 derive(Display, EnumIter),
101 strum(serialize_all = "title_case")
102)]
103pub enum MidiInputAction {
104 Tap,
105 SetMainDimmer,
106 #[strum_discriminants(strum(serialize = "Set Main Dimmer (Relative)"))]
108 SetMainDimmerRelative {
109 encoder: MidiEncoder,
110 },
111 SetBlackout,
112 ToggleBlackout,
114 SetSpeedAdd,
116 SetSpeedMultiply,
117 #[strum_discriminants(strum(serialize = "Set Speed (Relative)"))]
119 SetSpeedRelative {
120 encoder: MidiEncoder,
121 },
122 #[strum_discriminants(strum(serialize = "Select Scene (Distributed 0-127)"))]
123 SelectSceneDistributed,
124 #[strum_discriminants(strum(serialize = "Select Scene (Relative)"))]
126 SelectSceneRelative {
127 encoder: MidiEncoder,
128 },
129 #[strum_discriminants(strum(serialize = "Select Row (Relative)"))]
133 SelectSceneRow {
134 encoder: MidiEncoder,
135 },
136 #[strum_discriminants(strum(serialize = "Select Column (Relative)"))]
140 SelectSceneColumn {
141 encoder: MidiEncoder,
142 },
143 SelectScene {
144 target: MidiSceneTarget,
145 },
146 SelectMode {
148 mode: u8,
149 },
150 #[strum_discriminants(strum(serialize = "Select Effect"))]
152 SelectSceneEffect {
153 index: u8,
154 },
155 #[strum_discriminants(strum(serialize = "Select Effect (Relative)"))]
157 SelectSceneEffectRelative {
158 encoder: MidiEncoder,
159 },
160 #[strum_discriminants(strum(serialize = "Select Effect (Distributed 0-127)"))]
162 SelectSceneEffectDistributed,
163 ToggleSceneActive {
164 target: MidiSceneTarget,
165 },
166 SetSceneActive {
167 target: MidiSceneTarget,
168 },
169 FlashScene {
170 target: MidiSceneTarget,
171 },
172 SetSceneOpacity {
173 target: MidiSceneTarget,
174 },
175 SetSceneInputDimmer {
176 target: MidiSceneTarget,
177 },
178 SetSceneBeatOffset {
179 target: MidiSceneTarget,
180 },
181 SetSceneIgnoreMainDimmer {
182 target: MidiSceneTarget,
183 },
184 SetSceneSetOffsetOnFlash {
185 target: MidiSceneTarget,
186 },
187 #[strum_discriminants(strum(serialize = "Set Effect Setting (n,n) f32"))]
188 SetSceneEffectSettingF32 {
189 target: MidiSceneTarget,
190 #[serde(rename = "effect_index")]
191 effect: MidiEffectTarget,
192 setting_index: u8,
193 },
194 #[strum_discriminants(strum(serialize = "Set Effect Opacity"))]
195 SetSceneEffectOpacity {
196 target: MidiSceneTarget,
197 #[serde(rename = "effect_index")]
198 effect: MidiEffectTarget,
199 },
200 #[strum_discriminants(strum(serialize = "Set Effect Color Shift"))]
201 SetSceneEffectColorShift {
202 target: MidiSceneTarget,
203 #[serde(rename = "effect_index")]
204 effect: MidiEffectTarget,
205 },
206 #[strum_discriminants(strum(serialize = "Set Effect Beat Progression"))]
207 SetSceneEffectBeatProgression {
208 target: MidiSceneTarget,
209 #[serde(rename = "effect_index")]
210 effect: MidiEffectTarget,
211 },
212 #[strum_discriminants(strum(serialize = "Set Effect Beat Offset"))]
213 SetSceneEffectBeatOffset {
214 target: MidiSceneTarget,
215 #[serde(rename = "effect_index")]
216 effect: MidiEffectTarget,
217 },
218 #[strum_discriminants(strum(serialize = "Set Effect Speed Exponent"))]
219 SetSceneEffectSpeedExponent {
220 target: MidiSceneTarget,
221 #[serde(rename = "effect_index")]
222 effect: MidiEffectTarget,
223 },
224 #[serde(alias = "SetSceneEffectGroupIndex")]
228 #[strum_discriminants(strum(serialize = "Set Effect Layout"))]
229 SetSceneEffectLayout {
230 target: MidiSceneTarget,
231 #[serde(rename = "effect_index")]
232 effect: MidiEffectTarget,
233 },
234}
235
236#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default, EnumDiscriminants)]
239#[strum_discriminants(
240 name(MidiSceneTargetKind),
241 derive(Display, EnumIter),
242 strum(serialize_all = "title_case")
243)]
244pub enum MidiSceneTarget {
245 #[default]
246 #[strum_discriminants(strum(serialize = "Selected Scene"))]
247 Selected,
248 #[strum_discriminants(strum(serialize = "Quick Scene"))]
249 Quick { index: u8 },
250 #[strum_discriminants(strum(serialize = "Grid Scene"))]
251 Grid { row: u8, col: u8 },
252 #[strum_discriminants(strum(serialize = "Row Scene"))]
254 Row { col: u8 },
255 #[strum_discriminants(strum(serialize = "Column Scene"))]
257 Column { row: u8 },
258}
259pub fn normalize_controller_key(port_name: &str) -> String {
265 let trimmed = strip_alsa_address(port_name.trim());
266 if let Some(prefix) = trimmed.strip_suffix(']')
267 && let Some((base, suffix)) = prefix.rsplit_once(" [")
268 && !base.is_empty()
269 && !suffix.is_empty()
270 && suffix.chars().all(|ch| ch.is_ascii_digit())
271 {
272 return base.to_owned();
273 }
274
275 trimmed.to_owned()
276}
277
278fn strip_alsa_address(name: &str) -> &str {
280 let Some((base, address)) = name.rsplit_once(' ') else {
281 return name;
282 };
283 let is_number = |part: &str| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit());
284 match address.split_once(':') {
285 Some((client, port)) if !base.is_empty() && is_number(client) && is_number(port) => {
286 base.trim_end()
287 }
288 _ => name,
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn a_full_meter_lights_all_twelve_leds_of_its_strip() {
298 let meter = MidiMeterOutput {
299 strip: 2,
300 source: MidiValueSource::default(),
301 };
302 assert_eq!(meter.message(1.0), [0xD0, 0x2C]);
303 assert_eq!(meter.message(0.0), [0xD0, 0x20]);
304 assert_eq!(meter.message(2.0), [0xD0, 0x2C]);
305 }
306
307 #[test]
308 fn alsa_addresses_are_not_part_of_the_controller_key() {
309 assert_eq!(
310 normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 32:0"),
311 "iCON P1-X1:iCON P1-X1 MIDI 1"
312 );
313 assert_eq!(
314 normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 36:0"),
315 normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 32:0")
316 );
317 }
318
319 #[test]
320 fn numbered_port_names_keep_their_number() {
321 assert_eq!(normalize_controller_key("APC40 [2]"), "APC40");
322 assert_eq!(normalize_controller_key("Port 2"), "Port 2");
323 }
324
325 #[test]
328 fn group_index_bindings_saved_before_the_patch_load_as_layout_bindings() {
329 let action: MidiInputAction = serde_json::from_str(
330 r#"{"SetSceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
331 )
332 .expect("an old group index action must load");
333 assert_eq!(
334 action,
335 MidiInputAction::SetSceneEffectLayout {
336 target: MidiSceneTarget::Selected,
337 effect: MidiEffectTarget::Index { index: 1 },
338 }
339 );
340
341 let source: MidiValueSource = serde_json::from_str(
342 r#"{"SceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
343 )
344 .expect("an old group index feedback must load");
345 assert_eq!(
346 source,
347 MidiValueSource::SceneEffectLayout {
348 target: MidiSceneTarget::Selected,
349 effect: MidiEffectTarget::Index { index: 1 },
350 }
351 );
352 }
353
354 #[test]
358 fn mappings_saved_before_named_controls_load() {
359 let mapping: MidiControllerMapping = serde_json::from_str(
360 r#"{
361 "input_bindings": [],
362 "output_bindings": [{
363 "name": "Tap light",
364 "kind": {"Value": {"status": 144, "data1": 82, "min": 0, "max": 127,
365 "active_value": 127, "source": {"BeatFlankPulse": {
366 "start_beat": 0.0, "end_beat": 0.5, "blackout_blink_value": null}}}}
367 }],
368 "color_mappings": []
369 }"#,
370 )
371 .expect("an old mapping must load");
372 assert!(mapping.controls.is_empty());
373 assert_eq!(
374 mapping.output_bindings[0].kind.source(),
375 Some(&MidiValueSource::BeatFlankPulse {
376 start_beat: 0.0,
377 end_beat: 0.5,
378 blackout_blink_value: None,
379 every_beat: false,
380 })
381 );
382 }
383
384 #[test]
387 fn kinds_read_as_the_controller_window_labels() {
388 use strum::IntoEnumIterator;
389 let actions: Vec<String> = MidiInputActionKind::iter().map(|k| k.to_string()).collect();
390 assert_eq!(
391 actions,
392 [
393 "Tap",
394 "Set Main Dimmer",
395 "Set Main Dimmer (Relative)",
396 "Set Blackout",
397 "Toggle Blackout",
398 "Set Speed Add",
399 "Set Speed Multiply",
400 "Set Speed (Relative)",
401 "Select Scene (Distributed 0-127)",
402 "Select Scene (Relative)",
403 "Select Row (Relative)",
404 "Select Column (Relative)",
405 "Select Scene",
406 "Select Mode",
407 "Select Effect",
408 "Select Effect (Relative)",
409 "Select Effect (Distributed 0-127)",
410 "Toggle Scene Active",
411 "Set Scene Active",
412 "Flash Scene",
413 "Set Scene Opacity",
414 "Set Scene Input Dimmer",
415 "Set Scene Beat Offset",
416 "Set Scene Ignore Main Dimmer",
417 "Set Scene Set Offset On Flash",
418 "Set Effect Setting (n,n) f32",
419 "Set Effect Opacity",
420 "Set Effect Color Shift",
421 "Set Effect Beat Progression",
422 "Set Effect Beat Offset",
423 "Set Effect Speed Exponent",
424 "Set Effect Layout",
425 ]
426 );
427 let sources: Vec<String> = MidiValueSourceKind::iter().map(|k| k.to_string()).collect();
428 assert_eq!(
429 sources,
430 [
431 "Selected Scene Opacity",
432 "Selected Scene (Distributed 0-127)",
433 "Selected Effect (Distributed 0-127)",
434 "Selected Scene Opacity Live",
435 "Effect Live Value",
436 "Effect Selected",
437 "Selected Scene Input Dimmer",
438 "Selected Scene Beat Offset",
439 "Selected Scene Ignore Main Dimmer",
440 "Selected Scene Set Offset On Flash",
441 "Main Dimmer",
442 "Beat Flank",
443 "Blackout",
444 "Scene Opacity",
445 "Scene Opacity Live",
446 "Scene Input Dimmer",
447 "Scene Beat Offset",
448 "Scene Ignore Main Dimmer",
449 "Scene Set Offset On Flash",
450 "Scene Active",
451 "Scene Flashed",
452 "Scene Selected",
453 "Mode Active",
454 "Scene Effect Setting (n,n) f32",
455 "Effect Opacity",
456 "Effect Color Shift",
457 "Effect Beat Progression",
458 "Effect Beat Offset",
459 "Effect Speed Exponent",
460 "Effect Layout",
461 ]
462 );
463 let targets: Vec<String> = MidiSceneTargetKind::iter().map(|k| k.to_string()).collect();
464 assert_eq!(
465 targets,
466 [
467 "Selected Scene",
468 "Quick Scene",
469 "Grid Scene",
470 "Row Scene",
471 "Column Scene"
472 ]
473 );
474 let effects: Vec<String> = MidiEffectTargetKind::iter()
475 .map(|k| k.to_string())
476 .collect();
477 assert_eq!(effects, ["Selected Effect", "Effect Index"]);
478 let outputs: Vec<String> = MidiOutputBindingSelector::iter()
479 .map(|k| k.to_string())
480 .collect();
481 assert_eq!(
482 outputs,
483 ["Single Value", "Scene Color Mapping", "Mackie Meter"]
484 );
485 assert_eq!(MidiColorSource::default().to_string(), "Scene Color");
486 }
487}