use crate::{
midi::normalize_controller_key,
storage::asset::midi_controller::{MidiInputAction, MidiSceneTarget},
ui::action::UiAction,
};
use std::collections::HashMap;
use super::{
ActiveRuntimeMapping, MidiRuntimeSnapshot, RuntimeControllerKey,
scene_state::scene_target_to_union,
};
fn distributed_scene_index_from_midi(value: u8, scene_count: usize) -> Option<usize> {
if scene_count == 0 {
return None;
}
Some((((usize::from(value) + 1) * scene_count).saturating_sub(1)) / 128)
}
pub(super) fn handle_input_from_snapshot(
snapshot: &MidiRuntimeSnapshot,
port_name: &str,
message: &[u8],
) -> bool {
if message.len() != 3 {
return false;
}
let mapping = resolve_mapping_for_port(&snapshot.mappings_by_controller, port_name);
let Some(mapping) = mapping else {
return false;
};
let status = message[0];
let data1 = message[1];
let value = message[2];
let mut handled_any = false;
for binding in &mapping.mapping.input_bindings {
if !trigger_matches(&binding.trigger, status, data1) {
continue;
}
let action = match binding.action {
MidiInputAction::Tap => Some(UiAction::Tap),
MidiInputAction::SetMainDimmer => {
Some(UiAction::SetMainDimmer(f32::from(value) / 127.0))
}
MidiInputAction::SetBlackout => Some(UiAction::SetBlackout(value > 0)),
MidiInputAction::SetSpeedAdd => {
let delta = if value > 64 {
f32::from(value) - 128.0
} else {
f32::from(value)
};
Some(UiAction::SpeedAdd(delta))
}
MidiInputAction::SetSpeedMultiply => {
Some(UiAction::SpeedMultiply(if value > 63 { 2.0 } else { 0.5 }))
}
MidiInputAction::SelectSceneDistributed => {
distributed_scene_index_from_midi(value, snapshot.scene_locations_row_major.len())
.and_then(|index| snapshot.scene_locations_row_major.get(index).copied())
.map(UiAction::SelectSceneByLocation)
}
MidiInputAction::SelectScene { ref target } => match target {
MidiSceneTarget::Selected => None,
_ => scene_target_to_union(target).map(UiAction::SelectScene),
},
MidiInputAction::ToggleSceneActive { ref target } => {
scene_target_to_union(target).map(UiAction::ToggleSceneActive)
}
MidiInputAction::SetSceneActive { ref target } => scene_target_to_union(target)
.map(|target| UiAction::SetSceneActive(target, value > 0)),
MidiInputAction::FlashScene { ref target } => scene_target_to_union(target)
.map(|target| UiAction::SetSceneFlash(target, value > 0)),
MidiInputAction::SetSceneOpacity { ref target } => scene_target_to_union(target)
.map(|target| UiAction::SetSceneOpacity(target, f32::from(value) / 127.0)),
MidiInputAction::SetSceneInputDimmer { ref target } => scene_target_to_union(target)
.map(|target| UiAction::SetSceneInputDimmer(target, f32::from(value) / 127.0)),
MidiInputAction::SetSceneBeatOffset { ref target } => scene_target_to_union(target)
.map(|target| UiAction::SetSceneBeatOffset(target, f32::from(value) / 127.0)),
MidiInputAction::SetSceneIgnoreMainDimmer { ref target } => {
scene_target_to_union(target)
.map(|target| UiAction::SetSceneIgnoreMainDimmer(target, value > 63))
}
MidiInputAction::SetSceneSetOffsetOnFlash { ref target } => {
scene_target_to_union(target)
.map(|target| UiAction::SetSceneSetOffsetOnFlash(target, value > 63))
}
MidiInputAction::SetSceneEffectSettingF32 {
ref target,
effect_index,
setting_index,
} => scene_target_to_union(target).map(|target| {
UiAction::SetSceneEffectAnimationConfigF32(
target,
effect_index as usize,
setting_index as usize,
f32::from(value) / 127.0,
)
}),
MidiInputAction::SetSceneEffectOpacity {
ref target,
effect_index,
} => scene_target_to_union(target).map(|target| {
UiAction::SetSceneEffectOpacity(
target,
effect_index as usize,
f32::from(value) / 127.0,
)
}),
MidiInputAction::SetSceneEffectColorShift {
ref target,
effect_index,
} => scene_target_to_union(target).map(|target| {
UiAction::SetSceneEffectColorShift(
target,
effect_index as usize,
f32::from(value) / 127.0,
)
}),
MidiInputAction::SetSceneEffectBeatProgression {
ref target,
effect_index,
} => scene_target_to_union(target).map(|target| {
UiAction::SetSceneEffectBeatProgression(
target,
effect_index as usize,
f32::from(value) / 127.0,
)
}),
MidiInputAction::SetSceneEffectBeatOffset {
ref target,
effect_index,
} => scene_target_to_union(target).map(|target| {
UiAction::SetSceneEffectBeatOffset(
target,
effect_index as usize,
f32::from(value) / 127.0,
)
}),
MidiInputAction::SetSceneEffectSpeedExponent {
ref target,
effect_index,
} => scene_target_to_union(target).map(|target| {
UiAction::SetSceneEffectSpeedExponent(
target,
effect_index as usize,
i32::from(value) - 63,
)
}),
MidiInputAction::SetSceneEffectGroupIndex {
ref target,
effect_index,
} => scene_target_to_union(target).map(|target| {
UiAction::SetSceneEffectGroupIndex(target, effect_index as usize, value as usize)
}),
};
if let Some(action) = action {
action.enqueue();
handled_any = true;
}
}
handled_any
}
fn trigger_matches(
trigger: &crate::storage::asset::midi_controller::MidiTrigger,
status: u8,
data1: u8,
) -> bool {
if trigger.status != status {
return false;
}
if trigger.match_data1 && trigger.data1 != data1 {
return false;
}
true
}
fn resolve_mapping_for_port(
mappings_by_controller: &HashMap<RuntimeControllerKey, ActiveRuntimeMapping>,
port_name: &str,
) -> Option<ActiveRuntimeMapping> {
let normalized_port_name = normalize_controller_key(port_name);
mappings_by_controller.iter().find_map(|(key, mapping)| {
if matches!(
key,
RuntimeControllerKey::PortName(name)
if name == port_name || normalize_controller_key(name) == normalized_port_name
) {
Some(mapping.clone())
} else {
None
}
})
}
#[cfg(test)]
mod tests {
use super::distributed_scene_index_from_midi;
#[test]
fn distributed_scene_index_spans_full_range() {
assert_eq!(distributed_scene_index_from_midi(0, 8), Some(0));
assert_eq!(distributed_scene_index_from_midi(63, 8), Some(3));
assert_eq!(distributed_scene_index_from_midi(64, 8), Some(4));
assert_eq!(distributed_scene_index_from_midi(127, 8), Some(7));
}
#[test]
fn distributed_scene_index_handles_empty_scene_list() {
assert_eq!(distributed_scene_index_from_midi(0, 0), None);
}
}