Add Dolphin MP3 decode-thread ring, share it with Linux, dedupe common code
Fixes the main-thread hitch reported on Dolphin (and, less severely, elsewhere) whenever the MP3 decode buffer needed refilling: audioStreamMp3Read() -> audioStreamMp3DecoderDecodeFrame() does real work synchronously (asset I/O + libmad decode), previously called directly from each platform's once-per-frame Feed(), blocking rendering for a frame on every refill. Adds a background decode-ahead ring (src/duskmad/audiostreammp3ring.c): a one-shot-per-pass thread reads via the existing audioStreamRead() into a ring buffer ahead of need; Feed() becomes a cheap, lock-protected drain instead of a decode call. Reuses this project's own thread_t/threadmutex_t (mutex+condvar) primitives, mirroring PSP's own background-thread-plus-ring precedent. Fixes a real (if narrow) race in thread.c along the way: threadHandler() reset thread->threadId outside the mutex it also used to signal STOPPED, which a stop-then-immediately-restart pattern (needed once per pass: play/seek/loop-restart) could hit as a stale-threadId assertion. Initially built Dolphin-only, then generalized: the ring doesn't touch ASND at all, so it was straightforward to share with Linux too, moving both it and the libmad decoder into a new top-level src/duskmad/ - a shared-capability directory in the same vein as src/duskgl or src/dusknetwork, pulled in by whichever DUSK_TARGET_SYSTEM values need it (linux/knulli, wii/gamecube) rather than PSP, which keeps its own hardware sceMp3 decoder and ring untouched. With both platforms now sharing real code, deduped what was left: audioStreamComputeEndFrame() (loop-segment math, byte-identical between Linux/Dolphin) moved to the generic audio/stream/audiostream.c, and audioStreamMp3Ring*IfNeeded()/audioStreamReadForPlayback() wrappers (in duskmad) collapse each platform's own `if(stream->type == AUDIO_STREAM_TYPE_MP3)` branches at every Init/Dispose/Buffer/Feed call site into one check each, living in the ring module itself. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
fc1fc9f170
commit
b564d0b1c1
17 files changed
+822
-70
No files matched your search
@@ -11,15 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
)
|
||||
|
||||
# No hardware MP3 decoder used on Dolphin (see audiostreammp3decoder.h's
|
||||
# own comment on why libogc's MP3Player wrapper isn't a fit) - use the
|
||||
# same shared libmad-based software backend as Linux, sourced directly
|
||||
# from src/dusk/audio rather than through its own unconditional
|
||||
# CMakeLists.txt so platforms with a hardware decoder (PSP) never pull
|
||||
# libmad in at all. Unlike Linux, libmad here comes bundled with the
|
||||
# devkitPPC/libogc toolchain itself (see cmake/targets/dolphin.cmake's
|
||||
# own "mad" link entry) rather than via find_package() - unnecessary
|
||||
# for, and not reliable when cross-compiling.
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
"${DUSK_SOURCES_DIR}/dusk/audio/stream/audiostreammp3decodersw.c"
|
||||
)
|
||||
# own comment on why libogc's MP3Player wrapper isn't a fit) - uses the
|
||||
# shared libmad-based software backend + decode-ahead ring in
|
||||
# src/duskmad instead (also used by Linux), see that directory's own
|
||||
# CMakeLists.txt for how it's sourced/linked.
|
||||
@@ -35,12 +35,21 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream) {
|
||||
stream->platform.volumeLeft = 0;
|
||||
stream->platform.volumeRight = 0;
|
||||
|
||||
errorChain(audioStreamMp3RingInitIfNeeded(stream));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
// Must happen first: audioStreamMp3Dispose() (called right after this
|
||||
// function returns, from the shared audioStreamDispose()) tears down
|
||||
// stream->mp3's decoder/file - the decode thread must be fully parked
|
||||
// before that, not just before this function's own ASND/buffer cleanup
|
||||
// below.
|
||||
errorChain(audioStreamMp3RingDisposeIfNeeded(stream));
|
||||
|
||||
ASND_StopVoice(stream->platform.voiceId);
|
||||
memoryFree(stream->platform.buffer[0]);
|
||||
memoryFree(stream->platform.buffer[1]);
|
||||
@@ -53,25 +62,15 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
||||
|
||||
const size_t totalFrames = audioStreamGetTotalFrames(stream);
|
||||
|
||||
// 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 (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;
|
||||
// This pass's stop point - the current loop segment's end, or the true
|
||||
// end of the clip if not looping (or if startFrame already landed past
|
||||
// the loop end, e.g. a seek into an outro). See
|
||||
// audioStreamComputeEndFrame()'s own comment.
|
||||
const size_t endFrame = audioStreamComputeEndFrame(stream, startFrame, totalFrames);
|
||||
|
||||
float_t leftFactor, rightFactor;
|
||||
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||
@@ -89,16 +88,32 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
||||
stream->platform.nextBufferIndex = 0;
|
||||
stream->platform.primed = false;
|
||||
|
||||
// For MP3 streams, the background decode thread reads via the same
|
||||
// stream->mp3 decoder/file audioStreamSeek() below is about to mutate -
|
||||
// must be fully stopped first, every time a new pass begins (initial
|
||||
// play, explicit seek, or loop restart), not just once at stream
|
||||
// teardown. Blocks until that's guaranteed and resets the ring to empty
|
||||
// for this fresh pass.
|
||||
errorChain(audioStreamMp3RingStopIfNeeded(stream));
|
||||
|
||||
errorChain(audioStreamSeek(stream, startFrame));
|
||||
stream->platform.position = startFrame;
|
||||
stream->platform.endFrame = endFrame;
|
||||
|
||||
// Launch this pass's decode thread now that position/endFrame are
|
||||
// finalized - it starts filling the ring immediately in the background,
|
||||
// ahead of audioStreamDolphinFeed() below even asking for anything.
|
||||
errorChain(audioStreamMp3RingStartIfNeeded(stream, startFrame, 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.
|
||||
// everything after this. For MP3 streams this first call will likely
|
||||
// find the ring still empty (the decode thread just started) and do
|
||||
// nothing - the next few audioStreamDolphinIsFinished() polls pick it
|
||||
// up once decode has caught up.
|
||||
errorChain(audioStreamDolphinFeed(stream));
|
||||
|
||||
errorOk();
|
||||
@@ -138,9 +153,20 @@ errorret_t audioStreamDolphinFeed(audiostream_t *stream) {
|
||||
|
||||
uint8_t *buffer = stream->platform.buffer[stream->platform.nextBufferIndex];
|
||||
size_t framesRead = 0;
|
||||
errorChain(audioStreamRead(
|
||||
stream, (int16_t *) buffer, framesToRead, &framesRead
|
||||
bool_t notReadyYet = false;
|
||||
// For MP3 streams, drains already-decoded PCM out of the background ring
|
||||
// instead of decoding directly - the expensive part (asset I/O + libmad
|
||||
// decode) already happened on the decode thread, off the main thread.
|
||||
// See audiostreammp3ring.h's own top-of-file comment for why. For
|
||||
// everything else, this is just a plain audioStreamRead().
|
||||
errorChain(audioStreamReadForPlayback(
|
||||
stream, (int16_t *) buffer, framesToRead, &framesRead, ¬ReadyYet
|
||||
));
|
||||
if(notReadyYet) {
|
||||
// Decode hasn't caught up to a full window yet - not an error, just
|
||||
// try again next poll.
|
||||
errorOk();
|
||||
}
|
||||
|
||||
const size_t bytesRead = framesRead * frameSize;
|
||||
const size_t alignedBytes = (bytesRead + 31) & ~((size_t) 31);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "audiostreammp3ring.h"
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
@@ -63,13 +64,20 @@ typedef struct {
|
||||
// call for that pass's first (ASND_SetVoice()) window.
|
||||
int32_t volumeLeft;
|
||||
int32_t volumeRight;
|
||||
|
||||
// Background decode-ahead ring, shared with Linux - only initialized/used
|
||||
// for AUDIO_STREAM_TYPE_MP3 streams (PCM/WAV reads are cheap plain
|
||||
// copies and don't need this). See audiostreammp3ring.h's own comment.
|
||||
audiostreammp3ring_t mp3Ring;
|
||||
} audiostreamdolphin_t;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* count, AUDIO_DOLPHIN_WINDOW_FRAMES frames each). For MP3 streams, also
|
||||
* initializes the background decode-ahead ring (audioStreamMp3RingInit()) -
|
||||
* see audiostreammp3ring.h's own comment.
|
||||
*
|
||||
* @param stream The audio stream to initialize.
|
||||
* @return Error state if any.
|
||||
@@ -78,8 +86,10 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* for MP3 streams, first fully stops and frees the background decode ring
|
||||
* (audioStreamMp3RingDispose()) - must happen before the shared MP3
|
||||
* layer tears down its decoder/file - then stops the 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.
|
||||
@@ -90,10 +100,14 @@ errorret_t audioStreamDolphinDispose(audiostream_t *stream);
|
||||
* 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.
|
||||
* fresh pass never plays out stale queued windows from the last one). For
|
||||
* MP3 streams, also stops the previous pass's background decode thread
|
||||
* (audioStreamMp3RingStop(), which must complete before the seek
|
||||
* below - see its own comment) and starts a fresh one for this pass
|
||||
* (audioStreamMp3RingStart()) once position/endFrame are finalized.
|
||||
* Finally primes the first window via audioStreamDolphinFeed() - for MP3
|
||||
* streams this may find the ring still empty and do nothing yet; the next
|
||||
* few polls pick it up once decode has caught up.
|
||||
*
|
||||
* @param stream The audio stream to output.
|
||||
* @return Error state if any.
|
||||
@@ -110,6 +124,10 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream);
|
||||
* ASND_AddVoice()). A no-op once this pass's platform.position has
|
||||
* reached platform.endFrame.
|
||||
*
|
||||
* For MP3 streams, the PCM data comes from draining the background decode
|
||||
* ring (audioStreamMp3RingDrain()) instead of decoding directly -
|
||||
* see audiostreammp3ring.h's own comment for why.
|
||||
*
|
||||
* @param stream The audio stream to feed.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
// see that header's own documentation for the actual struct/interface.
|
||||
// Conveniently, libmad ships as part of libogc itself (gc/mad.h) rather
|
||||
// than needing to be fetched separately, unlike on Linux.
|
||||
#include "audio/stream/audiostreammp3decodersw.h"
|
||||
#include "audiostreammp3decodersw.h"
|
||||
Reference in new issue
Block a user