use midir::{MidiInput, MidiInputPort as RawMidiInputPort};
use crate::{
error::{Error, Result},
midi::MidiEvent,
playback::{AudioHandle, MidiSink},
};
const CLIENT_NAME: &str = "vst3-host";
fn midi_err(context: &str, err: impl std::fmt::Display) -> Error {
Error::MidiError(format!("{context}: {err}"))
}
#[derive(Clone)]
pub struct MidiInputPort {
name: String,
raw: RawMidiInputPort,
}
impl MidiInputPort {
pub fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Debug for MidiInputPort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MidiInputPort")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
pub struct MidiInputConnection {
_inner: midir::MidiInputConnection<()>,
}
pub fn list_midi_input_ports() -> Result<Vec<MidiInputPort>> {
let input = MidiInput::new(CLIENT_NAME).map_err(|e| midi_err("init MIDI input", e))?;
let ports = input
.ports()
.into_iter()
.map(|raw| {
let name = input
.port_name(&raw)
.unwrap_or_else(|_| "Unknown MIDI input".to_string());
MidiInputPort { name, raw }
})
.collect();
Ok(ports)
}
pub fn connect<F>(port: &MidiInputPort, mut callback: F) -> Result<MidiInputConnection>
where
F: FnMut(MidiEvent) + Send + 'static,
{
let input = MidiInput::new(CLIENT_NAME).map_err(|e| midi_err("init MIDI input", e))?;
let inner = input
.connect(
&port.raw,
&port.name,
move |_timestamp, bytes, ()| {
if let Some(event) = parse_midi(bytes) {
callback(event);
}
},
(),
)
.map_err(|e| midi_err("connect MIDI input", e))?;
Ok(MidiInputConnection { _inner: inner })
}
pub fn bind_to_handle(port: &MidiInputPort, handle: &AudioHandle) -> Result<MidiInputConnection> {
let sink: MidiSink = handle.midi_sink();
connect(port, move |event| {
sink.send_midi(event);
})
}
fn parse_midi(bytes: &[u8]) -> Option<MidiEvent> {
MidiEvent::from_midi_bytes(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::midi::MidiChannel;
#[test]
fn list_midi_input_ports_does_not_error() {
match list_midi_input_ports() {
Ok(_) => {}
Err(Error::MidiError(msg)) if msg.contains("could not be initialized") => {
eprintln!("skipping: no MIDI subsystem available ({msg})");
}
Err(e) => panic!("enumeration failed: {e:?}"),
}
}
#[test]
fn parse_midi_forwards_channel_voice_and_drops_the_rest() {
assert_eq!(
parse_midi(&[0x90, 60, 100]),
Some(MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 60,
velocity: 100,
})
);
assert_eq!(
parse_midi(&[0xB0, 1, 64]),
Some(MidiEvent::ControlChange {
channel: MidiChannel::Ch1,
controller: 1,
value: 64,
})
);
assert_eq!(
parse_midi(&[0xC0, 5]),
Some(MidiEvent::ProgramChange {
channel: MidiChannel::Ch1,
program: 5,
})
);
assert_eq!(parse_midi(&[0xF0, 1, 2]), None);
assert_eq!(parse_midi(&[]), None);
}
}