You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Giga: render audio from the DAC interrupt so long collections don't drop sound #290
On the Arduino Giga R1, audio drops out for about 50 ms whenever the garbage collector runs longer than the DAC ring holds. The ring is 93 ms. Synth Station on the Giga hits this roughly once a minute. A flash write (140-280 ms for a /flash file) does the same.
The cause is that the Giga renders the audio graph from the interpreter's own loop, since there are no threads on that port. Anything that keeps the interpreter busy longer than the ring lets the DAC run dry. A collection under Synth Station takes 100-150 ms, and it grows with the number of live objects.
What would change
Render the graph from an interrupt tied to the DAC/I2S DMA instead of from the interpreter. A collection can't delay an interrupt, so long collections stop causing dropouts. The same approach applies to any board without threads, which is every board that renders from the interpreter today, and it leaves the boards that already render on a separate thread (the ESP32 family) as they are.
Flash writes only stop causing dropouts on the Giga, and only if the render interrupt runs at a priority the QSPI flash code doesn't mask. On RP2, i.MX RT, nRF and SAMD a flash write halts the CPU or disables interrupts, so those would get the collection fix only.
Things to settle
Nodes that allocate, raise or read files can't run in an interrupt. They'd be refused when attached, with an error naming the node, and Python-written graph parts would feed the graph through a ring filled from the interpreter.
The render priority on the Giga: high enough to survive flash writes, low enough not to starve SPI and display DMA.
Keeping today's interpreter-driven mode as a fallback for a release.
How we'd know it works
Synth Station on the Giga for ten minutes with a forced collection every few seconds and periodic flash writes: no dropouts, where the current build fails the same run.
Scope, to settle before work starts: this changes the STM32H7 driver (audioif_stm32.c) only. The ESP32, unix and Windows drivers already render on a thread of their own and stay exactly as they are. Anything the engine in audiodsp needs for an interrupt render, such as refusing nodes that allocate, raise or read files, is off unless a port asks for interrupt rendering. Those three hosts must render the same output before and after, checked with their existing tests and a render comparison.
#291 adds a reason for this: on the Giga, aioble's Python callbacks hold off the render the same way a long collection does, and the Bluetooth stack on its own doesn't.
Measured on a GIGA R1 with the Display Shield, with #392 and audiodsp's interrupt hooks. Synth Station's electric piano held a chord for ten minutes while the board ran gc.collect() every 3 s and wrote 32 KB to /flash every 30 s:
Firmware
Starved frames
Of played
Longest gc
Longest write
Interpreter render (before)
1,000,908
28,808,278 (3.5%)
111 ms
856 ms
Interrupt render
0
28,659,242
148 ms
1,172 ms
Interrupt build, render_mode("interpreter"), 2 min
194,806
5,768,392 (3.4%)
107 ms
860 ms
On the three things to settle:
Priority. The render interrupt runs at NVIC priority 7, just below the flash code's mask (6) and above I2C, the timers and PendSV. The flash code masks one block operation at a time rather than a whole write, so the render catches up between blocks: writes of over a second each starved nothing. The longest render was 24.9 ms, and the deepest stack 4,396 of its 12,288 bytes. A full-screen LVGL refresh took a median 97 ms on the interpreter render and 99 ms on the interrupt render, with nothing starved during either.
Nodes that allocate. The render runs with the heap locked, so a pull that tries to allocate gets a MemoryError, which the interrupt catches and counts rather than crashing. The engine's pull doesn't allocate or raise, so nothing is refused at attach time.
Fallback._audioif.render_mode("interpreter") switches back at run time, and a build against an audiodsp without the hooks stays on the interpreter.
On unix, a render through the audiodsp branch is byte-identical to one through its main. The ESP32, unix and Windows drivers don't change.
Fixed by #392, with audiodsp#403 (and audiodsp#405 for the CircuitPython build). The GIGA now renders its audio from the DAC interrupt, at NVIC priority 7, just below the QSPI flash code's mask, so a collection, a flash write or Python callbacks can't hold it up. The flash code masks one block at a time, and the render catches up between blocks.
Measured on a GIGA R1 with the Display Shield. Synth Station ran for 10 minutes with gc.collect() every 3 s and a 32 KB /flash write every 30 s. The build before starved 1,000,908 of 28.8M frames (3.5%); this one starved 0 of 28.7M, with collections up to 148 ms and writes up to 1.17 s. The longest render was 24.9 ms, and a full-screen LVGL refresh took a median 99 ms against 97 ms before.
render_mode("interpreter") keeps the old behaviour as a fallback. The ESP32, unix and Windows drivers didn't change, and unix renders are byte-identical before and after.
Not measured on the board: the merged build itself, which also carries audiodsp's 20 ms level ramps. The figures above come from the same driver, built against audiodsp before those landed.
Activity
bdbarnett commentedon Nov 10, 2026
Scope, to settle before work starts: this changes the STM32H7 driver (
audioif_stm32.c) only. The ESP32, unix and Windows drivers already render on a thread of their own and stay exactly as they are. Anything the engine in audiodsp needs for an interrupt render, such as refusing nodes that allocate, raise or read files, is off unless a port asks for interrupt rendering. Those three hosts must render the same output before and after, checked with their existing tests and a render comparison.#291 adds a reason for this: on the Giga, aioble's Python callbacks hold off the render the same way a long collection does, and the Bluetooth stack on its own doesn't.
bdbarnett commentedon Nov 10, 2026
Measured on a GIGA R1 with the Display Shield, with #392 and audiodsp's interrupt hooks. Synth Station's electric piano held a chord for ten minutes while the board ran
gc.collect()every 3 s and wrote 32 KB to/flashevery 30 s:render_mode("interpreter"), 2 minOn the three things to settle:
_audioif.render_mode("interpreter")switches back at run time, and a build against an audiodsp without the hooks stays on the interpreter.On unix, a render through the audiodsp branch is byte-identical to one through its main. The ESP32, unix and Windows drivers don't change.
bdbarnett commentedon Nov 10, 2026
Fixed by #392, with audiodsp#403 (and audiodsp#405 for the CircuitPython build). The GIGA now renders its audio from the DAC interrupt, at NVIC priority 7, just below the QSPI flash code's mask, so a collection, a flash write or Python callbacks can't hold it up. The flash code masks one block at a time, and the render catches up between blocks.
Measured on a GIGA R1 with the Display Shield. Synth Station ran for 10 minutes with
gc.collect()every 3 s and a 32 KB /flash write every 30 s. The build before starved 1,000,908 of 28.8M frames (3.5%); this one starved 0 of 28.7M, with collections up to 148 ms and writes up to 1.17 s. The longest render was 24.9 ms, and a full-screen LVGL refresh took a median 99 ms against 97 ms before.render_mode("interpreter")keeps the old behaviour as a fallback. The ESP32, unix and Windows drivers didn't change, and unix renders are byte-identical before and after.Not measured on the board: the merged build itself, which also carries audiodsp's 20 ms level ramps. The figures above come from the same driver, built against audiodsp before those landed.