Fix crackling PSP audio: feed hardware in small chunks from a thread

The original implementation reserved a channel sized to the whole
buffer (~44160 samples) and sent it in one sceAudioOutputPannedBlocking
call. Confirmed against the PSP SDK's own samples (pspaudiolib,
the mp3 sample) that this deviates from how PSP audio hardware is
actually meant to be driven: small fixed-size chunks (1024 frames,
matching pspaudiolib's own convention) fed continuously, decoupled
from the render loop. Reproduced as crackling on real hardware.

Now reserves a 1024-frame channel and runs a dedicated thread (the
project's existing thread_t abstraction, already used by asset.c and
already working on PSP via DUSK_THREAD_PTHREAD) that streams the
buffer chunk-by-chunk until exhausted, re-reading volume/pan every
chunk so live changes take effect mid-playback. Finish detection
switched from polling sceAudioGetChannelRestLength (which can't tell
"between chunks" from "actually done" once chunked) to a flag the
feeder thread sets on completion, same shape as the Dolphin voice
callback.

Verified via the dusk-psp toolchain and PPSSPP headless (channel now
reserved at 1024 frames instead of 44160, onEnd still fires once).
Real-hardware crackle-free confirmation pending.
This commit is contained in:
2026-08-31 11:03:30 -05:00
parent 0f4ee5d965
commit da275cfd52
2 changed files with 84 additions and 51 deletions
+26 -10
View File
@@ -7,16 +7,22 @@
#pragma once
#include "error/error.h"
#include "thread/thread.h"
typedef struct audiostream_s audiostream_t;
typedef struct {
// Reserved hardware output channel, from sceAudioChReserve.
// 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;
// Number of samples (frames) per output call the channel was reserved
// with. 64-aligned, between PSP_AUDIO_SAMPLE_MIN and PSP_AUDIO_SAMPLE_MAX.
int samples;
// Dedicated thread that feeds the channel chunk-by-chunk for the
// duration of playback, independent of the engine's frame rate.
thread_t thread;
// Set by audioStreamPSPThreadFeed() once it has fed the last chunk.
volatile bool_t finished;
} audiostreampsp_t;
/**
@@ -29,8 +35,8 @@ typedef struct {
errorret_t audioStreamPSPInit(audiostream_t *stream);
/**
* Disposes the PSP-specific playback state of an audio stream, releasing
* its hardware output channel.
* Disposes the PSP-specific playback state of an audio stream, stopping its
* feeder thread and releasing its hardware output channel.
*
* @param stream The audio stream to dispose.
* @return Error state if any.
@@ -38,8 +44,8 @@ errorret_t audioStreamPSPInit(audiostream_t *stream);
errorret_t audioStreamPSPDispose(audiostream_t *stream);
/**
* Sends the stream's currently staged PCM data (stream->data) to its
* reserved hardware output channel.
* Starts the stream's feeder thread, which streams stream->data to its
* reserved hardware output channel in small chunks until exhausted.
*
* @param stream The audio stream to output.
* @return Error state if any.
@@ -47,10 +53,20 @@ errorret_t audioStreamPSPDispose(audiostream_t *stream);
errorret_t audioStreamPSPBuffer(audiostream_t *stream);
/**
* Checks whether the stream's reserved hardware channel has finished
* playing its currently buffered data.
* Checks whether the stream's feeder thread has finished feeding its
* currently buffered data.
*
* @param stream The audio stream to check.
* @return true if playback has finished, false otherwise.
*/
bool_t audioStreamPSPIsFinished(audiostream_t *stream);
/**
* Feeder thread entry point. Streams stream->data (passed via thread->data)
* to its hardware channel in fixed-size chunks, blocking naturally on each
* sceAudioOutputPannedBlocking() call, until the whole buffer has been sent
* or the thread is asked to stop.
*
* @param thread The running thread_t, with data set to the audiostream_t.
*/
void audioStreamPSPThreadFeed(thread_t *thread);