Repository navigation
Play sound through the DVI cable on RP2350 boards - #11510
mikeysklar wants to merge 11 commits into
Conversation
picodvi.AudioOut(framebuffer) plays any 48 kHz audiocore sample (WaveFile, RawSample, Mixer, synthio, MP3) through the DVI cable, for displays with speakers. It is shaped like audiobusio.I2SOut. Audio goes out as 48 kHz stereo data islands in the horizontal sync of the 640-wide output, built from two pre-encoded frame banks that the existing frame interrupt swaps and a background callback refills. Nothing changes on the cable until an AudioOut is created, and deinit gives the memory back. The packet encoder comes from pico_hdmi. CIRCUITPY_PICODVI_AUDIOOUT turns it off for boards without a DVI connector. Initial work by Phillip Torrone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ladyada-eagleclaw
left a comment
There was a problem hiding this comment.
AI-assisted review of 7445a68ab77b3fefda35c6af6c0888574c2472e1, using source inspection and focused host-side tests. I recommend addressing these four issues before merging.
-
[P1] Prevent recursive audio refills.
AudioOut.c:159-160, alsoresume()at lines 181-183.These call
audioout_refill()directly, outside the background callback runner's reentry protection. Reading a WAV from an SD card runs background tasks inside the RP2350 SPI transfer, so a queued frame callback can enter the same refill while the outer read still holds the card's SPI lock. The nested read can time out and setsource_done = true, stopping playback even if the outer read succeeds. Please guard both synchronous refill paths against reentry. A host test simulating the recursive read/error reproducedsource_done=1with zero samples queued; this was not a hardware reproduction. -
[P2] Handle empty looping samples.
AudioOut.c:64-76.audio.play(audiocore.RawSample(bytearray(), sample_rate=48000), loop=True)never makes progress: each fetch returns zero frames andGET_BUFFER_DONE, then the loop resets the sample and retries indefinitely. There is no pending-interrupt check in this loop. Please reject or terminate playback when an entire loop produces no frames. The unchanged staging code with the RawSample buffer implementation timed out in the host test. -
[P2] Validate the framebuffer before running a queued callback.
dvi_audio.c.inc:252-256.Framebuffers occupy the reusable static
display_buses[]union. Deinitialization clears the audio pointer but leaves an already queued refill callback. If the slot is reused before that callback runs, such as releasing DVI and constructing a FourWire bus, the callback still casts the slot to a framebuffer. FourWire's SPI pointer occupies the same position asdvi_audio, so this can dereference unrelated memory and crash. This ordering is possible when calling an importedrelease_displaysfunction, whose VM CALL_FUNCTION path does not drain background callbacks after the call. Please validate that the callback still refers to the active framebuffer before accessing its audio state, or invalidate the queued callback during teardown. This finding is from source inspection. -
[P2] Correct the audio clock-regeneration packet cadence.
dvi_audio.c.inc:237-239.ACR packets are emitted on lines 0, 4, ..., 132: 34 packets per video frame, followed by a 493-line interval until the next ACR packet. For
N=6144and 48 kHz, HDMI requires an average CTS transmission rate of128 * 48000 / 6144 = 1000per second, near the nominal transmission times. Please schedule ACR packets across the frame at that cadence. This is a protocol-compliance issue; receiver-specific audible failures have not been measured. Reference: HDMI 1.3 section 7.8.2, printed page 111.
Validation: inspected the complete PR diff, sample providers, SPI/background callback paths, display teardown/reuse, and packet encoding; git diff --check passed. The host tests used unchanged staging/refill functions with stubbed surrounding interfaces. No hardware tests were performed. The merge from main leaves the reviewed audio code unchanged.
play() and resume() refill directly. Background tasks that run during an SD card read could start the frame refill inside that one and stop playback early. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
play(RawSample(bytearray()), loop=True) looped forever without making progress. Stop when a whole pass gives no frames. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The framebuffer can be deinitialized and its storage reused by another display before a queued refill runs. Only run it for the active framebuffer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ACR packets went on every 4th blanking line: 34 per frame, then nothing for the rest of the frame. Send them every 37.5 lines instead, about 1000 per second as N = 6144 at 48 kHz expects, including on active lines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tannewt
left a comment
There was a problem hiding this comment.
Some questions about the object lifetime. I think the AudioOut API looks good. Want to have it on by default on Fruit Jam? Maybe play a start up sound? It should be on board then too.
| #if CIRCUITPY_PICODVI_AUDIOOUT | ||
| { MP_ROM_QSTR(MP_QSTR_AudioOut), MP_ROM_PTR(&picodvi_audioout_type) }, |
There was a problem hiding this comment.
Don't conditionalize the name. instead, have the make_new raise NotImplementedError(). That is a bit clearer than an unknown name.
| self->sample = MP_OBJ_NULL; | ||
| // Keep this object alive while it is attached, as the frame interrupt | ||
| // schedules refills for it. | ||
| MP_STATE_PORT(picodvi_audioout) = MP_OBJ_FROM_PTR(self); |
There was a problem hiding this comment.
Want to tie it's lifetime to the Framebuffer instead? That way it could live outside the VM.
There was a problem hiding this comment.
Why .inc? I think this could either be in the file itself or a separate source file that is linked with the other. Including a c source is weird.
| } picodvi_framebuffer_obj_t; | ||
|
|
||
| #if CIRCUITPY_PICODVI_AUDIOOUT | ||
| // Audio over DVI, used by picodvi.AudioOut. |
There was a problem hiding this comment.
Why can't these go in AudioOut.h?
|
@ladyada-eagleclaw thanks! Fixed all four:
|
AudioOut was missing from picodvi where it is not supported, such as on RP2040. Keep the name and have the constructor raise NotImplementedError instead, as pulseio.PulseOut does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No more .c.inc files. The framebuffer audio code moves into AudioOut.c, its only user, and the frame interrupt hooks are declared in AudioOut.h. The pico_hdmi packet encoder is now dvi_audio_packet.c with a header. The DVI timing defines both files use move to Framebuffer_RP2350.h. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The framebuffer now holds its AudioOut instead of a global root pointer, and runs its refills directly. Framebuffer deinit and VM reset release it through the framebuffer, and displayio marks it for the GC when the framebuffer lives in a display slot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@tannewt thanks! Changes:
Follow-up PR:
|
|
Thanks for the fixes. The empty-sample and recursive-refill host checks now pass on One part of finding 4 remains: the average ACR rate. The packets are now spread across the frame, but At the default RP2350 clock, the calculation is:
A host calculation of this loop confirms the 17-packet count. It happens to give exactly 1,000/sec with a 25 MHz pixel clock / 50 Hz frame rate, so that case would miss this issue. Please preserve the ACR timing phase across frame boundaries, including the idle/silence output. At 60 Hz, three frames should carry 50 ACR packets in total, rather than the current 51. The packet distribution fix is an improvement, but I would keep this part of the original finding open. Reference: HDMI section 7.8.2, printed page 111. No receiver-specific audible failure has been measured. |
The ACR schedule restarted every frame, giving 17 per frame, 1020 per second at 60 Hz. Carry the timing from frame to frame instead: 17, 17, 16, so 1000 per second. When nothing is playing, the refill now queues silence frames so the timing carries on; the fixed base list is only used if no frame is ready. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@ladyada-eagleclaw thanks, good catch on the rate:
|
… picodvi-audioout
What
New
picodvi.AudioOutplays sound through the DVI cable.Why
Fruit Jam games can make sound on a TV with no extra parts.
Works like
audiobusio.I2SOut. Nothing changes on the cable until an AudioOut exists.Initial work by Phillip Torrone. Packet encoder from pico_hdmi (Unlicense).
Hardware tested
Fruit Jam,
11.0.0-alpha.1-33-gf03d8b6641, Ubuntu 26.04 and a monitor with speakers. Not tested: Metro RP2350, Feather RP2350.How I tested it
tests/circuitpython-manual/picodvi/audioout_formats.pyaudioout_bad_values.pyaudioout_release.pyScope
48 kHz only. Fits 320x240, or 640x480 up to 4-bit color.
CIRCUITPY_PICODVI_AUDIOOUT = 0turns it off.AI assistance
Claude Code helped write and test this. I listened to the tones and read the code.
🤖 Generated with Claude Code