Channel tags, stems, master chain, FM, K envelope, slide, UDP control, reconnect
Grid grammar: - a base-36 glyph above a column's top operator tags it with a MIDI channel; '>' right of the tag makes the channel mono-legato (60 ms slide) - operators below a '!' form the stereo master chain: Z delay, W chorus, plus any filter/shaper - X: phase modulation by the cell to the left (and above), index from the right - K: control envelope; left of an operator it drives that operator's left param (2KL5 = 303 filter sweep) - per-note params: @ velocity, ^ key tracking, ? random; registers now read on the digit scale, so MIDI CC and UDP registers have full range I/O: - MIDI keeps the channel nibble - UDP (orca's ';', port 49161): k7 register, *3 bang a column, @34O write a glyph, #name load a patch - --stems n: one mono PipeWire node per channel tag (galdr.ch<tag>), sample-aligned with the master; separate nodes because a playback stream's AUX ports deliver no data in PipeWire 1.6 - the audio thread reconnects when PipeWire restarts; status line shows it - patches: ~/hometaping/<date>.galdr by default, .galdr extension, Ctrl+S, GALDR_DEBUG=<file> logs events Examples with render tests: drums, orchestra, acid. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b53fdd5a6d
commit
4bf13a2bf0
+67
-15
@@ -1,5 +1,6 @@
|
||||
//! 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;
|
||||
@@ -8,34 +9,85 @@ use rtrb::Producer;
|
||||
|
||||
use crate::engine::Event;
|
||||
|
||||
pub fn connect(prod: Producer<Event>, port_name: Option<String>) -> Result<MidiInputConnection<Producer<Event>>> {
|
||||
/// 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>>> {
|
||||
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>| {
|
||||
if msg.len() < 2 {
|
||||
return;
|
||||
if let Some(ev) = decode(msg) {
|
||||
let _ = prod.push(ev);
|
||||
}
|
||||
let status = msg[0] & 0xF0;
|
||||
let ev = match status {
|
||||
0x90 if msg[2] > 0 => Event::NoteOn { note: msg[1], vel: msg[2] },
|
||||
0x80 => Event::NoteOff { note: msg[1] },
|
||||
0x90 => Event::NoteOff { note: msg[1] }, // note-on velocity 0
|
||||
0xB0 => Event::Cc { idx: msg[1], val: msg[2] },
|
||||
_ => return,
|
||||
};
|
||||
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))
|
||||
.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}"))
|
||||
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}"))
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user