2026-09-13 13:24:18 +02:00
|
|
|
//! 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.
|
2026-10-08 18:44:40 +02:00
|
|
|
//! The channel nibble is kept: tagged columns voice only their channel.
|
2026-09-13 13:24:18 +02:00
|
|
|
|
|
|
|
|
use anyhow::{anyhow, Result};
|
|
|
|
|
use midir::os::unix::VirtualInput;
|
|
|
|
|
use midir::{MidiInput, MidiInputConnection};
|
|
|
|
|
use rtrb::Producer;
|
|
|
|
|
|
|
|
|
|
use crate::engine::Event;
|
|
|
|
|
|
2026-10-08 18:44:40 +02:00
|
|
|
/// Decode one MIDI message into an engine event.
|
|
|
|
|
pub fn decode(msg: &[u8]) -> Option<Event> {
|
|
|
|
|
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<Event>,
|
|
|
|
|
port_name: Option<String>,
|
|
|
|
|
) -> Result<MidiInputConnection<Producer<Event>>> {
|
2026-09-13 13:24:18 +02:00
|
|
|
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<Event>| {
|
2026-10-08 18:44:40 +02:00
|
|
|
if let Some(ev) = decode(msg) {
|
|
|
|
|
let _ = prod.push(ev);
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(name) = port_name {
|
|
|
|
|
let ports = input.ports();
|
|
|
|
|
let port = ports
|
|
|
|
|
.iter()
|
2026-10-08 18:44:40 +02:00
|
|
|
.find(|p| {
|
|
|
|
|
input
|
|
|
|
|
.port_name(p)
|
|
|
|
|
.map(|n| n.contains(&name))
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
})
|
2026-09-13 13:24:18 +02:00
|
|
|
.cloned()
|
|
|
|
|
.ok_or_else(|| anyhow!("no MIDI input port matching {name:?}"))?;
|
2026-10-08 18:44:40 +02:00
|
|
|
input
|
|
|
|
|
.connect(&port, "galdr-in", cb, prod)
|
|
|
|
|
.map_err(|e| anyhow!("MIDI connect: {e}"))
|
2026-09-13 13:24:18 +02:00
|
|
|
} else {
|
2026-10-08 18:44:40 +02:00
|
|
|
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);
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|
|
|
|
|
}
|