Clean up audio subsystem duplication, implement seeking and loop regions

- Extract audioStreamGetPanFactors() into the shared layer, replacing
  identical pan-to-LR math duplicated in the PSP and Dolphin backends
  (and dropping a dead clamp - directionality's int8_t range already
  guarantees pan stays in [-1, 1]).
- Implement audioStreamSetPosition() for real (was previously a no-op
  that computed a value and threw it away) and add
  audioStreamSetLoopPoints() to actually drive loopStart/loopTo, which
  were previously dead fields with no setter at all. Both are threaded
  through all three platform backends:
  - PSP: the feeder thread now bounds each pass by the loop segment
    and wraps to loopTo instead of always frame 0, while still filling
    hardware chunks gaplessly.
  - Dolphin: loopTo/loopStart map directly onto ansnd's existing
    loop_start_offset/loop_end_offset, and startFrame onto start_offset.
  - Linux: Buffer() now queues only the current segment, clearing the
    SDL queue on an explicit seek but preserving the existing
    overlap-based gapless loop restart otherwise.
- Linux: skip the mix scratch-buffer entirely at full volume, queuing
  stream->data directly instead of allocating/zeroing/copying into one
  every buffer call for no reason.

Verified no regression in the default (no seek, no loop points) case on
Linux/PPSSPP/Dolphin (-a LLE), and verified seek + loop-region behavior
manually via a temporary engine.c smoke-test tweak (reverted) showing
the expected faster loop cadence and seek-then-loop sequencing.
This commit is contained in:
2026-08-31 17:18:49 -05:00
parent 8049e90853
commit 15bd9fc43c
6 changed files with 252 additions and 58 deletions
+50 -10
View File
@@ -9,6 +9,7 @@
#include "audio/audiostream.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
// How many frames of lead time to keep queued ahead of playback. Matches
// SDL_AudioSpec.samples below - the device's own internal buffer size - so
@@ -44,17 +45,56 @@ errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
uint8_t *mixed = memoryAllocate(stream->dataSize);
memoryZero(mixed, stream->dataSize);
SDL_MixAudioFormat(
mixed, stream->data, AUDIO_S16SYS, (Uint32) stream->dataSize,
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
);
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
const size_t totalFrames = stream->dataSize / frameSize;
int queued = SDL_QueueAudio(
stream->platform.device, mixed, (Uint32) stream->dataSize
);
memoryFree(mixed);
// Consumed synchronously (right here, in the same call that decided to
// (re)buffer) rather than left for later - see startFrame's own comment.
const size_t startFrame = mathMin(stream->startFrame, totalFrames);
const bool_t seeking = stream->seeking;
stream->startFrame = 0;
stream->seeking = false;
size_t endFrame = totalFrames;
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
endFrame = mathMin(
(size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames
);
}
// A seek (or a loop restart landing exactly on the loop end) can put
// startFrame at or past endFrame - e.g. seeking into an outro after the
// loop point. Play out to the true end of the buffer once instead, same
// as PSP.
if(startFrame >= endFrame) endFrame = totalFrames;
const size_t bytes = (endFrame - startFrame) * frameSize;
const uint8_t *segment = stream->data + (startFrame * frameSize);
// Only an explicit seek discards whatever's still queued and jumps -
// a natural loop restart deliberately leaves the previous pass's tail
// (AUDIO_LINUX_LEAD_FRAMES worth) queued and appends the new pass after
// it, which is what makes looping gapless (see IsFinished()'s comment).
// Clearing on every Buffer() call would destroy that overlap and
// reintroduce the exact gap this was built to avoid.
if(seeking) {
SDL_ClearQueuedAudio(stream->platform.device);
}
int queued;
if(stream->volume == 0xFF) {
// Nothing to mix at full volume - queue the segment directly instead of
// allocating/zeroing a same-size scratch buffer just to copy it in.
queued = SDL_QueueAudio(stream->platform.device, segment, (Uint32) bytes);
} else {
uint8_t *mixed = memoryAllocate(bytes);
memoryZero(mixed, bytes);
SDL_MixAudioFormat(
mixed, segment, AUDIO_S16SYS, (Uint32) bytes,
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
);
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytes);
memoryFree(mixed);
}
if(queued != 0) {
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());