use gled_model::asset::midi_controller::{
MidiControl, MidiControllerMapping, MidiInputAction, MidiOutputBindingKind, MidiTrigger,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ControlKind {
Button,
Pad,
Fader,
Knob,
Encoder,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Control {
pub(crate) name: String,
pub(crate) trigger: MidiTrigger,
pub(crate) kind: ControlKind,
pub(crate) named_by_message: bool,
}
pub(crate) fn controls_of(mapping: &MidiControllerMapping) -> Vec<Control> {
let mut controls: Vec<Control> = mapping
.controls
.iter()
.map(|control| Control {
name: control.name.clone(),
trigger: control.trigger.clone(),
kind: ControlKind::Button,
named_by_message: false,
})
.collect();
for binding in &mapping.input_bindings {
let trigger = press_trigger(&binding.trigger);
if control_index(&controls, &trigger).is_none() {
controls.push(derived_control(&binding.name, trigger));
}
}
for binding in &mapping.output_bindings {
let Some(trigger) = output_trigger(&binding.kind) else {
continue;
};
if output_control(&controls, &binding.kind).is_none() {
controls.push(derived_control(&binding.name, trigger));
}
}
for control in &mut controls {
control.kind = guess_kind(mapping, &control.trigger);
}
controls
}
pub(crate) fn control_index(controls: &[Control], trigger: &MidiTrigger) -> Option<usize> {
let press = press_trigger(trigger);
controls
.iter()
.position(|control| same_message(&control.trigger, &press))
}
pub(crate) fn output_control(controls: &[Control], kind: &MidiOutputBindingKind) -> Option<usize> {
match kind {
MidiOutputBindingKind::Meter(meter) => controls
.iter()
.position(|control| control.trigger.status == 0xE0 | (meter.strip & 0x07)),
_ => output_trigger(kind).and_then(|trigger| control_index(controls, &trigger)),
}
}
pub(crate) fn output_trigger(kind: &MidiOutputBindingKind) -> Option<MidiTrigger> {
let (status, data1) = match kind {
MidiOutputBindingKind::Value(value) => (value.status, value.data1),
MidiOutputBindingKind::SceneColorValue(color) => (color.status, color.data1),
MidiOutputBindingKind::Meter(meter) => (0xE0 | (meter.strip & 0x07), 0),
};
Some(MidiTrigger {
status,
data1,
match_data1: match_data1_for_status(status),
})
}
pub(crate) fn press_trigger(trigger: &MidiTrigger) -> MidiTrigger {
if trigger.status & 0xF0 == 0x80 {
MidiTrigger {
status: 0x90 | (trigger.status & 0x0F),
..trigger.clone()
}
} else {
trigger.clone()
}
}
pub(crate) fn match_data1_for_status(status: u8) -> bool {
!matches!(status & 0xF0, 0xC0 | 0xD0 | 0xE0)
}
fn same_message(a: &MidiTrigger, b: &MidiTrigger) -> bool {
a.status == b.status && (!a.match_data1 || !b.match_data1 || a.data1 == b.data1)
}
fn derived_control(binding_name: &str, trigger: MidiTrigger) -> Control {
let message = message_name(&trigger);
let name = name_prefix(binding_name).unwrap_or_else(|| message.clone());
Control {
named_by_message: name == message,
name,
trigger,
kind: ControlKind::Button,
}
}
fn name_prefix(name: &str) -> Option<String> {
const PARTS: [&str; 7] = [
" release",
" light",
" motor fader",
" meter",
" ring",
" pad color",
" value",
];
let (prefix, _) = name.split_once(':')?;
let prefix = prefix.trim();
let prefix = PARTS
.iter()
.find_map(|part| prefix.strip_suffix(part))
.unwrap_or(prefix)
.trim_end();
(!prefix.is_empty()).then(|| prefix.to_owned())
}
pub(crate) fn message_name(trigger: &MidiTrigger) -> String {
let channel = (trigger.status & 0x0F) + 1;
let on_channel = |name: String| {
if channel == 1 {
name
} else {
format!("{name} ยท ch {channel}")
}
};
match trigger.status & 0xF0 {
0x80 | 0x90 => on_channel(format!("Note {}", trigger.data1)),
0xA0 => on_channel(format!("Aftertouch {}", trigger.data1)),
0xB0 => on_channel(format!("CC {}", trigger.data1)),
0xC0 => format!("Program change ch {channel}"),
0xD0 => format!("Pressure ch {channel}"),
0xE0 => format!("Pitch bend ch {channel}"),
_ => format!("Message {}", trigger.status),
}
}
fn guess_kind(mapping: &MidiControllerMapping, trigger: &MidiTrigger) -> ControlKind {
match trigger.status & 0xF0 {
0xE0 => ControlKind::Fader,
0xB0 => {
let relative = mapping.input_bindings.iter().any(|binding| {
same_message(&binding.trigger, trigger) && relative_action(&binding.action)
});
if relative {
ControlKind::Encoder
} else {
ControlKind::Knob
}
}
_ => {
let shows_colors = mapping.output_bindings.iter().any(|binding| {
matches!(binding.kind, MidiOutputBindingKind::SceneColorValue(_))
&& output_trigger(&binding.kind)
.is_some_and(|output| same_message(&press_trigger(&output), trigger))
});
if shows_colors {
ControlKind::Pad
} else {
ControlKind::Button
}
}
}
}
pub(crate) fn relative_action(action: &MidiInputAction) -> bool {
matches!(
action,
MidiInputAction::SetMainDimmerRelative { .. }
| MidiInputAction::SetSpeedRelative { .. }
| MidiInputAction::SelectSceneRelative { .. }
| MidiInputAction::SelectSceneRow { .. }
| MidiInputAction::SelectSceneColumn { .. }
| MidiInputAction::SelectSceneEffectRelative { .. }
)
}
pub(crate) fn stored(control: &Control) -> MidiControl {
MidiControl {
name: control.name.clone(),
trigger: control.trigger.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::windows::midi_controllers::rules::test_mappings::{apc40, p1x};
fn named<'a>(controls: &'a [Control], name: &str) -> &'a Control {
controls
.iter()
.find(|control| control.name == name)
.unwrap_or_else(|| panic!("no control {name}"))
}
#[test]
fn old_mappings_name_their_controls_from_the_binding_names() {
let controls = controls_of(&p1x());
assert_eq!(named(&controls, "Fader 3").trigger.status, 0xE2);
assert_eq!(named(&controls, "Fader 3").kind, ControlKind::Fader);
assert_eq!(named(&controls, "Encoder 1").kind, ControlKind::Encoder);
assert_eq!(named(&controls, "S 1").kind, ControlKind::Button);
assert_eq!(named(&controls, "Encoder 1 push").trigger.data1, 32);
}
#[test]
fn the_p1x_has_one_control_per_physical_control() {
let controls = controls_of(&p1x());
assert_eq!(
controls.len(),
40,
"{:#?}",
controls.iter().map(|c| &c.name).collect::<Vec<_>>()
);
}
#[test]
fn a_button_bound_to_its_release_is_one_control() {
let controls = controls_of(&p1x());
assert_eq!(
controls
.iter()
.filter(|control| control.name == "S 1")
.count(),
1
);
assert!(controls.iter().all(|control| control.name != "S 1 release"));
}
#[test]
fn controls_without_named_bindings_are_named_by_their_message() {
let controls = controls_of(&apc40());
assert!(controls.iter().any(|control| control.name == "Note 32"));
assert_eq!(named(&controls, "Note 32").kind, ControlKind::Pad);
assert!(controls.iter().any(|control| control.name == "CC 7 ยท ch 2"));
}
#[test]
fn stored_controls_keep_their_names() {
let mut mapping = p1x();
mapping.controls = vec![MidiControl {
name: "Strip 3 fader".to_owned(),
trigger: MidiTrigger {
status: 0xE2,
data1: 0,
match_data1: false,
},
}];
let controls = controls_of(&mapping);
assert_eq!(controls[0].name, "Strip 3 fader");
assert!(controls.iter().all(|control| control.name != "Fader 3"));
}
}