Add MP3 audio stream support with hardware/software decoder backends

New ASSET_LOADER_TYPE_MP3 (hand-rolled MPEG-1/2/2.5 Layer III header
parser - no third-party dependency needed just for metadata, since PSP's
hardware path doesn't need one at all) plus a shared audiostreammp3.c
stream layer mirroring audiostreampcm.c's shape. Generalized the stream
dispatch (hoisted sampleRate/channels onto audiostream_t, added
audioStreamGetTotalFrames()/Seek()/Read()) so all three platform audio
backends keep working unchanged, just calling the generic names instead
of PCM-specific ones.

Two decoder backends behind one interface: PSP uses the real sceMp3
hardware decoder (firmware-offloaded, lazily initialized on first use);
Linux and Dolphin share one minimp3-based software decoder (public
domain, vendored via CMake FetchContent) - libogc's own MP3Player wraps
libmad (GPL) and drives its own output pipeline, not a fit for the
ansnd-based architecture already in place, so skipped in favor of the
shared minimp3 path.

Fixed three real bugs found via hardware/runtime testing along the way:
- LAME's Xing header counts its own placeholder frame in the declared
  total, which made playback stall permanently one frame short of the
  declared end (looked like "never loops") - fixed by subtracting it.
- sceMp3Decode() can return more PCM than one MPEG frame's worth in a
  single call (PSP's pcmBuf is provisioned for 2x), overflowing the
  shared per-frame decode buffer with no bound check - very intermittent
  corruption/clicking on real hardware. Widened the buffer to the real
  worst case and added an assertion.
- sceMp3ResetPlayPosition()'s exact internal reset semantics aren't
  documented precisely enough to trust for looping - occasionally
  disagreed with the fresh stream position fed right after, clicking at
  the loop boundary about 1 in 3-4 loops. Rewind now fully tears down and
  recreates the decoder instead, the same path already proven correct at
  first Init. Also widened the PSP ring buffer to absorb that now-heavier
  operation, capping each top-up call's own work so the bigger buffer
  doesn't turn into one long blocking decode burst instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 08:21:30 -05:00
parent ac8023d50f
commit f8f8a80a21
31 changed files with 1530 additions and 72 deletions
+9 -9
View File
@@ -29,9 +29,9 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream) {
SDL_AudioSpec desired;
memoryZero(&desired, sizeof(SDL_AudioSpec));
desired.freq = (int) stream->pcm.sampleRate;
desired.freq = (int) stream->sampleRate;
desired.format = AUDIO_S16SYS;
desired.channels = stream->pcm.channels;
desired.channels = stream->channels;
desired.samples = AUDIO_LINUX_LEAD_FRAMES;
stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
@@ -53,8 +53,8 @@ errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
const size_t totalFrames = audioStreamPcmGetTotalFrames(stream);
const size_t frameSize = stream->channels * sizeof(int16_t);
const size_t totalFrames = audioStreamGetTotalFrames(stream);
// Consumed synchronously (right here, in the same call that decided to
// (re)buffer) rather than left for later - see startFrame's own comment.
@@ -66,7 +66,7 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
size_t endFrame = totalFrames;
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
endFrame = mathMin(
(size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames
(size_t) (stream->loopStart * stream->sampleRate), totalFrames
);
}
// A seek (or a loop restart landing exactly on the loop end) can put
@@ -84,7 +84,7 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
if(seeking) {
SDL_ClearQueuedAudio(stream->platform.device);
}
errorChain(audioStreamPcmSeek(stream, startFrame));
errorChain(audioStreamSeek(stream, startFrame));
stream->platform.position = startFrame;
stream->platform.endFrame = endFrame;
@@ -99,7 +99,7 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
const size_t frameSize = stream->channels * sizeof(int16_t);
const size_t framesRemaining = (
stream->platform.position < stream->platform.endFrame
? stream->platform.endFrame - stream->platform.position
@@ -112,7 +112,7 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
int16_t *chunk = memoryAllocate(framesToRead * frameSize);
size_t framesRead = 0;
errorret_t ret = audioStreamPcmRead(stream, chunk, framesToRead, &framesRead);
errorret_t ret = audioStreamRead(stream, chunk, framesToRead, &framesRead);
if(errorIsNotOk(ret)) {
memoryFree(chunk);
errorChain(ret);
@@ -156,7 +156,7 @@ bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
// empty guarantees a gap by definition: silence has already started by
// the time "empty" can be observed.
const Uint32 leadBytes = (Uint32) (
AUDIO_LINUX_LEAD_FRAMES * stream->pcm.channels * sizeof(int16_t)
AUDIO_LINUX_LEAD_FRAMES * stream->channels * sizeof(int16_t)
);
if(SDL_GetQueuedAudioSize(stream->platform.device) > leadBytes) {