Files
dusk/src/duskpsp/audio/audiostreammp3decoder.c
T
YourWishesandClaude Sonnet 5 f8f8a80a21 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 <[email protected]>
2026-09-01 08:21:30 -05:00

262 lines
9.3 KiB
C

/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "audiostreammp3decoder.h"
#include "audio/audiostream.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <pspmp3.h>
#include <psputility.h>
// Per the PSP SDK's mp3 sample and pspmp3.h's own documentation - these
// are hard minimums sceMp3ReserveMp3Handle() requires, not tunable.
#define AUDIO_MP3_PSP_STREAM_BUF_SIZE (16 * 1024)
#define AUDIO_MP3_PSP_PCM_BUF_SIZE (16 * (1152 / 2))
// sceMp3 manages its own internal ring buffer of compressed bytes rather
// than being handed a whole file/buffer up front (unlike, say,
// zip_source_buffer_create elsewhere in this codebase) - it tells the
// caller exactly where in the source stream it wants more data from
// (sceMp3GetInfoToAddStreamData()'s srcpos) and how much room is free to
// receive it into, and the caller is expected to fetch exactly that and
// call sceMp3NotifyAddStreamData(). This decoder only ever feeds it
// forward, sequentially, from stream->mp3.file (never anything sceMp3
// itself wouldn't naturally request next), so despite the API being built
// for arbitrary positioning, this driver never needs to seek that file
// out of sequence - see audioStreamMp3DecoderFillStream()'s own assertion.
// End-of-stream sentinel returned by sceMp3Decode() once nothing more can
// be decoded - not documented in pspmp3.h, but confirmed by the PSP SDK's
// own mp3 sample (main.c), which explicitly excludes it from its generic
// "sceMp3Decode failed" error path.
#define AUDIO_MP3_PSP_DECODE_END_OF_STREAM 0x80671402
// Set once audioStreamMp3DecoderInit() has loaded sceMp3's firmware
// modules and initialized its resource pool - lazily, on the first MP3
// stream ever created, rather than unconditionally at engine startup, so
// a game that never plays an MP3 never pays for it. Guards
// audioStreamMp3DecoderGlobalDispose() so it only tears down what was
// actually set up.
static bool_t AUDIO_MP3_PSP_RESOURCES_READY = false;
errorret_t audioStreamMp3DecoderFillStream(audiostream_t *stream) {
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
SceUChar8 *dst = NULL;
SceInt32 toWrite = 0;
SceInt32 srcPos = 0;
SceInt32 status = sceMp3GetInfoToAddStreamData(
decoder->handle, &dst, &toWrite, &srcPos
);
if(status < 0) {
errorThrow("sceMp3GetInfoToAddStreamData failed: 0x%08X", status);
}
if(toWrite <= 0) {
errorOk();
}
assertTrue(
(size_t) srcPos == decoder->streamPosition,
"sceMp3 requested stream data out of the sequence this decoder feeds it in."
);
errorChain(assetFileRead(&stream->mp3.file, dst, (size_t) toWrite));
const size_t bytesRead = (size_t) stream->mp3.file.lastRead;
status = sceMp3NotifyAddStreamData(decoder->handle, (SceInt32) bytesRead);
if(status < 0) {
errorThrow("sceMp3NotifyAddStreamData failed: 0x%08X", status);
}
decoder->streamPosition += bytesRead;
if(bytesRead < (size_t) toWrite) decoder->endOfFile = true;
errorOk();
}
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
if(!AUDIO_MP3_PSP_RESOURCES_READY) {
int status = sceUtilityLoadModule(PSP_MODULE_AV_AVCODEC);
if(status < 0) {
errorThrow("Failed to load PSP_MODULE_AV_AVCODEC: 0x%08X", status);
}
status = sceUtilityLoadModule(PSP_MODULE_AV_MP3);
if(status < 0) {
errorThrow("Failed to load PSP_MODULE_AV_MP3: 0x%08X", status);
}
status = sceMp3InitResource();
if(status < 0) {
errorThrow("sceMp3InitResource failed: 0x%08X", status);
}
AUDIO_MP3_PSP_RESOURCES_READY = true;
}
const assetmp3file_t *mp3 = &stream->asset->data.mp3;
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
decoder->mp3Buf = memoryAlign(64, AUDIO_MP3_PSP_STREAM_BUF_SIZE);
decoder->pcmBuf = memoryAlign(64, AUDIO_MP3_PSP_PCM_BUF_SIZE);
decoder->streamPosition = mp3->dataOffset;
decoder->endOfFile = false;
SceMp3InitArg initArg;
memoryZero(&initArg, sizeof(initArg));
initArg.mp3StreamStart = (SceOff) mp3->dataOffset;
initArg.mp3StreamEnd = (SceOff) (mp3->dataOffset + mp3->dataSize);
initArg.mp3Buf = decoder->mp3Buf;
initArg.mp3BufSize = AUDIO_MP3_PSP_STREAM_BUF_SIZE;
initArg.pcmBuf = decoder->pcmBuf;
initArg.pcmBufSize = AUDIO_MP3_PSP_PCM_BUF_SIZE;
const SceInt32 handle = sceMp3ReserveMp3Handle(&initArg);
if(handle < 0) {
memoryFree(decoder->mp3Buf);
memoryFree(decoder->pcmBuf);
errorThrow("sceMp3ReserveMp3Handle failed: 0x%08X", handle);
}
decoder->handle = handle;
// sceMp3Init() needs some stream data already fed before it can
// determine the stream's format - see the PSP SDK's own mp3 sample.
errorChain(audioStreamMp3DecoderFillStream(stream));
const SceInt32 initStatus = sceMp3Init(decoder->handle);
if(initStatus < 0) {
sceMp3ReleaseMp3Handle(decoder->handle);
memoryFree(decoder->mp3Buf);
memoryFree(decoder->pcmBuf);
errorThrow("sceMp3Init failed: 0x%08X", initStatus);
}
errorOk();
}
errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
sceMp3ReleaseMp3Handle(decoder->handle);
memoryFree(decoder->mp3Buf);
decoder->mp3Buf = NULL;
memoryFree(decoder->pcmBuf);
decoder->pcmBuf = NULL;
errorOk();
}
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
const assetmp3file_t *mp3 = &stream->asset->data.mp3;
errorChain(assetFileRewind(&stream->mp3.file));
errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset));
// Deliberately not sceMp3ResetPlayPosition(): the SDK doesn't document
// precisely enough whether it fully clears sceMp3's own internal
// compressed-data ring buffer or just resets a playback cursor, leaving
// room for it to disagree with the fresh stream position we just fed it
// from - a mismatch that would only be audible as an occasional click
// right at the loop point, depending on how much of sceMp3's internal
// buffer happened to still hold stale data at reset time. Fully tearing
// down and recreating the decoder instead guarantees a clean state by
// construction, via the exact same path already proven correct for the
// stream's very first Init - one extra handle release/reserve cycle per
// loop wrap is negligible next to how rarely loops actually happen.
errorChain(audioStreamMp3DecoderDispose(stream));
errorChain(audioStreamMp3DecoderInit(stream));
errorOk();
}
errorret_t audioStreamMp3DecoderDecodeFrame(
audiostream_t *stream,
int16_t *out,
size_t *outFrames
) {
assertNotNull(stream, "Stream cannot be NULL.");
assertNotNull(out, "Out buffer cannot be NULL.");
assertNotNull(outFrames, "outFrames cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
const size_t frameSize = stream->channels * sizeof(int16_t);
for(;;) {
if(sceMp3CheckStreamDataNeeded(decoder->handle) > 0 && !decoder->endOfFile) {
errorChain(audioStreamMp3DecoderFillStream(stream));
}
SceShort16 *pcm = NULL;
const SceInt32 bytesDecoded = sceMp3Decode(decoder->handle, &pcm);
if(bytesDecoded > 0) {
// `out` (stream->mp3.pending) is sized for
// AUDIO_MP3_MAX_SAMPLES_PER_FRAME samples - sized specifically to
// cover sceMp3's pcmBuf provisioning (see this decoder's own
// AUDIO_MP3_PSP_PCM_BUF_SIZE comment). Asserted rather than trusted:
// a real hardware overflow here was previously silent, corrupting
// whatever followed stream->mp3.pending in memory, and only showed
// up as very intermittent audio clicking.
assertTrue(
(size_t) bytesDecoded <= AUDIO_MP3_MAX_SAMPLES_PER_FRAME * sizeof(int16_t),
"sceMp3Decode returned more PCM than the destination buffer can hold."
);
const size_t frames = (size_t) bytesDecoded / frameSize;
memoryCopy(out, pcm, (size_t) bytesDecoded);
*outFrames = frames;
errorOk();
}
if(
bytesDecoded < 0 &&
(uint32_t) bytesDecoded != AUDIO_MP3_PSP_DECODE_END_OF_STREAM
) {
errorThrow("sceMp3Decode failed: 0x%08X", bytesDecoded);
}
// Decoded nothing this call - if sceMp3 doesn't need more data either,
// it's genuinely out of frames to produce.
if(sceMp3CheckStreamDataNeeded(decoder->handle) <= 0) {
*outFrames = 0;
errorOk();
}
if(decoder->endOfFile) {
// sceMp3 wants more data but the file has none left - one last
// decode attempt already happened above with whatever was fed;
// nothing further will ever arrive.
*outFrames = 0;
errorOk();
}
}
}
errorret_t audioStreamMp3DecoderGlobalDispose(void) {
if(!AUDIO_MP3_PSP_RESOURCES_READY) {
errorOk();
}
const SceInt32 status = sceMp3TermResource();
if(status < 0) {
errorThrow("sceMp3TermResource failed: 0x%08X", status);
}
sceUtilityUnloadModule(PSP_MODULE_AV_MP3);
sceUtilityUnloadModule(PSP_MODULE_AV_AVCODEC);
AUDIO_MP3_PSP_RESOURCES_READY = false;
errorOk();
}