mod mode;
mod output;
mod target;
pub use mode::{MidiMode, MidiModeHighlight, binding_in_mode};
pub use output::{
MidiColorSource, MidiEffectParameter, MidiMeterOutput, MidiNamedSceneColorMapping,
MidiOutputBinding, MidiOutputBindingKind, MidiOutputBindingSelector, MidiSceneColorMessage,
MidiSceneColorMessageMap, MidiSceneColorValueOutput, MidiValueOutput, MidiValueSource,
MidiValueSourceKind,
};
pub use target::{MidiEffectTarget, MidiEffectTargetKind, MidiEncoder, MidiRelativeEncoding};
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,
#[serde(default)]
pub default_for_ports: Vec<String>,
}
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>,
#[serde(default)]
pub modes: Vec<MidiMode>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct MidiInputBinding {
pub name: String,
pub trigger: MidiTrigger,
pub action: MidiInputAction,
#[serde(default)]
pub mode: Option<u8>,
#[serde(default)]
pub touch: Option<MidiTrigger>,
}
impl Default for MidiInputBinding {
fn default() -> Self {
Self {
name: "Input".to_owned(),
trigger: MidiTrigger::default(),
action: MidiInputAction::Tap,
mode: None,
touch: None,
}
}
}
#[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,
#[strum_discriminants(strum(serialize = "Set Main Dimmer (Relative)"))]
SetMainDimmerRelative {
encoder: MidiEncoder,
},
SetBlackout,
ToggleBlackout,
SetSpeedAdd,
SetSpeedMultiply,
#[strum_discriminants(strum(serialize = "Set Speed (Relative)"))]
SetSpeedRelative {
encoder: MidiEncoder,
},
#[strum_discriminants(strum(serialize = "Select Scene (Distributed 0-127)"))]
SelectSceneDistributed,
#[strum_discriminants(strum(serialize = "Select Scene (Relative)"))]
SelectSceneRelative {
encoder: MidiEncoder,
},
#[strum_discriminants(strum(serialize = "Select Row (Relative)"))]
SelectSceneRow {
encoder: MidiEncoder,
},
#[strum_discriminants(strum(serialize = "Select Column (Relative)"))]
SelectSceneColumn {
encoder: MidiEncoder,
},
SelectScene {
target: MidiSceneTarget,
},
SelectMode {
mode: u8,
},
#[strum_discriminants(strum(serialize = "Select Effect"))]
SelectSceneEffect {
index: u8,
},
#[strum_discriminants(strum(serialize = "Select Effect (Relative)"))]
SelectSceneEffectRelative {
encoder: MidiEncoder,
},
#[strum_discriminants(strum(serialize = "Select Effect (Distributed 0-127)"))]
SelectSceneEffectDistributed,
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,
#[serde(rename = "effect_index")]
effect: MidiEffectTarget,
setting_index: u8,
},
#[strum_discriminants(strum(serialize = "Set Effect Opacity"))]
SetSceneEffectOpacity {
target: MidiSceneTarget,
#[serde(rename = "effect_index")]
effect: MidiEffectTarget,
},
#[strum_discriminants(strum(serialize = "Set Effect Color Shift"))]
SetSceneEffectColorShift {
target: MidiSceneTarget,
#[serde(rename = "effect_index")]
effect: MidiEffectTarget,
},
#[strum_discriminants(strum(serialize = "Set Effect Beat Progression"))]
SetSceneEffectBeatProgression {
target: MidiSceneTarget,
#[serde(rename = "effect_index")]
effect: MidiEffectTarget,
},
#[strum_discriminants(strum(serialize = "Set Effect Beat Offset"))]
SetSceneEffectBeatOffset {
target: MidiSceneTarget,
#[serde(rename = "effect_index")]
effect: MidiEffectTarget,
},
#[strum_discriminants(strum(serialize = "Set Effect Speed Exponent"))]
SetSceneEffectSpeedExponent {
target: MidiSceneTarget,
#[serde(rename = "effect_index")]
effect: MidiEffectTarget,
},
#[serde(alias = "SetSceneEffectGroupIndex")]
#[strum_discriminants(strum(serialize = "Set Effect Layout"))]
SetSceneEffectLayout {
target: MidiSceneTarget,
#[serde(rename = "effect_index")]
effect: MidiEffectTarget,
},
}
#[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 },
#[strum_discriminants(strum(serialize = "Row Scene"))]
Row { col: u8 },
#[strum_discriminants(strum(serialize = "Column Scene"))]
Column { row: u8 },
}
pub fn normalize_controller_key(port_name: &str) -> String {
let trimmed = strip_alsa_address(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()
}
fn strip_alsa_address(name: &str) -> &str {
let Some((base, address)) = name.rsplit_once(' ') else {
return name;
};
let is_number = |part: &str| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit());
match address.split_once(':') {
Some((client, port)) if !base.is_empty() && is_number(client) && is_number(port) => {
base.trim_end()
}
_ => name,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_full_meter_lights_all_twelve_leds_of_its_strip() {
let meter = MidiMeterOutput {
strip: 2,
source: MidiValueSource::default(),
};
assert_eq!(meter.message(1.0), [0xD0, 0x2C]);
assert_eq!(meter.message(0.0), [0xD0, 0x20]);
assert_eq!(meter.message(2.0), [0xD0, 0x2C]);
}
#[test]
fn alsa_addresses_are_not_part_of_the_controller_key() {
assert_eq!(
normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 32:0"),
"iCON P1-X1:iCON P1-X1 MIDI 1"
);
assert_eq!(
normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 36:0"),
normalize_controller_key("iCON P1-X1:iCON P1-X1 MIDI 1 32:0")
);
}
#[test]
fn numbered_port_names_keep_their_number() {
assert_eq!(normalize_controller_key("APC40 [2]"), "APC40");
assert_eq!(normalize_controller_key("Port 2"), "Port 2");
}
#[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: MidiEffectTarget::Index { 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: MidiEffectTarget::Index { 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 Main Dimmer (Relative)",
"Set Blackout",
"Toggle Blackout",
"Set Speed Add",
"Set Speed Multiply",
"Set Speed (Relative)",
"Select Scene (Distributed 0-127)",
"Select Scene (Relative)",
"Select Row (Relative)",
"Select Column (Relative)",
"Select Scene",
"Select Mode",
"Select Effect",
"Select Effect (Relative)",
"Select Effect (Distributed 0-127)",
"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 Effect (Distributed 0-127)",
"Selected Scene Opacity Live",
"Effect Live Value",
"Effect Selected",
"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 Selected",
"Mode Active",
"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",
"Row Scene",
"Column Scene"
]
);
let effects: Vec<String> = MidiEffectTargetKind::iter()
.map(|k| k.to_string())
.collect();
assert_eq!(effects, ["Selected Effect", "Effect Index"]);
let outputs: Vec<String> = MidiOutputBindingSelector::iter()
.map(|k| k.to_string())
.collect();
assert_eq!(
outputs,
["Single Value", "Scene Color Mapping", "Mackie Meter"]
);
assert_eq!(MidiColorSource::default().to_string(), "Scene Color");
}
}