Skip to main content

max / audiofiles

2.9 KB · 90 lines History Blame Raw
1 //! MIDI input: enumerate ports, connect/disconnect, and parse incoming messages.
2
3 use std::sync::Arc;
4 use std::time::Instant;
5
6 use audiofiles_browser::state::{MidiNoteEvent, SharedState};
7 use audiofiles_core::instrument::note_name;
8 use midir::{ConnectError, InitError, MidiInput, MidiInputConnection};
9 use thiserror::Error;
10 use tracing::instrument;
11
12 /// An active MIDI input connection. Dropping this disconnects.
13 pub struct MidiConnection {
14 _conn: MidiInputConnection<()>,
15 }
16
17 /// Errors from MIDI port enumeration and connection.
18 #[derive(Error, Debug)]
19 pub enum MidiError {
20 #[error("MIDI init: {0}")]
21 Init(#[from] InitError),
22 #[error("MIDI port index {idx} out of range ({count} ports available)")]
23 PortOutOfRange { idx: usize, count: usize },
24 #[error("MIDI connect: {0}")]
25 Connect(#[from] ConnectError<MidiInput>),
26 }
27
28 /// List available MIDI input port names.
29 #[instrument(skip_all)]
30 pub fn list_input_ports() -> Vec<String> {
31 let Ok(midi_in) = MidiInput::new("audiofiles-enumerate") else {
32 return Vec::new();
33 };
34 midi_in
35 .ports()
36 .iter()
37 .filter_map(|p| midi_in.port_name(p).ok())
38 .collect()
39 }
40
41 /// Connect to a MIDI input port by index.
42 ///
43 /// The callback parses note-on/note-off messages and calls `note_on`/`note_off`
44 /// on the instrument directly (low latency). Also pushes `MidiNoteEvent`s to
45 /// `shared.midi_recent_notes` for the GUI activity display.
46 #[instrument(skip_all)]
47 pub fn connect(port_index: usize, shared: Arc<SharedState>) -> Result<MidiConnection, MidiError> {
48 let midi_in = MidiInput::new("audiofiles-input")?;
49 let ports = midi_in.ports();
50 let port = ports
51 .get(port_index)
52 .ok_or(MidiError::PortOutOfRange { idx: port_index, count: ports.len() })?;
53
54 let shared_cb = shared.clone();
55 let conn = midi_in
56 .connect(
57 port,
58 "audiofiles-midi-in",
59 move |_timestamp, message, _| {
60 if message.len() < 3 {
61 return;
62 }
63 let status = message[0] & 0xF0;
64 let note = message[1];
65 let velocity = message[2];
66
67 match status {
68 0x90 if velocity > 0 => {
69 // Note On
70 shared_cb.instrument.lock().note_on(note, velocity);
71 shared_cb.midi_recent_notes.lock().push(MidiNoteEvent {
72 note,
73 velocity,
74 note_name: note_name(note),
75 timestamp: Instant::now(),
76 });
77 }
78 0x80 | 0x90 => {
79 // Note Off (0x80 or 0x90 with velocity 0)
80 shared_cb.instrument.lock().note_off(note);
81 }
82 _ => {}
83 }
84 },
85 (),
86 )?;
87
88 Ok(MidiConnection { _conn: conn })
89 }
90