Split PSP audio feeder into reader + player threads to fix crackle
sceAudioOutputPannedBlocking() occupies the calling thread for the full duration of the chunk it just submitted. That was fine while PCM chunks came out of a fully-resident buffer (a near-instant memcpy), but now that they're read from the asset on demand, that same read happens in between output calls on the same thread - any read slower than a memcpy opens a real gap in the hardware channel, heard as crackle. Split the single feeder thread in two: a reader thread that does all PCM I/O (seek/read, loop-wrap, fade prep) ahead of playback into a small 3-slot queue, and a player thread that only pulls ready chunks off the queue and outputs them. This overlaps I/O with hardware playback instead of serializing them. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -11,34 +11,92 @@
|
||||
|
||||
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
|
||||
|
||||
typedef struct {
|
||||
// Reserved hardware output channel, from sceAudioChReserve. Reserved with
|
||||
// a small fixed chunk size (AUDIO_PSP_CHUNK_FRAMES) - PSP audio hardware
|
||||
// expects continuous small-chunk feeding, not one large buffer per call.
|
||||
int channel;
|
||||
|
||||
// Persistent thread, created once in Init and alive for the stream's
|
||||
// whole lifetime - feeds the channel chunk-by-chunk for the duration of
|
||||
// playback, independent of the engine's frame rate. 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.
|
||||
// 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.
|
||||
thread_t thread;
|
||||
|
||||
// Set by audioStreamPSPBuffer() to wake the idling thread into feeding
|
||||
// a pass; cleared by the thread once it picks it up.
|
||||
// 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.
|
||||
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 feeder thread,
|
||||
// since the thread only wakes up asynchronously and audiostream_t's
|
||||
// shared field may already have moved on to a different value by then.
|
||||
// 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 thread once it has fed the last chunk of a pass.
|
||||
// Set by the player 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];
|
||||
|
||||
// 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];
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
} audiostreampsp_t;
|
||||
|
||||
/**
|
||||
@@ -60,9 +118,10 @@ errorret_t audioStreamPSPInit(audiostream_t *stream);
|
||||
errorret_t audioStreamPSPDispose(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Wakes the stream's persistent feeder thread to stream PCM data (read on
|
||||
* demand from the stream's asset via audioStreamPcmRead()) to its reserved
|
||||
* hardware output channel in small chunks until exhausted.
|
||||
* 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.
|
||||
*
|
||||
* @param stream The audio stream to output.
|
||||
* @return Error state if any.
|
||||
@@ -70,7 +129,7 @@ errorret_t audioStreamPSPDispose(audiostream_t *stream);
|
||||
errorret_t audioStreamPSPBuffer(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Checks whether the stream's feeder thread has finished feeding its
|
||||
* Checks whether the stream's player thread has finished outputting its
|
||||
* currently buffered data.
|
||||
*
|
||||
* @param stream The audio stream to check.
|
||||
@@ -79,15 +138,32 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream);
|
||||
bool_t audioStreamPSPIsFinished(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Feeder thread entry point, run once for the stream's whole lifetime.
|
||||
* Idles (polling playRequested) until woken by audioStreamPSPBuffer(),
|
||||
* then reads and streams the stream's asset PCM data (via the audiostream_t
|
||||
* passed as thread->data) to its hardware channel in fixed-size chunks,
|
||||
* blocking naturally on each
|
||||
* sceAudioOutputPannedBlocking() call, until the whole buffer has been
|
||||
* sent - then goes back to idling, ready for the next play request, until
|
||||
* the thread is asked to stop.
|
||||
* 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
|
||||
* 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