use std::{collections::HashMap, time::Duration};
use gled_model::{
asset::midi_controller::{MidiController, MidiTrigger},
asset_id::AssetId,
};
use web_time::Instant;
#[derive(Debug, Clone, Copy)]
pub(crate) struct MidiLearnRequest {
pub(crate) controller_id: AssetId<MidiController>,
pub(crate) target: MidiLearnTarget,
}
#[derive(Debug, Clone)]
struct LearnSession {
request: MidiLearnRequest,
started_at: Instant,
last_seen_at: Option<Instant>,
stats: HashMap<(u8, u8), LearnStats>,
}
#[derive(Debug, Clone, Copy)]
struct LearnStats {
count: u32,
min_value: u8,
max_value: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MidiLearnTarget {
InputBinding(usize),
OutputBinding(usize),
}
#[derive(Debug, Clone)]
pub(crate) struct MidiLearnCapture {
pub(crate) controller_id: AssetId<MidiController>,
pub(crate) target: MidiLearnTarget,
pub(crate) trigger: MidiTrigger,
}
#[derive(Debug, Default)]
pub(crate) struct LearnState {
incoming: Vec<[u8; 3]>,
request: Option<LearnSession>,
captured: Vec<MidiLearnCapture>,
}
impl LearnState {
pub(crate) fn set_streaming_enabled(&mut self, enabled: bool) {
if !enabled {
self.request = None;
}
}
pub(crate) fn feed(&mut self, message: [u8; 3]) {
self.incoming.push(message);
}
pub(crate) fn arm(&mut self, request: MidiLearnRequest) {
self.request.replace(LearnSession {
request,
started_at: Instant::now(),
last_seen_at: None,
stats: HashMap::new(),
});
}
pub(crate) fn active_request(&self) -> Option<MidiLearnRequest> {
self.request.as_ref().map(|session| session.request)
}
pub(crate) fn flush_timeouts(&mut self) {
self.poll_captures();
let now = Instant::now();
let Some(session) = self.request.as_ref() else {
return;
};
if !should_finalize(session, now) {
return;
}
if let Some(session) = self.request.take()
&& let Some(capture) = finalize_session(session)
{
self.captured.push(capture);
}
}
fn poll_captures(&mut self) {
for message in std::mem::take(&mut self.incoming) {
let now = Instant::now();
let mut maybe_finalized = None;
let Some(session) = self.request.as_mut() else {
continue;
};
let key = (message[0], message[1]);
let value = message[2];
session.last_seen_at = Some(now);
session
.stats
.entry(key)
.and_modify(|stats| {
stats.count += 1;
stats.min_value = stats.min_value.min(value);
stats.max_value = stats.max_value.max(value);
})
.or_insert(LearnStats {
count: 1,
min_value: value,
max_value: value,
});
if should_finalize(session, now) {
maybe_finalized = self.request.take();
}
if let Some(session) = maybe_finalized
&& let Some(capture) = finalize_session(session)
{
self.captured.push(capture);
}
}
}
pub(crate) fn pop_capture(&mut self) -> Option<MidiLearnCapture> {
self.captured.pop()
}
}
fn should_finalize(session: &LearnSession, now: Instant) -> bool {
const MIN_MESSAGES: u32 = 8;
const QUIET_TIME: Duration = Duration::from_millis(180);
const MAX_LEARN_TIME: Duration = Duration::from_secs(2);
let total_messages: u32 = session.stats.values().map(|stats| stats.count).sum();
if total_messages >= MIN_MESSAGES {
return true;
}
if now.duration_since(session.started_at) >= MAX_LEARN_TIME && total_messages > 0 {
return true;
}
if let Some(last_seen_at) = session.last_seen_at
&& now.duration_since(last_seen_at) >= QUIET_TIME
&& total_messages > 0
{
return true;
}
false
}
fn finalize_session(session: LearnSession) -> Option<MidiLearnCapture> {
let (status, data1) = best_trigger(&session.stats)?;
Some(MidiLearnCapture {
controller_id: session.request.controller_id,
target: session.request.target,
trigger: MidiTrigger {
status,
data1,
match_data1: match_data1_for_status(status),
},
})
}
fn best_trigger(stats: &HashMap<(u8, u8), LearnStats>) -> Option<(u8, u8)> {
let mut canonical: HashMap<(u8, u8), (u32, u8)> = HashMap::new();
for (&(status, data1), message_stats) in stats {
let canonical_data1 = if status & 0xF0 == 0xB0 && (32..=63).contains(&data1) {
data1 - 32
} else {
data1
};
let value_span = message_stats
.max_value
.saturating_sub(message_stats.min_value);
canonical
.entry((status, canonical_data1))
.and_modify(|(count, span)| {
*count += message_stats.count;
*span = (*span).max(value_span);
})
.or_insert((message_stats.count, value_span));
}
canonical
.into_iter()
.max_by_key(|(_, (count, span))| ((*count as u64) << 8) + (*span as u64))
.map(|(key, _)| key)
}
fn match_data1_for_status(status: u8) -> bool {
!matches!(status & 0xF0, 0xC0 | 0xD0 | 0xE0)
}
#[derive(Clone, Debug, Default)]
pub(crate) struct MidiPortDiagnostics {
pub(crate) port_name: String,
pub(crate) input_connected: bool,
pub(crate) output_connected: bool,
pub(crate) input_connect_failed: u32,
pub(crate) output_connect_failed: u32,
pub(crate) message_count: u64,
}