Repository navigation
Sound never drops: audio slices written ahead of the ST - #7
Merged
Merged
Conversation
Measured on a Mega ST, with the ST reporting its audio cursor in the blit acknowledgement: every refill landed about 0.3 ms before the VBL, while the ST still played the slice (its last ~1.7 samples came from the next one), and the 20 ms gate on audio_render_frame() turned away about 4 refills a second even at 50 frames a second, so the ST replayed a stale 20 ms slice as often. With frames 30 ms late, 27 a second. The audio buffer is now 4 slices of 256 bytes, one VBL each (a VBL plays 224 bytes at 5,585 Hz). The ST's VBL handler moves Timer-B to the next slice and reports which one with a ROM3 read at $FB8600 + slice; it runs with the interrupts masked, so Timer-B never sees A0 anywhere but on a slice, and saves D0 (it interrupts the blit between a MOVEM load and its store). On the RP, a timer interrupt every 4 ms finds the ST's slice from the latest report in the ROM3 ring (commemul_latest(), read-only) and writes the two slices after it from a RAM FIFO; audio_render_frame() only keeps that FIFO topped up from the main loop with the app's fill callback, so a .YMS file on the SD card is still read there. An empty FIFO holds the last sample (an underrun, counted), never stale data. A sample plays at most AUDIO_FIFO_SLICES + 2 VBLs (120 ms) after the callback made it, and the main loop may stall as long. audioSlicesWritten, audioLateSlices and audioUnderruns are readable over SWD. The fill callback API is unchanged. After, same machine, .YMS streaming: 50.0 slices a second in the menu and every demo, with every frame 30 ms late, and with a 60 ms frame every second for a minute; no late slice, no underrun. With every frame 60 ms late (13 frames a second), one underrun in 15 s.
audioSlicesWritten (with its rate under --watch: 50 a second while the ST plays), audioLateSlices and audioUnderruns.
test_audio.c runs audio.c on the host against a simulated ST: at every VBL the ST moves to the next slice and reports it, the slice writer's timer runs five times per VBL, and the main loop tops up the FIFO once per frame unless stalled. Every slice the ST plays is the next piece of the stream, or the last sample held when the FIFO ran dry, and no slice changes while it plays; a main loop missing 3 frames costs nothing, one missing 8 holds the last sample for the difference; a writer stopped for 2 VBLs counts one late slice; an ST that leaves and boots again starts the count over. The .YMS reader: the header checks, a failed open that keeps the loop playing, the wrap at the end of the file to its data, a failed read that plays one slice of silence and starts the file again. shim/ gains pico/time.h (the test's clock and timers), hardware/sync.h, hardware/vreg.h and FatFs's ff.h (files in memory). Slices written one position off, a missing hold, or a wrap to the header each fail it.
The slices written ahead come from the FIFO too, so they add latency, not tolerance: the main loop may go AUDIO_FIFO_SLICES VBLs (80 ms) between two calls to audio_render_frame() without an underrun, and a sample still plays at most AUDIO_FIFO_SLICES + 2 VBLs (120 ms) after the callback made it. The commit that introduced the slices said the main loop may stall 120 ms; the host test showed 80, as did the hardware (one underrun at 13 frames a second, none with a 60 ms frame every second). Also noted in the writer: the ST reports its slice modulo 4, so a writer stalled 4 VBLs or more (80 ms with the interrupts off) counts no late slices for it; the slices still go where they should.
The comment above TIMERB_COUNT in userfw.s states the whole rate chain (Hz, bytes per VBL, slice size, the RP's rate, the .YMS and jingle rate), and test_layout.py now also checks the built-in jingle's rate against Timer-B. The comments that said the jingle was still converted at 6,269 Hz and detuned (it is at 5,585 Hz), that the YM plays one channel, and that the VBL handler resets A0 to the buffer's base are corrected. CLAUDE.md's audio pipeline describes the slices, the VBL handler, the timer-driven writer and its FIFO, the latency and stall limits, the underrun rule and the counters, with the before and after numbers; its region table and ROM3 windows list $FB86xx. The README's audio section, the app skill, the tools' docs and the changelog say the same for app writers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The audio stream no longer drops or tears. Before this change the RP rewrote the whole 1 KB audio buffer from the main loop while the ST played it. On the Mega ST, measured from the RP side:
How it works now
$FA4100is split into 4 slices of 256 bytes. The ST plays 224 bytes per VBL (112 samples at 5,585.45 Hz, two YM volumes each).$FB86xx, where xx is the slice number. It masks IRQs while it does this, so a Timer-B fire never sees A0 pointing at the report window.commemul_latest()) and fills slices up to 2 VBLs ahead of the ST. It never writes the slice being played.audio_render_frame()tops up that FIFO from the main loop, so the main loop may go 4 VBLs (80 ms) between calls without a gap. If the FIFO runs dry, the slice holds the last sample (silence, no click) and counts an underrun.audioSlicesWritten,audioLateSlicesandaudioUnderrunsare inswd.py counters..YMSstreaming.Tests
tests/host/test_audio.cruns the writer against a simulated ST: slice order, write-ahead, the FIFO, underruns and holding the last sample, slow main loops, and a stalled writer. It has 315 checks and runs under ASan/UBSan.test_layout.pychecks the slice count and size, the$FB86xxwindow and the jingle's sample rate against the m68k layout.Hardware (Mega ST, debug build)
.YMSstreamed logged one read failure, then silence. Frames went on at 50 fps, with no panic.Docs
CLAUDE.md,README.md, the framebuffer-app skill,tools/dev/README.mdandCHANGELOG.mddescribe the slices, the rate chain, the latency and the 80 ms tolerance.