Repository navigation
MIDI playback on the FunHouse piezo: status, and two remaining limitations #41
Description
Collecting the FunHouse MIDI work in one place, and recording two limitations that are not addressed by any of the PRs below and will bite later.
The hardware constraint everything follows from
The FunHouse is ESP32-S2. board.SPEAKER is GPIO42, a plain GPIO — not a DAC pin. The ESP32-S2 DAC is hardwired to GPIO17/GPIO18 (ports/espressif/common-hal/audioio/AudioOut.c), and GPIO18 is board.LIGHT, the ambient light sensor. So audioio cannot reach the piezo at all, and before this work there was no audiopwmio on the espressif port either.
Two working paths
A. pwmio + adafruit_midi_parser — works on stock firmware, no custom build. The piezo is driven as a PWM square wave, so playback is monophonic; where the file has a chord, sound the highest held note. synthio is still useful here for midi_to_hz(), but cannot render to a piezo itself — its output is a sample stream needing a DAC or I2S.
Note that MIDIPlayer.play() dispatches every event sharing an absolute tick before returning, so setting the PWM frequency inside on_note_on makes a chord chirp up through its notes. Push/pop a held-note list in the handlers and update the pin once per play().
B. audiopwmio + synthio — needs #33, which adds audiopwmio.PWMAudioOut to the ESP32-S2 (LEDC + a GPTimer ISR at the sample rate, 78.125 kHz carrier). This is polyphonic, up to 12 voices (CIRCUITPY_SYNTHIO_MAX_CHANNELS on espressif). Confirmed working on hardware.
MIDI parsing fixes for this path: #34, #35, #36, #37, #38. Combined UAT branch and firmware: #39.
Limitation 1 — synthio.from_file() is type 0 only
from_file() checks for the literal header MThd\0\0\0\6\0\0\0\1 and rejects anything else, so only single-track (format 0) MIDI loads. Most files in the wild are format 1. There is an upstream TODO acknowledging this, at shared-bindings/synthio/__init__.c:236:
// TODO: for a multi-track MIDI (type 1), return an AudioMixerWorkarounds today: flatten to format 0 on the host (mido, or any DAW export set to type 0), or bypass the file entirely and hand the raw track bytes to synthio.MidiTrack(buffer, tempo=...), which has no header check and takes tempo directly in ticks per second.
Not addressed by #34–#38 — those fix how a track is decoded, not how many tracks are accepted.
Limitation 2 — adafruit_midi_parser cannot parse 0xA0 / 0xD0
This affects path A only; it is a library issue, not a CircuitPython one.
adafruit_midi_parser dispatches on 0x80, 0x90, 0xB0, 0xC0, 0xE0, plus SysEx and meta. It has no branch for:
0xA0— polyphonic key pressure (poly aftertouch)0xD0— channel pressure (channel aftertouch)
Either one raises MIDIParseError: Unknown event type: 0x<nn> at offset <n> and the whole file fails to load. Both turn up routinely in DAW exports and in MIDI downloaded from the web, so a file that plays fine elsewhere can simply refuse to open.
The fix upstream is two elif branches skipping 2 and 1 data bytes respectively. Until then, either strip those events host-side or catch MIDIParseError and fall back.
For contrast, the native synthio.MidiTrack decoder does handle both (they fall into its "two data bytes to ignore" / "one data byte to ignore" cases), so path B is unaffected.
Neither limitation blocks the PRs above; recording them so the next person does not rediscover them the hard way.
🤖 Generated with Claude Code
Activity