Skip to content

MIDI playback on the FunHouse piezo: status, and two remaining limitations #41

Open

Description

@tyeth

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 AudioMixer

Workarounds 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions