mod output;
pub use output::{
MidiColorSource, MidiNamedSceneColorMapping, MidiOutputBinding, MidiOutputBindingKind,
MidiOutputBindingSelector, MidiSceneColorMessage, MidiSceneColorMessageMap,
MidiSceneColorValueOutput, MidiValueOutput, MidiValueSource, MidiValueSourceKind,
};
use super::AssetTrait;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumDiscriminants, EnumIter};
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct MidiController {
pub mapping: MidiControllerMapping,
}
impl AssetTrait for MidiController {
const DIR_NAME: &'static str = "midi_controllers";
const NAME: &'static str = "MIDI Controller";
const SHOW_NAME_IF_SELECTED: bool = true;
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct MidiControllerMapping {
pub input_bindings: Vec<MidiInputBinding>,
pub output_bindings: Vec<MidiOutputBinding>,
pub color_mappings: Vec<MidiNamedSceneColorMapping>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct MidiInputBinding {
pub name: String,
pub trigger: MidiTrigger,
pub action: MidiInputAction,
}
impl Default for MidiInputBinding {
fn default() -> Self {
Self {
name: "Input".to_owned(),
trigger: MidiTrigger::default(),
action: MidiInputAction::Tap,
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct MidiTrigger {
pub status: u8,
pub data1: u8,
pub match_data1: bool,
}
impl Default for MidiTrigger {
fn default() -> Self {
Self {
status: 0,
data1: 0,
match_data1: true,
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, EnumDiscriminants)]
#[strum_discriminants(
name(MidiInputActionKind),
derive(Display, EnumIter),
strum(serialize_all = "title_case")
)]
pub enum MidiInputAction {
Tap,
SetMainDimmer,
SetBlackout,
SetSpeedAdd,
SetSpeedMultiply,
#[strum_discriminants(strum(serialize = "Select Scene (Distributed 0-127)"))]
SelectSceneDistributed,
SelectScene {
target: MidiSceneTarget,
},
ToggleSceneActive {
target: MidiSceneTarget,
},
SetSceneActive {
target: MidiSceneTarget,
},
FlashScene {
target: MidiSceneTarget,
},
SetSceneOpacity {
target: MidiSceneTarget,
},
SetSceneInputDimmer {
target: MidiSceneTarget,
},
SetSceneBeatOffset {
target: MidiSceneTarget,
},
SetSceneIgnoreMainDimmer {
target: MidiSceneTarget,
},
SetSceneSetOffsetOnFlash {
target: MidiSceneTarget,
},
#[strum_discriminants(strum(serialize = "Set Effect Setting (n,n) f32"))]
SetSceneEffectSettingF32 {
target: MidiSceneTarget,
effect_index: u8,
setting_index: u8,
},
#[strum_discriminants(strum(serialize = "Set Effect Opacity"))]
SetSceneEffectOpacity {
target: MidiSceneTarget,
effect_index: u8,
},
#[strum_discriminants(strum(serialize = "Set Effect Color Shift"))]
SetSceneEffectColorShift {
target: MidiSceneTarget,
effect_index: u8,
},
#[strum_discriminants(strum(serialize = "Set Effect Beat Progression"))]
SetSceneEffectBeatProgression {
target: MidiSceneTarget,
effect_index: u8,
},
#[strum_discriminants(strum(serialize = "Set Effect Beat Offset"))]
SetSceneEffectBeatOffset {
target: MidiSceneTarget,
effect_index: u8,
},
#[strum_discriminants(strum(serialize = "Set Effect Speed Exponent"))]
SetSceneEffectSpeedExponent {
target: MidiSceneTarget,
effect_index: u8,
},
#[serde(alias = "SetSceneEffectGroupIndex")]
#[strum_discriminants(strum(serialize = "Set Effect Layout"))]
SetSceneEffectLayout {
target: MidiSceneTarget,
effect_index: u8,
},
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default, EnumDiscriminants)]
#[strum_discriminants(
name(MidiSceneTargetKind),
derive(Display, EnumIter),
strum(serialize_all = "title_case")
)]
pub enum MidiSceneTarget {
#[default]
#[strum_discriminants(strum(serialize = "Selected Scene"))]
Selected,
#[strum_discriminants(strum(serialize = "Quick Scene"))]
Quick { index: u8 },
#[strum_discriminants(strum(serialize = "Grid Scene"))]
Grid { row: u8, col: u8 },
}
pub fn normalize_controller_key(port_name: &str) -> String {
let trimmed = port_name.trim();
if let Some(prefix) = trimmed.strip_suffix(']')
&& let Some((base, suffix)) = prefix.rsplit_once(" [")
&& !base.is_empty()
&& !suffix.is_empty()
&& suffix.chars().all(|ch| ch.is_ascii_digit())
{
return base.to_owned();
}
trimmed.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn group_index_bindings_saved_before_the_patch_load_as_layout_bindings() {
let action: MidiInputAction = serde_json::from_str(
r#"{"SetSceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
)
.expect("an old group index action must load");
assert_eq!(
action,
MidiInputAction::SetSceneEffectLayout {
target: MidiSceneTarget::Selected,
effect_index: 1,
}
);
let source: MidiValueSource = serde_json::from_str(
r#"{"SceneEffectGroupIndex": {"target": "Selected", "effect_index": 1}}"#,
)
.expect("an old group index feedback must load");
assert_eq!(
source,
MidiValueSource::SceneEffectLayout {
target: MidiSceneTarget::Selected,
effect_index: 1,
}
);
}
#[test]
fn kinds_read_as_the_controller_window_labels() {
use strum::IntoEnumIterator;
let actions: Vec<String> = MidiInputActionKind::iter().map(|k| k.to_string()).collect();
assert_eq!(
actions,
[
"Tap",
"Set Main Dimmer",
"Set Blackout",
"Set Speed Add",
"Set Speed Multiply",
"Select Scene (Distributed 0-127)",
"Select Scene",
"Toggle Scene Active",
"Set Scene Active",
"Flash Scene",
"Set Scene Opacity",
"Set Scene Input Dimmer",
"Set Scene Beat Offset",
"Set Scene Ignore Main Dimmer",
"Set Scene Set Offset On Flash",
"Set Effect Setting (n,n) f32",
"Set Effect Opacity",
"Set Effect Color Shift",
"Set Effect Beat Progression",
"Set Effect Beat Offset",
"Set Effect Speed Exponent",
"Set Effect Layout",
]
);
let sources: Vec<String> = MidiValueSourceKind::iter().map(|k| k.to_string()).collect();
assert_eq!(
sources,
[
"Selected Scene Opacity",
"Selected Scene (Distributed 0-127)",
"Selected Scene Input Dimmer",
"Selected Scene Beat Offset",
"Selected Scene Ignore Main Dimmer",
"Selected Scene Set Offset On Flash",
"Main Dimmer",
"Beat Flank",
"Blackout",
"Scene Opacity",
"Scene Input Dimmer",
"Scene Beat Offset",
"Scene Ignore Main Dimmer",
"Scene Set Offset On Flash",
"Scene Active",
"Scene Flashed",
"Scene Effect Setting (n,n) f32",
"Effect Opacity",
"Effect Color Shift",
"Effect Beat Progression",
"Effect Beat Offset",
"Effect Speed Exponent",
"Effect Layout",
]
);
let targets: Vec<String> = MidiSceneTargetKind::iter().map(|k| k.to_string()).collect();
assert_eq!(targets, ["Selected Scene", "Quick Scene", "Grid Scene"]);
let outputs: Vec<String> = MidiOutputBindingSelector::iter()
.map(|k| k.to_string())
.collect();
assert_eq!(outputs, ["Single Value", "Scene Color Mapping"]);
assert_eq!(MidiColorSource::default().to_string(), "Scene Color");
}
}