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:
+216
-247
@@ -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;
|
||||
}
|
||||
stream->platform.readRequested = false;
|
||||
|
||||
const size_t totalFrames = audioStreamPcmGetTotalFrames(stream);
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// 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 &&
|
||||
// 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);
|
||||
|
||||
const size_t slot = stream->platform.queueHead;
|
||||
int16_t *chunk = stream->platform.queueChunk[slot];
|
||||
if(framesToRead > 0) {
|
||||
int16_t *scratch = stream->platform.scratch;
|
||||
|
||||
size_t framesRead = 0;
|
||||
if(errorIsNotOk(
|
||||
audioStreamPcmRead(stream, chunk, framesThisChunk, &framesRead)
|
||||
audioStreamPcmRead(stream, scratch, framesToRead, &framesRead)
|
||||
)) {
|
||||
readFailed = true;
|
||||
stream->platform.readFailed = true;
|
||||
break;
|
||||
return;
|
||||
}
|
||||
if(framesRead < framesThisChunk) {
|
||||
if(framesRead < framesToRead) {
|
||||
// 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.
|
||||
// truncated) - pad what's missing with silence.
|
||||
memoryZero(
|
||||
chunk + (framesRead * channels),
|
||||
(framesThisChunk - framesRead) * frameSize
|
||||
scratch + (framesRead * channels), (framesToRead - 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
|
||||
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
|
||||
);
|
||||
}
|
||||
} 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);
|
||||
stream->platform.ringWritePos =
|
||||
(stream->platform.ringWritePos + framesToRead) % AUDIO_PSP_RING_FRAMES;
|
||||
stream->platform.ringFilled += framesToRead;
|
||||
stream->platform.framesEnqueued += framesToRead;
|
||||
threadMutexUnlock(&stream->platform.ringLock);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
stream->platform.readPosition += framesToRead;
|
||||
}
|
||||
|
||||
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(reachesSegmentEnd) {
|
||||
if(willLoop) {
|
||||
position = loopToFrame + wrapFrames;
|
||||
if(errorIsNotOk(audioStreamPcmSeek(stream, stream->platform.loopToFrame))) {
|
||||
stream->platform.readFailed = true;
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
reachedEnd = true;
|
||||
}
|
||||
} else {
|
||||
position += framesThisChunk;
|
||||
}
|
||||
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.
|
||||
// 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++;
|
||||
} else {
|
||||
reachedEnd = true;
|
||||
}
|
||||
}
|
||||
threadMutexUnlock(&stream->platform.ringLock);
|
||||
|
||||
if(isFinalChunk) reachedEnd = true;
|
||||
}
|
||||
|
||||
stream->platform.finished = true;
|
||||
}
|
||||
|
||||
memoryFree(chunk);
|
||||
}
|
||||
|
||||
+129
-100
@@ -11,17 +11,28 @@
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
// Depth of the read-ahead queue between the reader and player threads (see
|
||||
// audiostreampsp_t below) - how many fully-prepared hardware chunks the
|
||||
// reader is allowed to get ahead of what the player is currently
|
||||
// outputting. Each slot is one AUDIO_PSP_CHUNK_FRAMES chunk (a few KB), so
|
||||
// this costs very little memory; it exists purely to give PCM I/O (now a
|
||||
// real Memory Stick/zip read per chunk, not a RAM copy - see
|
||||
// audioStreamPcmRead()) enough of a cushion to never stall the player
|
||||
// thread's real-time output loop. 3 was picked as "more than one" (so a
|
||||
// single slow read doesn't immediately starve playback) without holding
|
||||
// much more than necessary.
|
||||
#define AUDIO_PSP_QUEUE_DEPTH 3
|
||||
// How far ahead of hardware playback the ring buffer is allowed to hold
|
||||
// already-decoded PCM - mirrors dusklinux's AUDIO_LINUX_WINDOW_FRAMES.
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
// Max number of pending loop-wrap events (see audiostreampsploopmarker_t
|
||||
// below) the ring can remember at once. Sized generously relative to how
|
||||
// many loop wraps could conceivably be produced ahead of playback within
|
||||
// one ring's worth of lookahead; a loop shorter than
|
||||
// AUDIO_PSP_RING_FRAMES / AUDIO_PSP_LOOP_MARKER_MAX frames could in theory
|
||||
// overflow this, in which case onLoop simply won't fire for the excess
|
||||
// wraps until the queue drains - not hit by anything in this codebase
|
||||
// today (the shared test tone's loop is the whole 1-second clip).
|
||||
#define AUDIO_PSP_LOOP_MARKER_MAX 8
|
||||
|
||||
typedef struct {
|
||||
// Reserved hardware output channel, from sceAudioChReserve. Reserved with
|
||||
@@ -29,74 +40,88 @@ typedef struct {
|
||||
// expects continuous small-chunk feeding, not one large buffer per call.
|
||||
int channel;
|
||||
|
||||
// Persistent "player" thread, created once in Init and alive for the
|
||||
// stream's whole lifetime - the only thread that ever calls
|
||||
// sceAudioOutputPannedBlocking(), taking chunks off the queue below
|
||||
// rather than reading PCM data itself. Re-spawning a thread on every
|
||||
// Buffer() call (e.g. every loop restart) was real, avoidable overhead -
|
||||
// a plain OS thread creation, on top of everything else - heard as a
|
||||
// small gap between loops; this thread just idles (polling
|
||||
// playRequested) between plays instead of exiting.
|
||||
// Sole persistent thread, created once in Init and alive for the
|
||||
// stream's whole lifetime - the only thing that ever calls
|
||||
// sceAudioOutputPannedBlocking(). Never touches the asset/PCM layer
|
||||
// itself; only ever drains the ring buffer below in fixed
|
||||
// AUDIO_PSP_CHUNK_FRAMES chunks, so hardware output timing is never at
|
||||
// the mercy of a Memory Stick/zip read. Idles (polling playRequested)
|
||||
// between plays instead of exiting, same reasoning as before.
|
||||
thread_t thread;
|
||||
|
||||
// Persistent "reader" thread, created once in Init alongside `thread` -
|
||||
// does all PCM I/O (audioStreamPcmSeek()/audioStreamPcmRead(), plus loop
|
||||
// wrap/fade preparation) into the queue below, running ahead of what
|
||||
// `thread` is currently outputting. sceAudioOutputPannedBlocking() blocks
|
||||
// the calling thread for the full duration of the chunk it just
|
||||
// submitted, so there is no spare time on that thread to also do I/O
|
||||
// in between calls without stalling the hardware channel - that stall is
|
||||
// exactly what was heard as severe crackling once PCM reads stopped
|
||||
// being a fully-resident-buffer memcpy (fast, always well inside the
|
||||
// ~23ms chunk budget) and started being real, sometimes-slow reads. This
|
||||
// second thread is what buys that time back.
|
||||
thread_t readerThread;
|
||||
|
||||
// Set by audioStreamPSPBuffer() to wake the idling player thread into a
|
||||
// new pass; cleared by that thread once it picks it up.
|
||||
// Set by audioStreamPSPBuffer() to wake the idling thread into a new
|
||||
// pass; cleared by the thread once it picks it up.
|
||||
volatile bool_t playRequested;
|
||||
|
||||
// Same as playRequested, but for the reader thread - set/cleared
|
||||
// independently since the two threads pick up a new pass at slightly
|
||||
// different times (whichever wakes from its idle poll first).
|
||||
volatile bool_t readRequested;
|
||||
|
||||
// Frame offset the next pass should start from - captured synchronously
|
||||
// from audiostream_t.startFrame by audioStreamPSPBuffer() (which also
|
||||
// resets that field to 0) rather than read directly by the reader
|
||||
// thread, since that thread only wakes up asynchronously and
|
||||
// audiostream_t's shared field may already have moved on to a different
|
||||
// value by then.
|
||||
size_t startFrame;
|
||||
|
||||
// Set by the player thread once it has output the last chunk of a pass.
|
||||
// Set by the thread once it has output the last chunk of a pass.
|
||||
volatile bool_t finished;
|
||||
|
||||
// Bounded queue of fully-prepared, constant-size (AUDIO_PSP_CHUNK_FRAMES)
|
||||
// hardware chunks handed from the reader thread to the player thread.
|
||||
// queueHead/queueTail/queueCount are only ever touched while holding
|
||||
// queueLock. Each queueChunk[] buffer is allocated once (in Init) and
|
||||
// reused for the stream's whole lifetime.
|
||||
int16_t *queueChunk[AUDIO_PSP_QUEUE_DEPTH];
|
||||
// Ring buffer of already-decoded, contiguous PCM. Filled by
|
||||
// audioStreamPSPTopUp() (runs on the MAIN thread, called once per engine
|
||||
// Update() while playing - see audioStreamPSPIsFinished(), same pattern
|
||||
// dusklinux uses for audioStreamLinuxFeed()) and drained by the output
|
||||
// thread above. A loop wrap is just a seek back to loopToFrame partway
|
||||
// through filling - the ring itself holds one seamless, contiguous
|
||||
// stream of samples with no seams to splice, unlike the old per-hardware
|
||||
// -chunk design; the output thread doesn't need to know where a loop
|
||||
// boundary falls, only when it has *played past* one (see the loop
|
||||
// marker fields below).
|
||||
//
|
||||
// ringReadPos/ringWritePos/ringFilled are the only fields touched by
|
||||
// both the main thread (producer) and the output thread (consumer);
|
||||
// both must hold ringLock to touch any of them.
|
||||
int16_t *ring; // AUDIO_PSP_RING_FRAMES * channels * sizeof(int16_t) bytes
|
||||
size_t ringReadPos; // next frame index the output thread reads (wraps)
|
||||
size_t ringWritePos; // next frame index the main thread writes (wraps)
|
||||
size_t ringFilled; // valid frames currently in the ring
|
||||
threadmutex_t ringLock;
|
||||
|
||||
// Per-slot bookkeeping the player thread needs once it plays that chunk:
|
||||
// whether this was the pass's true final chunk (queueReachedEnd) and, if
|
||||
// so, whether it was a loop wrap (queueLooped, bump loopCount and keep
|
||||
// going) or the genuine end (stop and set finished).
|
||||
bool_t queueReachedEnd[AUDIO_PSP_QUEUE_DEPTH];
|
||||
bool_t queueLooped[AUDIO_PSP_QUEUE_DEPTH];
|
||||
// Scratch buffer audioStreamPcmRead() decodes into before it's copied
|
||||
// into the ring - sized to the ring's full capacity since a single
|
||||
// top-up call can need to fill the entire thing at once (e.g. right at
|
||||
// pass start, or after a frame-rate dip let the ring run dry). Main-
|
||||
// thread-only, like the rest of the top-up state below.
|
||||
int16_t *scratch;
|
||||
|
||||
size_t queueHead; // Next slot index the reader thread will fill.
|
||||
size_t queueTail; // Next slot index the player thread will consume.
|
||||
size_t queueCount; // Number of filled-and-ready slots.
|
||||
threadmutex_t queueLock;
|
||||
// Total frames ever written into the ring this pass (monotonic, unlike
|
||||
// ringWritePos which wraps) - what loopMarkerFrames[] values are
|
||||
// expressed in, so they stay comparable to framesOutput below regardless
|
||||
// of how many times the physical ring has wrapped around.
|
||||
size_t framesEnqueued;
|
||||
|
||||
// Set by the reader thread if a seek/read fails mid-pass (corrupt or
|
||||
// truncated asset, I/O error) - observed by the player thread once it
|
||||
// drains whatever was already queued, so the pass still ends cleanly
|
||||
// instead of the player waiting forever for a chunk that will never
|
||||
// arrive.
|
||||
volatile bool_t readFailed;
|
||||
// FIFO of pending loop-wrap events: audioStreamPSPTopUp() records
|
||||
// framesEnqueued's value at the moment it seeks back to loopToFrame:
|
||||
// once the output thread's own running framesOutput reaches that value,
|
||||
// it has just played the last sample before the wrap and bumps
|
||||
// loopCount (never calls onLoop directly - see the loop over these in
|
||||
// audiostreampsp.c for why). Guarded by ringLock, same as the ring
|
||||
// itself.
|
||||
size_t loopMarkerFrames[AUDIO_PSP_LOOP_MARKER_MAX];
|
||||
size_t loopMarkerHead;
|
||||
size_t loopMarkerCount;
|
||||
|
||||
// Running count of frames the output thread has sent to hardware since
|
||||
// the current pass started - what loopMarkerFrames[] positions are
|
||||
// compared against.
|
||||
size_t framesOutput;
|
||||
|
||||
// Main-thread-only sequencing state for the current pass (loop points,
|
||||
// read position) - only ever touched by audioStreamPSPBuffer()/
|
||||
// audioStreamPSPTopUp(), both of which only ever run on the main thread,
|
||||
// so none of this needs locking.
|
||||
size_t totalFrames;
|
||||
size_t loopEndFrame;
|
||||
size_t loopToFrame;
|
||||
size_t readPosition;
|
||||
|
||||
// Set by audioStreamPSPTopUp() once it has written the pass's true final
|
||||
// frame (not a loop wrap - a genuine, non-looping end or a read/seek
|
||||
// failure) - it stops reading further once either is set. The output
|
||||
// thread treats "readReachedEnd (or readFailed) and the ring holds at
|
||||
// most one hardware chunk" as its cue that whatever's left is the
|
||||
// pass's last, possibly-partial chunk.
|
||||
bool_t readReachedEnd;
|
||||
bool_t readFailed;
|
||||
} audiostreampsp_t;
|
||||
|
||||
/**
|
||||
@@ -110,7 +135,7 @@ errorret_t audioStreamPSPInit(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Disposes the PSP-specific playback state of an audio stream, stopping its
|
||||
* feeder thread and releasing its hardware output channel.
|
||||
* output thread and releasing its hardware output channel.
|
||||
*
|
||||
* @param stream The audio stream to dispose.
|
||||
* @return Error state if any.
|
||||
@@ -118,10 +143,11 @@ errorret_t audioStreamPSPInit(audiostream_t *stream);
|
||||
errorret_t audioStreamPSPDispose(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Wakes the stream's persistent reader and player threads to stream PCM
|
||||
* data (read ahead from the stream's asset via audioStreamPcmRead() by the
|
||||
* reader thread) to its reserved hardware output channel in small chunks
|
||||
* until exhausted.
|
||||
* Starts a new playback pass: resets the ring buffer and loop-point
|
||||
* bookkeeping, seeks the asset to the pass's start frame, and wakes the
|
||||
* persistent output thread. Actual PCM reading happens afterward, driven
|
||||
* by audioStreamPSPTopUp() (see audioStreamPSPIsFinished()) rather than
|
||||
* here.
|
||||
*
|
||||
* @param stream The audio stream to output.
|
||||
* @return Error state if any.
|
||||
@@ -129,8 +155,27 @@ errorret_t audioStreamPSPDispose(audiostream_t *stream);
|
||||
errorret_t audioStreamPSPBuffer(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Checks whether the stream's player thread has finished outputting its
|
||||
* currently buffered data.
|
||||
* Tops up the stream's ring buffer from the main thread if it has fallen
|
||||
* below AUDIO_PSP_LEAD_FRAMES, reading enough of the asset in one call to
|
||||
* fill whatever room the ring currently has (bounded by the current loop
|
||||
* segment/clip end) - not just one small fixed step, so a temporary
|
||||
* engine frame-rate dip can't let production fall permanently behind
|
||||
* playback's consumption rate. Seeks back to loopToFrame and records a
|
||||
* loop marker if this read crosses a looping pass's loop point, or marks
|
||||
* readReachedEnd on a genuine end/failure. Called once per engine Update()
|
||||
* via audioStreamPSPIsFinished(), same as dusklinux's own feed-on-poll
|
||||
* pattern. A no-op once readReachedEnd or readFailed is set, since there's
|
||||
* nothing left to add.
|
||||
*
|
||||
* @param stream The audio stream to top up.
|
||||
*/
|
||||
void audioStreamPSPTopUp(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Checks whether the stream's output thread has finished outputting its
|
||||
* currently buffered data - also drives audioStreamPSPTopUp() each call,
|
||||
* since this is invoked exactly once per engine Update() while the stream
|
||||
* is playing (see audioStreamUpdate()).
|
||||
*
|
||||
* @param stream The audio stream to check.
|
||||
* @return true if playback has finished, false otherwise.
|
||||
@@ -138,32 +183,16 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream);
|
||||
bool_t audioStreamPSPIsFinished(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Player thread entry point, run once for the stream's whole lifetime.
|
||||
* Idles (polling playRequested) until woken by audioStreamPSPBuffer(), then
|
||||
* takes fully-prepared chunks off the queue (filled by the reader thread -
|
||||
* see audioStreamPSPThreadRead()) and outputs them to the hardware channel
|
||||
* one at a time, blocking naturally on each sceAudioOutputPannedBlocking()
|
||||
* call, until the pass's final chunk has been sent - then goes back to
|
||||
* Output thread entry point, run once for the stream's whole lifetime.
|
||||
* Idles (polling playRequested) until woken by audioStreamPSPBuffer(),
|
||||
* then repeatedly waits for a full hardware chunk to become available in
|
||||
* the ring buffer (topped up by the main thread - see
|
||||
* audioStreamPSPTopUp()) and outputs it, blocking naturally on each
|
||||
* sceAudioOutputPannedBlocking() call, until the pass's final
|
||||
* (possibly-partial, faded) chunk has been sent - then goes back to
|
||||
* idling, ready for the next play request, until the thread is asked to
|
||||
* stop. Never touches the asset/PCM layer directly.
|
||||
*
|
||||
* @param thread The running thread_t, with data set to the audiostream_t.
|
||||
*/
|
||||
void audioStreamPSPThreadFeed(thread_t *thread);
|
||||
|
||||
/**
|
||||
* Reader thread entry point, run once for the stream's whole lifetime.
|
||||
* Idles (polling readRequested) until woken by audioStreamPSPBuffer(), then
|
||||
* reads the stream's asset PCM data (via audioStreamPcmSeek()/
|
||||
* audioStreamPcmRead()) and prepares fixed-size hardware chunks (applying
|
||||
* loop wrap/fade, same as the player thread used to do inline), pushing
|
||||
* each onto the queue for the player thread to consume - running ahead of
|
||||
* playback rather than in lockstep with it, so PCM I/O latency never
|
||||
* stalls the player thread's real-time output loop. Stops producing once
|
||||
* it queues the pass's final chunk (or a read/seek fails - see
|
||||
* audiostreampsp_t.readFailed), then goes back to idling until the thread
|
||||
* is asked to stop.
|
||||
*
|
||||
* @param thread The running thread_t, with data set to the audiostream_t.
|
||||
*/
|
||||
void audioStreamPSPThreadRead(thread_t *thread);
|
||||
|
||||
Reference in New Issue
Block a user