Files
dusk/src/duskdolphin/audio/audiostreamdolphin.c
T
YourWishes 15bd9fc43c 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.
2026-08-31 17:18:49 -05:00

130 lines
4.4 KiB
C

/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "audiostreamdolphin.h"
#include "audio/audiostream.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <ansndlib.h>
#include <ogc/cache.h>
#include <ogc/system.h>
errorret_t audioStreamDolphinInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
const s32 voiceId = ansnd_allocate_voice();
if(voiceId < 0) {
errorThrow(
"Failed to allocate ansnd voice: %s", ansnd_get_error_string(voiceId)
);
}
stream->platform.voiceId = voiceId;
stream->platform.finished = false;
errorOk();
}
errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
ansnd_deallocate_voice((u32) stream->platform.voiceId);
errorOk();
}
errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
float_t leftFactor, rightFactor;
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
const float_t baseVolume = (float_t) stream->volume / 255.0f;
stream->platform.finished = false;
ansnd_pcm_voice_config_t config;
memoryZero(&config, sizeof(ansnd_pcm_voice_config_t));
config.samplerate = stream->pcm.sampleRate;
config.format = ANSND_VOICE_PCM_FORMAT_SIGNED_16_PCM;
config.channels = stream->pcm.channels;
config.pitch = 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
// ANSND_ERROR_INVALID_MEMORY) - it needs the physical address instead.
// Flush first so the DMA sees what the CPU actually wrote, not stale
// memory contents.
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 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 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 = (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);
if(result != ANSND_ERROR_OK) {
errorThrow(
"Failed to configure ansnd voice: %s", ansnd_get_error_string(result)
);
}
result = ansnd_start_voice((u32) stream->platform.voiceId);
if(result != ANSND_ERROR_OK) {
errorThrow(
"Failed to start ansnd voice: %s", ansnd_get_error_string(result)
);
}
errorOk();
}
bool_t audioStreamDolphinIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
return stream->platform.finished;
}
void audioStreamDolphinVoiceCallback(void *userPointer, int32_t voiceState) {
audiostream_t *stream = (audiostream_t *) userPointer;
if(
voiceState == ANSND_VOICE_STATE_FINISHED ||
voiceState == ANSND_VOICE_STATE_STOPPED ||
voiceState == ANSND_VOICE_STATE_ERROR
) {
stream->platform.finished = true;
}
}