From 47ffb46aa28659fc6fddcc2c576ec0d804864e79 Mon Sep 17 00:00:00 2001 From: Me Here Date: Wed, 3 Jun 2026 15:31:07 -0500 Subject: [PATCH] pm-grid: fix dropped notes in chords (queue USB-MIDI packets) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bulk MIDI endpoint holds one 4-byte packet until the host reads it (~once per USB frame), so calling send_bytes twice for simultaneous lane hits dropped the 2nd note (WouldBlock, silently ignored). Queue note-ons in a VecDeque and drain one-per-poll, keeping the rest for the next iteration — chords now play in full (staggered ~1ms, imperceptible) instead of all-but-one. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/pm-grid/src/main.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/rust/pm-grid/src/main.rs b/rust/pm-grid/src/main.rs index 9849306..a88830f 100644 --- a/rust/pm-grid/src/main.rs +++ b/rust/pm-grid/src/main.rs @@ -19,6 +19,7 @@ #![no_main] extern crate alloc; +use alloc::collections::VecDeque; use alloc::string::String; use alloc::vec::Vec; use embedded_alloc::LlffHeap as Heap; @@ -781,6 +782,9 @@ fn main() -> ! { let (mut nextrep_x, mut nextrep_y) = (0i64, 0i64); let mut last_frame_us = 0i64; let mut hb_us = 0i64; + // pending USB-MIDI packets: the bulk endpoint holds one 4-byte packet at a time, so simultaneous + // notes are queued here and drained one-per-poll (otherwise the 2nd note of a chord is dropped). + let mut note_q: VecDeque<[u8; 4]> = VecDeque::new(); loop { let us = now_us(); @@ -842,10 +846,21 @@ fn main() -> ! { px = x; py = y; - // ---- scheduler: advance clocks, send a USB-MIDI note-on per lane hit (ch10) ---- + // ---- scheduler: advance clocks, queue a USB-MIDI note-on per lane hit (ch10) ---- app.tick(now_ns, |note, vel| { - let _ = midi.send_bytes([0x09, 0x99, note, vel]); // cable 0, note-on, channel 10 + if note_q.len() < 64 { + note_q.push_back([0x09, 0x99, note, vel]); // cable 0, note-on, channel 10 + } }); + // drain the queue to the endpoint: send until it's busy (WouldBlock), keep the rest for the + // next poll. This is why chords play in full instead of dropping all but the first note. + while let Some(&pkt) = note_q.front() { + if midi.send_bytes(pkt).is_ok() { + note_q.pop_front(); + } else { + break; + } + } // ---- ticker scroll advance (~120ms) ---- // (uses the frame clock implicitly; scroll_off wraps mod scroll_total)