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
+17 -16
View File
@@ -42,10 +42,8 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
// 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;
float_t leftFactor, rightFactor;
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
const float_t baseVolume = (float_t) stream->volume / 255.0f;
@@ -57,8 +55,8 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
config.format = ANSND_VOICE_PCM_FORMAT_SIGNED_16_PCM;
config.channels = stream->pcm.channels;
config.pitch = 1.0f;
config.left_volume = baseVolume * (pan > 0 ? (1.0f - pan) : 1.0f);
config.right_volume = baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f);
config.left_volume = baseVolume * leftFactor;
config.right_volume = baseVolume * rightFactor;
// The DSP DMAs frame_data_ptr directly out of main memory, bypassing the
// CPU cache, and rejects any pointer in cached virtual address space
// (0x8xxxxxxx.. - checked as "negative" internally, returned as
@@ -68,28 +66,31 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
DCFlushRange(stream->data, stream->dataSize);
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(stream->data);
config.frame_count = (u32) (stream->dataSize / frameSize);
config.start_offset = (u32) stream->startFrame;
stream->startFrame = 0;
stream->seeking = false;
config.voice_callback = audioStreamDolphinVoiceCallback;
config.stream_callback = NULL; // single-buffer playback
config.user_pointer = stream;
// Loop entirely in hardware rather than mirroring PSP/Linux's
// detect-finished-then-restart-from-software approach: ansnd's DSP mixer
// wraps frame_count-1 back to 0 on its own, so there's no restart latency
// to create a gap in the first place (unlike a software restart, which
// always costs at least a little). Only restart-from-the-very-start is
// supported elsewhere in this codebase (loopTo isn't honored yet), which
// matches exactly what loop_start_offset=0 gives here.
// wraps loop_end_offset back to loop_start_offset on its own, so there's
// no restart latency to create a gap in the first place (unlike a
// software restart, which always costs at least a little).
//
// Trade-off: onLoop never fires for a Dolphin voice looping this way -
// there's no ANSND_VOICE_STATE for "wrapped", only state transitions like
// FINISHED/STOPPED, which a looping voice never reaches. And unlike
// PSP's per-restart tail fade, the DSP does no smoothing at the wrap
// point - it requires the source data's last frame to already flow
// cleanly into its first (true today only because the shared 441Hz test
// tone was deliberately chosen to divide evenly into the sample rate).
// point - it requires loopStart's frame to already flow cleanly into
// loopTo's (true today only because the shared 441Hz test tone was
// deliberately chosen to divide evenly into the sample rate).
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
config.loop_start_offset = 0;
config.loop_end_offset = config.frame_count - 1;
config.loop_start_offset = (u32) (stream->loopTo * stream->pcm.sampleRate);
config.loop_end_offset = stream->loopStart >= 0
? (u32) (stream->loopStart * stream->pcm.sampleRate) - 1
: config.frame_count - 1;
}
s32 result = ansnd_configure_pcm_voice((u32) stream->platform.voiceId, &config);