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:
@@ -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;
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
@@ -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,139 +51,160 @@ 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();
|
||||
}
|
||||
|
||||
errorret_t audioStreamDolphinFeed(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
if(stream->platform.position >= stream->platform.endFrame) {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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 -
|
||||
|
||||
Reference in New Issue
Block a user