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