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:
@@ -13,6 +13,7 @@ if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
|||||||
add_subdirectory(dusklinux)
|
add_subdirectory(dusklinux)
|
||||||
add_subdirectory(dusksdl2)
|
add_subdirectory(dusksdl2)
|
||||||
add_subdirectory(duskgl)
|
add_subdirectory(duskgl)
|
||||||
|
add_subdirectory(duskmad)
|
||||||
|
|
||||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
||||||
add_subdirectory(duskpsp)
|
add_subdirectory(duskpsp)
|
||||||
@@ -26,5 +27,6 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
|
|||||||
|
|
||||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||||
add_subdirectory(duskdolphin)
|
add_subdirectory(duskdolphin)
|
||||||
|
add_subdirectory(duskmad)
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "audiostream.h"
|
#include "audiostream.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
|
||||||
errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
|
errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
@@ -154,6 +155,24 @@ void audioStreamGetPanFactors(
|
|||||||
*outRight = pan < 0 ? (1.0f + pan) : 1.0f;
|
*outRight = pan < 0 ? (1.0f + pan) : 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t audioStreamComputeEndFrame(
|
||||||
|
const audiostream_t *stream,
|
||||||
|
const size_t startFrame,
|
||||||
|
const size_t totalFrames
|
||||||
|
) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
|
size_t endFrame = totalFrames;
|
||||||
|
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
|
||||||
|
endFrame = mathMin(
|
||||||
|
(size_t) (stream->loopStart * stream->sampleRate), totalFrames
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if(startFrame >= endFrame) endFrame = totalFrames;
|
||||||
|
|
||||||
|
return endFrame;
|
||||||
|
}
|
||||||
|
|
||||||
void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume) {
|
void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
stream->volume = volume;
|
stream->volume = volume;
|
||||||
|
|||||||
@@ -261,6 +261,27 @@ void audioStreamGetPanFactors(
|
|||||||
float_t *outRight
|
float_t *outRight
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the stop frame for a playback pass starting at startFrame: the
|
||||||
|
* current loop segment's end (stream->loopStart, converted to frames and
|
||||||
|
* clamped to totalFrames) if looping is enabled and a loop start is set,
|
||||||
|
* or totalFrames itself otherwise - including when startFrame has already
|
||||||
|
* reached or passed the computed loop end (e.g. a seek landing in an
|
||||||
|
* outro past the loop point), in which case this pass plays out to the
|
||||||
|
* true end of the stream once instead of only the loop segment.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to compute this for.
|
||||||
|
* @param startFrame This pass's starting frame offset.
|
||||||
|
* @param totalFrames The stream's total decoded frame count
|
||||||
|
* (audioStreamGetTotalFrames()).
|
||||||
|
* @return This pass's stop frame offset.
|
||||||
|
*/
|
||||||
|
size_t audioStreamComputeEndFrame(
|
||||||
|
const audiostream_t *stream,
|
||||||
|
const size_t startFrame,
|
||||||
|
const size_t totalFrames
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the playback volume of the given audio stream.
|
* Sets the playback volume of the given audio stream.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ bool_t threadShouldStop(thread_t *thread) {
|
|||||||
|
|
||||||
threadMutexLock(&thread->stateMutex);
|
threadMutexLock(&thread->stateMutex);
|
||||||
thread->state = THREAD_STATE_STOPPED;
|
thread->state = THREAD_STATE_STOPPED;
|
||||||
|
thread->threadId = 0;
|
||||||
threadMutexSignal(&thread->stateMutex);
|
threadMutexSignal(&thread->stateMutex);
|
||||||
threadMutexUnlock(&thread->stateMutex);
|
threadMutexUnlock(&thread->stateMutex);
|
||||||
thread->threadId = 0;
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -11,15 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
)
|
)
|
||||||
|
|
||||||
# No hardware MP3 decoder used on Dolphin (see audiostreammp3decoder.h's
|
# 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
|
# own comment on why libogc's MP3Player wrapper isn't a fit) - uses the
|
||||||
# same shared libmad-based software backend as Linux, sourced directly
|
# shared libmad-based software backend + decode-ahead ring in
|
||||||
# from src/dusk/audio rather than through its own unconditional
|
# src/duskmad instead (also used by Linux), see that directory's own
|
||||||
# CMakeLists.txt so platforms with a hardware decoder (PSP) never pull
|
# CMakeLists.txt for how it's sourced/linked.
|
||||||
# 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"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -35,12 +35,21 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream) {
|
|||||||
stream->platform.volumeLeft = 0;
|
stream->platform.volumeLeft = 0;
|
||||||
stream->platform.volumeRight = 0;
|
stream->platform.volumeRight = 0;
|
||||||
|
|
||||||
|
errorChain(audioStreamMp3RingInitIfNeeded(stream));
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
|
errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
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);
|
ASND_StopVoice(stream->platform.voiceId);
|
||||||
memoryFree(stream->platform.buffer[0]);
|
memoryFree(stream->platform.buffer[0]);
|
||||||
memoryFree(stream->platform.buffer[1]);
|
memoryFree(stream->platform.buffer[1]);
|
||||||
@@ -53,25 +62,15 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
|
|
||||||
const size_t totalFrames = audioStreamGetTotalFrames(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);
|
const size_t startFrame = mathMin(stream->startFrame, totalFrames);
|
||||||
stream->startFrame = 0;
|
stream->startFrame = 0;
|
||||||
stream->seeking = false;
|
stream->seeking = false;
|
||||||
|
|
||||||
// A seek (or a loop restart landing exactly on the loop end) can put
|
// This pass's stop point - the current loop segment's end, or the true
|
||||||
// startFrame at or past endFrame - e.g. seeking into an outro after the
|
// end of the clip if not looping (or if startFrame already landed past
|
||||||
// loop point. Play out to the true end of the buffer once instead, same
|
// the loop end, e.g. a seek into an outro). See
|
||||||
// as PSP/Linux.
|
// audioStreamComputeEndFrame()'s own comment.
|
||||||
if(startFrame >= endFrame) endFrame = totalFrames;
|
const size_t endFrame = audioStreamComputeEndFrame(stream, startFrame, totalFrames);
|
||||||
|
|
||||||
float_t leftFactor, rightFactor;
|
float_t leftFactor, rightFactor;
|
||||||
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||||
@@ -89,16 +88,32 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
stream->platform.nextBufferIndex = 0;
|
stream->platform.nextBufferIndex = 0;
|
||||||
stream->platform.primed = false;
|
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));
|
errorChain(audioStreamSeek(stream, startFrame));
|
||||||
stream->platform.position = startFrame;
|
stream->platform.position = startFrame;
|
||||||
stream->platform.endFrame = endFrame;
|
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
|
// 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
|
// back-to-back with this one, since ASND_SetVoice() leaves VOICE_UPDATE
|
||||||
// set until its own audio-DMA-interrupt tick picks it up, and
|
// set until its own audio-DMA-interrupt tick picks it up, and
|
||||||
// ASND_AddVoice() refuses to queue anything while that's still pending
|
// ASND_AddVoice() refuses to queue anything while that's still pending
|
||||||
// (see audioStreamDolphinFeed()'s own comment). Later polls queue
|
// (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));
|
errorChain(audioStreamDolphinFeed(stream));
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
@@ -138,9 +153,20 @@ errorret_t audioStreamDolphinFeed(audiostream_t *stream) {
|
|||||||
|
|
||||||
uint8_t *buffer = stream->platform.buffer[stream->platform.nextBufferIndex];
|
uint8_t *buffer = stream->platform.buffer[stream->platform.nextBufferIndex];
|
||||||
size_t framesRead = 0;
|
size_t framesRead = 0;
|
||||||
errorChain(audioStreamRead(
|
bool_t notReadyYet = false;
|
||||||
stream, (int16_t *) buffer, framesToRead, &framesRead
|
// 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 bytesRead = framesRead * frameSize;
|
||||||
const size_t alignedBytes = (bytesRead + 31) & ~((size_t) 31);
|
const size_t alignedBytes = (bytesRead + 31) & ~((size_t) 31);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "audiostreammp3ring.h"
|
||||||
|
|
||||||
typedef struct audiostream_s audiostream_t;
|
typedef struct audiostream_s audiostream_t;
|
||||||
|
|
||||||
@@ -63,13 +64,20 @@ typedef struct {
|
|||||||
// call for that pass's first (ASND_SetVoice()) window.
|
// call for that pass's first (ASND_SetVoice()) window.
|
||||||
int32_t volumeLeft;
|
int32_t volumeLeft;
|
||||||
int32_t volumeRight;
|
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;
|
} audiostreamdolphin_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the GameCube/Wii-specific playback state of an audio stream:
|
* Initializes the GameCube/Wii-specific playback state of an audio stream:
|
||||||
* allocates an ASND voice slot (ASND_GetFirstUnusedVoice()) and this
|
* allocates an ASND voice slot (ASND_GetFirstUnusedVoice()) and this
|
||||||
* stream's two fixed-size double-buffer windows (sized from its channel
|
* 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.
|
* @param stream The audio stream to initialize.
|
||||||
* @return Error state if any.
|
* @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:
|
* Disposes the GameCube/Wii-specific playback state of an audio stream:
|
||||||
* stops its voice (releasing the slot back to ASND_GetFirstUnusedVoice())
|
* for MP3 streams, first fully stops and frees the background decode ring
|
||||||
* and frees its two buffers.
|
* (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.
|
* @param stream The audio stream to dispose.
|
||||||
* @return Error state if any.
|
* @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
|
* Starts a new playback pass: seeks the stream's decode position to
|
||||||
* stream->startFrame, determines this pass's loop-segment end, resolves
|
* stream->startFrame, determines this pass's loop-segment end, resolves
|
||||||
* this pass's volume/pan, stops whatever the voice was doing before (so a
|
* 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
|
* fresh pass never plays out stale queued windows from the last one). For
|
||||||
* primes it via audioStreamDolphinFeed() - once for the first
|
* MP3 streams, also stops the previous pass's background decode thread
|
||||||
* (ASND_SetVoice()) window, once more to also queue the second
|
* (audioStreamMp3RingStop(), which must complete before the seek
|
||||||
* (ASND_AddVoice()) window ahead of time.
|
* 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.
|
* @param stream The audio stream to output.
|
||||||
* @return Error state if any.
|
* @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
|
* ASND_AddVoice()). A no-op once this pass's platform.position has
|
||||||
* reached platform.endFrame.
|
* 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.
|
* @param stream The audio stream to feed.
|
||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
// see that header's own documentation for the actual struct/interface.
|
// see that header's own documentation for the actual struct/interface.
|
||||||
// Conveniently, libmad ships as part of libogc itself (gc/mad.h) rather
|
// Conveniently, libmad ships as part of libogc itself (gc/mad.h) rather
|
||||||
// than needing to be fetched separately, unlike on Linux.
|
// than needing to be fetched separately, unlike on Linux.
|
||||||
#include "audio/stream/audiostreammp3decodersw.h"
|
#include "audiostreammp3decodersw.h"
|
||||||
|
|||||||
@@ -10,17 +10,14 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
audiostreamlinux.c
|
audiostreamlinux.c
|
||||||
)
|
)
|
||||||
|
|
||||||
# No hardware MP3 decoder on Linux - use the shared libmad-based software
|
# No hardware MP3 decoder on Linux - uses the shared libmad-based software
|
||||||
# backend (also used by Dolphin), sourced directly from src/dusk/audio
|
# backend + decode-ahead ring in src/duskmad instead (also used by
|
||||||
# rather than through its own unconditional CMakeLists.txt so platforms
|
# Dolphin). See audiostreammp3decodersw.h's own comment on why libmad
|
||||||
# with a hardware decoder (PSP) never pull libmad in at all. See
|
# (GPL-licensed) replaced this project's original minimp3-based
|
||||||
# audiostreammp3decodersw.h's own comment on why libmad (GPL-licensed)
|
# implementation. Linking stays here (rather than in src/duskmad itself)
|
||||||
# replaced this project's original minimp3-based implementation.
|
# since Linux and Dolphin genuinely obtain libmad differently -
|
||||||
|
# find_package() here, the bundled devkitPPC/libogc toolchain copy there.
|
||||||
if(NOT libmad_FOUND)
|
if(NOT libmad_FOUND)
|
||||||
find_package(libmad REQUIRED)
|
find_package(libmad REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC libmad::mad)
|
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());
|
errorThrow("Failed to open SDL2 audio device: %s", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorChain(audioStreamMp3RingInitIfNeeded(stream));
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
|
errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
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);
|
SDL_CloseAudioDevice(stream->platform.device);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
@@ -63,17 +71,11 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
|||||||
stream->startFrame = 0;
|
stream->startFrame = 0;
|
||||||
stream->seeking = false;
|
stream->seeking = false;
|
||||||
|
|
||||||
size_t endFrame = totalFrames;
|
// This pass's stop point - the current loop segment's end, or the true
|
||||||
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
|
// end of the clip if not looping (or if startFrame already landed past
|
||||||
endFrame = mathMin(
|
// the loop end, e.g. a seek into an outro). See
|
||||||
(size_t) (stream->loopStart * stream->sampleRate), totalFrames
|
// audioStreamComputeEndFrame()'s own comment.
|
||||||
);
|
const size_t endFrame = audioStreamComputeEndFrame(stream, startFrame, 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;
|
|
||||||
|
|
||||||
// Only an explicit seek discards whatever's still queued and jumps -
|
// Only an explicit seek discards whatever's still queued and jumps -
|
||||||
// a natural loop restart deliberately leaves the previous pass's tail
|
// a natural loop restart deliberately leaves the previous pass's tail
|
||||||
@@ -84,6 +86,17 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
|||||||
if(seeking) {
|
if(seeking) {
|
||||||
SDL_ClearQueuedAudio(stream->platform.device);
|
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));
|
errorChain(audioStreamSeek(stream, startFrame));
|
||||||
|
|
||||||
stream->platform.position = startFrame;
|
stream->platform.position = startFrame;
|
||||||
@@ -91,6 +104,11 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
|||||||
stream->platform.passStartTicks = SDL_GetTicks();
|
stream->platform.passStartTicks = SDL_GetTicks();
|
||||||
stream->platform.passStartPosition = startFrame;
|
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));
|
errorChain(audioStreamLinuxFeed(stream));
|
||||||
|
|
||||||
SDL_PauseAudioDevice(stream->platform.device, 0);
|
SDL_PauseAudioDevice(stream->platform.device, 0);
|
||||||
@@ -150,11 +168,25 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
|
|||||||
|
|
||||||
int16_t *chunk = memoryAllocate(framesToRead * frameSize);
|
int16_t *chunk = memoryAllocate(framesToRead * frameSize);
|
||||||
size_t framesRead = 0;
|
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)) {
|
if(errorIsNotOk(ret)) {
|
||||||
memoryFree(chunk);
|
memoryFree(chunk);
|
||||||
errorChain(ret);
|
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;
|
const size_t bytesRead = framesRead * frameSize;
|
||||||
int queued;
|
int queued;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "audiostreammp3ring.h"
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
typedef struct audiostream_s audiostream_t;
|
typedef struct audiostream_s audiostream_t;
|
||||||
@@ -31,11 +32,19 @@ typedef struct {
|
|||||||
// SDL_GetQueuedAudioSize() alone).
|
// SDL_GetQueuedAudioSize() alone).
|
||||||
Uint32 passStartTicks;
|
Uint32 passStartTicks;
|
||||||
size_t passStartPosition;
|
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;
|
} audiostreamlinux_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the Linux-specific playback state of an audio stream by
|
* 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.
|
* @param stream The audio stream to initialize.
|
||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
@@ -43,8 +52,10 @@ typedef struct {
|
|||||||
errorret_t audioStreamLinuxInit(audiostream_t *stream);
|
errorret_t audioStreamLinuxInit(audiostream_t *stream);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes the Linux-specific playback state of an audio stream, closing
|
* Disposes the Linux-specific playback state of an audio stream: for MP3
|
||||||
* its SDL2 audio device.
|
* 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.
|
* @param stream The audio stream to dispose.
|
||||||
* @return Error state if any.
|
* @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
|
* 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
|
* stream->startFrame (clearing the SDL queue first if this is an explicit
|
||||||
* seek rather than a natural loop restart - see stream->seeking's own
|
* seek rather than a natural loop restart - see stream->seeking's own
|
||||||
* comment), determines this pass's loop-segment end, and queues the first
|
* comment), determines this pass's loop-segment end. For MP3 streams, also
|
||||||
* window via audioStreamLinuxFeed().
|
* 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.
|
* @param stream The audio stream to output.
|
||||||
* @return Error state if any.
|
* @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.
|
* advancing platform.position by however many frames were actually read.
|
||||||
* A no-op once platform.position has reached platform.endFrame.
|
* 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
|
* 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
|
* pass at once) specifically so a long clip is never fully resident in
|
||||||
* memory at once - called both by audioStreamLinuxBuffer() (the first
|
* memory at once - called both by audioStreamLinuxBuffer() (the first
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
// header's own documentation for the actual struct/interface, including
|
// header's own documentation for the actual struct/interface, including
|
||||||
// why libmad (GPL-licensed) was chosen over the MIT-licensed minimp3 this
|
// why libmad (GPL-licensed) was chosen over the MIT-licensed minimp3 this
|
||||||
// used to be.
|
// used to be.
|
||||||
#include "audio/stream/audiostreammp3decodersw.h"
|
#include "audiostreammp3decodersw.h"
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# libmad-based MP3 decode, shared by every platform that doesn't have (or
|
||||||
|
# use) a hardware MP3 decoder - currently Linux/knulli and Dolphin
|
||||||
|
# (Wii/GameCube), both via the same software decoder and background
|
||||||
|
# decode-ahead ring (audiostreammp3decodersw.c, audiostreammp3ring.c). See
|
||||||
|
# src/CMakeLists.txt for which DUSK_TARGET_SYSTEM values pull this
|
||||||
|
# directory in at all - PSP uses its own hardware sceMp3 decoder instead
|
||||||
|
# (src/duskpsp/audio/audiostreammp3decoder.c) and never references
|
||||||
|
# anything here.
|
||||||
|
#
|
||||||
|
# Note: libmad is GPL-licensed, unlike the rest of this project - see
|
||||||
|
# audiostreammp3decodersw.h's own comment. Linking/finding libmad itself
|
||||||
|
# stays with each platform's own audio CMakeLists.txt (src/dusklinux/audio,
|
||||||
|
# cmake/targets/dolphin.cmake) rather than living here, since Linux and
|
||||||
|
# Dolphin genuinely obtain it differently (find_package() vs the bundled
|
||||||
|
# devkitPPC/libogc toolchain copy) - this file only owns the shared source.
|
||||||
|
|
||||||
|
# Includes
|
||||||
|
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
audiostreammp3decodersw.c
|
||||||
|
audiostreammp3ring.c
|
||||||
|
)
|
||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "audiostreammp3decodersw.h"
|
#include "audiostreammp3decodersw.h"
|
||||||
#include "audiostream.h"
|
#include "audio/stream/audiostream.h"
|
||||||
#include "audiostreammp3.h"
|
#include "audio/stream/audiostreammp3.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "audiostreammp3ring.h"
|
||||||
|
#include "audio/stream/audiostream.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingInit(audiostream_t *stream) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||||
|
|
||||||
|
audiostreammp3ring_t *ring = &stream->platform.mp3Ring;
|
||||||
|
const size_t frameSize = stream->channels * sizeof(int16_t);
|
||||||
|
|
||||||
|
ring->data = (int16_t *) memoryAllocate(AUDIO_MP3_RING_FRAMES * frameSize);
|
||||||
|
ring->scratch = (int16_t *) memoryAllocate(AUDIO_MP3_RING_STEP_FRAMES * frameSize);
|
||||||
|
ring->readPos = 0;
|
||||||
|
ring->writePos = 0;
|
||||||
|
ring->filled = 0;
|
||||||
|
ring->reachedEnd = false;
|
||||||
|
ring->failed = false;
|
||||||
|
ring->startFrame = 0;
|
||||||
|
ring->endFrame = 0;
|
||||||
|
threadMutexInit(&ring->lock);
|
||||||
|
threadInit(&ring->thread, audioStreamMp3RingThreadFunc);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingDispose(audiostream_t *stream) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||||
|
|
||||||
|
audiostreammp3ring_t *ring = &stream->platform.mp3Ring;
|
||||||
|
|
||||||
|
// Must fully park the decode thread before freeing anything it might
|
||||||
|
// still be touching, or before the caller lets the shared MP3 layer
|
||||||
|
// tear down stream->mp3's decoder/file out from under it.
|
||||||
|
threadStop(&ring->thread);
|
||||||
|
threadMutexDispose(&ring->lock);
|
||||||
|
memoryFree(ring->data);
|
||||||
|
memoryFree(ring->scratch);
|
||||||
|
ring->data = NULL;
|
||||||
|
ring->scratch = NULL;
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingStop(audiostream_t *stream) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||||
|
|
||||||
|
audiostreammp3ring_t *ring = &stream->platform.mp3Ring;
|
||||||
|
|
||||||
|
// Blocking - guarantees the decode thread is fully parked (not mid-read)
|
||||||
|
// before the caller runs audioStreamSeek(), which mutates the same
|
||||||
|
// stream->mp3 decoder/file this thread reads via audioStreamRead(). A
|
||||||
|
// safe no-op if the thread was never started or already finished this
|
||||||
|
// pass on its own (reached endFrame).
|
||||||
|
threadStop(&ring->thread);
|
||||||
|
|
||||||
|
threadMutexLock(&ring->lock);
|
||||||
|
ring->readPos = 0;
|
||||||
|
ring->writePos = 0;
|
||||||
|
ring->filled = 0;
|
||||||
|
ring->reachedEnd = false;
|
||||||
|
ring->failed = false;
|
||||||
|
threadMutexUnlock(&ring->lock);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingStart(
|
||||||
|
audiostream_t *stream, size_t startFrame, size_t endFrame
|
||||||
|
) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||||
|
|
||||||
|
audiostreammp3ring_t *ring = &stream->platform.mp3Ring;
|
||||||
|
|
||||||
|
// Finalized here, before the thread is launched - thread creation itself
|
||||||
|
// is the happens-before edge that makes these writes visible to the new
|
||||||
|
// thread, so no lock is needed around this handoff.
|
||||||
|
ring->startFrame = startFrame;
|
||||||
|
ring->endFrame = endFrame;
|
||||||
|
ring->thread.data = stream;
|
||||||
|
threadStartRequest(&ring->thread);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t audioStreamMp3RingDrain(
|
||||||
|
audiostream_t *stream,
|
||||||
|
int16_t *out,
|
||||||
|
size_t maxFrames,
|
||||||
|
bool_t *outDecodeDone
|
||||||
|
) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertNotNull(out, "Out buffer cannot be NULL.");
|
||||||
|
assertNotNull(outDecodeDone, "outDecodeDone cannot be NULL.");
|
||||||
|
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||||
|
|
||||||
|
audiostreammp3ring_t *ring = &stream->platform.mp3Ring;
|
||||||
|
const size_t channels = stream->channels;
|
||||||
|
|
||||||
|
threadMutexLock(&ring->lock);
|
||||||
|
const size_t available = ring->filled;
|
||||||
|
const bool_t decodeDone = ring->reachedEnd || ring->failed;
|
||||||
|
|
||||||
|
// Wait for a full window rather than handing back a partial one, unless
|
||||||
|
// decode has genuinely nothing more to give this pass - keeps a
|
||||||
|
// platform's own output handoff cadence matching its previous
|
||||||
|
// whole-window-per-call behavior instead of fragmenting into many tiny
|
||||||
|
// calls just because decode happened to be a little behind at this exact
|
||||||
|
// poll.
|
||||||
|
if(available < maxFrames && !decodeDone) {
|
||||||
|
threadMutexUnlock(&ring->lock);
|
||||||
|
*outDecodeDone = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t toCopy = mathMin(available, maxFrames);
|
||||||
|
const size_t firstPart = mathMin(toCopy, AUDIO_MP3_RING_FRAMES - ring->readPos);
|
||||||
|
memoryCopy(out, ring->data + ring->readPos * channels, firstPart * channels * sizeof(int16_t));
|
||||||
|
if(toCopy > firstPart) {
|
||||||
|
memoryCopy(
|
||||||
|
out + firstPart * channels, ring->data,
|
||||||
|
(toCopy - firstPart) * channels * sizeof(int16_t)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ring->readPos = (ring->readPos + toCopy) % AUDIO_MP3_RING_FRAMES;
|
||||||
|
ring->filled -= toCopy;
|
||||||
|
threadMutexUnlock(&ring->lock);
|
||||||
|
|
||||||
|
*outDecodeDone = decodeDone;
|
||||||
|
return toCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t audioStreamMp3RingFailed(const audiostream_t *stream) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||||
|
|
||||||
|
return stream->platform.mp3Ring.failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audioStreamMp3RingThreadFunc(thread_t *thread) {
|
||||||
|
assertNotNull(thread, "Thread cannot be NULL.");
|
||||||
|
|
||||||
|
audiostream_t *stream = (audiostream_t *) thread->data;
|
||||||
|
audiostreammp3ring_t *ring = &stream->platform.mp3Ring;
|
||||||
|
const size_t channels = stream->channels;
|
||||||
|
|
||||||
|
// Captured once - only ever written by audioStreamMp3RingStart() before
|
||||||
|
// this thread was launched (a platform's Feed() advances its own
|
||||||
|
// position field as it drains the ring, never these). Tracks its own
|
||||||
|
// progress in a thread-local counter rather than re-reading endFrame
|
||||||
|
// repeatedly, though it wouldn't change mid-pass either way.
|
||||||
|
const size_t startFrame = ring->startFrame;
|
||||||
|
const size_t endFrame = ring->endFrame;
|
||||||
|
size_t decoded = 0;
|
||||||
|
|
||||||
|
while(!threadShouldStop(thread)) {
|
||||||
|
threadMutexLock(&ring->lock);
|
||||||
|
const size_t room = AUDIO_MP3_RING_FRAMES - ring->filled;
|
||||||
|
threadMutexUnlock(&ring->lock);
|
||||||
|
|
||||||
|
if(room == 0) {
|
||||||
|
// Never sleeps while holding the lock - re-checks threadShouldStop()
|
||||||
|
// immediately after waking, bounding a stop request's worst-case
|
||||||
|
// wait here to one poll interval, not a whole ring's worth of idle.
|
||||||
|
usleep(AUDIO_MP3_RING_POLL_MICROS);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(startFrame + decoded >= endFrame) break;
|
||||||
|
const size_t framesRemaining = endFrame - (startFrame + decoded);
|
||||||
|
const size_t step = mathMin(
|
||||||
|
mathMin(room, framesRemaining), (size_t) AUDIO_MP3_RING_STEP_FRAMES
|
||||||
|
);
|
||||||
|
|
||||||
|
size_t framesRead = 0;
|
||||||
|
// The expensive call (asset I/O + libmad decode) - deliberately
|
||||||
|
// outside any lock, so it never blocks the main thread's own
|
||||||
|
// audioStreamMp3RingDrain() calls while it runs.
|
||||||
|
errorret_t ret = audioStreamRead(stream, ring->scratch, step, &framesRead);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
errorCatch(errorPrint(ret));
|
||||||
|
threadMutexLock(&ring->lock);
|
||||||
|
ring->failed = true;
|
||||||
|
threadMutexUnlock(&ring->lock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(framesRead == 0) break; // Genuine early end of underlying data.
|
||||||
|
|
||||||
|
threadMutexLock(&ring->lock);
|
||||||
|
const size_t firstPart = mathMin(
|
||||||
|
framesRead, AUDIO_MP3_RING_FRAMES - ring->writePos
|
||||||
|
);
|
||||||
|
memoryCopy(
|
||||||
|
ring->data + ring->writePos * channels, ring->scratch,
|
||||||
|
firstPart * channels * sizeof(int16_t)
|
||||||
|
);
|
||||||
|
if(framesRead > firstPart) {
|
||||||
|
memoryCopy(
|
||||||
|
ring->data, ring->scratch + firstPart * channels,
|
||||||
|
(framesRead - firstPart) * channels * sizeof(int16_t)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ring->writePos = (ring->writePos + framesRead) % AUDIO_MP3_RING_FRAMES;
|
||||||
|
ring->filled += framesRead;
|
||||||
|
threadMutexUnlock(&ring->lock);
|
||||||
|
|
||||||
|
decoded += framesRead;
|
||||||
|
// Loops back to the top - threadShouldStop() is rechecked before the
|
||||||
|
// NEXT audioStreamRead(), not just once at the top of an outer loop,
|
||||||
|
// so a stop request doesn't have to wait through a whole pass.
|
||||||
|
}
|
||||||
|
|
||||||
|
threadMutexLock(&ring->lock);
|
||||||
|
ring->reachedEnd = true;
|
||||||
|
threadMutexUnlock(&ring->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingInitIfNeeded(audiostream_t *stream) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
|
if(stream->type == AUDIO_STREAM_TYPE_MP3) {
|
||||||
|
errorChain(audioStreamMp3RingInit(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingDisposeIfNeeded(audiostream_t *stream) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
|
if(stream->type == AUDIO_STREAM_TYPE_MP3) {
|
||||||
|
errorChain(audioStreamMp3RingDispose(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingStopIfNeeded(audiostream_t *stream) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
|
if(stream->type == AUDIO_STREAM_TYPE_MP3) {
|
||||||
|
errorChain(audioStreamMp3RingStop(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamMp3RingStartIfNeeded(
|
||||||
|
audiostream_t *stream, size_t startFrame, size_t endFrame
|
||||||
|
) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
|
if(stream->type == AUDIO_STREAM_TYPE_MP3) {
|
||||||
|
errorChain(audioStreamMp3RingStart(stream, startFrame, endFrame));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioStreamReadForPlayback(
|
||||||
|
audiostream_t *stream,
|
||||||
|
int16_t *out,
|
||||||
|
size_t maxFrames,
|
||||||
|
size_t *outFramesRead,
|
||||||
|
bool_t *outNotReadyYet
|
||||||
|
) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertNotNull(out, "Out buffer cannot be NULL.");
|
||||||
|
assertNotNull(outFramesRead, "outFramesRead cannot be NULL.");
|
||||||
|
assertNotNull(outNotReadyYet, "outNotReadyYet cannot be NULL.");
|
||||||
|
|
||||||
|
*outNotReadyYet = false;
|
||||||
|
|
||||||
|
if(stream->type == AUDIO_STREAM_TYPE_MP3) {
|
||||||
|
bool_t decodeDone = false;
|
||||||
|
*outFramesRead = audioStreamMp3RingDrain(stream, out, maxFrames, &decodeDone);
|
||||||
|
if(*outFramesRead == 0 && !decodeDone) {
|
||||||
|
*outNotReadyYet = true;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
if(*outFramesRead == 0 && audioStreamMp3RingFailed(stream)) {
|
||||||
|
errorThrow("Background MP3 decode failed.");
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(audioStreamRead(stream, out, maxFrames, outFramesRead));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "thread/thread.h"
|
||||||
|
#include "thread/threadmutex.h"
|
||||||
|
|
||||||
|
typedef struct audiostream_s audiostream_t;
|
||||||
|
|
||||||
|
// How many frames of decoded PCM the ring holds - deliberately generous
|
||||||
|
// (~1.5s at 44100Hz stereo, 256KB) so the background decode thread can run
|
||||||
|
// well ahead of what a platform's Feed() actually needs per poll, without
|
||||||
|
// being a meaningful fraction of even GameCube's 24MB (~1.1%) total RAM.
|
||||||
|
#define AUDIO_MP3_RING_FRAMES (64 * 1024)
|
||||||
|
|
||||||
|
// How many frames the decode thread reads per audioStreamRead() call -
|
||||||
|
// bounds both normal decode-thread iteration granularity and, more
|
||||||
|
// importantly, audioStreamMp3RingStop()'s worst-case blocking wait (roughly
|
||||||
|
// one of these reads' worth of asset I/O + libmad decode, since the thread
|
||||||
|
// only rechecks threadShouldStop() between reads, not mid-read).
|
||||||
|
#define AUDIO_MP3_RING_STEP_FRAMES 4096
|
||||||
|
|
||||||
|
// How long the decode thread sleeps between polls when the ring is already
|
||||||
|
// full - matches the shared asset-load thread's own idle-poll interval
|
||||||
|
// (src/dusk/asset/asset.c) rather than inventing a new convention.
|
||||||
|
#define AUDIO_MP3_RING_POLL_MICROS 1000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Background MP3 decode-ahead ring, shared by every platform whose MP3
|
||||||
|
* decode is expensive enough to hitch the main thread if done synchronously
|
||||||
|
* (currently Linux and Dolphin, both via the libmad-based software decoder
|
||||||
|
* - audiostreammp3decodersw.c; PSP doesn't use this, see below). Moves
|
||||||
|
* audioStreamRead()'s real cost (asset I/O + libmad decode) off the main
|
||||||
|
* thread onto a background thread that decodes ahead into this ring - a
|
||||||
|
* platform's own Feed() then drains already-decoded PCM out of it (a
|
||||||
|
* cheap, lock-protected copy) instead of decoding directly. Sits upstream
|
||||||
|
* of whatever a platform does with the PCM afterward (SDL_QueueAudio on
|
||||||
|
* Linux, ASND_SetVoice()/ASND_AddVoice() on Dolphin) - this struct knows
|
||||||
|
* nothing about either.
|
||||||
|
*
|
||||||
|
* Only used for AUDIO_STREAM_TYPE_MP3 streams - PCM/WAV reads are already
|
||||||
|
* cheap plain memory copies and don't need this. Not used by PSP: its
|
||||||
|
* hardware sceMp3 decoder is a completely different API (see
|
||||||
|
* src/duskpsp/audio/audiostreammp3decoder.c), and PSP's own audio backend
|
||||||
|
* already has its own background thread + ring - for *output* (draining
|
||||||
|
* to hardware), not decode - layering this under it would just be a
|
||||||
|
* second, redundant ring.
|
||||||
|
*
|
||||||
|
* The background thread is one-shot per playback pass, not a persistent
|
||||||
|
* worker (unlike PSP's own output thread, which idles cheaply between
|
||||||
|
* passes) - a fresh thread is started for every new pass (initial play,
|
||||||
|
* explicit seek, or loop restart), since those are rare relative to
|
||||||
|
* per-frame polls and a brief synchronous stop-then-restart at pass
|
||||||
|
* boundaries is a fine trade for not needing a mid-pass "abandon and
|
||||||
|
* reset" handshake.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
int16_t *data; // AUDIO_MP3_RING_FRAMES * channels * sizeof(int16_t)
|
||||||
|
int16_t *scratch; // AUDIO_MP3_RING_STEP_FRAMES * channels * sizeof(int16_t),
|
||||||
|
// the decode thread's own read target - never touched
|
||||||
|
// by the main thread, so it needs no lock protection.
|
||||||
|
|
||||||
|
// Guards exactly filled/readPos/writePos/reachedEnd/failed below,
|
||||||
|
// grouped together for simplicity rather than split per-field (same
|
||||||
|
// rationale as the PSP backend's own ring lock). Never held across
|
||||||
|
// audioStreamRead() or usleep() (decode thread) or a platform's own
|
||||||
|
// output call (main thread).
|
||||||
|
threadmutex_t lock;
|
||||||
|
|
||||||
|
// Next frame audioStreamMp3RingDrain() (main thread) reads.
|
||||||
|
size_t readPos;
|
||||||
|
// Next frame the decode thread writes.
|
||||||
|
size_t writePos;
|
||||||
|
// How many valid frames are currently buffered.
|
||||||
|
size_t filled;
|
||||||
|
|
||||||
|
// Set by the decode thread once it has produced every frame this pass
|
||||||
|
// will ever have (reached endFrame, or the underlying read returned 0
|
||||||
|
// frames early) - tells Drain() there's nothing more coming, so it
|
||||||
|
// should hand back whatever's left rather than waiting for a full
|
||||||
|
// window that will never arrive.
|
||||||
|
bool_t reachedEnd;
|
||||||
|
// Set by the decode thread if audioStreamRead() itself errored - Drain()
|
||||||
|
// treats this the same as reachedEnd for draining purposes, but a
|
||||||
|
// platform's Feed() should surface it as a real thrown error once the
|
||||||
|
// ring is empty.
|
||||||
|
bool_t failed;
|
||||||
|
|
||||||
|
// This pass's frame bounds, set by audioStreamMp3RingStart() before the
|
||||||
|
// thread is launched and read-only after (thread creation is the
|
||||||
|
// happens-before edge) - stored here rather than read from a platform's
|
||||||
|
// own platform_t fields, since this struct is shared across platforms
|
||||||
|
// whose own position/endFrame-equivalent fields aren't guaranteed to
|
||||||
|
// exist or be named the same way.
|
||||||
|
size_t startFrame;
|
||||||
|
size_t endFrame;
|
||||||
|
|
||||||
|
// One-shot per pass - see this struct's own top-of-file comment.
|
||||||
|
thread_t thread;
|
||||||
|
} audiostreammp3ring_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocates this stream's ring/scratch buffers (sized from its channel
|
||||||
|
* count) and initializes its mutex and thread_t - does not start decoding
|
||||||
|
* anything yet, that's audioStreamMp3RingStart()'s job. Call once, from a
|
||||||
|
* platform's own Init(), only for AUDIO_STREAM_TYPE_MP3 streams.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingInit(audiostream_t *stream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocking-stops the decode thread (a no-op if it was never started or has
|
||||||
|
* already finished this pass) and frees the ring/scratch buffers. Call
|
||||||
|
* from a platform's own Dispose() to guarantee the thread is fully parked
|
||||||
|
* before the rest of this stream's (and the shared MP3 layer's) teardown
|
||||||
|
* runs.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingDispose(audiostream_t *stream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocking-stops the decode thread (safe no-op if not running) and resets
|
||||||
|
* the ring to empty, ready for a fresh pass. Must be called (and must
|
||||||
|
* return) before audioStreamSeek() runs for a new pass, since the decode
|
||||||
|
* thread reads via the same stream->mp3 decoder/file audioStreamSeek()
|
||||||
|
* mutates - and before audioStreamMp3RingStart() launches the next one.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to stop. Must be AUDIO_STREAM_TYPE_MP3.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingStop(audiostream_t *stream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a fresh one-shot decode thread for the pass a platform's own
|
||||||
|
* Buffer() just set up (stream->platform.position/endFrame - or
|
||||||
|
* equivalent, whatever the platform's own struct calls them - already
|
||||||
|
* finalized) - decodes forward via audioStreamRead() into the ring until
|
||||||
|
* it reaches endFrame, filling back up whenever audioStreamMp3RingDrain()
|
||||||
|
* makes room.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to start decoding. Must be AUDIO_STREAM_TYPE_MP3.
|
||||||
|
* @param startFrame This pass's starting frame offset.
|
||||||
|
* @param endFrame This pass's stop frame offset.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingStart(
|
||||||
|
audiostream_t *stream, size_t startFrame, size_t endFrame
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies up to maxFrames already-decoded frames out of the ring into `out`,
|
||||||
|
* advancing the ring's read cursor. Only returns fewer than maxFrames (down
|
||||||
|
* to 0) once *outDecodeDone is also true (the decode thread has produced
|
||||||
|
* everything it ever will for this pass, or hit an error) - otherwise it
|
||||||
|
* waits for a full window rather than handing back a partial one, so a
|
||||||
|
* platform's own output handoff doesn't fragment into many tiny calls just
|
||||||
|
* because decode happened to be a little behind at this exact poll.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to drain from. Must be AUDIO_STREAM_TYPE_MP3.
|
||||||
|
* @param out Destination buffer, sized for at least maxFrames frames.
|
||||||
|
* @param maxFrames Maximum number of frames to copy out.
|
||||||
|
* @param outDecodeDone Set to true if the decode thread has nothing more to
|
||||||
|
* give this pass (cleanly finished or failed).
|
||||||
|
* @return The number of frames actually copied out.
|
||||||
|
*/
|
||||||
|
size_t audioStreamMp3RingDrain(
|
||||||
|
audiostream_t *stream,
|
||||||
|
int16_t *out,
|
||||||
|
size_t maxFrames,
|
||||||
|
bool_t *outDecodeDone
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the decode thread hit a real read error this pass (see the
|
||||||
|
* `failed` field's own comment) - a platform's Feed() checks this once
|
||||||
|
* Drain() has returned 0 frames with decode reported done, to decide
|
||||||
|
* whether that's a genuine end of stream or something to surface as an
|
||||||
|
* error.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to check. Must be AUDIO_STREAM_TYPE_MP3.
|
||||||
|
* @return true if the decode thread failed this pass.
|
||||||
|
*/
|
||||||
|
bool_t audioStreamMp3RingFailed(const audiostream_t *stream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The decode thread's own entry point (passed to threadInit()). Reads
|
||||||
|
* forward via audioStreamRead() in AUDIO_MP3_RING_STEP_FRAMES chunks,
|
||||||
|
* writing into the ring whenever there's room, until it reaches this
|
||||||
|
* pass's endFrame, the underlying read returns 0 frames, or a real read
|
||||||
|
* error occurs - never touches stream->onLoop/onEnd or any other
|
||||||
|
* main-thread-owned stream state, only the ring and its own captured
|
||||||
|
* startFrame/endFrame.
|
||||||
|
*
|
||||||
|
* @param thread This ring's own thread_t (thread->data is the audiostream_t).
|
||||||
|
*/
|
||||||
|
void audioStreamMp3RingThreadFunc(thread_t *thread);
|
||||||
|
|
||||||
|
// The functions below are thin, type-checking wrappers around the ones
|
||||||
|
// above, meant to be called unconditionally from a platform's own
|
||||||
|
// Init()/Dispose()/Buffer()/Feed() - each is a no-op (or, for
|
||||||
|
// audioStreamReadForPlayback(), falls through to a plain audioStreamRead())
|
||||||
|
// for anything other than an AUDIO_STREAM_TYPE_MP3 stream, so callers don't
|
||||||
|
// need their own `if(stream->type == AUDIO_STREAM_TYPE_MP3)` check at every
|
||||||
|
// call site. This is what actually keeps Linux's and Dolphin's own
|
||||||
|
// Init/Dispose/Buffer/Feed implementations close to each other - see
|
||||||
|
// audiostreamlinux.c/audiostreamdolphin.c for how they're used.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls audioStreamMp3RingInit() if stream is an MP3 stream, otherwise a
|
||||||
|
* no-op.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to initialize.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingInitIfNeeded(audiostream_t *stream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls audioStreamMp3RingDispose() if stream is an MP3 stream, otherwise a
|
||||||
|
* no-op.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to dispose.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingDisposeIfNeeded(audiostream_t *stream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls audioStreamMp3RingStop() if stream is an MP3 stream, otherwise a
|
||||||
|
* no-op. Safe (and cheap) to call unconditionally at the start of every
|
||||||
|
* platform Buffer() pass, before audioStreamSeek().
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to stop.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingStopIfNeeded(audiostream_t *stream);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls audioStreamMp3RingStart() if stream is an MP3 stream, otherwise a
|
||||||
|
* no-op. Safe to call unconditionally once a platform Buffer() pass has
|
||||||
|
* finalized its startFrame/endFrame, after audioStreamSeek().
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to start decoding.
|
||||||
|
* @param startFrame This pass's starting frame offset.
|
||||||
|
* @param endFrame This pass's stop frame offset.
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamMp3RingStartIfNeeded(
|
||||||
|
audiostream_t *stream, size_t startFrame, size_t endFrame
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads up to maxFrames frames of decoded PCM for playback into `out`: for
|
||||||
|
* MP3 streams, drains the background decode ring
|
||||||
|
* (audioStreamMp3RingDrain()); for anything else, reads directly via
|
||||||
|
* audioStreamRead(). The single call a platform's own Feed() needs to get
|
||||||
|
* its next window's worth of PCM, regardless of stream type.
|
||||||
|
*
|
||||||
|
* *outNotReadyYet distinguishes the two ways this can return 0 frames
|
||||||
|
* without erroring: true means an MP3 stream's decode thread simply hasn't
|
||||||
|
* produced a full window yet (retry next poll, don't treat as end of
|
||||||
|
* stream); false with 0 frames means genuinely nothing left to read this
|
||||||
|
* pass. A background MP3 decode failure surfaces as a real thrown error
|
||||||
|
* (via errorThrow) rather than through either of these.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to read from.
|
||||||
|
* @param out Destination buffer, sized for at least maxFrames frames.
|
||||||
|
* @param maxFrames Maximum number of frames to read.
|
||||||
|
* @param outFramesRead Set to the number of frames actually read.
|
||||||
|
* @param outNotReadyYet Set to true if this returned 0 frames only because
|
||||||
|
* decode hasn't caught up yet, not because playback
|
||||||
|
* has actually reached the end.
|
||||||
|
* @return Error state - a background MP3 decode failure surfaces here.
|
||||||
|
*/
|
||||||
|
errorret_t audioStreamReadForPlayback(
|
||||||
|
audiostream_t *stream,
|
||||||
|
int16_t *out,
|
||||||
|
size_t maxFrames,
|
||||||
|
size_t *outFramesRead,
|
||||||
|
bool_t *outNotReadyYet
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user