Skip to content

midi_out: skip USB gadget when no host is enumerated (fixes MIDI clock out over TRS without USB)#955

Merged
bwhitman merged 1 commit into
mainfrom
fix-midi-out-usb-unmounted-stall
Jul 20, 2026
Merged

midi_out: skip USB gadget when no host is enumerated (fixes MIDI clock out over TRS without USB)#955
bwhitman merged 1 commit into
mainfrom
fix-midi-out-usb-unmounted-stall

Conversation

@bwhitman

Copy link
Copy Markdown
Collaborator

Problem

Reported on Discord: with tulip.external_midi_sync(send=True) on AMYboard, MIDI clock goes out over both USB and the TRS MIDI out port while USB is attached — but with USB disconnected (eurorack power only), the TRS out goes nearly silent and the board appears to hang; a downstream device "occasionally responds".

Root cause

midi_out() always tries the USB gadget branch when AMY_MIDI_IS_USB_GADGET is configured. TinyUSB's tud_midi_stream_write has no mount check — it just fills the class driver's tx FIFO, and write_flush silently fails when no host has enumerated the device. So with no USB host attached, the FIFO fills after ~16 messages and every subsequent write returns 0, which sends midi_out into its stall-recovery loop (mp_usbd_task() + vTaskDelay(1) × 1000) — ~1 second per byte — before falling through to the UART write.

MIDI clock out ticks come from sequencer_check_and_fill()midi_clock_out_tick() in the render path, so at 24 PPQ this wedges the sequencer and reduces the TRS/DIN clock from 48 bytes/sec to ~1/sec at 120 bpm — exactly the observed "occasionally responds" behavior. The same stall hits any midi_out caller (e.g. Tulip's tulip.midi_out()) once USB is unplugged.

Fix

Guard the gadget branch with tud_ready() (enumerated and not suspended). Unpowered / charger-only / detached USB skips the gadget write instantly and the UART gets bytes at full rate; behavior with a real host attached is unchanged.

Testing

  • make test: 117 tests pass
  • tulipcc AMYboard firmware (idf.py -DMICROPY_BOARD=AMYBOARD build) builds clean against this branch

🤖 Generated with Claude Code

tud_midi_stream_write has no mount check: with no USB host attached
(e.g. AMYboard on eurorack power alone) it fills its tx FIFO, then
returns 0 forever, and midi_out's stall-recovery loop burns ~1s per
byte before falling through to the UART write. MIDI clock out
(external_midi_sync send mode, 24 PPQ) hits this from the render path,
wedging the sequencer and reducing TRS/DIN clock from 48/sec to ~1/sec
at 120 bpm. Guard the gadget branch with tud_ready() so USB is only
attempted when a host has enumerated us and the bus isn't suspended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🎛️ AMY HW CI (AMYboard bench)

Flashed this PR's AMY (LoadTestChord: 6-voice Juno patch=1, one held note every 2 s) onto the physical AMYboard and measured the smoothed render load as the chord grows — back-to-back with the same sketch built at the PR's merge base, so Δ is this PR's own cost.

PASS — the bench ran the test to completion.

notes held main @ c829ff2 this PR Δ
1 1385 1383 -2
2 1548 1543 -5
3 2136 2134 -2
4 2328 2324 -4
5 2975 2968 -7
6 3169 3163 -6

Full chord settled render μs: 3164 (was 3170, Δ -0.2%) (peak 3173, 39 samples)

⬇️ Artifacts: serial log · load trace · report

Self-hosted bench (amyboardci). FAIL means only that the test could not run — the load values are informational, with no threshold and no audio compare. See tools/arduino_loadsweep/.

@bwhitman
bwhitman merged commit 009d0b4 into main Jul 20, 2026
12 checks passed
@bwhitman

Copy link
Copy Markdown
Collaborator Author

⛓️ tulipcc integration PR opened

This merge was pinned into tulipcc for full-system CI: shorepine/tulipcc#1215

Test it there and merge that PR to move tulipcc onto this AMY.

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.

1 participant