Skip to content

Fix MIDI running status + sequencer freeze under external MIDI clock (F8)#748

Merged
bwhitman merged 3 commits into
mainfrom
claude/adoring-vaughan-47d64f
Jun 21, 2026
Merged

Fix MIDI running status + sequencer freeze under external MIDI clock (F8)#748
bwhitman merged 3 commits into
mainfrom
claude/adoring-vaughan-47d64f

Conversation

@bwhitman

@bwhitman bwhitman commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #747. Addresses real-world reports of an AMYboard "crashing" / no longer receiving MIDI when an external device streams MIDI clock (F8).

Three related F8 / MIDI-clock bugs, in two configs:

1. Running status broken by interleaved real-time bytes (default config)

System Real-Time messages (0xF80xFF: clock, start, stop, active sensing) may be interleaved anywhere in the stream — even between the data bytes of another message — and must not disturb running status. convert_midi_bytes_to_messages overwrote current_midi_message[0] for every status byte, so a clock byte (24 ppqn while a sequencer plays) clobbered the stored note-on status. The next running-status note's data bytes then resolved to status 0xF0, matched no case, and were silently dropped:

90 3C 40     note-on, dispatched fine — running status now 0x90
F8           clock → current_midi_message[0] becomes 0xF8   ← clobbered
3E           status = 0xF8 & 0xF0 = 0xF0, matches no case → byte DROPPED
40           same → DROPPED

This is the "stops receiving MIDI" symptom in default config (sync off, F8 is otherwise a no-op). Fix: real-time bytes (>= 0xF8) dispatch via a scratch buffer and never touch current_midi_message[] / midi_message_slot; a genuine new status byte also resets midi_message_slot so a truncated message self-heals.

2. Sequencer freeze under external clock (sync enabled)

sequencer_external_clock was a one-way latch: sequencer_midi_clock_tick() set it true on the first F8 and nothing ever cleared it, so once the external clock stopped, sequencer_check_and_fill() early-returned forever and the internal sequencer froze until reboot — turning sync back off did not recover it. Fix: sequencer_external_clock_disable() (clears the latch, resumes internal clocking, re-anchors the tick timer), called from amy_external_midi_sync(0), making "sync off" a real recovery path. The internal-until-first-external-tick handover is preserved.

3. Transport (FA/FC) ungated

MIDI Start/Stop drove the sequencer unconditionally, so a connected DAW's transport could halt the AMYboard's own internal sequence even with sync off. Both are now gated behind external_midi_sync_enabled.

Testing

  • make amy-module clean (-Wall -Wextra, no warnings); full suite 95 tests pass.
  • Added a regression test TestMidiRunningStatusClock that feeds a raw byte stream through the parser — three running-status note-ons with an interleaved 0xF8 clock and 0xFE active-sensing — and asserts all three notes sound. Verified non-vacuous: it fails against the pre-fix parser (err=-30.1 dB, two notes dropped) and is bit-exact with the fix (err=-100.0 dB).
  • This closes the underlying gap — the byte-stream parser previously had no coverage (inject_midi bypasses it). New amy.inject_midi_bytes() binding routes bytes through convert_midi_bytes_to_messages.

🤖 Generated with Claude Code

System Real-Time messages (0xF8-0xFF: clock, start, stop, active sensing)
may be interleaved anywhere in the stream -- even between the data bytes of
another message -- and must not disturb running status. convert_midi_bytes_to_messages
overwrote current_midi_message[0] for every status byte, so a clock byte
(streamed at 24 ppqn while a sequencer plays) clobbered the stored note-on
status. The next running-status note's data bytes then resolved to status
0xF0, matched no case, and were silently dropped.

Route real-time bytes through a scratch buffer so they never touch
current_midi_message[] or midi_message_slot. Also reset midi_message_slot on
a genuine new status byte so a truncated message self-heals instead of
desyncing the data-byte parity of the next one.

Fixes #747

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bwhitman

bwhitman commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

🎛️ AMYboard HW CI

Dispatched this PR's amy SHA to the tulipcc bench — it builds the AMYboard firmware with your change and runs it on the physical board (audio spectral-compared to a committed reference). Results post here in a few minutes.

…nc switch

Two follow-on fixes to the F8/MIDI-clock handling, for users who enable
external sync (tulip.external_midi_sync(True)):

1. sequencer_external_clock was a one-way latch: sequencer_midi_clock_tick()
   set it true on the first F8 and nothing ever cleared it, so once an
   external clock stopped, sequencer_check_and_fill() early-returned forever
   and the internal sequencer froze until reboot -- turning sync back off did
   not recover it. Add sequencer_external_clock_disable() (clears the latch,
   resumes internal clocking, re-anchors the tick timer) and call it from
   amy_external_midi_sync(0), making "sync off" a real recovery path.

2. MIDI Start (FA) / Stop (FC) drove the sequencer unconditionally, so a
   connected DAW's transport could halt the AMYboard's own internal sequence
   even with sync off. Gate both behind external_midi_sync_enabled.

The internal-until-first-external-tick handover is preserved: the latch still
switches to external clock on the first F8 while sync is enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bwhitman bwhitman changed the title Fix MIDI running status broken by interleaved real-time messages Fix MIDI running status + sequencer freeze under external MIDI clock (F8) Jun 21, 2026
bwhitman added a commit to shorepine/tulipcc that referenced this pull request Jun 21, 2026
Bumps the amy submodule from 694cb22 to 612e3cf so this preview also includes
the second commit on shorepine/amy#748: the external-clock sequencer-freeze
fix and the FA/FC transport gating. Lets the preview exercise the full F8
behavior, not just running status.
…bytes

The byte-stream parser (convert_midi_bytes_to_messages) had no test coverage:
inject_midi hands a pre-formed 3-byte message straight to
amy_event_midi_message_received, bypassing the parser entirely -- which is why
the running-status bug went unnoticed.

- pyamy: add inject_midi_bytes(data, usb=0) binding that feeds a raw byte
  stream through convert_midi_bytes_to_messages, plus the amy.inject_midi_bytes
  Python wrapper.
- test: TestMidiRunningStatusClock sends three note-ons with the 0x90 status
  byte given once, interleaving a MIDI clock (0xF8) before note 2 and an
  active-sensing (0xFE) before note 3. All three notes (60, 62, 64) must sound.

Verified the test catches the regression: against the pre-fix parser it fails
at err=-30.1 dB (notes 62 and 64 dropped); with the fix it is bit-exact
(err=-100.0 dB). Full suite: 95 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bwhitman added a commit to shorepine/tulipcc that referenced this pull request Jun 21, 2026
Rebases the preview onto current tulipcc main and re-pins the amy submodule to
the latest shorepine/amy#748 head (d4b740b: running-status + sequencer-freeze
fixes + the regression test). Matches the amy-PR HWCI build that already passed
on the bench; regenerates the firmware after a transient corrupt-flash boot
loop on the prior preview build.
@bwhitman

Copy link
Copy Markdown
Collaborator Author

🎛️ HW CI (physical bench)

Built from this AMY PR, pinned into the tulipcc amy submodule.

AMYboard (USB-MIDI + AMY zP → audio): ✅ PASS — flashed this PR’s firmware; all checks matched the references.

Tulip (TULIP4_R11; serial-REPL audio + WiFi screenshot): ✅ PASS — flashed this PR’s firmware; all checks matched the references.

⬇️ Artifacts: recordings · screenshot · serial logs · run logs

Self-hosted bench. Audio spectral-compared to ref/hwci_basic.wav + ref/tulip_basic.wav; Tulip screenshot pixel-compared to ref/tulip_screenshot.png. Both analog outs share one capture card, so the tests run sequentially.

@bwhitman bwhitman merged commit 055b70f into main Jun 21, 2026
1 check passed
pull Bot pushed a commit to manmuqingshan/tulipcc that referenced this pull request Jun 22, 2026
The AMYboard bench occasionally fails with "board MIDI never appeared": the
board panic-loops in the ESP-IDF 2nd-stage bootloader (Guru Meditation,
StoreProhibited) before USB-MIDI enumerates. This is NOT a firmware bug --
the crashing image's bootloader is byte-identical to builds that boot fine
(diffing the two full images over the bootloader region turns up only the
embedded __TIME__ build string + the derived checksum/SHA), so it's a
transient flash-read/power/reset glitch after the ~61 s USB flash.
Investigated via shorepine/amy#748.

Wrap flash+boot in a retry loop (--boot-attempts, default 3). On "MIDI never
appeared", stop the attempt's boot logger (freeing the dongle so esptool can
drive reset), re-flash -- which forces ROM download mode, a clean known-good
reset -- and wait again. A genuinely broken build still fails every attempt,
so real regressions aren't masked; each failed attempt's crash log is kept in
the combined serial log.

wait_for_board() now returns None on timeout instead of sys.exit. Verified the
loop with mocked flash/wait: all-fail does N re-flashes then exits; a boot on
attempt 2 flashes twice then proceeds.
pull Bot pushed a commit to manmuqingshan/tulipcc that referenced this pull request Jun 22, 2026
python.md covered sending MIDI notes (midi_out + amy.AMY_MIDI) but not clock
sync. Add a "Syncing to MIDI clock" subsection:

- Following: tulip.external_midi_sync(True/False) to slave AMYboard to an
  external F8 clock (off by default); F8 sets tempo, FA/FC start/stop while
  enabled.
- Sending: emit FA/F8/FC with tulip.midi_out(), and a tempo-locked clock
  generator via tulip.seq_add_callback() (48 PPQ internal vs 24 PPQ MIDI ->
  period=2).

The FA/FC-gating and "sync off restores internal clock" semantics described
here land with shorepine/amy#748.
pull Bot pushed a commit to manmuqingshan/tulipcc that referenced this pull request Jun 22, 2026
Updates the amy submodule ff59eb27 -> 055b70f (current amy main), pulling in
the merged shorepine/amy#748:
  - Fix MIDI running status broken by interleaved real-time bytes (F8/FE):
    a clock byte no longer clobbers the stored status, so running-status notes
    after an F8 aren't dropped.
  - Fix sequencer freeze under external MIDI clock; gate MIDI transport (FA/FC)
    behind external_midi_sync.
  - Regression test for the parser.
Also includes amy#749 (docs: sending MIDI out via wave=AMY_MIDI) and the
AMYboard hardware-CI trigger plumbing.

Validated on the physical AMYboard bench via the amy#748 amypr HWCI run (PASS).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Please support MIDI Running Status

1 participant