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:
@@ -61,6 +61,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
|||||||
zstd
|
zstd
|
||||||
z
|
z
|
||||||
lzma
|
lzma
|
||||||
|
ansnd
|
||||||
)
|
)
|
||||||
|
|
||||||
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
|||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(asset)
|
add_subdirectory(asset)
|
||||||
|
add_subdirectory(audio)
|
||||||
add_subdirectory(log)
|
add_subdirectory(log)
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(input)
|
add_subdirectory(input)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -6,8 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "audiodolphin.h"
|
#include "audiodolphin.h"
|
||||||
|
#include <ansndlib.h>
|
||||||
|
|
||||||
errorret_t audioDolphinInit() {
|
errorret_t audioDolphinInit() {
|
||||||
|
ansnd_initialize();
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,5 +19,7 @@ errorret_t audioDolphinUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t audioDolphinDispose() {
|
errorret_t audioDolphinDispose() {
|
||||||
|
ansnd_uninitialize();
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,94 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "audiostreamdolphin.h"
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,14 @@
|
|||||||
typedef struct audiostream_s audiostream_t;
|
typedef struct audiostream_s audiostream_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
// Allocated ansnd voice id.
|
||||||
int32_t voiceId;
|
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;
|
} audiostreamdolphin_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,3 +56,15 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream);
|
|||||||
* @return true if playback has finished, false otherwise.
|
* @return true if playback has finished, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool_t audioStreamDolphinIsFinished(audiostream_t *stream);
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user