Fixed PSP trying to load entirely into memory

This commit is contained in:
2026-08-31 19:07:51 -05:00
parent 15bd9fc43c
commit d37109f5f7
26 changed files with 995 additions and 232 deletions
+88 -31
View File
@@ -10,12 +10,20 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include "error/error.h"
// How many frames of lead time to keep queued ahead of playback. Matches
// SDL_AudioSpec.samples below - the device's own internal buffer size - so
// this is "never let the queue run drier than one SDL-internal buffer."
#define AUDIO_LINUX_LEAD_FRAMES 4096
// How many frames audioStreamLinuxFeed() reads and queues per call - a
// few multiples of the lead margin, so one top-up comfortably outlasts
// the time between Update() calls without ever needing to hold more than
// this much decoded audio in memory at once, regardless of how long the
// underlying clip is.
#define AUDIO_LINUX_WINDOW_FRAMES (AUDIO_LINUX_LEAD_FRAMES * 4)
errorret_t audioStreamLinuxInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
@@ -46,7 +54,7 @@ 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 = stream->dataSize / frameSize;
const size_t totalFrames = audioStreamPcmGetTotalFrames(stream);
// Consumed synchronously (right here, in the same call that decided to
// (re)buffer) rather than left for later - see startFrame's own comment.
@@ -67,57 +75,106 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
// as PSP.
if(startFrame >= endFrame) endFrame = totalFrames;
const size_t bytes = (endFrame - startFrame) * frameSize;
const uint8_t *segment = stream->data + (startFrame * frameSize);
// Only an explicit seek discards whatever's still queued and jumps -
// a natural loop restart deliberately leaves the previous pass's tail
// (AUDIO_LINUX_LEAD_FRAMES worth) queued and appends the new pass after
// it, which is what makes looping gapless (see IsFinished()'s comment).
// Clearing on every Buffer() call would destroy that overlap and
// Clearing here on every pass would destroy that overlap and
// reintroduce the exact gap this was built to avoid.
if(seeking) {
SDL_ClearQueuedAudio(stream->platform.device);
}
errorChain(audioStreamPcmSeek(stream, startFrame));
int queued;
if(stream->volume == 0xFF) {
// Nothing to mix at full volume - queue the segment directly instead of
// allocating/zeroing a same-size scratch buffer just to copy it in.
queued = SDL_QueueAudio(stream->platform.device, segment, (Uint32) bytes);
} else {
uint8_t *mixed = memoryAllocate(bytes);
memoryZero(mixed, bytes);
SDL_MixAudioFormat(
mixed, segment, AUDIO_S16SYS, (Uint32) bytes,
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
);
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytes);
memoryFree(mixed);
}
stream->platform.position = startFrame;
stream->platform.endFrame = endFrame;
if(queued != 0) {
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
}
errorChain(audioStreamLinuxFeed(stream));
SDL_PauseAudioDevice(stream->platform.device, 0);
errorOk();
}
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 framesRemaining = (
stream->platform.position < stream->platform.endFrame
? stream->platform.endFrame - stream->platform.position
: 0
);
const size_t framesToRead = mathMin(framesRemaining, AUDIO_LINUX_WINDOW_FRAMES);
if(framesToRead == 0) {
errorOk();
}
int16_t *chunk = memoryAllocate(framesToRead * frameSize);
size_t framesRead = 0;
errorret_t ret = audioStreamPcmRead(stream, chunk, framesToRead, &framesRead);
if(errorIsNotOk(ret)) {
memoryFree(chunk);
errorChain(ret);
}
const size_t bytesRead = framesRead * frameSize;
int queued;
if(stream->volume == 0xFF) {
// Nothing to mix at full volume - queue the window directly instead of
// allocating/zeroing a same-size scratch buffer just to copy it in.
queued = SDL_QueueAudio(stream->platform.device, chunk, (Uint32) bytesRead);
} else {
uint8_t *mixed = memoryAllocate(bytesRead);
memoryZero(mixed, bytesRead);
SDL_MixAudioFormat(
mixed, (uint8_t *) chunk, AUDIO_S16SYS, (Uint32) bytesRead,
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
);
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytesRead);
memoryFree(mixed);
}
memoryFree(chunk);
if(queued != 0) {
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
}
stream->platform.position += framesRead;
errorOk();
}
bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
// Reports "finished" (ready to be re-buffered) with AUDIO_LINUX_LEAD_FRAMES
// of margin still queued, rather than waiting for the queue to actually
// run dry. SDL_QueueAudio only ever appends to a FIFO, so queuing the next
// loop this early never causes overlap - it just avoids ever going silent
// while our once-per-frame Update() notices and catches up. Waiting for
// truly empty (as this used to) guarantees a gap by definition: silence
// has already started by the time "empty" can be observed.
// Reports "finished" (ready to loop/end) with AUDIO_LINUX_LEAD_FRAMES of
// margin still queued, rather than waiting for the queue to actually run
// dry - queuing the next pass this early never causes overlap (SDL's
// queue is a plain FIFO), it just avoids ever going silent while our
// once-per-frame Update() notices and catches up. Waiting for truly
// 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)
);
return SDL_GetQueuedAudioSize(stream->platform.device) <= leadBytes;
if(SDL_GetQueuedAudioSize(stream->platform.device) > leadBytes) {
return false;
}
// Below the lead margin - if more of the current pass is left to read,
// top up now rather than reporting finished, which would otherwise
// trigger a full loop-restart/end while still mid-pass, just because
// the queue happened to run low.
if(stream->platform.position < stream->platform.endFrame) {
errorret_t ret = audioStreamLinuxFeed(stream);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return true; // Can't recover - let the shared layer end/loop it.
}
return false;
}
return true;
}