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 "util/memory.h"
#include <pspaudio.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) { errorret_t audioStreamPSPInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL."); 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 const int format = stream->pcm.channels == 1
? PSP_AUDIO_FORMAT_MONO ? PSP_AUDIO_FORMAT_MONO
: PSP_AUDIO_FORMAT_STEREO; : 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) { if(channel < 0) {
errorThrow("Failed to reserve PSP audio channel: 0x%08X", channel); errorThrow("Failed to reserve PSP audio channel: 0x%08X", channel);
} }
stream->platform.channel = channel; stream->platform.channel = channel;
stream->platform.samples = samples; stream->platform.finished = false;
threadInit(&stream->platform.thread, audioStreamPSPThreadFeed);
errorOk(); errorOk();
} }
@@ -60,6 +57,7 @@ errorret_t audioStreamPSPInit(audiostream_t *stream) {
errorret_t audioStreamPSPDispose(audiostream_t *stream) { errorret_t audioStreamPSPDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL."); assertNotNull(stream, "Stream cannot be NULL.");
threadStop(&stream->platform.thread);
sceAudioChRelease(stream->platform.channel); sceAudioChRelease(stream->platform.channel);
errorOk(); errorOk();
@@ -68,33 +66,9 @@ errorret_t audioStreamPSPDispose(audiostream_t *stream) {
errorret_t audioStreamPSPBuffer(audiostream_t *stream) { errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL."); assertNotNull(stream, "Stream cannot be NULL.");
const size_t frameSize = stream->pcm.channels * sizeof(int16_t); stream->platform.finished = false;
const size_t bufferSize = (size_t) stream->platform.samples * frameSize; stream->platform.thread.data = stream;
threadStartRequest(&stream->platform.thread);
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);
}
errorOk(); errorOk();
} }
@@ -102,5 +76,48 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
bool_t audioStreamPSPIsFinished(audiostream_t *stream) { bool_t audioStreamPSPIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL."); 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;
} }
+26 -10
View File
@@ -7,16 +7,22 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "thread/thread.h"
typedef struct audiostream_s audiostream_t; typedef struct audiostream_s audiostream_t;
typedef struct { 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; int channel;
// Number of samples (frames) per output call the channel was reserved // Dedicated thread that feeds the channel chunk-by-chunk for the
// with. 64-aligned, between PSP_AUDIO_SAMPLE_MIN and PSP_AUDIO_SAMPLE_MAX. // duration of playback, independent of the engine's frame rate.
int samples; thread_t thread;
// Set by audioStreamPSPThreadFeed() once it has fed the last chunk.
volatile bool_t finished;
} audiostreampsp_t; } audiostreampsp_t;
/** /**
@@ -29,8 +35,8 @@ typedef struct {
errorret_t audioStreamPSPInit(audiostream_t *stream); errorret_t audioStreamPSPInit(audiostream_t *stream);
/** /**
* Disposes the PSP-specific playback state of an audio stream, releasing * Disposes the PSP-specific playback state of an audio stream, stopping its
* its hardware output channel. * feeder thread and releasing its hardware output channel.
* *
* @param stream The audio stream to dispose. * @param stream The audio stream to dispose.
* @return Error state if any. * @return Error state if any.
@@ -38,8 +44,8 @@ errorret_t audioStreamPSPInit(audiostream_t *stream);
errorret_t audioStreamPSPDispose(audiostream_t *stream); errorret_t audioStreamPSPDispose(audiostream_t *stream);
/** /**
* Sends the stream's currently staged PCM data (stream->data) to its * Starts the stream's feeder thread, which streams stream->data to its
* reserved hardware output channel. * reserved hardware output channel in small chunks until exhausted.
* *
* @param stream The audio stream to output. * @param stream The audio stream to output.
* @return Error state if any. * @return Error state if any.
@@ -47,10 +53,20 @@ errorret_t audioStreamPSPDispose(audiostream_t *stream);
errorret_t audioStreamPSPBuffer(audiostream_t *stream); errorret_t audioStreamPSPBuffer(audiostream_t *stream);
/** /**
* Checks whether the stream's reserved hardware channel has finished * Checks whether the stream's feeder thread has finished feeding its
* playing its currently buffered data. * currently buffered data.
* *
* @param stream The audio stream to check. * @param stream The audio stream to check.
* @return true if playback has finished, false otherwise. * @return true if playback has finished, false otherwise.
*/ */
bool_t audioStreamPSPIsFinished(audiostream_t *stream); 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);