Rewrite Dolphin audio to use libogc's standard ASND API

Replaces the ansnd-based backend (a third-party library with a custom
DSP ucode Dolphin's HLE audio emulation doesn't recognize, requiring
-a LLE as a workaround) with libogc's own asndlib - the standard,
HLE-supported audio API most GC/Wii homebrew already uses.

Also fixes three real bugs found while getting a large (41MB) WAV
asset actually playing on Dolphin:

- audiostreamdolphin.c previously buffered a stream's entire loop
  segment into one allocation up front (inherited from the ansnd
  design) - fine for a short test tone, but an out-of-memory crash for
  a real multi-minute track on GameCube's 24MB (or even Wii's 88MB)
  total RAM. Rewritten to stream bounded windows via ASND's real
  double-buffer primitives (ASND_SetVoice + ASND_AddVoice, polled from
  the main thread - never from an interrupt callback), matching how
  Linux/PSP already work.

- ASND_TestVoiceBufferReady() returns SND_OK (0) when ready and
  SND_BUSY (1) when not, in the actual toolchain fork
  (extremscorner/libogc2) this project builds against - opposite of a
  plain boolean. Treating the raw result as one meant a genuinely
  ready voice's SND_OK read as false, silently skipping every
  ASND_AddVoice() call forever after a stream's first buffered window.

- audioStreamPcmRead()'s 16-bit fast path read WAV's little-endian
  sample bytes straight into the output buffer with no byte-swap -
  correct by coincidence on every little-endian platform this engine
  targets (Linux, PSP), but reversed every sample's bytes on
  Dolphin/GameCube/Wii's big-endian PowerPC.

libmad is GPL-licensed (see audiostreammp3decodersw.h's own note from
the prior MP3 change); libogc's asndlib is under a permissive
BSD-style license, so this doesn't add another such dependency.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-02 12:49:53 -05:00
co-authored by Claude Sonnet 5
parent fd5bcfae90
commit fc1fc9f170
7 changed files with 248 additions and 167 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
zstd
z
lzma
ansnd
asnd
mad
)
+1 -1
View File
@@ -134,7 +134,7 @@ typedef struct audiostream_s {
audiostreammp3_t mp3;
};
// Platform-specific playback state (e.g. SDL2 device, PSP channel, ansnd
// Platform-specific playback state (e.g. SDL2 device, PSP channel, ASND
// voice). Defined by each platform's audiostreamplatform.h.
audiostreamplatform_t platform;
} audiostream_t;
+11 -1
View File
@@ -10,6 +10,7 @@
#include "assert/assert.h"
#include "util/math.h"
#include "util/memory.h"
#include "util/endian.h"
errorret_t audioStreamPcmInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
@@ -131,10 +132,19 @@ errorret_t audioStreamPcmRead(
}
if(sourceSampleBytes == sizeof(int16_t)) {
// Output format already matches the source - read straight through.
// Output format already matches the source in width, but WAV samples
// are always little-endian per the format spec regardless of host -
// read straight through, then fix up byte order in place. A no-op on
// every little-endian platform (endianLittleToHost16() compiles down
// to returning its argument unchanged there) - only actually swaps
// anything on a big-endian host (Dolphin/GameCube/Wii).
errorChain(assetFileRead(
&stream->pcm.file, buffer, framesToRead * sourceFrameSize
));
const size_t sampleCount = framesToRead * channels;
for(size_t i = 0; i < sampleCount; i++) {
buffer[i] = (int16_t) endianLittleToHost16((uint16_t) buffer[i]);
}
} else {
// 24-bit source (the only other width assetWavParseHeader() accepts) -
// read the raw 3-byte samples into a scratch buffer, then truncate
+6 -3
View File
@@ -6,10 +6,13 @@
*/
#include "audiodolphin.h"
#include <ansndlib.h>
#include <asndlib.h>
errorret_t audioDolphinInit() {
ansnd_initialize();
ASND_Init();
// ASND_Init() starts the sound engine paused - unpause immediately so
// voices actually play once configured.
ASND_Pause(0);
errorOk();
}
@@ -19,7 +22,7 @@ errorret_t audioDolphinUpdate() {
}
errorret_t audioDolphinDispose() {
ansnd_uninitialize();
ASND_End();
errorOk();
}
+141 -116
View File
@@ -10,23 +10,30 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include <ansndlib.h>
#include <ogc/cache.h>
#include <ogc/system.h>
#include <asndlib.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)
);
const s32 voiceId = ASND_GetFirstUnusedVoice();
if(voiceId == SND_INVALID) {
errorThrow("No free ASND voice available.");
}
const size_t frameSize = stream->channels * sizeof(int16_t);
const size_t rawBytes = AUDIO_DOLPHIN_WINDOW_FRAMES * frameSize;
const size_t alignedBytes = (rawBytes + 31) & ~((size_t) 31);
stream->platform.voiceId = voiceId;
stream->platform.finished = false;
stream->platform.buffer = NULL;
stream->platform.bufferCapacityBytes = alignedBytes;
stream->platform.buffer[0] = (uint8_t *) memoryAlign(32, alignedBytes);
stream->platform.buffer[1] = (uint8_t *) memoryAlign(32, alignedBytes);
stream->platform.nextBufferIndex = 0;
stream->platform.primed = false;
stream->platform.position = 0;
stream->platform.endFrame = 0;
stream->platform.volumeLeft = 0;
stream->platform.volumeRight = 0;
errorOk();
}
@@ -34,12 +41,9 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream) {
errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
ansnd_deallocate_voice((u32) stream->platform.voiceId);
if(stream->platform.buffer != NULL) {
memoryFree(stream->platform.buffer);
stream->platform.buffer = NULL;
}
ASND_StopVoice(stream->platform.voiceId);
memoryFree(stream->platform.buffer[0]);
memoryFree(stream->platform.buffer[1]);
errorOk();
}
@@ -47,121 +51,143 @@ errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
const size_t frameSize = stream->channels * sizeof(int16_t);
const size_t totalFrames = audioStreamGetTotalFrames(stream);
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) the DSP wraps within, once looping is enabled -
// defaulting to the whole clip (loopStart == -1, loopTo == 0) so
// behaviour is unchanged when no explicit loop points are configured.
// Same math as PSP/Linux - see their own comments.
const size_t loopEndFrame = stream->loopStart >= 0
? mathMin((size_t) (stream->loopStart * stream->sampleRate), totalFrames)
: totalFrames;
const size_t loopToFrame = mathMin(
(size_t) (stream->loopTo * stream->sampleRate), loopEndFrame
// endFrame is this pass's stop point - the current loop segment's end,
// or the true end of the clip if not looping. Same math as PSP/Linux -
// see their own comments.
size_t endFrame = totalFrames;
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
endFrame = mathMin(
(size_t) (stream->loopStart * stream->sampleRate), totalFrames
);
}
const size_t startFrame = mathMin(stream->startFrame, totalFrames);
stream->startFrame = 0;
stream->seeking = false;
// A seek can legitimately land past the loop segment's end (e.g. into an
// outro) - buffer out to the true end of the clip in that case, same as
// PSP/Linux, instead of only ever buffering the loop segment.
const size_t bufferFrames = startFrame < loopEndFrame ? loopEndFrame : totalFrames;
// A seek (or a loop restart landing exactly on the loop end) can put
// startFrame at or past endFrame - e.g. seeking into an outro after the
// loop point. Play out to the true end of the buffer once instead, same
// as PSP/Linux.
if(startFrame >= endFrame) endFrame = totalFrames;
float_t leftFactor, rightFactor;
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
const float_t baseVolume = (float_t) stream->volume / 255.0f;
stream->platform.volumeLeft = (s32) mathClamp(baseVolume * leftFactor * MAX_VOLUME, 0, MAX_VOLUME);
stream->platform.volumeRight = (s32) mathClamp(baseVolume * rightFactor * MAX_VOLUME, 0, MAX_VOLUME);
stream->platform.finished = false;
// A fresh pass discards whatever windows were previously queued (both
// ASND buffer slots) rather than letting it play out - a natural loop
// restart costs a small gap here (unlike Linux, which deliberately
// leaves its own tail queued for gapless looping); acceptable for now,
// see this backend's own history of iterating on click/gap reduction
// once real hardware confirms it's actually audible.
ASND_StopVoice(stream->platform.voiceId);
stream->platform.nextBufferIndex = 0;
stream->platform.primed = false;
// ansnd's hardware loop needs one contiguous buffer spanning everything
// the voice might play - both start_offset and the loop segment address
// the same frame_data_ptr - so (unlike PSP/Linux, which stream bounded
// windows on demand) this reads the whole segment up front. Still
// bounded to the current loop segment's length rather than the whole
// clip if the clip is longer than one loop - see this struct field's
// own comment. Re-read fresh every Buffer() call.
if(stream->platform.buffer != NULL) {
memoryFree(stream->platform.buffer);
stream->platform.buffer = NULL;
errorChain(audioStreamSeek(stream, startFrame));
stream->platform.position = startFrame;
stream->platform.endFrame = endFrame;
// Prime only the first buffer here - the second window can't be queued
// back-to-back with this one, since ASND_SetVoice() leaves VOICE_UPDATE
// set until its own audio-DMA-interrupt tick picks it up, and
// ASND_AddVoice() refuses to queue anything while that's still pending
// (see audioStreamDolphinFeed()'s own comment). Later polls queue
// everything after this.
errorChain(audioStreamDolphinFeed(stream));
errorOk();
}
errorChain(audioStreamSeek(stream, 0));
uint8_t *buffer = (uint8_t *) memoryAllocate(bufferFrames * frameSize);
size_t framesRead = 0;
errorret_t readRet = audioStreamRead(
stream, (int16_t *) buffer, bufferFrames, &framesRead
);
if(errorIsNotOk(readRet)) {
memoryFree(buffer);
errorChain(readRet);
}
if(framesRead < bufferFrames) {
// The asset is shorter than its declared header size (corrupt or
// truncated) - pad what's missing with silence.
memoryZero(
buffer + (framesRead * frameSize), (bufferFrames - framesRead) * frameSize
);
}
stream->platform.buffer = buffer;
errorret_t audioStreamDolphinFeed(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
ansnd_pcm_voice_config_t config;
memoryZero(&config, sizeof(ansnd_pcm_voice_config_t));
config.samplerate = stream->sampleRate;
config.format = ANSND_VOICE_PCM_FORMAT_SIGNED_16_PCM;
config.channels = stream->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(buffer, bufferFrames * frameSize);
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(buffer);
config.frame_count = (u32) bufferFrames;
config.start_offset = (u32) startFrame;
config.voice_callback = audioStreamDolphinVoiceCallback;
config.stream_callback = NULL; // single-buffer playback
config.user_pointer = stream;
if(stream->platform.position >= stream->platform.endFrame) {
errorOk();
}
// 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).
// The very first window of a pass always has room (nothing's queued
// yet) - every window after that needs an actual free slot, which only
// opens up once ASND has finished consuming the previous "primary"
// buffer and promoted the "secondary" one in its place. This is only a
// fast-path skip, not a full guarantee ASND_AddVoice() will actually
// succeed below - see that call's own comment.
//
// 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) loopToFrame;
config.loop_end_offset = (u32) loopEndFrame - 1;
// ASND_TestVoiceBufferReady() returns SND_OK (0) when ready and
// SND_BUSY (1) when not - opposite of a plain boolean. Comparing
// explicitly against SND_OK matters: treating the raw result as a bool
// reads a genuinely-ready voice's SND_OK (0) as false/not-ready,
// silently skipping every ASND_AddVoice() call forever after the first
// window - confirmed the hard way (only ever heard a clip's first
// buffered window, nothing ever played after it).
const bool_t ready = ASND_TestVoiceBufferReady(stream->platform.voiceId) == SND_OK;
if(stream->platform.primed && !ready) {
errorOk();
}
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)
const size_t frameSize = stream->channels * sizeof(int16_t);
const size_t framesRemaining = stream->platform.endFrame - stream->platform.position;
const size_t framesToRead = mathMin(
(size_t) AUDIO_DOLPHIN_WINDOW_FRAMES, framesRemaining
);
uint8_t *buffer = stream->platform.buffer[stream->platform.nextBufferIndex];
size_t framesRead = 0;
errorChain(audioStreamRead(
stream, (int16_t *) buffer, framesToRead, &framesRead
));
const size_t bytesRead = framesRead * frameSize;
const size_t alignedBytes = (bytesRead + 31) & ~((size_t) 31);
if(alignedBytes > bytesRead) {
// Pad up to ASND's required 32-byte alignment - only matters for
// whatever the final (possibly short) window of a pass is, since
// every full window is already a multiple of 32 bytes by
// construction (bufferCapacityBytes itself is pre-aligned).
memoryZero(buffer + bytesRead, alignedBytes - bytesRead);
}
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)
if(!stream->platform.primed) {
const s32 format = stream->channels == 1 ? VOICE_MONO_16BIT : VOICE_STEREO_16BIT;
const s32 pitch = (s32) stream->sampleRate;
const s32 result = ASND_SetVoice(
stream->platform.voiceId, format, pitch, 0,
buffer, (s32) alignedBytes,
stream->platform.volumeLeft, stream->platform.volumeRight, NULL
);
if(result != SND_OK) {
errorThrow("Failed to set ASND voice: %d", result);
}
stream->platform.primed = true;
} else {
// ASND_SetVoice() leaves VOICE_UPDATE set in this voice's flags until
// ASND's own audio-DMA-interrupt tick actually picks up the change -
// ASND_AddVoice() refuses to queue anything while that (or its own
// VOICE_UPDATEADD, set the same way on a successful call here) is
// still pending, returning SND_INVALID rather than SND_BUSY for it
// despite this being a purely transient, expected condition, not a
// real usage error. ASND_TestVoiceBufferReady() above doesn't catch
// this - it only checks whether the buffer slot itself is empty, a
// completely separate piece of state from these flags - so treat
// *any* non-OK result here as "not ready yet, retry on the next
// poll" rather than a hard failure: don't advance position or
// rotate the buffer index, just leave this same window to be reread
// and requeued next time.
const s32 result = ASND_AddVoice(
stream->platform.voiceId, buffer, (s32) alignedBytes
);
if(result != SND_OK) {
errorOk();
}
}
stream->platform.position += framesRead;
stream->platform.nextBufferIndex = 1 - stream->platform.nextBufferIndex;
errorOk();
}
@@ -169,17 +195,16 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
bool_t audioStreamDolphinIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
return stream->platform.finished;
if(stream->platform.position < stream->platform.endFrame) {
errorret_t ret = audioStreamDolphinFeed(stream);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return true; // Can't recover - let the shared layer end/loop it.
}
return false;
}
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;
}
// Nothing left to read for this pass - finished once ASND has actually
// drained both buffer slots too, not just when we stopped feeding it.
return ASND_StatusVoice(stream->platform.voiceId) == SND_UNUSED;
}
+82 -39
View File
@@ -10,33 +10,66 @@
typedef struct audiostream_s audiostream_t;
// How many frames of PCM each of the two rotating buffers holds - just
// needs to be comfortably longer than one Update() tick's worth of real
// time, with margin, so polling once per frame never lets ASND run dry.
// Sized in frames (not bytes) to match PSP/Linux's own window constants;
// at 44100Hz mono this is ~371ms per buffer, ~743ms of total lookahead
// across both. Deliberately NOT "read the whole segment" (this backend's
// original design, inherited from its ansnd-based predecessor) - that
// allocated size scaled with asset length and crashed GameCube/Wii's
// limited RAM (24MB/88MB total) outright on a real multi-minute WAV.
#define AUDIO_DOLPHIN_WINDOW_FRAMES 16384
typedef struct {
// Allocated ansnd voice id.
// Allocated ASND voice slot (0-15, see ASND_GetFirstUnusedVoice()).
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;
// Two fixed-size, 32-byte-aligned scratch buffers (ASND requires this
// alignment), allocated once in Init() and reused for the stream's
// whole lifetime - never resized/reallocated per pass, unlike this
// backend's previous whole-segment-per-Buffer()-call design. Rotated
// as a classic double buffer: one plays via ASND_SetVoice() (the
// "primary" slot), the other is queued ahead via ASND_AddVoice() (the
// "secondary" slot) - see audioStreamDolphinFeed()'s own comment for
// the exact protocol.
uint8_t *buffer[2];
// ansnd's DSP hardware loop (loop_start_offset/loop_end_offset) needs one
// contiguous physical buffer spanning the whole segment it may play -
// both the pre-loop lead-in and the loop segment itself, since the DSP
// addresses both as offsets into the same frame_data_ptr. Unlike
// PSP/Linux (which stream bounded windows on demand), a Dolphin voice
// can't be handed data incrementally mid-playback, so this buffer is
// read from the asset and owned here for the lifetime of one pass -
// bounded to the current loop segment's length (not the whole clip, if
// the clip is longer than one loop), freed/reallocated on each
// audioStreamDolphinBuffer() call and released for good in
// audioStreamDolphinDispose().
uint8_t *buffer;
// Byte capacity of each buffer[] slot - fixed at Init() from this
// stream's channel count (frames * channels * sizeof(int16_t), padded
// to 32 bytes).
size_t bufferCapacityBytes;
// Which buffer[] slot audioStreamDolphinFeed() fills next - alternates
// between 0 and 1 every time it successfully hands one to ASND.
size_t nextBufferIndex;
// Set once audioStreamDolphinBuffer() has handed the very first window
// of a pass to ASND_SetVoice() - every window after that goes through
// ASND_AddVoice() instead, which requires an already-active voice.
// Cleared at the start of every new pass.
bool_t primed;
// This pass's own read cursor and stop point, in frames relative to
// the start of the decoded stream - mirrors audiostreamlinux_t.position/
// endFrame exactly (this backend now streams bounded windows on demand
// just like Linux/PSP, rather than buffering a whole segment up front).
size_t position;
size_t endFrame;
// This pass's per-voice volume, resolved once in audioStreamDolphinBuffer()
// (pan/volume are only ever set via ASND_SetVoice(), not per-window
// ASND_AddVoice() calls) and reused by every audioStreamDolphinFeed()
// call for that pass's first (ASND_SetVoice()) window.
int32_t volumeLeft;
int32_t volumeRight;
} audiostreamdolphin_t;
/**
* Initializes the GameCube/Wii-specific playback state of an audio stream
* (e.g. allocating a voice via AESND_AllocateVoice/ansnd equivalent).
* Initializes the GameCube/Wii-specific playback state of an audio stream:
* allocates an ASND voice slot (ASND_GetFirstUnusedVoice()) and this
* stream's two fixed-size double-buffer windows (sized from its channel
* count, AUDIO_DOLPHIN_WINDOW_FRAMES frames each).
*
* @param stream The audio stream to initialize.
* @return Error state if any.
@@ -44,8 +77,9 @@ typedef struct {
errorret_t audioStreamDolphinInit(audiostream_t *stream);
/**
* Disposes the GameCube/Wii-specific playback state of an audio stream,
* releasing its voice.
* Disposes the GameCube/Wii-specific playback state of an audio stream:
* stops its voice (releasing the slot back to ASND_GetFirstUnusedVoice())
* and frees its two buffers.
*
* @param stream The audio stream to dispose.
* @return Error state if any.
@@ -53,9 +87,13 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream);
errorret_t audioStreamDolphinDispose(audiostream_t *stream);
/**
* Reads the current loop segment's PCM data from the stream's asset (see
* audiostreamdolphin_t.buffer's own comment) and sends it to its
* allocated voice.
* Starts a new playback pass: seeks the stream's decode position to
* stream->startFrame, determines this pass's loop-segment end, resolves
* this pass's volume/pan, stops whatever the voice was doing before (so a
* fresh pass never plays out stale queued windows from the last one), and
* primes it via audioStreamDolphinFeed() - once for the first
* (ASND_SetVoice()) window, once more to also queue the second
* (ASND_AddVoice()) window ahead of time.
*
* @param stream The audio stream to output.
* @return Error state if any.
@@ -63,22 +101,27 @@ errorret_t audioStreamDolphinDispose(audiostream_t *stream);
errorret_t audioStreamDolphinBuffer(audiostream_t *stream);
/**
* Checks whether the stream's voice has finished playing its currently
* buffered data.
* Reads and queues one bounded window (AUDIO_DOLPHIN_WINDOW_FRAMES) of
* PCM data, if (and only if) ASND's voice actually has room for another
* one right now: the very first call of a pass always does (there's
* nothing queued yet - this hands it to ASND_SetVoice()), and every call
* after that only proceeds once ASND_TestVoiceBufferReady() confirms the
* voice's secondary buffer slot has been freed (handed to
* ASND_AddVoice()). A no-op once this pass's platform.position has
* reached platform.endFrame.
*
* @param stream The audio stream to feed.
* @return Error state if any.
*/
errorret_t audioStreamDolphinFeed(audiostream_t *stream);
/**
* Checks whether the stream's voice has finished playing all of this
* pass's data. As a side effect, tops up the voice's buffers (via
* audioStreamDolphinFeed()) whenever there's room and more of the current
* pass left to read, rather than reporting finished early.
*
* @param stream The audio stream to check.
* @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);
@@ -9,7 +9,7 @@
// GameCube/Wii have no usable hardware MP3 decoder for this engine's
// architecture (libogc's MP3Player wraps libmad but drives its own
// internal audio output, bypassing the ansnd-based pipeline
// internal audio output, bypassing the ASND-based pipeline
// audiostreamdolphin.c already owns - not a fit here), so Dolphin uses the
// same shared libmad-based software backend as Linux, driven directly at
// the mad_stream/mad_frame/mad_synth level instead of through MP3Player -