vst3_host/midi_input.rs
1//! Bind a live MIDI input device and forward its messages as [`MidiEvent`]s.
2//!
3//! Enabled by the `midi-input` feature. Wraps the [`midir`] crate so callers never touch its
4//! types directly: [`list_midi_input_ports`] enumerates the available input ports, and
5//! [`MidiInputPort`] is a name + opaque handle you pass to [`connect`] or [`bind_to_handle`].
6//!
7//! Both connection forms return a [`MidiInputConnection`] guard; dropping it closes the port and
8//! stops delivery.
9//!
10//! ## Threading
11//!
12//! The callback you pass to [`connect`] runs on a thread owned by `midir` (the OS MIDI driver
13//! thread), **not** the thread that called `connect`. Treat it like an audio callback: do not
14//! block, allocate heavily, or panic in it. [`bind_to_handle`] only calls
15//! [`AudioHandle::send_midi`], which is lock-free and non-blocking, so it is safe to use from
16//! that thread.
17//!
18//! ## Example
19//!
20//! ```no_run
21//! use vst3_host::midi_input::{self, MidiInputConnection};
22//!
23//! # fn main() -> vst3_host::Result<()> {
24//! let ports = midi_input::list_midi_input_ports()?;
25//! let Some(port) = ports.first() else {
26//! return Ok(());
27//! };
28//!
29//! // Low-level: receive parsed events on midir's thread.
30//! let _conn: MidiInputConnection = midi_input::connect(port, |event| {
31//! println!("{event:?}");
32//! })?;
33//! # Ok(())
34//! # }
35//! ```
36
37use midir::{MidiInput, MidiInputPort as RawMidiInputPort};
38
39use crate::{
40 error::{Error, Result},
41 midi::MidiEvent,
42 playback::{AudioHandle, MidiSink},
43};
44
45/// The client name `midir` advertises to the OS when enumerating or opening ports.
46const CLIENT_NAME: &str = "vst3-host";
47
48/// Map a `midir` error into the library's [`Error::MidiError`].
49fn midi_err(context: &str, err: impl std::fmt::Display) -> Error {
50 Error::MidiError(format!("{context}: {err}"))
51}
52
53/// A discovered MIDI input port: its human-readable name plus the opaque handle used to open it.
54///
55/// Obtain these from [`list_midi_input_ports`]. The handle is tied to the port as the OS reported
56/// it at enumeration time; if the device is unplugged, [`connect`] will fail.
57#[derive(Clone)]
58pub struct MidiInputPort {
59 name: String,
60 raw: RawMidiInputPort,
61}
62
63impl MidiInputPort {
64 /// The port's display name (e.g. the device or virtual-port name).
65 pub fn name(&self) -> &str {
66 &self.name
67 }
68}
69
70impl std::fmt::Debug for MidiInputPort {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("MidiInputPort")
73 .field("name", &self.name)
74 .finish_non_exhaustive()
75 }
76}
77
78/// An open connection to a MIDI input port.
79///
80/// Holds the underlying `midir` connection alive; **dropping it closes the port** and stops the
81/// callback. Keep it for as long as you want to receive MIDI.
82pub struct MidiInputConnection {
83 // `midir::MidiInputConnection` is generic over the callback's user-data type; we use `()` and
84 // close it via `Drop`. Boxed only so the public type isn't generic.
85 _inner: midir::MidiInputConnection<()>,
86}
87
88/// List the MIDI input ports currently available on the system.
89///
90/// Returns an empty vector when no input ports are present (not an error). Fails only if the
91/// platform MIDI subsystem cannot be initialized.
92pub fn list_midi_input_ports() -> Result<Vec<MidiInputPort>> {
93 let input = MidiInput::new(CLIENT_NAME).map_err(|e| midi_err("init MIDI input", e))?;
94 let ports = input
95 .ports()
96 .into_iter()
97 .map(|raw| {
98 let name = input
99 .port_name(&raw)
100 .unwrap_or_else(|_| "Unknown MIDI input".to_string());
101 MidiInputPort { name, raw }
102 })
103 .collect();
104 Ok(ports)
105}
106
107/// Open `port` and deliver each parseable incoming message to `callback` as a [`MidiEvent`].
108///
109/// Raw bytes are parsed with [`MidiEvent::from_midi_bytes`]; messages that don't parse (SysEx,
110/// realtime, program change, truncated data) are silently ignored.
111///
112/// `callback` runs on a `midir`-owned thread — see the [module docs](self#threading). It must be
113/// `Send + 'static`. The returned [`MidiInputConnection`] keeps the port open until dropped.
114pub fn connect<F>(port: &MidiInputPort, mut callback: F) -> Result<MidiInputConnection>
115where
116 F: FnMut(MidiEvent) + Send + 'static,
117{
118 let input = MidiInput::new(CLIENT_NAME).map_err(|e| midi_err("init MIDI input", e))?;
119 let inner = input
120 .connect(
121 &port.raw,
122 &port.name,
123 move |_timestamp, bytes, ()| {
124 if let Some(event) = parse_midi(bytes) {
125 callback(event);
126 }
127 },
128 (),
129 )
130 .map_err(|e| midi_err("connect MIDI input", e))?;
131 Ok(MidiInputConnection { _inner: inner })
132}
133
134/// Open `port` and forward every parseable incoming message into a running [`AudioHandle`].
135///
136/// A convenience over [`connect`]: each received [`MidiEvent`] is pushed into the plugin's
137/// command ring (via the handle's [`MidiSink`]), so notes/CC played on the device reach the
138/// plugin. A `Send` sink is captured into the callback, so the connection keeps forwarding as
139/// long as the returned guard lives — independent of the `AudioHandle`'s own lifetime.
140///
141/// Events are dropped silently if the audio command ring is full (the same drop-on-full behavior
142/// as [`AudioHandle::send_midi`]). The returned [`MidiInputConnection`] keeps the port open until
143/// dropped.
144pub fn bind_to_handle(port: &MidiInputPort, handle: &AudioHandle) -> Result<MidiInputConnection> {
145 let sink: MidiSink = handle.midi_sink();
146 connect(port, move |event| {
147 sink.send_midi(event);
148 })
149}
150
151/// Parse a raw MIDI message into a [`MidiEvent`], returning `None` for messages the library does
152/// not forward. Factored out of the `midir` callback so it can be unit-tested without hardware.
153fn parse_midi(bytes: &[u8]) -> Option<MidiEvent> {
154 MidiEvent::from_midi_bytes(bytes)
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::midi::MidiChannel;
161
162 #[test]
163 fn list_midi_input_ports_does_not_error() {
164 // On a machine with a MIDI subsystem, enumeration must succeed (possibly empty).
165 // Headless CI has no ALSA sequencer (`/dev/snd/seq`), so `midir` can't initialize a
166 // backend at all — that's an absent subsystem, not an enumeration bug, so accept it.
167 match list_midi_input_ports() {
168 Ok(_) => {}
169 Err(Error::MidiError(msg)) if msg.contains("could not be initialized") => {
170 eprintln!("skipping: no MIDI subsystem available ({msg})");
171 }
172 Err(e) => panic!("enumeration failed: {e:?}"),
173 }
174 }
175
176 #[test]
177 fn parse_midi_forwards_channel_voice_and_drops_the_rest() {
178 assert_eq!(
179 parse_midi(&[0x90, 60, 100]),
180 Some(MidiEvent::NoteOn {
181 channel: MidiChannel::Ch1,
182 note: 60,
183 velocity: 100,
184 })
185 );
186 assert_eq!(
187 parse_midi(&[0xB0, 1, 64]),
188 Some(MidiEvent::ControlChange {
189 channel: MidiChannel::Ch1,
190 controller: 1,
191 value: 64,
192 })
193 );
194 // Program change is forwarded (the host routes it to program selection).
195 assert_eq!(
196 parse_midi(&[0xC0, 5]),
197 Some(MidiEvent::ProgramChange {
198 channel: MidiChannel::Ch1,
199 program: 5,
200 })
201 );
202 // SysEx, empty, and other non-channel-voice messages are dropped.
203 assert_eq!(parse_midi(&[0xF0, 1, 2]), None);
204 assert_eq!(parse_midi(&[]), None);
205 }
206}