use std::sync::mpsc::{channel, Receiver, Sender};
use crate::error::{IoError, IoResult};
use crate::midi_backend::MidiBackend;
use crate::midi_message::MidiMessage;
pub struct MidirBackend {
rx: Receiver<MidiMessage>,
_conn: midir::MidiInputConnection<()>,
}
impl MidirBackend {
pub fn list_ports(client_name: &str) -> IoResult<()> {
let mi =
midir::MidiInput::new(client_name).map_err(|e| IoError::Init(format!("midir: {e}")))?;
let ports = mi.ports();
for (i, p) in ports.iter().enumerate() {
let name = mi.port_name(p).unwrap_or_else(|_| "?".into());
eprintln!(" MIDI port #{i}: {name}");
}
eprintln!(" ({} ports total)", ports.len());
Ok(())
}
pub fn new(name: &str) -> IoResult<Self> {
let mi = midir::MidiInput::new(name).map_err(|e| IoError::Init(format!("midir: {e}")))?;
let ports = mi.ports();
if ports.is_empty() {
return Err(IoError::DeviceNotFound(
"no MIDI input ports available".into(),
));
}
let find_real = |pname: &str| -> bool {
let lower = pname.to_lowercase();
!lower.contains("through") && !lower.contains("loop") && !lower.contains("rtmidi")
};
let mut chosen = 0usize;
for (i, p) in ports.iter().enumerate() {
let pname = mi.port_name(p).unwrap_or_else(|_| "?".into());
if find_real(&pname) {
chosen = i;
break;
}
}
Self::connect(name, |midi_in, all_ports| {
let pname = midi_in
.port_name(&all_ports[chosen])
.unwrap_or_else(|_| "?".into());
Ok((chosen, pname))
})
}
pub fn new_by_port(name: &str, port_index: usize) -> IoResult<Self> {
Self::connect(name, |midi_in, ports| {
if port_index >= ports.len() {
return Err(IoError::DeviceNotFound(format!(
"port index {} out of range ({} total)",
port_index,
ports.len()
)));
}
Ok((
port_index,
midi_in
.port_name(&ports[port_index])
.unwrap_or_else(|_| "?".into()),
))
})
}
pub fn new_by_name(name: &str, port_name: &str) -> IoResult<Self> {
Self::connect(name, |midi_in, ports| {
for (i, p) in ports.iter().enumerate() {
let pname = midi_in.port_name(p).unwrap_or_else(|_| "?".into());
if pname.contains(port_name) {
return Ok((i, pname));
}
}
Err(IoError::DeviceNotFound(format!(
"no MIDI port matching '{}' ({} ports available)",
port_name,
ports.len()
)))
})
}
fn connect(
name: &str,
find: impl FnOnce(&midir::MidiInput, &[midir::MidiInputPort]) -> IoResult<(usize, String)>,
) -> IoResult<Self> {
let midi_in =
midir::MidiInput::new(name).map_err(|e| IoError::Init(format!("midir: {e}")))?;
let ports = midi_in.ports();
if ports.is_empty() {
return Err(IoError::DeviceNotFound(
"no MIDI input ports available".into(),
));
}
let (port_idx, port_name) = find(&midi_in, &ports)?;
let port = &ports[port_idx];
let (tx, rx): (Sender<MidiMessage>, Receiver<MidiMessage>) = channel();
let conn = midi_in
.connect(
port,
"rill-midi-in",
move |_timestamp, message, _data| {
let msg = bytes_to_midi(message);
let _ = tx.send(msg);
},
(),
)
.map_err(|e| IoError::Init(format!("midir connect: {e}")))?;
log::info!(
"midir: connected to port #{port_idx} '{port_name}' ({} total)",
ports.len()
);
Ok(Self { rx, _conn: conn })
}
}
impl MidiBackend for MidirBackend {
fn poll(&mut self) -> IoResult<Vec<MidiMessage>> {
let mut events = Vec::new();
loop {
match self.rx.try_recv() {
Ok(msg) => events.push(msg),
Err(std::sync::mpsc::TryRecvError::Empty) => break,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
return Err(IoError::Backend("midir channel disconnected".into()));
}
}
}
Ok(events)
}
}
fn bytes_to_midi(bytes: &[u8]) -> MidiMessage {
match bytes.len() {
0 => MidiMessage::new(0, 0, 0),
1 => MidiMessage::new(bytes[0], 0, 0),
2 => MidiMessage::new(bytes[0], bytes[1], 0),
_ => MidiMessage::new(bytes[0], bytes[1], bytes[2]),
}
}