From 0f4ee5d965cdf873381cacc2b69f9b05ad9eb2dc Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 31 Aug 2026 10:53:22 -0500 Subject: [PATCH] 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. --- cmake/targets/dolphin.cmake | 1 + src/duskdolphin/CMakeLists.txt | 1 + src/duskdolphin/audio/CMakeLists.txt | 11 +++ src/duskdolphin/audio/audiodolphin.c | 5 ++ src/duskdolphin/audio/audiostreamdolphin.c | 91 ++++++++++++++++++++++ src/duskdolphin/audio/audiostreamdolphin.h | 19 +++++ 6 files changed, 128 insertions(+) create mode 100644 src/duskdolphin/audio/CMakeLists.txt diff --git a/cmake/targets/dolphin.cmake b/cmake/targets/dolphin.cmake index 98e342bf..5e84a6ec 100644 --- a/cmake/targets/dolphin.cmake +++ b/cmake/targets/dolphin.cmake @@ -61,6 +61,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE zstd z lzma + ansnd ) if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") diff --git a/src/duskdolphin/CMakeLists.txt b/src/duskdolphin/CMakeLists.txt index a5323674..9aa02ee2 100644 --- a/src/duskdolphin/CMakeLists.txt +++ b/src/duskdolphin/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME} # Subdirs add_subdirectory(asset) +add_subdirectory(audio) add_subdirectory(log) add_subdirectory(display) add_subdirectory(input) diff --git a/src/duskdolphin/audio/CMakeLists.txt b/src/duskdolphin/audio/CMakeLists.txt new file mode 100644 index 00000000..8df15097 --- /dev/null +++ b/src/duskdolphin/audio/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + audiodolphin.c + audiostreamdolphin.c +) diff --git a/src/duskdolphin/audio/audiodolphin.c b/src/duskdolphin/audio/audiodolphin.c index 103a7078..19a95b90 100644 --- a/src/duskdolphin/audio/audiodolphin.c +++ b/src/duskdolphin/audio/audiodolphin.c @@ -6,8 +6,11 @@ */ #include "audiodolphin.h" +#include errorret_t audioDolphinInit() { + ansnd_initialize(); + errorOk(); } @@ -16,5 +19,7 @@ errorret_t audioDolphinUpdate() { } errorret_t audioDolphinDispose() { + ansnd_uninitialize(); + errorOk(); } diff --git a/src/duskdolphin/audio/audiostreamdolphin.c b/src/duskdolphin/audio/audiostreamdolphin.c index fe7bded2..d161c829 100644 --- a/src/duskdolphin/audio/audiostreamdolphin.c +++ b/src/duskdolphin/audio/audiostreamdolphin.c @@ -6,3 +6,94 @@ */ #include "audiostreamdolphin.h" +#include "audio/audiostream.h" +#include "assert/assert.h" +#include "util/memory.h" +#include + +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; + } +} diff --git a/src/duskdolphin/audio/audiostreamdolphin.h b/src/duskdolphin/audio/audiostreamdolphin.h index eb339395..de38e983 100644 --- a/src/duskdolphin/audio/audiostreamdolphin.h +++ b/src/duskdolphin/audio/audiostreamdolphin.h @@ -11,7 +11,14 @@ typedef struct audiostream_s audiostream_t; typedef struct { + // Allocated ansnd voice id. int32_t voiceId; + + // Set by audioStreamDolphinVoiceCallback(), which ansnd invokes from its + // own audio DMA interrupt context. There is no polling API on the voice + // itself (unlike PSP/Linux), so this flag is how audioStreamDolphinIsFinished() + // answers without a callback of its own. + volatile bool_t finished; } audiostreamdolphin_t; /** @@ -49,3 +56,15 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream); * @return true if playback has finished, false otherwise. */ bool_t audioStreamDolphinIsFinished(audiostream_t *stream); + +/** + * ansnd voice state callback, registered on every voice by + * audioStreamDolphinBuffer(). Runs from ansnd's audio DMA interrupt context, + * not the main thread. Marks the stream (passed as userPointer) finished + * once its voice stops running. + * + * @param userPointer The audiostream_t this callback belongs to. + * @param voiceState The voice's new state, one of the ANSND_VOICE_STATE_* + * values. + */ +void audioStreamDolphinVoiceCallback(void *userPointer, int32_t voiceState);