/** * 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 #include #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); 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; } }