Fix PSP end-of-playback click via tail fade + exact final chunk length

The feeder's final chunk zero-padded up to the full 1024-frame
reserved size (e.g. 956 padding samples for a 68-sample remainder),
jumping straight from whatever amplitude the waveform ended at down
to silence - an audible click. Fixed two ways: fade the last 32 real
frames linearly to zero before any padding begins (also covers the
case where total length is an exact multiple of the chunk size, where
the waveform would otherwise just stop abruptly with no padding at
all), and declare only the real sample count via
sceAudioSetChannelDataLen (64-aligned) for the final chunk instead of
padding out to the full reserved length, shrinking the leftover
padding to under 64 samples. Channel length gets reset to the full
chunk size at the start of each feed pass in case a previous
playback left it shortened.

Verified via PPSSPP: sceAudioSetChannelDataLen(7, 1024) then (7, 128)
for the tone's 68-sample remainder, onEnd still fires once.
This commit is contained in:
2026-08-31 12:12:32 -05:00
parent 50c5621d8a
commit ea35472ef8
+38 -1
View File
@@ -26,6 +26,12 @@
// it above main so audio feeding always wins scheduling contention. // it above main so audio feeding always wins scheduling contention.
#define AUDIO_PSP_THREAD_PRIORITY 18 #define AUDIO_PSP_THREAD_PRIORITY 18
// How many frames at the very end of a stream get linearly faded to zero,
// avoiding the audible click of the waveform stopping at a non-zero
// amplitude (either into padding on a partial final chunk, or the DAC
// just stopping outright on an exact-multiple-length one).
#define AUDIO_PSP_FADE_FRAMES 32
errorret_t audioStreamPSPInit(audiostream_t *stream) { errorret_t audioStreamPSPInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL."); assertNotNull(stream, "Stream cannot be NULL.");
@@ -95,10 +101,15 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
sceKernelChangeThreadPriority(sceKernelGetThreadId(), AUDIO_PSP_THREAD_PRIORITY); sceKernelChangeThreadPriority(sceKernelGetThreadId(), AUDIO_PSP_THREAD_PRIORITY);
audiostream_t *stream = (audiostream_t *) thread->data; audiostream_t *stream = (audiostream_t *) thread->data;
const size_t frameSize = stream->pcm.channels * sizeof(int16_t); const size_t channels = stream->pcm.channels;
const size_t frameSize = channels * sizeof(int16_t);
const size_t totalFrames = stream->dataSize / frameSize; const size_t totalFrames = stream->dataSize / frameSize;
const size_t chunkSize = AUDIO_PSP_CHUNK_FRAMES * 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); int16_t *chunk = memoryAllocate(chunkSize);
size_t position = 0; size_t position = 0;
@@ -107,12 +118,38 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES
? framesRemaining ? framesRemaining
: AUDIO_PSP_CHUNK_FRAMES; : AUDIO_PSP_CHUNK_FRAMES;
const bool_t isLastChunk = framesThisChunk == framesRemaining;
memoryZero(chunk, chunkSize); memoryZero(chunk, chunkSize);
memoryCopy( memoryCopy(
chunk, stream->data + (position * frameSize), framesThisChunk * frameSize chunk, stream->data + (position * frameSize), framesThisChunk * frameSize
); );
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
// jump is what was heard as a click at the end of playback.
const size_t fadeFrames = framesThisChunk < AUDIO_PSP_FADE_FRAMES
? framesThisChunk
: AUDIO_PSP_FADE_FRAMES;
for(size_t i = 0; i < fadeFrames; i++) {
const size_t frame = framesThisChunk - fadeFrames + i;
const float_t factor = 1.0f - ((float_t) (i + 1) / (float_t) fadeFrames);
for(size_t c = 0; c < channels; c++) {
int16_t *sample = &chunk[frame * channels + c];
*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 // Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality
// take effect mid-playback, unlike the platform's other one-shot calls. // take effect mid-playback, unlike the platform's other one-shot calls.
float_t pan = (float_t) stream->directionality / 128.0f; float_t pan = (float_t) stream->directionality / 128.0f;