Skip to content

Two profiles: 50 fps, or 25 fps with 22 kHz and 25 kHz sound - #12

Merged
diegoparrilla merged 2 commits into
release/v1.1.0from
epic/18-sound-profiles
Sep 30, 2026
Merged

diegoparrilla merged 2 commits into
release/v1.1.0from
epic/18-sound-profiles

Conversation

@diegoparrilla

Copy link
Copy Markdown
Contributor

What

A compile-time choice for the app between two ways of sharing the ST's time between the frame copy and the sound (APP_PROFILE, rp/src/include/profile.h):

PROFILE_50FPS (default) PROFILE_25FPS
Frames every VBL, 50 fps every second VBL, 25 fps on every machine
YM (plain ST, Mega ST) 5,585 Hz 21,943 Hz (Timer-B count 28)
DMA chip (STE, Mega STE) 12,517 Hz 25,033 Hz
Suits arcade games, full-screen effects story games with rich sound (laserdisc-style)
APP_PROFILE=PROFILE_25FPS ./build.sh pico_w release <uuid>
  • One ST image for both profiles. fb_init() writes the profile into shared slot 8 ($FA4030), and userfw reads it once at boot. From it, the ST sets Timer-B's count, the DMA chip's mode and lead, and the VBLs per frame. A new frame is taken no sooner than that many VBLs after the last one. The constants of both profiles are named on both sides, and test_layout.py checks that they agree.
  • Bigger audio buffers.
    • The audio buffer grows to 4 KB, with YM slices of 1 KB (878 bytes per VBL at 21,943 Hz).
    • The DMA ring grows to 4 KB at $6F000, with its position reported by 16 bytes.
    • APP_FREE now starts at $FA5180 (about 12.4 KB).
  • Sound assets are unchanged. YM sources (.YMS files, the built-in jingle) stay at 5,585 Hz and are resampled to the output rate.
  • RAM. The two outputs' FIFOs share their memory, which keeps the 25 fps debug build within the RP's RAM.
  • Build IDs. A 25 fps build's ID ends in +25fps, and tools/dev/flash.sh builds it in its own folder. Both examples' CMake files pass APP_PROFILE on.
  • The ST's stopwatch replaces the slack report (both used Timer-A).
    • It's TIME_STUDY = 1 in userfw.s, off by default: Timer-A runs free at 4.07 µs per tick.
    • The VBL and four points of the loop are reported on one timeline ($FB8B / $FB8C / $FB8F).
    • tools/dev/swd.py stopwatch prints them.

Tests

  • Host tests: make -C tests/host test passes. test_audio_dma.c is built and run under both profiles, and test_layout.py checks the constants of both.
  • Builds: debug and release, each in both profiles, compile without warnings. So does the game example at 25 fps.
  • Mega ST (YM):
    • At 25 fps: 24.9 fps. The full copy with 22 kHz sound takes 29.9 ms and leaves 9.75 ms before the next frame. With the mouse moved flat out: 8.9 ms, 1,429 packets, no IKBD error, no late audio slice or underrun. The sound was clean.
    • At 50 fps (release): 50.0 fps with the same trade-offs as before.
    • The 25 fps release build was verified separately.
  • Mega STE (DMA and blitter): at 25 fps, the DMA chip plays at 25 kHz and the sound is clean. At 50 fps: 50.1 fps with the mouse moving and no errors.

The app chooses at compile time how the ST's time is shared between the
frame copy and the sound (APP_PROFILE, rp/src/include/profile.h):
PROFILE_50FPS, the default, keeps a frame every VBL with the YM at
5,585 Hz and the DMA chip at 12,517 Hz; PROFILE_25FPS takes a frame every
second VBL, on every machine, and plays the YM at 21,943 Hz (Timer-B count
28) and the DMA chip at 25,033 Hz. On a plain ST the full-screen copy and
22 kHz sound then take about 35 ms of each 40 ms frame.

The ST image is the same for both: fb_init() writes the profile into
shared slot 8, and userfw reads it once at boot for Timer-B's count, the
DMA chip's mode and lead, and the VBLs a frame (it takes a new frame no
sooner than that many VBLs after the last, counted by its VBL handler).
The constants of both profiles are named on both sides and checked by
test_layout.py; APP_PROFILE comes from the environment through CMake, and
a 25 fps build's ID ends in +25fps (flash.sh builds it in its own folder).

The audio buffer grows to 4 KB (YM slices of 1 KB: 878 bytes a VBL at
21,943 Hz), the DMA ring to 4 KB at $6F000 with its position reported by
16 bytes; APP_FREE starts at $FA5180. YM sources (.YMS files, the jingle)
stay at 5,585 Hz and are resampled to the output's rate. The two outputs'
FIFOs share their memory, which keeps the 25 fps debug build within the
RP's RAM. test_audio_dma.c runs under both profiles.

The ST's stopwatch replaces the slack report, which used the same Timer-A:
TIME_STUDY = 1 in userfw.s (off by default) runs Timer-A free at 4.07 us
a tick, counts its wraps with an interrupt, and reports the VBL and four
points of the loop on one timeline ($FB8B/8C/8F). Debug builds keep the
points and the slack before the VBL that may start the next frame;
tools/dev/swd.py stopwatch reads them.

Measured: Mega ST at 25 fps, the copy with 22 kHz sound takes 29.9 ms and
leaves 9.75 ms before the next frame (8.9 with the mouse moved flat out,
no IKBD error); Mega STE at 25 fps plays the DMA chip at 25 kHz. At
50 fps both machines behave as before.
What each profile trades, how to choose it (APP_PROFILE) and what was
measured, in CLAUDE.md, the README (with the audio section's rates and the
FIFO), the skill and the changelog; the moved audio buffer, DMA ring and
APP_FREE; the stopwatch in place of the slack report, and swd.py
stopwatch in the dev tools' README.
@diegoparrilla
diegoparrilla merged commit 40d8265 into release/v1.1.0 Sep 30, 2026
2 checks passed
@diegoparrilla
diegoparrilla deleted the epic/18-sound-profiles branch September 30, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant