//! MIDI input. galdr owns a virtual input port named "galdr" (symmetric with //! orca's virtual output), so orca or any client connects to it in the graph. //! The channel nibble is kept: tagged columns voice only their channel. use anyhow::{anyhow, Result}; use midir::os::unix::VirtualInput; use midir::{MidiInput, MidiInputConnection}; use rtrb::Producer; use crate::engine::Event; /// Decode one MIDI message into an engine event. pub fn decode(msg: &[u8]) -> Option { if msg.len() < 3 { return None; } let ch = msg[0] & 0x0F; match msg[0] & 0xF0 { 0x90 if msg[2] > 0 => Some(Event::NoteOn { ch, note: msg[1], vel: msg[2], }), 0x80 | 0x90 => Some(Event::NoteOff { ch, note: msg[1] }), // note-on velocity 0 0xB0 => Some(Event::Cc { idx: msg[1], val: msg[2], }), _ => None, } } pub fn connect( prod: Producer, port_name: Option, ) -> Result>> { let mut input = MidiInput::new("galdr").map_err(|e| anyhow!("MIDI init: {e}"))?; input.ignore(midir::Ignore::None); let cb = move |_stamp: u64, msg: &[u8], prod: &mut Producer| { if let Some(ev) = decode(msg) { let _ = prod.push(ev); } }; if let Some(name) = port_name { let ports = input.ports(); let port = ports .iter() .find(|p| { input .port_name(p) .map(|n| n.contains(&name)) .unwrap_or(false) }) .cloned() .ok_or_else(|| anyhow!("no MIDI input port matching {name:?}"))?; input .connect(&port, "galdr-in", cb, prod) .map_err(|e| anyhow!("MIDI connect: {e}")) } else { input .create_virtual("galdr", cb, prod) .map_err(|e| anyhow!("create virtual MIDI port: {e}")) } } #[cfg(test)] mod tests { use super::*; #[test] fn decode_keeps_channel() { assert_eq!( decode(&[0x92, 60, 100]), Some(Event::NoteOn { ch: 2, note: 60, vel: 100 }) ); assert_eq!( decode(&[0x8F, 60, 0]), Some(Event::NoteOff { ch: 15, note: 60 }) ); assert_eq!( decode(&[0x90, 60, 0]), Some(Event::NoteOff { ch: 0, note: 60 }) ); assert_eq!(decode(&[0xB3, 7, 64]), Some(Event::Cc { idx: 7, val: 64 })); assert_eq!(decode(&[0xF8]), None); } }