Implement GameCube/Wii audio playback via libansnd

Allocates an ansnd voice per stream and configures/starts it in
single-buffer mode (no continuous re-feed needed yet, matching the
PSP/Linux single-shot approach). Real linear panning from
directionality via left/right voice volume. ansnd has no polling API
for voice state, so finish detection uses a voice_callback (fired
from ansnd's own audio DMA interrupt, not the main thread) that sets
a flag audioStreamDolphinIsFinished() reads.

Links libansnd (new for this project) and wires duskdolphin/audio
into the CMake build for the first time.

Verified via the dusk-dolphin toolchain: both GameCube and Wii
targets compile and link cleanly. Runtime verification in headless
Dolphin was inconclusive - confirmed via an A/B test against a
pre-audio baseline build that a pre-existing MMIO flood (unrelated
to this change, reproduces identically with zero audio code present)
keeps the harness from reaching the game's own steady state before
timing out.
This commit is contained in:
2026-08-31 10:53:22 -05:00
parent af8c0f5ccd
commit 0f4ee5d965
6 changed files with 128 additions and 0 deletions
@@ -6,3 +6,94 @@
*/
#include "audiostreamdolphin.h"
#include "audio/audiostream.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <ansndlib.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);
// 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;
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 * (pan > 0 ? (1.0f - pan) : 1.0f);
config.right_volume = baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f);
config.frame_data_ptr = (u32) stream->data;
config.frame_count = (u32) (stream->dataSize / frameSize);
config.voice_callback = audioStreamDolphinVoiceCallback;
config.stream_callback = NULL; // single-buffer playback
config.user_pointer = stream;
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;
}
}