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`], which covers every channel-voice
110/// message — note on/off, control change, program change, pitch bend, and both aftertouch
111/// forms. Anything else (SysEx, system/realtime, running status, truncated data) does not
112/// parse and is silently ignored.
113///
114/// `callback` runs on a `midir`-owned thread — see the [module docs](self#threading). It must be
115/// `Send + 'static`. The returned [`MidiInputConnection`] keeps the port open until dropped.
116pub fn connect<F>(port: &MidiInputPort, mut callback: F) -> Result<MidiInputConnection>
117where
118    F: FnMut(MidiEvent) + Send + 'static,
119{
120    let input = MidiInput::new(CLIENT_NAME).map_err(|e| midi_err("init MIDI input", e))?;
121    let inner = input
122        .connect(
123            &port.raw,
124            &port.name,
125            move |_timestamp, bytes, ()| {
126                if let Some(event) = parse_midi(bytes) {
127                    callback(event);
128                }
129            },
130            (),
131        )
132        .map_err(|e| midi_err("connect MIDI input", e))?;
133    Ok(MidiInputConnection { _inner: inner })
134}
135
136/// Open `port` and forward every parseable incoming message into a running [`AudioHandle`].
137///
138/// A convenience over [`connect`]: each received [`MidiEvent`] is pushed into the plugin's
139/// command ring (via the handle's [`MidiSink`]), so notes/CC played on the device reach the
140/// plugin. A `Send` sink is captured into the callback, so the connection keeps forwarding as
141/// long as the returned guard lives — independent of the `AudioHandle`'s own lifetime.
142///
143/// Events are dropped silently if the audio command ring is full (the same drop-on-full behavior
144/// as [`AudioHandle::send_midi`]). The returned [`MidiInputConnection`] keeps the port open until
145/// dropped.
146pub fn bind_to_handle(port: &MidiInputPort, handle: &AudioHandle) -> Result<MidiInputConnection> {
147    let sink: MidiSink = handle.midi_sink();
148    connect(port, move |event| {
149        sink.send_midi(event);
150    })
151}
152
153/// Parse a raw MIDI message into a [`MidiEvent`], returning `None` for messages the library does
154/// not forward. Factored out of the `midir` callback so it can be unit-tested without hardware.
155fn parse_midi(bytes: &[u8]) -> Option<MidiEvent> {
156    MidiEvent::from_midi_bytes(bytes)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::midi::MidiChannel;
163
164    #[test]
165    fn list_midi_input_ports_does_not_error() {
166        // On a machine with a MIDI subsystem, enumeration must succeed (possibly empty).
167        // Headless CI has no ALSA sequencer (`/dev/snd/seq`), so `midir` can't initialize a
168        // backend at all — that's an absent subsystem, not an enumeration bug, so accept it.
169        match list_midi_input_ports() {
170            Ok(_) => {}
171            Err(Error::MidiError(msg)) if msg.contains("could not be initialized") => {
172                eprintln!("skipping: no MIDI subsystem available ({msg})");
173            }
174            Err(e) => panic!("enumeration failed: {e:?}"),
175        }
176    }
177
178    #[test]
179    fn parse_midi_forwards_channel_voice_and_drops_the_rest() {
180        assert_eq!(
181            parse_midi(&[0x90, 60, 100]),
182            Some(MidiEvent::NoteOn {
183                channel: MidiChannel::Ch1,
184                note: 60,
185                velocity: 100,
186            })
187        );
188        assert_eq!(
189            parse_midi(&[0xB0, 1, 64]),
190            Some(MidiEvent::ControlChange {
191                channel: MidiChannel::Ch1,
192                controller: 1,
193                value: 64,
194            })
195        );
196        // Program change is forwarded (the host routes it to program selection).
197        assert_eq!(
198            parse_midi(&[0xC0, 5]),
199            Some(MidiEvent::ProgramChange {
200                channel: MidiChannel::Ch1,
201                program: 5,
202            })
203        );
204        // SysEx, empty, and other non-channel-voice messages are dropped.
205        assert_eq!(parse_midi(&[0xF0, 1, 2]), None);
206        assert_eq!(parse_midi(&[]), None);
207    }
208}