//! MIDI input: enumerate ports, connect/disconnect, and parse incoming messages. use std::sync::Arc; use std::time::Instant; use audiofiles_browser::state::{MidiNoteEvent, SharedState}; use audiofiles_core::instrument::note_name; use midir::{MidiInput, MidiInputConnection}; use tracing::instrument; /// An active MIDI input connection. Dropping this disconnects. pub struct MidiConnection { _conn: MidiInputConnection<()>, } /// List available MIDI input port names. #[instrument(skip_all)] pub fn list_input_ports() -> Vec { let Ok(midi_in) = MidiInput::new("audiofiles-enumerate") else { return Vec::new(); }; midi_in .ports() .iter() .filter_map(|p| midi_in.port_name(p).ok()) .collect() } /// Connect to a MIDI input port by index. /// /// The callback parses note-on/note-off messages and calls `note_on`/`note_off` /// on the instrument directly (low latency). Also pushes `MidiNoteEvent`s to /// `shared.midi_recent_notes` for the GUI activity display. #[instrument(skip_all)] pub fn connect(port_index: usize, shared: Arc) -> Result { let midi_in = MidiInput::new("audiofiles-input") .map_err(|e| format!("MIDI init: {e}"))?; let ports = midi_in.ports(); let port = ports .get(port_index) .ok_or_else(|| format!("MIDI port index {port_index} out of range"))?; let shared_cb = shared.clone(); let conn = midi_in .connect( port, "audiofiles-midi-in", move |_timestamp, message, _| { if message.len() < 3 { return; } let status = message[0] & 0xF0; let note = message[1]; let velocity = message[2]; match status { 0x90 if velocity > 0 => { // Note On shared_cb.instrument.lock().note_on(note, velocity); shared_cb.midi_recent_notes.lock().push(MidiNoteEvent { note, velocity, note_name: note_name(note), timestamp: Instant::now(), }); } 0x80 | 0x90 => { // Note Off (0x80 or 0x90 with velocity 0) shared_cb.instrument.lock().note_off(note); } _ => {} } }, (), ) .map_err(|e| format!("MIDI connect: {e}"))?; Ok(MidiConnection { _conn: conn }) }