Fixed PSP trying to load entirely into memory
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,17 @@ typedef struct audiostream_s audiostream_t;
|
||||
|
||||
typedef struct {
|
||||
SDL_AudioDeviceID device;
|
||||
|
||||
// Frame offset (relative to the start of the PCM data) the next
|
||||
// audioStreamLinuxFeed() call should read from - advances as each
|
||||
// window is queued, so a long clip is streamed progressively rather
|
||||
// than read/queued all at once.
|
||||
size_t position;
|
||||
|
||||
// Frame offset this pass stops at (the current loop segment's end, or
|
||||
// the true end of the clip if not looping) - recomputed by
|
||||
// audioStreamLinuxBuffer() at the start of each pass.
|
||||
size_t endFrame;
|
||||
} audiostreamlinux_t;
|
||||
|
||||
/**
|
||||
@@ -34,17 +45,39 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream);
|
||||
errorret_t audioStreamLinuxDispose(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Sends the stream's currently staged PCM data (stream->data) to its SDL2
|
||||
* audio device and starts/resumes playback.
|
||||
* Starts a new playback pass: seeks the stream's PCM read cursor to
|
||||
* stream->startFrame (clearing the SDL queue first if this is an explicit
|
||||
* seek rather than a natural loop restart - see stream->seeking's own
|
||||
* comment), determines this pass's loop-segment end, and queues the first
|
||||
* window via audioStreamLinuxFeed().
|
||||
*
|
||||
* @param stream The audio stream to output.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t audioStreamLinuxBuffer(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Reads and queues one bounded window of PCM data (AUDIO_LINUX_WINDOW_FRAMES)
|
||||
* from the stream's current platform.position up to platform.endFrame,
|
||||
* advancing platform.position by however many frames were actually read.
|
||||
* A no-op once platform.position has reached platform.endFrame.
|
||||
*
|
||||
* Kept as a small, bounded read/queue operation (rather than the whole
|
||||
* pass at once) specifically so a long clip is never fully resident in
|
||||
* memory at once - called both by audioStreamLinuxBuffer() (the first
|
||||
* window of a pass) and by audioStreamLinuxIsFinished() (subsequent
|
||||
* top-ups as the SDL queue drains).
|
||||
*
|
||||
* @param stream The audio stream to feed.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t audioStreamLinuxFeed(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Checks whether the stream's SDL2 audio device has finished playing all
|
||||
* queued data.
|
||||
* queued data for the current pass. As a side effect, tops up the queue
|
||||
* (via audioStreamLinuxFeed()) whenever it's running low but more of the
|
||||
* current pass remains to be read, rather than reporting finished early.
|
||||
*
|
||||
* @param stream The audio stream to check.
|
||||
* @return true if playback has finished, false otherwise.
|
||||
|
||||
Reference in New Issue
Block a user