Skip to main content

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}