1mod output;
2
3pub use output::{
4 MidiColorSource, MidiNamedSceneColorMapping, MidiOutputBinding, MidiOutputBindingKind,
5 MidiOutputBindingSelector, MidiSceneColorMessage, MidiSceneColorMessageMap,
6 MidiSceneColorValueOutput, MidiValueOutput, MidiValueSource, MidiValueSourceKind,
7};
8
9use super::AssetTrait;
10use serde::{Deserialize, Serialize};
11use strum::{Display, EnumDiscriminants, EnumIter};
12
13#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
14pub struct MidiController {
15 pub mapping: MidiControllerMapping,
16}
17
18impl AssetTrait for MidiController {
19 const DIR_NAME: &'static str = "midi_controllers";
20 const NAME: &'static str = "MIDI Controller";
21 const SHOW_NAME_IF_SELECTED: bool = true;
22}
23
24#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
25pub struct MidiControllerMapping {
26 pub input_bindings: Vec<MidiInputBinding>,
27 pub output_bindings: Vec<MidiOutputBinding>,
28 pub color_mappings: Vec<MidiNamedSceneColorMapping>,
29}
30
31#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
32pub struct MidiInputBinding {
33 pub name: String,
34 pub trigger: MidiTrigger,
35 pub action: MidiInputAction,
36}
37
38impl Default for MidiInputBinding {
39 fn default() -> Self {
40 Self {
41 name: "Input".to_owned(),
42 trigger: MidiTrigger::default(),
43 action: MidiInputAction::Tap,
44 }
45 }
46}
47
48#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
49pub struct MidiTrigger {
50 pub status: u8,
51 pub data1: u8,
52 pub match_data1: bool,
53}
54
55impl Default for MidiTrigger {
56 fn default() -> Self {
57 Self {
58 status: 0,
59 data1: 0,
60 match_data1: true,
61 }
62 }
63}
64
65#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, EnumDiscriminants)]
68#[strum_discriminants(
69 name(MidiInputActionKind),
70 derive(Display, EnumIter),
71 strum(serialize_all = "title_case")
72)]
73pub enum MidiInputAction {
74 Tap,
75 SetMainDimmer,
76 SetBlackout,
77 SetSpeedAdd,
78 SetSpeedMultiply,
79 #[strum_discriminants(strum(serialize = "Select Scene (Distributed 0-127)"))]
80 SelectSceneDistributed,
81 SelectScene {
82 target: MidiSceneTarget,
83 },
84 ToggleSceneActive {
85 target: MidiSceneTarget,
86 },
87 SetSceneActive {
88 target: MidiSceneTarget,
89 },
90 FlashScene {
91 target: MidiSceneTarget,
92 },
93 SetSceneOpacity {
94 target: MidiSceneTarget,
95 },
96 SetSceneInputDimmer {
97 target: MidiSceneTarget,
98 },
99 SetSceneBeatOffset {
100 target: MidiSceneTarget,
101 },
102 SetSceneIgnoreMainDimmer {
103 target: MidiSceneTarget,
104 },
105 SetSceneSetOffsetOnFlash {
106 target: MidiSceneTarget,
107 },
108 #[strum_discriminants(strum(serialize = "Set Effect Setting (n,n) f32"))]
109 SetSceneEffectSettingF32 {
110 target: MidiSceneTarget,
111 effect_index: u8,
112 setting_index: u8,
113 },
114 #[strum_discriminants(strum(serialize = "Set Effect Opacity"))]
115 SetSceneEffectOpacity {
116 target: MidiSceneTarget,
117 effect_index: u8,
118 },
119 #[strum_discriminants(strum(serialize = "Set Effect Color Shift"))]
120 SetSceneEffectColorShift {
121 target: MidiSceneTarget,
122 effect_index: u8,
123 },
124 #[strum_discriminants(strum(serialize = "Set Effect Beat Progression"))]
125 SetSceneEffectBeatProgression {
126 target: MidiSceneTarget,
127 effect_index: u8,
128 },
129 #[strum_discriminants(strum(serialize = "Set Effect Beat Offset"))]
130 SetSceneEffectBeatOffset {
131 target: MidiSceneTarget,
132 effect_index: u8,
133 },
134 #[strum_discriminants(strum(serialize = "Set Effect Speed Exponent"))]
135 SetSceneEffectSpeedExponent {
136 target: MidiSceneTarget,
137 effect_index: u8,
138 },
139 #[serde(alias = "SetSceneEffectGroupIndex")]
143 #[strum_discriminants(strum(serialize = "Set Effect Layout"))]
144 SetSceneEffectLayout {
145 target: MidiSceneTarget,
146 effect_index: u8,
147 },
148}
149
150#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default, EnumDiscriminants)]
153#[strum_discriminants(
154 name(MidiSceneTargetKind),
155 derive(Display, EnumIter),
156 strum(serialize_all = "title_case")
157)]
158pub enum MidiSceneTarget {
159 #[default]
160 #[strum_discriminants(strum(serialize = "Selected Scene"))]
161 Selected,
162 #[strum_discriminants(strum(serialize = "Quick Scene"))]
163 Quick { index: u8 },
164 #[strum_discriminants(strum(serialize = "Grid Scene"))]
165 Grid { row: u8, col: u8 },
166}
167pub fn normalize_controller_key(port_name: &str) -> String {
171 let trimmed = port_name.trim();
172 if let Some(prefix) = trimmed.strip_suffix(']')
173 && let Some((base, suffix)) = prefix.rsplit_once(" [")
174 && !base.is_empty()
175 && !suffix.is_empty()
176 && suffix.chars().all(|ch| ch.is_ascii_digit())
177 {
178 return base.to_owned();
179 }
180
181 trimmed.to_owned()
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
191 fn group_index_bindings_saved_before_the_patch_load_as_layout_bindings() {
192 let action: MidiInputAction = serde_json::from_str(
193 r#"{"SetSceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
194 )
195 .expect("an old group index action must load");
196 assert_eq!(
197 action,
198 MidiInputAction::SetSceneEffectLayout {
199 target: MidiSceneTarget::Selected,
200 effect_index: 1,
201 }
202 );
203
204 let source: MidiValueSource = serde_json::from_str(
205 r#"{"SceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
206 )
207 .expect("an old group index feedback must load");
208 assert_eq!(
209 source,
210 MidiValueSource::SceneEffectLayout {
211 target: MidiSceneTarget::Selected,
212 effect_index: 1,
213 }
214 );
215 }
216
217 #[test]
220 fn kinds_read_as_the_controller_window_labels() {
221 use strum::IntoEnumIterator;
222 let actions: Vec<String> = MidiInputActionKind::iter().map(|k| k.to_string()).collect();
223 assert_eq!(
224 actions,
225 [
226 "Tap",
227 "Set Main Dimmer",
228 "Set Blackout",
229 "Set Speed Add",
230 "Set Speed Multiply",
231 "Select Scene (Distributed 0-127)",
232 "Select Scene",
233 "Toggle Scene Active",
234 "Set Scene Active",
235 "Flash Scene",
236 "Set Scene Opacity",
237 "Set Scene Input Dimmer",
238 "Set Scene Beat Offset",
239 "Set Scene Ignore Main Dimmer",
240 "Set Scene Set Offset On Flash",
241 "Set Effect Setting (n,n) f32",
242 "Set Effect Opacity",
243 "Set Effect Color Shift",
244 "Set Effect Beat Progression",
245 "Set Effect Beat Offset",
246 "Set Effect Speed Exponent",
247 "Set Effect Layout",
248 ]
249 );
250 let sources: Vec<String> = MidiValueSourceKind::iter().map(|k| k.to_string()).collect();
251 assert_eq!(
252 sources,
253 [
254 "Selected Scene Opacity",
255 "Selected Scene (Distributed 0-127)",
256 "Selected Scene Input Dimmer",
257 "Selected Scene Beat Offset",
258 "Selected Scene Ignore Main Dimmer",
259 "Selected Scene Set Offset On Flash",
260 "Main Dimmer",
261 "Beat Flank",
262 "Blackout",
263 "Scene Opacity",
264 "Scene Input Dimmer",
265 "Scene Beat Offset",
266 "Scene Ignore Main Dimmer",
267 "Scene Set Offset On Flash",
268 "Scene Active",
269 "Scene Flashed",
270 "Scene Effect Setting (n,n) f32",
271 "Effect Opacity",
272 "Effect Color Shift",
273 "Effect Beat Progression",
274 "Effect Beat Offset",
275 "Effect Speed Exponent",
276 "Effect Layout",
277 ]
278 );
279 let targets: Vec<String> = MidiSceneTargetKind::iter().map(|k| k.to_string()).collect();
280 assert_eq!(targets, ["Selected Scene", "Quick Scene", "Grid Scene"]);
281 let outputs: Vec<String> = MidiOutputBindingSelector::iter()
282 .map(|k| k.to_string())
283 .collect();
284 assert_eq!(outputs, ["Single Value", "Scene Color Mapping"]);
285 assert_eq!(MidiColorSource::default().to_string(), "Scene Color");
286 }
287}