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
+58 -41
View File
@@ -11,6 +11,11 @@
#include "util/memory.h"
#include <pspaudio.h>
// 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.
#define AUDIO_PSP_CHUNK_FRAMES 1024
errorret_t audioStreamPSPInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
@@ -31,28 +36,20 @@ errorret_t audioStreamPSPInit(audiostream_t *stream) {
);
}
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
const size_t frameCount = stream->dataSize / frameSize;
int samples = (int) PSP_AUDIO_SAMPLE_ALIGN(frameCount);
if(samples < PSP_AUDIO_SAMPLE_MIN) samples = PSP_AUDIO_SAMPLE_MIN;
if(samples > PSP_AUDIO_SAMPLE_MAX) {
// TODO: sources longer than ~1.5s need chunked streaming across
// multiple buffer calls instead of a single one.
samples = PSP_AUDIO_SAMPLE_MAX;
}
const int format = stream->pcm.channels == 1
? PSP_AUDIO_FORMAT_MONO
: PSP_AUDIO_FORMAT_STEREO;
const int channel = sceAudioChReserve(PSP_AUDIO_NEXT_CHANNEL, samples, format);
const int channel = sceAudioChReserve(
PSP_AUDIO_NEXT_CHANNEL, AUDIO_PSP_CHUNK_FRAMES, format
);
if(channel < 0) {
errorThrow("Failed to reserve PSP audio channel: 0x%08X", channel);
}
stream->platform.channel = channel;
stream->platform.samples = samples;
stream->platform.finished = false;
threadInit(&stream->platform.thread, audioStreamPSPThreadFeed);
errorOk();
}
@@ -60,6 +57,7 @@ errorret_t audioStreamPSPInit(audiostream_t *stream) {
errorret_t audioStreamPSPDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
threadStop(&stream->platform.thread);
sceAudioChRelease(stream->platform.channel);
errorOk();
@@ -68,33 +66,9 @@ errorret_t audioStreamPSPDispose(audiostream_t *stream) {
errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
const size_t bufferSize = (size_t) stream->platform.samples * frameSize;
int16_t *buffer = memoryAllocate(bufferSize);
memoryZero(buffer, bufferSize);
memoryCopy(
buffer, stream->data,
stream->dataSize < bufferSize ? stream->dataSize : bufferSize
);
// Simple linear pan; AUDIO_STREAM_LEFT/CENTER/RIGHT map to -128..0..127.
float_t pan = (float_t) stream->directionality / 128.0f;
if(pan < -1.0f) pan = -1.0f;
if(pan > 1.0f) pan = 1.0f;
const int baseVolume = (stream->volume * PSP_AUDIO_VOLUME_MAX) / 0xFF;
const int leftVolume = (int) (baseVolume * (pan > 0 ? (1.0f - pan) : 1.0f));
const int rightVolume = (int) (baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f));
const int result = sceAudioOutputPannedBlocking(
stream->platform.channel, leftVolume, rightVolume, buffer
);
memoryFree(buffer);
if(result < 0) {
errorThrow("Failed to output PSP audio data: 0x%08X", result);
}
stream->platform.finished = false;
stream->platform.thread.data = stream;
threadStartRequest(&stream->platform.thread);
errorOk();
}
@@ -102,5 +76,48 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
bool_t audioStreamPSPIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
return sceAudioGetChannelRestLength(stream->platform.channel) <= 0;
return stream->platform.finished;
}
void audioStreamPSPThreadFeed(thread_t *thread) {
assertNotNull(thread, "Thread cannot be NULL.");
audiostream_t *stream = (audiostream_t *) thread->data;
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
const size_t totalFrames = stream->dataSize / frameSize;
const size_t chunkSize = AUDIO_PSP_CHUNK_FRAMES * frameSize;
int16_t *chunk = memoryAllocate(chunkSize);
size_t position = 0;
while(!threadShouldStop(thread) && position < totalFrames) {
const size_t framesRemaining = totalFrames - position;
const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES
? framesRemaining
: AUDIO_PSP_CHUNK_FRAMES;
memoryZero(chunk, chunkSize);
memoryCopy(
chunk, stream->data + (position * frameSize), framesThisChunk * frameSize
);
// Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality
// take effect mid-playback, unlike the platform's other one-shot calls.
float_t pan = (float_t) stream->directionality / 128.0f;
if(pan < -1.0f) pan = -1.0f;
if(pan > 1.0f) pan = 1.0f;
const int baseVolume = (stream->volume * PSP_AUDIO_VOLUME_MAX) / 0xFF;
const int leftVolume = (int) (baseVolume * (pan > 0 ? (1.0f - pan) : 1.0f));
const int rightVolume = (int) (baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f));
sceAudioOutputPannedBlocking(
stream->platform.channel, leftVolume, rightVolume, chunk
);
position += framesThisChunk;
}
memoryFree(chunk);
stream->platform.finished = true;
}