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:
@@ -10,17 +10,14 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
audiostreamlinux.c
|
||||
)
|
||||
|
||||
# No hardware MP3 decoder on Linux - use the shared libmad-based software
|
||||
# backend (also used by Dolphin), 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. See
|
||||
# audiostreammp3decodersw.h's own comment on why libmad (GPL-licensed)
|
||||
# replaced this project's original minimp3-based implementation.
|
||||
# No hardware MP3 decoder on Linux - uses the shared libmad-based software
|
||||
# backend + decode-ahead ring in src/duskmad instead (also used by
|
||||
# Dolphin). See audiostreammp3decodersw.h's own comment on why libmad
|
||||
# (GPL-licensed) replaced this project's original minimp3-based
|
||||
# implementation. Linking stays here (rather than in src/duskmad itself)
|
||||
# since Linux and Dolphin genuinely obtain libmad differently -
|
||||
# find_package() here, the bundled devkitPPC/libogc toolchain copy there.
|
||||
if(NOT libmad_FOUND)
|
||||
find_package(libmad REQUIRED)
|
||||
endif()
|
||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC libmad::mad)
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
"${DUSK_SOURCES_DIR}/dusk/audio/stream/audiostreammp3decodersw.c"
|
||||
)
|
||||
|
||||
@@ -39,12 +39,20 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream) {
|
||||
errorThrow("Failed to open SDL2 audio device: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
errorChain(audioStreamMp3RingInitIfNeeded(stream));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamLinuxDispose(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 SDL cleanup below.
|
||||
errorChain(audioStreamMp3RingDisposeIfNeeded(stream));
|
||||
|
||||
SDL_CloseAudioDevice(stream->platform.device);
|
||||
|
||||
errorOk();
|
||||
@@ -63,17 +71,11 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
stream->startFrame = 0;
|
||||
stream->seeking = false;
|
||||
|
||||
size_t endFrame = totalFrames;
|
||||
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
|
||||
endFrame = mathMin(
|
||||
(size_t) (stream->loopStart * stream->sampleRate), 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.
|
||||
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);
|
||||
|
||||
// Only an explicit seek discards whatever's still queued and jumps -
|
||||
// a natural loop restart deliberately leaves the previous pass's tail
|
||||
@@ -84,6 +86,17 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
if(seeking) {
|
||||
SDL_ClearQueuedAudio(stream->platform.device);
|
||||
}
|
||||
|
||||
// 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. Unlike the
|
||||
// SDL_ClearQueuedAudio() call above, this is unconditional regardless of
|
||||
// the seeking flag: that flag only controls whether the SDL queue's
|
||||
// leftover tail is discarded (for gapless natural loop restarts), not
|
||||
// whether decode needs to re-seek - MP3 has no cheap seek either way,
|
||||
// see audioStreamSeek()'s own comment.
|
||||
errorChain(audioStreamMp3RingStopIfNeeded(stream));
|
||||
|
||||
errorChain(audioStreamSeek(stream, startFrame));
|
||||
|
||||
stream->platform.position = startFrame;
|
||||
@@ -91,6 +104,11 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
stream->platform.passStartTicks = SDL_GetTicks();
|
||||
stream->platform.passStartPosition = startFrame;
|
||||
|
||||
// Launch this pass's decode thread now that position/endFrame are
|
||||
// finalized - it starts filling the ring immediately in the background,
|
||||
// ahead of audioStreamLinuxFeed() below even asking for anything.
|
||||
errorChain(audioStreamMp3RingStartIfNeeded(stream, startFrame, endFrame));
|
||||
|
||||
errorChain(audioStreamLinuxFeed(stream));
|
||||
|
||||
SDL_PauseAudioDevice(stream->platform.device, 0);
|
||||
@@ -150,11 +168,25 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
|
||||
|
||||
int16_t *chunk = memoryAllocate(framesToRead * frameSize);
|
||||
size_t framesRead = 0;
|
||||
errorret_t ret = audioStreamRead(stream, chunk, 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().
|
||||
errorret_t ret = audioStreamReadForPlayback(
|
||||
stream, chunk, framesToRead, &framesRead, ¬ReadyYet
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(chunk);
|
||||
errorChain(ret);
|
||||
}
|
||||
if(notReadyYet) {
|
||||
// Decode hasn't caught up to a full window yet - not an error, just
|
||||
// try again next poll.
|
||||
memoryFree(chunk);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
const size_t bytesRead = framesRead * frameSize;
|
||||
int queued;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "audiostreammp3ring.h"
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
@@ -31,11 +32,19 @@ typedef struct {
|
||||
// SDL_GetQueuedAudioSize() alone).
|
||||
Uint32 passStartTicks;
|
||||
size_t passStartPosition;
|
||||
|
||||
// Background decode-ahead ring, shared with Dolphin - 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;
|
||||
} audiostreamlinux_t;
|
||||
|
||||
/**
|
||||
* Initializes the Linux-specific playback state of an audio stream by
|
||||
* opening an SDL2 audio device matching the stream's PCM format.
|
||||
* opening an SDL2 audio device matching the stream's PCM format. 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.
|
||||
@@ -43,8 +52,10 @@ typedef struct {
|
||||
errorret_t audioStreamLinuxInit(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Disposes the Linux-specific playback state of an audio stream, closing
|
||||
* its SDL2 audio device.
|
||||
* Disposes the Linux-specific playback state of an audio stream: 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 closes the SDL2 audio device.
|
||||
*
|
||||
* @param stream The audio stream to dispose.
|
||||
* @return Error state if any.
|
||||
@@ -55,8 +66,14 @@ errorret_t audioStreamLinuxDispose(audiostream_t *stream);
|
||||
* Starts a new playback pass: seeks the stream's PCM read cursor to
|
||||
* stream->startFrame (clearing the SDL queue first if this is an explicit
|
||||
* seek rather than a natural loop restart - see stream->seeking's own
|
||||
* comment), determines this pass's loop-segment end, and queues the first
|
||||
* window via audioStreamLinuxFeed().
|
||||
* comment), determines this pass's loop-segment end. 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
|
||||
* queues the first window via audioStreamLinuxFeed() - for MP3 streams
|
||||
* this may find the ring still empty and queue 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.
|
||||
@@ -69,6 +86,10 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream);
|
||||
* advancing platform.position by however many frames were actually read.
|
||||
* A no-op once 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.
|
||||
*
|
||||
* Kept as a small, bounded read/queue operation (rather than the whole
|
||||
* pass at once) specifically so a long clip is never fully resident in
|
||||
* memory at once - called both by audioStreamLinuxBuffer() (the first
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
// header's own documentation for the actual struct/interface, including
|
||||
// why libmad (GPL-licensed) was chosen over the MIT-licensed minimp3 this
|
||||
// used to be.
|
||||
#include "audio/stream/audiostreammp3decodersw.h"
|
||||
#include "audiostreammp3decodersw.h"
|
||||
|
||||
Reference in New Issue
Block a user