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);