15bd9fc43c
- 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.
130 lines
4.4 KiB
C
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;
|
|
}
|
|
}
|