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:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user