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]>
This commit is contained in:
2026-09-01 08:21:30 -05:00
co-authored by Claude Sonnet 5
parent ac8023d50f
commit f8f8a80a21
31 changed files with 1530 additions and 72 deletions
+1
View File
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
audiopsp.c
audiostreampsp.c
audiostreammp3decoder.c
)
+3
View File
@@ -6,6 +6,7 @@
*/
#include "audiopsp.h"
#include "audiostreammp3decoder.h"
errorret_t audioPSPInit() {
errorOk();
@@ -16,5 +17,7 @@ errorret_t audioPSPUpdate() {
}
errorret_t audioPSPDispose() {
errorChain(audioStreamMp3DecoderGlobalDispose());
errorOk();
}
+261
View File
@@ -0,0 +1,261 @@
/**
* 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();
}
+126
View File
@@ -0,0 +1,126 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include <psptypes.h>
typedef struct audiostream_s audiostream_t;
/**
* Hardware MP3 decoder state (sceMp3) for a single stream. Firmware-side
* decode, off the CPU entirely - see audiostreammp3decoder.c's top-of-file
* comment for the streaming model this drives (sceMp3 manages its own
* internal compressed-byte ring buffer, pulling more from
* stream->mp3.file on demand via audioStreamMp3DecoderFillStream()).
*/
typedef struct {
// Handle returned by sceMp3ReserveMp3Handle(), valid for this stream's
// whole lifetime.
SceInt32 handle;
// Buffers owned by this decoder and registered with sceMp3 via
// SceMp3InitArg - sized to the SDK's documented minimums (mp3Buf must be
// >= 8192 bytes, pcmBuf >= 9216 bytes). Allocated once in
// audioStreamMp3DecoderInit(), freed in audioStreamMp3DecoderDispose().
uint8_t *mp3Buf;
uint8_t *pcmBuf;
// This decoder's own record of how many compressed bytes have been fed
// to sceMp3 so far (relative to the start of the whole file, matching
// stream->mp3.file's own position) - sceMp3GetInfoToAddStreamData()
// reports back the absolute source position it wants read from next,
// and since this decoder only ever feeds it forward, sequentially, that
// position should always match this - asserted rather than silently
// trusted, in audioStreamMp3DecoderFillStream().
size_t streamPosition;
// Set once assetFileRead() returns fewer bytes than requested while
// filling sceMp3's stream buffer - there's no more compressed data to
// add, though sceMp3 may still have enough already-fed data to decode
// one or more final frames.
bool_t endOfFile;
} audiostreammp3decoder_t;
/**
* Feeds sceMp3 more compressed bytes if (and only as much as) it currently
* wants: asks it where in the stream and how much (sceMp3GetInfoToAddStreamData()),
* reads exactly that from stream->mp3.file, and hands it back
* (sceMp3NotifyAddStreamData()). A no-op if sceMp3 doesn't currently want
* more. See this file's own top-of-file comment on why the source
* position sceMp3 requests is asserted rather than actually used to seek.
*
* @param stream The audio stream to feed. Must be AUDIO_STREAM_TYPE_MP3.
* @return Error indicating success or failure.
*/
errorret_t audioStreamMp3DecoderFillStream(audiostream_t *stream);
/**
* Initializes the hardware MP3 decoder for the given stream: lazily loads
* the sceMp3/AVCODEC firmware modules and initializes sceMp3's resource
* pool the first time any stream ever needs it (see
* audioStreamMp3DecoderGlobalDispose()'s own comment), reserves a handle
* sized from the stream's asset metadata, and feeds it enough initial
* compressed data for sceMp3Init() to succeed.
*
* @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3.
* @return Error indicating success or failure.
*/
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream);
/**
* Disposes the hardware MP3 decoder for the given stream: releases its
* sceMp3 handle and frees its buffers. Does not tear down sceMp3's global
* resource pool - see audioStreamMp3DecoderGlobalDispose().
*
* @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3.
* @return Error indicating success or failure.
*/
errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream);
/**
* Resets the decoder to the very start of the compressed stream: rewinds
* stream->mp3.file back to the asset's parsed data offset, resets this
* decoder's own streamPosition to match, and calls
* sceMp3ResetPlayPosition() so sceMp3 requests stream data from the start
* again. See audioStreamMp3Seek()'s own comment on why every seek goes
* through here.
*
* @param stream The audio stream to rewind. Must be AUDIO_STREAM_TYPE_MP3.
* @return Error indicating success or failure.
*/
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream);
/**
* Decodes the next MPEG frame's worth of PCM samples, feeding sceMp3's
* internal stream buffer from stream->mp3.file as it requests more.
* Writes decoded samples to `out` (sized for at least
* AUDIO_MP3_MAX_SAMPLES_PER_FRAME int16_t values) and sets *outFrames to
* how many frames (not samples) were produced - 0 once sceMp3 reports it
* both has nothing left to decode and needs no more stream data (a
* genuine end of stream).
*
* @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3.
* @param out Destination buffer for decoded samples.
* @param outFrames Set to the number of frames actually decoded.
* @return Error indicating success or failure.
*/
errorret_t audioStreamMp3DecoderDecodeFrame(
audiostream_t *stream,
int16_t *out,
size_t *outFrames
);
/**
* Tears down sceMp3's global resource pool and unloads its firmware
* modules, if audioStreamMp3DecoderInit() ever actually initialized them -
* a no-op otherwise. Called once from audioPSPDispose(), regardless of
* whether any MP3 stream was ever created.
*
* @return Error indicating success or failure.
*/
errorret_t audioStreamMp3DecoderGlobalDispose(void);
+33 -25
View File
@@ -42,24 +42,24 @@
errorret_t audioStreamPSPInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
if(stream->pcm.channels != 1 && stream->pcm.channels != 2) {
if(stream->channels != 1 && stream->channels != 2) {
errorThrow(
"PSP audio only supports mono or stereo PCM, got %d channels.",
stream->pcm.channels
stream->channels
);
}
// sceAudioChReserve's hardware channels always run at the PSP's native
// 44100Hz; there is no per-channel sample rate. Arbitrary rates would need
// sceAudioSRCChReserve's single exclusive channel instead.
if(stream->pcm.sampleRate != 44100) {
if(stream->sampleRate != 44100) {
errorThrow(
"PSP audio channels are fixed at 44100Hz, got %uHz.",
stream->pcm.sampleRate
stream->sampleRate
);
}
const int format = stream->pcm.channels == 1
const int format = stream->channels == 1
? PSP_AUDIO_FORMAT_MONO
: PSP_AUDIO_FORMAT_STEREO;
@@ -75,7 +75,7 @@ errorret_t audioStreamPSPInit(audiostream_t *stream) {
stream->platform.playRequested = false;
const size_t ringSize =
AUDIO_PSP_RING_FRAMES * stream->pcm.channels * sizeof(int16_t);
AUDIO_PSP_RING_FRAMES * stream->channels * sizeof(int16_t);
stream->platform.ring = memoryAllocate(ringSize);
stream->platform.scratch = memoryAllocate(ringSize);
threadMutexInit(&stream->platform.ringLock);
@@ -109,7 +109,7 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
stream->platform.readReachedEnd = false;
stream->platform.readFailed = false;
stream->platform.totalFrames = audioStreamPcmGetTotalFrames(stream);
stream->platform.totalFrames = audioStreamGetTotalFrames(stream);
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) that a looping pass wraps within, once it's reached -
@@ -117,12 +117,12 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
// behaviour is unchanged when no explicit loop points are configured.
stream->platform.loopEndFrame = stream->loopStart >= 0
? mathMin(
(size_t) (stream->loopStart * stream->pcm.sampleRate),
(size_t) (stream->loopStart * stream->sampleRate),
stream->platform.totalFrames
)
: stream->platform.totalFrames;
stream->platform.loopToFrame = mathMin(
(size_t) (stream->loopTo * stream->pcm.sampleRate),
(size_t) (stream->loopTo * stream->sampleRate),
stream->platform.loopEndFrame
);
@@ -130,7 +130,7 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
stream->startFrame = 0;
stream->seeking = false;
errorChain(audioStreamPcmSeek(stream, startFrame));
errorChain(audioStreamSeek(stream, startFrame));
stream->platform.readPosition = startFrame;
threadMutexLock(&stream->platform.ringLock);
@@ -158,20 +158,28 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
threadMutexUnlock(&stream->platform.ringLock);
if(filled >= AUDIO_PSP_LEAD_FRAMES) return;
const size_t channels = stream->pcm.channels;
const size_t channels = stream->channels;
const size_t frameSize = channels * sizeof(int16_t);
// Fill however much room the ring actually has in this one call, not
// just a small fixed step - otherwise a temporary engine frame-rate dip
// (below roughly one hardware chunk's worth of playback time per frame)
// would let production permanently fall behind consumption, since a
// fixed-size top-up per call can never make up lost ground. Bounded by
// the ring's own physical capacity, and by loopEndFrame (the loop
// segment's end) - except a seek can legitimately land past it (e.g.
// into an outro after the loop point), in which case read out to the
// true end of the clip once instead of underflowing
// framesRemainingInSegment.
const size_t room = AUDIO_PSP_RING_FRAMES - filled;
// Fill however much room the ring has, up to one bounded step, not just
// whatever's needed to reach LEAD in a single shot - AUDIO_PSP_RING_FRAMES
// is now sized to absorb a slow MP3 loop-wrap (see its own comment), and
// greedily trying to fill all of it in one call right after a fresh
// Buffer() (when the ring starts empty) would turn the very first
// top-up into one long blocking decode burst on the main thread instead
// of many small ones - worse for startup smoothness, not better. Still
// uncapped per se: reaching LEAD just takes a few calls (a few engine
// frames) instead of one, which comfortably keeps up with real-time
// consumption the same way an unbounded fill would, since a temporary
// frame-rate dip only slows how fast the ring tops up, never how much
// room is left to fill on the next call. Bounded by the ring's own
// physical capacity, and by loopEndFrame (the loop segment's end) -
// except a seek can legitimately land past it (e.g. into an outro after
// the loop point), in which case read out to the true end of the clip
// once instead of underflowing framesRemainingInSegment.
const size_t room = mathMin(
AUDIO_PSP_RING_FRAMES - filled, (size_t) AUDIO_PSP_TOPUP_STEP_FRAMES
);
const size_t currentEndFrame = stream->platform.readPosition < stream->platform.loopEndFrame
? stream->platform.loopEndFrame
: stream->platform.totalFrames;
@@ -186,7 +194,7 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
size_t framesRead = 0;
if(errorIsNotOk(
audioStreamPcmRead(stream, scratch, framesToRead, &framesRead)
audioStreamRead(stream, scratch, framesToRead, &framesRead)
)) {
stream->platform.readFailed = true;
return;
@@ -219,7 +227,7 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
if(reachesSegmentEnd) {
if(willLoop) {
if(errorIsNotOk(audioStreamPcmSeek(stream, stream->platform.loopToFrame))) {
if(errorIsNotOk(audioStreamSeek(stream, stream->platform.loopToFrame))) {
stream->platform.readFailed = true;
return;
}
@@ -258,7 +266,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
sceKernelChangeThreadPriority(sceKernelGetThreadId(), AUDIO_PSP_THREAD_PRIORITY);
audiostream_t *stream = (audiostream_t *) thread->data;
const size_t channels = stream->pcm.channels;
const size_t channels = stream->channels;
const size_t frameSize = channels * sizeof(int16_t);
int16_t *chunk = memoryAllocate(AUDIO_PSP_CHUNK_FRAMES * frameSize);
+22 -3
View File
@@ -16,13 +16,32 @@ typedef struct audiostream_s audiostream_t;
// Physical capacity of the ring; must comfortably exceed
// AUDIO_PSP_LEAD_FRAMES plus one top-up step so a single top-up call can
// never overwrite data the output thread hasn't consumed yet.
#define AUDIO_PSP_RING_FRAMES 16384
//
// Sized well above what a PCM stream would ever need on its own, because
// an MP3 stream's loop-wrap (audioStreamMp3DecoderRewind() on PSP) fully
// tears down and recreates the hardware decoder - a real handle release/
// reserve round-trip plus a fresh ~16KB Memory Stick read to re-prime
// sceMp3's stream buffer - all synchronously, inside the same TopUp() call
// that's supposed to be keeping this ring fed. That's slow enough,
// occasionally, to eat into a tighter lead margin and click right at the
// loop point (confirmed on real hardware) - this gives it generous room to
// do so without the output thread ever catching up to empty.
#define AUDIO_PSP_RING_FRAMES 49152
// Top-up threshold - audioStreamPSPTopUp() (called once per engine
// Update(), see audioStreamPSPIsFinished()) only reads more data once the
// ring drops below this, same trigger dusklinux uses for its own lead
// margin.
#define AUDIO_PSP_LEAD_FRAMES 4096
// margin. See AUDIO_PSP_RING_FRAMES's own comment for why this is sized
// well beyond dusklinux's equivalent.
#define AUDIO_PSP_LEAD_FRAMES 12288
// Max frames audioStreamPSPTopUp() will read in one call, regardless of
// how much ring room is actually free - keeps each call's worst-case
// blocking time on the main thread bounded and roughly constant even
// though AUDIO_PSP_RING_FRAMES/AUDIO_PSP_LEAD_FRAMES are large; reaching
// LEAD from an empty ring just takes a few calls (a few engine frames)
// instead of one long one. See AUDIO_PSP_RING_FRAMES's own comment.
#define AUDIO_PSP_TOPUP_STEP_FRAMES 4096
// Max number of pending loop-wrap events (see audiostreampsploopmarker_t
// below) the ring can remember at once. Sized generously relative to how