Fix PSP end-of-playback click: constant chunk size + trailing silence

The previous fix (shrinking the final chunk's declared length via
sceAudioSetChannelDataLen) still clicked on real hardware. Reverted
that in favor of two changes that don't rely on that call's
undocumented mid-stream behavior: every call now sends a full,
constant AUDIO_PSP_CHUNK_FRAMES buffer with the tail simply
zero-padded (channel length is never changed after the initial
reserve), and a couple of extra all-silence chunks are fed after the
real audio + fade so the channel keeps being actively driven at zero
for a moment rather than stopping outright, in case some of the click
was the channel/DAC settling rather than a pure sample-domain
discontinuity.

Confirmed fixed on real PSP hardware.
This commit is contained in:
2026-08-31 12:16:03 -05:00
parent ea35472ef8
commit 7ad735552a
+22 -13
View File
@@ -32,6 +32,12 @@
// just stopping outright on an exact-multiple-length one).
#define AUDIO_PSP_FADE_FRAMES 32
// A couple of extra all-silence chunks fed after the real audio (and
// after the fade above), so the channel keeps being actively driven at
// zero for a moment rather than stopping outright - mitigates a possible
// hardware/DAC settling pop on top of the digital-domain fade.
#define AUDIO_PSP_TRAILING_SILENT_CHUNKS 2
errorret_t audioStreamPSPInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
@@ -106,13 +112,13 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
const size_t totalFrames = stream->dataSize / frameSize;
const size_t chunkSize = AUDIO_PSP_CHUNK_FRAMES * frameSize;
// A previous playback pass may have left this shortened for its own
// final partial chunk (see below) - restore the full chunk length.
sceAudioSetChannelDataLen(stream->platform.channel, AUDIO_PSP_CHUNK_FRAMES);
int16_t *chunk = memoryAllocate(chunkSize);
size_t position = 0;
// 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 final chunk is zero-padded out to that full size.
while(!threadShouldStop(thread) && position < totalFrames) {
const size_t framesRemaining = totalFrames - position;
const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES
@@ -127,7 +133,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
if(isLastChunk) {
// Fade the tail down to zero so the waveform never stops (or meets
// the zero-padding below) at a non-zero amplitude - that abrupt
// the zero-padding above) at a non-zero amplitude - that abrupt
// jump is what was heard as a click at the end of playback.
const size_t fadeFrames = framesThisChunk < AUDIO_PSP_FADE_FRAMES
? framesThisChunk
@@ -140,14 +146,6 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
*sample = (int16_t) ((float_t) *sample * factor);
}
}
// Only declare the real sample count for output instead of the full
// reserved chunk size, 64-aligned (sceAudioChReserve's own
// constraint - not documented for this call, but not worth risking).
const int alignedFrames = (int) PSP_AUDIO_SAMPLE_ALIGN(framesThisChunk);
if((size_t) alignedFrames != AUDIO_PSP_CHUNK_FRAMES) {
sceAudioSetChannelDataLen(stream->platform.channel, alignedFrames);
}
}
// Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality
@@ -167,6 +165,17 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
position += framesThisChunk;
}
// A couple of extra all-silence chunks so the channel keeps being
// actively driven at zero for a moment rather than stopping outright.
memoryZero(chunk, chunkSize);
for(
uint8_t i = 0;
!threadShouldStop(thread) && i < AUDIO_PSP_TRAILING_SILENT_CHUNKS;
i++
) {
sceAudioOutputBlocking(stream->platform.channel, 0, chunk);
}
memoryFree(chunk);
stream->platform.finished = true;
}