More robust midi implementation
Some checks failed
Deploy Website / deploy (push) Failing after 4m58s

This commit is contained in:
2026-01-31 23:58:57 +01:00
parent 15a4300db5
commit 2100b82dad
12 changed files with 393 additions and 201 deletions

View File

@@ -53,16 +53,16 @@ pub enum AudioCommand {
#[derive(Clone, Debug)]
pub enum MidiCommand {
NoteOn { channel: u8, note: u8, velocity: u8 },
NoteOff { channel: u8, note: u8 },
CC { channel: u8, cc: u8, value: u8 },
PitchBend { channel: u8, value: u16 },
Pressure { channel: u8, value: u8 },
ProgramChange { channel: u8, program: u8 },
Clock,
Start,
Stop,
Continue,
NoteOn { device: u8, channel: u8, note: u8, velocity: u8 },
NoteOff { device: u8, channel: u8, note: u8 },
CC { device: u8, channel: u8, cc: u8, value: u8 },
PitchBend { device: u8, channel: u8, value: u16 },
Pressure { device: u8, channel: u8, value: u8 },
ProgramChange { device: u8, channel: u8, program: u8 },
Clock { device: u8 },
Start { device: u8 },
Stop { device: u8 },
Continue { device: u8 },
}
pub enum SeqCommand {
@@ -484,7 +484,7 @@ pub(crate) struct SequencerState {
key_cache: KeyCache,
buf_audio_commands: Vec<TimestampedCommand>,
cc_memory: Option<CcMemory>,
active_notes: HashMap<(u8, u8), ActiveNote>,
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
}
impl SequencerState {
@@ -951,12 +951,12 @@ fn sequencer_loop(
if let Some((midi_cmd, dur)) = parse_midi_command(&tsc.cmd) {
match midi_tx.load().try_send(midi_cmd.clone()) {
Ok(()) => {
if let (MidiCommand::NoteOn { channel, note, .. }, Some(dur_secs)) =
if let (MidiCommand::NoteOn { device, channel, note, .. }, Some(dur_secs)) =
(&midi_cmd, dur)
{
let dur_us = (dur_secs * 1_000_000.0) as i64;
seq_state.active_notes.insert(
(*channel, *note),
(*device, *channel, *note),
ActiveNote {
off_time_us: current_time_us + dur_us,
start_time_us: current_time_us,
@@ -985,22 +985,24 @@ fn sequencer_loop(
const MAX_NOTE_DURATION_US: i64 = 30_000_000; // 30 second safety timeout
if output.flush_midi_notes {
for ((channel, note), _) in seq_state.active_notes.drain() {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { channel, note });
for ((device, channel, note), _) in seq_state.active_notes.drain() {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { device, channel, note });
}
// Send MIDI panic (CC 123 = All Notes Off) on all 16 channels
for chan in 0..16u8 {
let _ = midi_tx
.load()
.try_send(MidiCommand::CC { channel: chan, cc: 123, value: 0 });
// Send MIDI panic (CC 123 = All Notes Off) on all 16 channels for all devices
for dev in 0..4u8 {
for chan in 0..16u8 {
let _ = midi_tx
.load()
.try_send(MidiCommand::CC { device: dev, channel: chan, cc: 123, value: 0 });
}
}
} else {
seq_state.active_notes.retain(|&(channel, note), active| {
seq_state.active_notes.retain(|&(device, channel, note), active| {
let should_release = current_time_us >= active.off_time_us;
let timed_out = (current_time_us - active.start_time_us) > MAX_NOTE_DURATION_US;
if should_release || timed_out {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { channel, note });
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { device, channel, note });
false
} else {
true
@@ -1026,15 +1028,25 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
if parts.len() < 2 {
return None;
}
let find_param = |key: &str| -> Option<&str> {
parts.iter()
.position(|&s| s == key)
.and_then(|i| parts.get(i + 1).copied())
};
let device: u8 = find_param("dev").and_then(|s| s.parse().ok()).unwrap_or(0);
match parts[1] {
"note" => {
// /midi/note/<note>/vel/<vel>/chan/<chan>/dur/<dur>
// /midi/note/<note>/vel/<vel>/chan/<chan>/dur/<dur>/dev/<dev>
let note: u8 = parts.get(2)?.parse().ok()?;
let vel: u8 = parts.get(4)?.parse().ok()?;
let chan: u8 = parts.get(6)?.parse().ok()?;
let dur: Option<f64> = parts.get(8).and_then(|s| s.parse().ok());
let vel: u8 = find_param("vel")?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
let dur: Option<f64> = find_param("dur").and_then(|s| s.parse().ok());
Some((
MidiCommand::NoteOn {
device,
channel: chan,
note,
velocity: vel,
@@ -1043,12 +1055,13 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
))
}
"cc" => {
// /midi/cc/<cc>/<val>/chan/<chan>
// /midi/cc/<cc>/<val>/chan/<chan>/dev/<dev>
let cc: u8 = parts.get(2)?.parse().ok()?;
let val: u8 = parts.get(3)?.parse().ok()?;
let chan: u8 = parts.get(5)?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((
MidiCommand::CC {
device,
channel: chan,
cc,
value: val,
@@ -1057,27 +1070,27 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
))
}
"bend" => {
// /midi/bend/<value>/chan/<chan>
// /midi/bend/<value>/chan/<chan>/dev/<dev>
let value: u16 = parts.get(2)?.parse().ok()?;
let chan: u8 = parts.get(4)?.parse().ok()?;
Some((MidiCommand::PitchBend { channel: chan, value }, None))
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::PitchBend { device, channel: chan, value }, None))
}
"pressure" => {
// /midi/pressure/<value>/chan/<chan>
// /midi/pressure/<value>/chan/<chan>/dev/<dev>
let value: u8 = parts.get(2)?.parse().ok()?;
let chan: u8 = parts.get(4)?.parse().ok()?;
Some((MidiCommand::Pressure { channel: chan, value }, None))
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::Pressure { device, channel: chan, value }, None))
}
"program" => {
// /midi/program/<value>/chan/<chan>
// /midi/program/<value>/chan/<chan>/dev/<dev>
let program: u8 = parts.get(2)?.parse().ok()?;
let chan: u8 = parts.get(4)?.parse().ok()?;
Some((MidiCommand::ProgramChange { channel: chan, program }, None))
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::ProgramChange { device, channel: chan, program }, None))
}
"clock" => Some((MidiCommand::Clock, None)),
"start" => Some((MidiCommand::Start, None)),
"stop" => Some((MidiCommand::Stop, None)),
"continue" => Some((MidiCommand::Continue, None)),
"clock" => Some((MidiCommand::Clock { device }, None)),
"start" => Some((MidiCommand::Start { device }, None)),
"stop" => Some((MidiCommand::Stop { device }, None)),
"continue" => Some((MidiCommand::Continue { device }, None)),
_ => None,
}
}