//! 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::{ConnectError, InitError, MidiInput, MidiInputConnection}; use thiserror::Error; use tracing::instrument; /// An active MIDI input connection. Dropping this disconnects. pub struct MidiConnection { _conn: MidiInputConnection<()>, } /// Errors from MIDI port enumeration and connection. #[derive(Error, Debug)] pub enum MidiError { #[error("MIDI init: {0}")] Init(#[from] InitError), #[error("MIDI port index {idx} out of range ({count} ports available)")] PortOutOfRange { idx: usize, count: usize }, #[error("MIDI connect: {0}")] Connect(#[from] ConnectError), } /// 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")?; let ports = midi_in.ports(); let port = ports .get(port_index) .ok_or(MidiError::PortOutOfRange { idx: port_index, count: ports.len() })?; 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); } _ => {} } }, (), )?; Ok(MidiConnection { _conn: conn }) }