Rework PSP audio into a single output thread + main-thread top-up

The reader+player two-thread design (previous commit) still crackled -
both threads shared the same elevated real-time priority and could
contend for the PSP's single core right at the moment the player thread
needed to resume after its blocking output call returned, worse the
bigger the hardware chunk. Confirmed fixed on real hardware (Memory Stick
and pspsh) by removing the second real-time thread entirely.

PCM data now lives in a ring buffer topped up from the MAIN thread once
per engine Update(), mirroring dusklinux's own already-working
audioStreamLinuxFeed()/IsFinished() pattern (same lead/window sizing)
instead of a bespoke second thread. The sole remaining PSP-specific
thread only drains the ring and calls sceAudioOutputPannedBlocking(),
never touching the asset/PCM layer. Loop wraps are now just a transparent
seek-and-continue while filling the ring (it holds one seamless sample
stream, no per-chunk splicing needed) - a small FIFO of loop markers is
the only thing still needed to fire onLoop at the correct audible moment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 20:09:15 -05:00
parent 769f2f5702
commit ac8023d50f
2 changed files with 359 additions and 361 deletions
+230 -261
View File
@@ -15,17 +15,17 @@
// Matches pspaudiolib's own PSP_NUM_AUDIO_SAMPLES convention - PSP audio
// hardware is meant to be fed small chunks continuously, not one large
// buffer per call.
// buffer per call. This is purely an output granularity now - see
// audioStreamPSPTopUp() for how much it reads per call.
#define AUDIO_PSP_CHUNK_FRAMES 1024
// PSP priorities are inverted (lower = higher priority). This project's
// main thread runs at the PSPSDK default of 32 (PSP_MAIN_THREAD_PRIORITY
// is never overridden). A pthread created with default attributes runs at
// 60 - LOWER priority than the main/render thread - so under load either
// worker thread would starve and the hardware channel underruns, heard as
// 60 - LOWER priority than the main/render thread - so under load the
// output thread would starve and the hardware channel underruns, heard as
// jitter/crackle that gets worse the busier (lower-fps) a frame is. Raise
// both the player and reader threads above main so audio feeding always
// wins scheduling contention.
// it above main so audio feeding always wins scheduling contention.
#define AUDIO_PSP_THREAD_PRIORITY 18
// How many frames at the very end of a stream get linearly faded to zero,
@@ -34,12 +34,9 @@
// just stopping outright on an exact-multiple-length one).
#define AUDIO_PSP_FADE_FRAMES 32
// How long the persistent player/reader threads sleep between polls - both
// for a new play request while idle, and for queue slots while active.
// Cheap compared to spawning a whole new OS thread per play (what this
// replaced) - worst case this is the latency added between one playback
// pass finishing and the next one starting, or between a queue slot
// freeing up and the waiting thread noticing.
// How long the persistent output thread sleeps between checks - both for
// a new play request while idle, and for the ring buffer to have a full
// chunk ready while active.
#define AUDIO_PSP_IDLE_POLL_MICROS 500
errorret_t audioStreamPSPInit(audiostream_t *stream) {
@@ -76,42 +73,31 @@ errorret_t audioStreamPSPInit(audiostream_t *stream) {
stream->platform.channel = channel;
stream->platform.finished = false;
stream->platform.playRequested = false;
stream->platform.readRequested = false;
stream->platform.readFailed = false;
stream->platform.queueHead = 0;
stream->platform.queueTail = 0;
stream->platform.queueCount = 0;
const size_t chunkSize =
AUDIO_PSP_CHUNK_FRAMES * stream->pcm.channels * sizeof(int16_t);
for(size_t i = 0; i < AUDIO_PSP_QUEUE_DEPTH; i++) {
stream->platform.queueChunk[i] = memoryAllocate(chunkSize);
}
threadMutexInit(&stream->platform.queueLock);
const size_t ringSize =
AUDIO_PSP_RING_FRAMES * stream->pcm.channels * sizeof(int16_t);
stream->platform.ring = memoryAllocate(ringSize);
stream->platform.scratch = memoryAllocate(ringSize);
threadMutexInit(&stream->platform.ringLock);
threadInit(&stream->platform.thread, audioStreamPSPThreadFeed);
stream->platform.thread.data = stream;
threadStartRequest(&stream->platform.thread);
threadInit(&stream->platform.readerThread, audioStreamPSPThreadRead);
stream->platform.readerThread.data = stream;
threadStartRequest(&stream->platform.readerThread);
errorOk();
}
errorret_t audioStreamPSPDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
threadStop(&stream->platform.readerThread);
threadStop(&stream->platform.thread);
sceAudioChRelease(stream->platform.channel);
for(size_t i = 0; i < AUDIO_PSP_QUEUE_DEPTH; i++) {
memoryFree(stream->platform.queueChunk[i]);
stream->platform.queueChunk[i] = NULL;
}
threadMutexDispose(&stream->platform.queueLock);
memoryFree(stream->platform.ring);
stream->platform.ring = NULL;
memoryFree(stream->platform.scratch);
stream->platform.scratch = NULL;
threadMutexDispose(&stream->platform.ringLock);
errorOk();
}
@@ -120,222 +106,161 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
stream->platform.finished = false;
stream->platform.startFrame = stream->startFrame;
stream->startFrame = 0;
stream->seeking = false;
stream->platform.readReachedEnd = false;
stream->platform.readFailed = false;
threadMutexLock(&stream->platform.queueLock);
stream->platform.queueHead = 0;
stream->platform.queueTail = 0;
stream->platform.queueCount = 0;
threadMutexUnlock(&stream->platform.queueLock);
stream->platform.totalFrames = audioStreamPcmGetTotalFrames(stream);
// Give the reader a head start filling the queue before the player
// starts draining it - not required for correctness (the player just
// waits for the first ready slot either way), but avoids guaranteeing an
// initial stall on every pass.
stream->platform.readRequested = true;
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) that a looping pass wraps within, once it's reached -
// defaulting to the whole clip (loopStart == -1, loopTo == 0) so
// behaviour is unchanged when no explicit loop points are configured.
stream->platform.loopEndFrame = stream->loopStart >= 0
? mathMin(
(size_t) (stream->loopStart * stream->pcm.sampleRate),
stream->platform.totalFrames
)
: stream->platform.totalFrames;
stream->platform.loopToFrame = mathMin(
(size_t) (stream->loopTo * stream->pcm.sampleRate),
stream->platform.loopEndFrame
);
const size_t startFrame = mathMin(stream->startFrame, stream->platform.totalFrames);
stream->startFrame = 0;
stream->seeking = false;
errorChain(audioStreamPcmSeek(stream, startFrame));
stream->platform.readPosition = startFrame;
threadMutexLock(&stream->platform.ringLock);
stream->platform.ringReadPos = 0;
stream->platform.ringWritePos = 0;
stream->platform.ringFilled = 0;
stream->platform.framesEnqueued = 0;
stream->platform.loopMarkerHead = 0;
stream->platform.loopMarkerCount = 0;
threadMutexUnlock(&stream->platform.ringLock);
stream->platform.framesOutput = 0;
stream->platform.playRequested = true;
errorOk();
}
bool_t audioStreamPSPIsFinished(audiostream_t *stream) {
void audioStreamPSPTopUp(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
return stream->platform.finished;
}
if(stream->platform.readReachedEnd || stream->platform.readFailed) return;
void audioStreamPSPThreadRead(thread_t *thread) {
assertNotNull(thread, "Thread cannot be NULL.");
threadMutexLock(&stream->platform.ringLock);
const size_t filled = stream->platform.ringFilled;
threadMutexUnlock(&stream->platform.ringLock);
if(filled >= AUDIO_PSP_LEAD_FRAMES) return;
sceKernelChangeThreadPriority(sceKernelGetThreadId(), AUDIO_PSP_THREAD_PRIORITY);
audiostream_t *stream = (audiostream_t *) thread->data;
const size_t channels = stream->pcm.channels;
const size_t frameSize = channels * sizeof(int16_t);
// Runs for the stream's whole lifetime - idles here between plays rather
// than exiting, same as the player thread.
while(!threadShouldStop(thread)) {
if(!stream->platform.readRequested) {
sceKernelDelayThread(AUDIO_PSP_IDLE_POLL_MICROS);
continue;
// 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;
const size_t currentEndFrame = stream->platform.readPosition < stream->platform.loopEndFrame
? stream->platform.loopEndFrame
: stream->platform.totalFrames;
const size_t framesRemainingInSegment = currentEndFrame - stream->platform.readPosition;
const size_t framesToRead = mathMin(room, framesRemainingInSegment);
const bool_t reachesSegmentEnd = framesToRead == framesRemainingInSegment;
const bool_t willLoop = reachesSegmentEnd &&
(stream->state & AUDIO_STREAM_STATE_LOOPING);
if(framesToRead > 0) {
int16_t *scratch = stream->platform.scratch;
size_t framesRead = 0;
if(errorIsNotOk(
audioStreamPcmRead(stream, scratch, framesToRead, &framesRead)
)) {
stream->platform.readFailed = true;
return;
}
if(framesRead < framesToRead) {
// The asset is shorter than its declared header size (corrupt or
// truncated) - pad what's missing with silence.
memoryZero(
scratch + (framesRead * channels), (framesToRead - framesRead) * frameSize
);
}
stream->platform.readRequested = false;
const size_t totalFrames = audioStreamPcmGetTotalFrames(stream);
threadMutexLock(&stream->platform.ringLock);
for(size_t i = 0; i < framesToRead; i++) {
const size_t writeIndex = (stream->platform.ringWritePos + i) % AUDIO_PSP_RING_FRAMES;
memoryCopy(
stream->platform.ring + (writeIndex * channels),
scratch + (i * channels),
frameSize
);
}
stream->platform.ringWritePos =
(stream->platform.ringWritePos + framesToRead) % AUDIO_PSP_RING_FRAMES;
stream->platform.ringFilled += framesToRead;
stream->platform.framesEnqueued += framesToRead;
threadMutexUnlock(&stream->platform.ringLock);
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) that a looping pass wraps within, once it's reached -
// defaulting to the whole buffer (loopStart == -1, loopTo == 0) so
// behaviour is unchanged when no explicit loop points are configured.
const size_t loopEndFrame = stream->loopStart >= 0
? mathMin((size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames)
: totalFrames;
const size_t loopToFrame = mathMin(
(size_t) (stream->loopTo * stream->pcm.sampleRate), loopEndFrame
);
stream->platform.readPosition += framesToRead;
}
// Only the very first pass honors an explicit seek (audioStreamSetPosition,
// captured into platform.startFrame by audioStreamPSPBuffer()) - every
// subsequent loop wraps to loopToFrame instead.
size_t position = stream->platform.startFrame;
bool_t reachedEnd = false;
bool_t readFailed = errorIsNotOk(audioStreamPcmSeek(stream, position));
if(readFailed) stream->platform.readFailed = true;
while(!readFailed && !threadShouldStop(thread) && !reachedEnd) {
// Wait for a free slot in the queue before preparing the next chunk -
// this is the only thing that paces the reader against the player.
// If the thread is asked to stop while waiting, give up on this pass
// entirely rather than finishing it.
bool_t stopRequested = false;
for(;;) {
threadMutexLock(&stream->platform.queueLock);
bool_t hasRoom = stream->platform.queueCount < AUDIO_PSP_QUEUE_DEPTH;
threadMutexUnlock(&stream->platform.queueLock);
if(hasRoom) break;
if(threadShouldStop(thread)) {
stopRequested = true;
break;
}
sceKernelDelayThread(AUDIO_PSP_IDLE_POLL_MICROS);
}
if(stopRequested) break;
// Normally bounded by loopEndFrame (the loop segment's end), but a
// seek can legitimately land past it (e.g. into an outro after the
// loop point) - in that case play out to the true end of the buffer
// once instead of underflowing framesRemaining.
const size_t currentEndFrame = position < loopEndFrame
? loopEndFrame
: totalFrames;
const size_t framesRemaining = currentEndFrame - position;
const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES
? framesRemaining
: AUDIO_PSP_CHUNK_FRAMES;
const bool_t reachesEndThisChunk = framesThisChunk == framesRemaining;
const bool_t willLoop = reachesEndThisChunk &&
(stream->state & AUDIO_STREAM_STATE_LOOPING);
const size_t slot = stream->platform.queueHead;
int16_t *chunk = stream->platform.queueChunk[slot];
size_t framesRead = 0;
if(errorIsNotOk(
audioStreamPcmRead(stream, chunk, framesThisChunk, &framesRead)
)) {
readFailed = true;
if(reachesSegmentEnd) {
if(willLoop) {
if(errorIsNotOk(audioStreamPcmSeek(stream, stream->platform.loopToFrame))) {
stream->platform.readFailed = true;
break;
return;
}
if(framesRead < framesThisChunk) {
// The asset is shorter than its declared header size (corrupt or
// truncated) - pad what's missing with silence rather than play
// whatever was left in `chunk` from a previous pass.
memoryZero(
chunk + (framesRead * channels),
(framesThisChunk - framesRead) * frameSize
);
}
const size_t remainderFrames = AUDIO_PSP_CHUNK_FRAMES - framesThisChunk;
size_t wrapFrames = 0;
if(reachesEndThisChunk && willLoop) {
// Instead of padding the rest of this constant-size chunk with
// silence, fill it with the start of the loop segment (loopToFrame)
// - a looping stream should never have dead air baked into its
// output at all. Not faded (unlike the true-end case below): a wrap
// only sounds seamless if the source data already loops cleanly
// (true for the shared test tone, deliberately authored at 441Hz
// for exactly this), matching the same assumption Dolphin's native
// hardware looping makes - imperfectly-authored loop content will
// still click here.
//
// NOTE: if the loop segment (loopEndFrame - loopToFrame) is shorter
// than one hardware chunk (AUDIO_PSP_CHUNK_FRAMES, ~23ms @ 44100Hz),
// only a single copy of it fills the remainder here rather than
// repeating it to fill the whole chunk - any leftover space is
// zero-padded (a brief, audible gap) and loopCount still only
// increments once per hardware chunk, not once per actual loop
// repeat. Not hit by anything in this codebase today (the shared
// test tone's default whole-buffer loop is 1 second), but a future
// short music-loop tail would need this generalized to a
// repeat-fill instead.
const size_t loopSegmentFrames = loopEndFrame - loopToFrame;
wrapFrames = remainderFrames < loopSegmentFrames
? remainderFrames
: loopSegmentFrames;
if(errorIsNotOk(audioStreamPcmSeek(stream, loopToFrame))) {
readFailed = true;
stream->platform.readFailed = true;
break;
}
size_t wrapRead = 0;
if(errorIsNotOk(audioStreamPcmRead(
stream, chunk + (framesThisChunk * channels), wrapFrames, &wrapRead
))) {
readFailed = true;
stream->platform.readFailed = true;
break;
}
if(wrapRead < remainderFrames) {
memoryZero(
chunk + ((framesThisChunk + wrapRead) * channels),
(remainderFrames - wrapRead) * frameSize
);
}
} else if(reachesEndThisChunk) {
// True final chunk (not looping again) - pad with silence and fade
// the real tail down to zero so the waveform never stops (or meets
// that padding) at a non-zero amplitude, which is what was heard
// as a click at the end of playback.
memoryZero(chunk + (framesThisChunk * channels), remainderFrames * frameSize);
const size_t fadeFrames = framesThisChunk < AUDIO_PSP_FADE_FRAMES
? framesThisChunk
: AUDIO_PSP_FADE_FRAMES;
for(size_t i = 0; i < fadeFrames; i++) {
const size_t frame = framesThisChunk - fadeFrames + i;
const float_t factor = 1.0f - ((float_t) (i + 1) / (float_t) fadeFrames);
for(size_t c = 0; c < channels; c++) {
int16_t *sample = &chunk[frame * channels + c];
*sample = (int16_t) ((float_t) *sample * factor);
}
}
}
threadMutexLock(&stream->platform.queueLock);
stream->platform.queueReachedEnd[slot] = reachesEndThisChunk;
stream->platform.queueLooped[slot] = reachesEndThisChunk && willLoop;
stream->platform.queueHead = (slot + 1) % AUDIO_PSP_QUEUE_DEPTH;
stream->platform.queueCount++;
threadMutexUnlock(&stream->platform.queueLock);
if(reachesEndThisChunk) {
if(willLoop) {
position = loopToFrame + wrapFrames;
} else {
reachedEnd = true;
}
} else {
position += framesThisChunk;
stream->platform.readPosition = stream->platform.loopToFrame;
threadMutexLock(&stream->platform.ringLock);
if(stream->platform.loopMarkerCount < AUDIO_PSP_LOOP_MARKER_MAX) {
const size_t index = (
stream->platform.loopMarkerHead + stream->platform.loopMarkerCount
) % AUDIO_PSP_LOOP_MARKER_MAX;
stream->platform.loopMarkerFrames[index] = stream->platform.framesEnqueued;
stream->platform.loopMarkerCount++;
}
threadMutexUnlock(&stream->platform.ringLock);
} else {
stream->platform.readReachedEnd = true;
}
}
}
bool_t audioStreamPSPIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
// Only ever called while this stream is actively playing (see
// audioStreamUpdate()) - piggybacking the per-frame top-up here mirrors
// dusklinux's own audioStreamLinuxIsFinished()/audioStreamLinuxFeed()
// pattern, rather than needing a dedicated thread to do PCM I/O.
audioStreamPSPTopUp(stream);
return stream->platform.finished;
}
void audioStreamPSPThreadFeed(thread_t *thread) {
assertNotNull(thread, "Thread cannot be NULL.");
sceKernelChangeThreadPriority(sceKernelGetThreadId(), AUDIO_PSP_THREAD_PRIORITY);
audiostream_t *stream = (audiostream_t *) thread->data;
const size_t channels = stream->pcm.channels;
const size_t frameSize = channels * sizeof(int16_t);
int16_t *chunk = memoryAllocate(AUDIO_PSP_CHUNK_FRAMES * frameSize);
// Runs for the stream's whole lifetime - idles here between plays rather
// than exiting, so a loop restart (or any replay) is just a flag flip
@@ -352,32 +277,25 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
// Every call always sends a full, constant-size AUDIO_PSP_CHUNK_FRAMES
// buffer - the channel is never re-declared to a different length, to
// avoid relying on sceAudioSetChannelDataLen's undocumented behavior
// mid-stream (the likely real cause of an earlier click). Loops
// internally (rather than going idle and waiting for the engine's
// once-per-frame Update() to notice finished and re-trigger Buffer())
// so a looping stream never has a detection-latency gap - the generic
// engine's finished+looping path is designed for platforms with no
// better option (see audiostream.c), but here the thread can just
// keep going and call onLoop itself instead.
// mid-stream. Loops internally (rather than going idle and waiting for
// the engine's once-per-frame Update() to notice finished and
// re-trigger Buffer()) so a looping stream never has a
// detection-latency gap.
while(!threadShouldStop(thread) && !reachedEnd) {
// Wait for the reader thread to have a chunk ready. sceAudioOutput-
// PannedBlocking() below blocks this thread for the full duration of
// whatever chunk it's given, so there is no spare time here to also
// do the PCM read/seek itself without stalling the hardware channel
// - that's exactly what the reader thread (audioStreamPSPThreadRead)
// exists to do concurrently, ahead of what's currently playing.
// Wait until either a full hardware chunk is ready in the ring, or
// the main thread has stopped producing (a genuine end or a read
// failure) and whatever's left (0..one chunk) is all there'll ever
// be - the pass's final, possibly-partial chunk.
size_t available = 0;
bool_t stopRequested = false;
size_t slot = 0;
for(;;) {
threadMutexLock(&stream->platform.queueLock);
bool_t hasChunk = stream->platform.queueCount > 0;
bool_t gaveUp = !hasChunk && stream->platform.readFailed;
slot = stream->platform.queueTail;
threadMutexUnlock(&stream->platform.queueLock);
if(hasChunk) break;
// The reader already failed and has nothing left queued - it will
// never produce another chunk for this pass, so stop waiting.
if(gaveUp || threadShouldStop(thread)) {
threadMutexLock(&stream->platform.ringLock);
available = stream->platform.ringFilled;
threadMutexUnlock(&stream->platform.ringLock);
if(available >= AUDIO_PSP_CHUNK_FRAMES) break;
if(stream->platform.readReachedEnd || stream->platform.readFailed) break;
if(threadShouldStop(thread)) {
stopRequested = true;
break;
}
@@ -385,9 +303,57 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
}
if(stopRequested) break;
int16_t *chunk = stream->platform.queueChunk[slot];
const bool_t chunkReachedEnd = stream->platform.queueReachedEnd[slot];
const bool_t chunkLooped = stream->platform.queueLooped[slot];
// Production has stopped and what's left fits in one chunk (0 up to
// AUDIO_PSP_CHUNK_FRAMES - it can never be more, since production
// never adds more once readReachedEnd/readFailed is set) - this is
// the pass's last chunk.
const bool_t isFinalChunk =
(stream->platform.readReachedEnd || stream->platform.readFailed) &&
available <= AUDIO_PSP_CHUNK_FRAMES;
const size_t framesThisChunk = isFinalChunk
? available
: AUDIO_PSP_CHUNK_FRAMES;
threadMutexLock(&stream->platform.ringLock);
for(size_t i = 0; i < framesThisChunk; i++) {
const size_t readIndex = (stream->platform.ringReadPos + i) % AUDIO_PSP_RING_FRAMES;
memoryCopy(
chunk + (i * channels),
stream->platform.ring + (readIndex * channels),
frameSize
);
}
stream->platform.ringReadPos =
(stream->platform.ringReadPos + framesThisChunk) % AUDIO_PSP_RING_FRAMES;
stream->platform.ringFilled -= framesThisChunk;
threadMutexUnlock(&stream->platform.ringLock);
if(framesThisChunk < AUDIO_PSP_CHUNK_FRAMES) {
// True final chunk is shorter than a full hardware chunk - pad the
// rest with silence rather than play whatever was left in `chunk`
// from a previous pass.
memoryZero(
chunk + (framesThisChunk * channels),
(AUDIO_PSP_CHUNK_FRAMES - framesThisChunk) * frameSize
);
}
if(isFinalChunk) {
// Fade the real tail down to zero so the waveform never stops (or
// meets padding) at a non-zero amplitude, which is what was heard
// as a click at the end of playback.
const size_t fadeFrames = framesThisChunk < AUDIO_PSP_FADE_FRAMES
? framesThisChunk
: AUDIO_PSP_FADE_FRAMES;
for(size_t i = 0; i < fadeFrames; i++) {
const size_t frame = framesThisChunk - fadeFrames + i;
const float_t factor = 1.0f - ((float_t) (i + 1) / (float_t) fadeFrames);
for(size_t c = 0; c < channels; c++) {
int16_t *sample = &chunk[frame * channels + c];
*sample = (int16_t) ((float_t) *sample * factor);
}
}
}
// Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality
// take effect mid-playback, unlike the platform's other one-shot calls.
@@ -402,29 +368,32 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
stream->platform.channel, leftVolume, rightVolume, chunk
);
// Only freed (queueCount decremented) after the blocking output call
// above returns - the reader thread must never be able to reuse this
// slot's buffer while sceAudioOutputPannedBlocking is still reading
// from it.
threadMutexLock(&stream->platform.queueLock);
stream->platform.queueTail = (slot + 1) % AUDIO_PSP_QUEUE_DEPTH;
stream->platform.queueCount--;
threadMutexUnlock(&stream->platform.queueLock);
stream->platform.framesOutput += framesThisChunk;
if(chunkReachedEnd) {
if(chunkLooped) {
// Never call stream->onLoop directly from this thread - it may do
// arbitrary, possibly-slow work (this is exactly what caused a
// loud crackle: onLoop console-printing took long enough to
// starve the next chunk). audioStreamUpdate() picks this up and
// fires onLoop safely from the main thread instead.
stream->loopCount++;
} else {
reachedEnd = true;
}
// Fire any loop markers this chunk just played past - never call
// stream->onLoop directly from this thread, since it may do
// arbitrary, possibly-slow work (this is exactly what caused a loud
// crackle in the past: onLoop console-printing took long enough to
// starve the next chunk). audioStreamUpdate() picks up loopCount
// changes and fires onLoop safely from the main thread instead.
threadMutexLock(&stream->platform.ringLock);
while(
stream->platform.loopMarkerCount > 0 &&
stream->platform.loopMarkerFrames[stream->platform.loopMarkerHead] <=
stream->platform.framesOutput
) {
stream->platform.loopMarkerHead =
(stream->platform.loopMarkerHead + 1) % AUDIO_PSP_LOOP_MARKER_MAX;
stream->platform.loopMarkerCount--;
stream->loopCount++;
}
threadMutexUnlock(&stream->platform.ringLock);
if(isFinalChunk) reachedEnd = true;
}
stream->platform.finished = true;
}
memoryFree(chunk);
}