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
+59 -18
View File
@@ -9,6 +9,7 @@
#include "audio/audiostream.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include <pspaudio.h>
#include <pspthreadman.h>
@@ -92,6 +93,9 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
stream->platform.finished = false;
stream->platform.startFrame = stream->startFrame;
stream->startFrame = 0;
stream->seeking = false;
stream->platform.playRequested = true;
errorOk();
@@ -125,7 +129,22 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
stream->platform.playRequested = false;
const size_t totalFrames = stream->dataSize / frameSize;
size_t position = 0;
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) that a looping pass wraps within, once it's reached -
// defaulting to the whole buffer (loopStart == -1, loopTo == 0) so
// behaviour is unchanged when no explicit loop points are configured.
const size_t loopEndFrame = stream->loopStart >= 0
? mathMin((size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames)
: totalFrames;
const size_t loopToFrame = mathMin(
(size_t) (stream->loopTo * stream->pcm.sampleRate), loopEndFrame
);
// Only the very first pass honors an explicit seek (audioStreamSetPosition,
// captured into platform.startFrame by audioStreamPSPBuffer()) - every
// subsequent loop wraps to loopToFrame instead.
size_t position = stream->platform.startFrame;
bool_t reachedEnd = false;
// Every call always sends a full, constant-size AUDIO_PSP_CHUNK_FRAMES
@@ -139,7 +158,14 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
// better option (see audiostream.c), but here the thread can just
// keep going and call onLoop itself instead.
while(!threadShouldStop(thread) && !reachedEnd) {
const size_t framesRemaining = totalFrames - position;
// Normally bounded by loopEndFrame (the loop segment's end), but a
// seek can legitimately land past it (e.g. into an outro after the
// loop point) - in that case play out to the true end of the buffer
// once instead of underflowing framesRemaining.
const size_t currentEndFrame = position < loopEndFrame
? loopEndFrame
: totalFrames;
const size_t framesRemaining = currentEndFrame - position;
const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES
? framesRemaining
: AUDIO_PSP_CHUNK_FRAMES;
@@ -156,17 +182,33 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
if(reachesEndThisChunk && willLoop) {
// Instead of padding the rest of this constant-size chunk with
// silence, fill it with the start of the next pass - a looping
// stream should never have dead air baked into its output at all.
// Not faded (unlike the true-end case below): a wrap only sounds
// seamless if the source data already loops cleanly (true for the
// shared test tone, deliberately authored at 441Hz for exactly
// this), matching the same assumption Dolphin's native hardware
// looping makes - imperfectly-authored loop content will still
// click here.
wrapFrames = remainderFrames < totalFrames ? remainderFrames : totalFrames;
// silence, fill it with the start of the loop segment (loopToFrame)
// - a looping stream should never have dead air baked into its
// output at all. Not faded (unlike the true-end case below): a wrap
// only sounds seamless if the source data already loops cleanly
// (true for the shared test tone, deliberately authored at 441Hz
// for exactly this), matching the same assumption Dolphin's native
// hardware looping makes - imperfectly-authored loop content will
// still click here.
//
// NOTE: if the loop segment (loopEndFrame - loopToFrame) is shorter
// than one hardware chunk (AUDIO_PSP_CHUNK_FRAMES, ~23ms @ 44100Hz),
// only a single copy of it fills the remainder here rather than
// repeating it to fill the whole chunk - any leftover space is
// zero-padded (a brief, audible gap) and loopCount still only
// increments once per hardware chunk, not once per actual loop
// repeat. Not hit by anything in this codebase today (the shared
// test tone's default whole-buffer loop is 1 second), but a future
// short music-loop tail would need this generalized to a
// repeat-fill instead.
const size_t loopSegmentFrames = loopEndFrame - loopToFrame;
wrapFrames = remainderFrames < loopSegmentFrames
? remainderFrames
: loopSegmentFrames;
memoryCopy(
chunk + (framesThisChunk * channels), stream->data, wrapFrames * frameSize
chunk + (framesThisChunk * channels),
stream->data + (loopToFrame * frameSize),
wrapFrames * frameSize
);
if(wrapFrames < remainderFrames) {
memoryZero(
@@ -196,13 +238,12 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
// 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;
float_t leftFactor, rightFactor;
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
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 leftVolume = (int) (baseVolume * leftFactor);
const int rightVolume = (int) (baseVolume * rightFactor);
sceAudioOutputPannedBlocking(
stream->platform.channel, leftVolume, rightVolume, chunk
@@ -216,7 +257,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
// starve the next chunk). audioStreamUpdate() picks this up and
// fires onLoop safely from the main thread instead.
stream->loopCount++;
position = wrapFrames;
position = loopToFrame + wrapFrames;
} else {
reachedEnd = true;
}