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:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 <mad.h>
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
// Size of the sliding compressed-byte window libmad decodes from - just
|
||||
// needs to comfortably exceed one MPEG frame's worth of bytes plus
|
||||
// whatever bit-reservoir carryover libmad itself buffers internally
|
||||
// (MAD_BUFFER_MDLEN), with room to refill in reasonably-sized chunks
|
||||
// rather than one frame at a time. Unlike this file's previous minimp3-
|
||||
// based implementation, libmad reports "not enough data" (MAD_ERROR_BUFLEN)
|
||||
// and "genuinely bad data" (every other error code) as distinct, explicit
|
||||
// signals rather than one overloaded return value - so, verified against a
|
||||
// real ~236s VBR file, this window doesn't need minimp3's 256KB (itself
|
||||
// only ~99.7% accurate) to decode every frame correctly.
|
||||
#define AUDIO_MP3_SW_BUFFER_SIZE (32 * 1024)
|
||||
|
||||
/**
|
||||
* Software MP3 decoder state, shared by every platform that doesn't have
|
||||
* (or doesn't use) a hardware MP3 decoder - currently Linux and Dolphin,
|
||||
* both via this same libmad-based implementation
|
||||
* (audiostreammp3decodersw.c). See audiostreammp3.h for how this plugs
|
||||
* into the shared MP3 stream layer, and audiostreammp3.h's own
|
||||
* documentation of audioStreamMp3DecoderInit()/Dispose()/Rewind()/
|
||||
* DecodeFrame() for the interface this and the PSP hardware backend
|
||||
* (src/duskpsp/audio/audiostreammp3decoder.c) both implement.
|
||||
*
|
||||
* Note this is libmad (https://www.underbit.com/products/mad/), used here
|
||||
* for its bitstream-accurate frame sync (see AUDIO_MP3_SW_BUFFER_SIZE's own
|
||||
* comment) - unlike the rest of this project, libmad is GPL-licensed, not
|
||||
* MIT. Any binary linking this file's compiled output must comply with the
|
||||
* GPL; a deliberate tradeoff made after minimp3 proved unable to decode a
|
||||
* real-world VBR file without audible, unresolvable data loss.
|
||||
*/
|
||||
typedef struct {
|
||||
struct mad_stream stream;
|
||||
struct mad_frame frame;
|
||||
struct mad_synth synth;
|
||||
|
||||
// libmad reads up to MAD_BUFFER_GUARD bytes past whatever it's given as
|
||||
// the end of valid data while decoding the final real frame(s) in a
|
||||
// buffer - the trailing MAD_BUFFER_GUARD bytes here are that padding
|
||||
// (zeroed once endOfFile is set), never counted in bufferFilled.
|
||||
uint8_t buffer[AUDIO_MP3_SW_BUFFER_SIZE + MAD_BUFFER_GUARD];
|
||||
size_t bufferFilled;
|
||||
|
||||
// Set once assetFileRead() returns fewer bytes than requested - there's
|
||||
// no more compressed data to refill `buffer` with, though whatever's
|
||||
// still in it may still decode into one or more final frames.
|
||||
bool_t endOfFile;
|
||||
} audiostreammp3decoder_t;
|
||||
|
||||
/**
|
||||
* Initializes the software MP3 decoder for the given stream - resets
|
||||
* libmad's stream/frame/synth state and the compressed-byte window (empty,
|
||||
* not yet filled from the asset).
|
||||
*
|
||||
* @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Disposes the software MP3 decoder for the given stream: tears down
|
||||
* libmad's stream/frame state (mad_stream_finish()/mad_frame_finish()) -
|
||||
* mad_synth carries no allocated state of its own, so there's nothing to
|
||||
* release for it.
|
||||
*
|
||||
* @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Resets the decoder to the very start of the compressed stream: rewinds
|
||||
* stream->mp3.file back to the asset's parsed data offset, resets libmad's
|
||||
* stream/frame/synth state and the compressed-byte window, so the next
|
||||
* DecodeFrame() call starts decoding from the first MPEG frame again. See
|
||||
* audioStreamMp3Seek()'s own comment on why every seek goes through here.
|
||||
*
|
||||
* @param stream The audio stream to rewind. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Tops up the compressed-byte window from stream->mp3.file: shifts
|
||||
* whatever libmad hasn't consumed yet (from its own stream.next_frame
|
||||
* cursor) to the front of the window, reads as much more as there's room
|
||||
* for, and re-buffers libmad's stream (mad_stream_buffer()) over the
|
||||
* result - padding with MAD_BUFFER_GUARD zero bytes once the file's
|
||||
* genuinely exhausted, since libmad may read slightly past the end of
|
||||
* valid data while decoding a final frame.
|
||||
*
|
||||
* @param stream The audio stream to refill. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderRefill(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Converts one libmad fixed-point PCM sample to a clamped 16-bit sample -
|
||||
* the standard scale() conversion from libmad's own reference examples
|
||||
* (minimad.c et al.), rounding to nearest and clamping to +/-1.0 rather
|
||||
* than wrapping on overflow.
|
||||
*
|
||||
* @param sample The fixed-point sample to convert (mad_fixed_t, Q1.MAD_F_FRACBITS).
|
||||
* @return The converted 16-bit signed sample.
|
||||
*/
|
||||
int16_t audioStreamMp3DecoderScale(mad_fixed_t sample);
|
||||
|
||||
/**
|
||||
* Decodes the next MPEG frame's worth of PCM samples, topping up the
|
||||
* compressed-byte window from stream->mp3.file as needed
|
||||
* (audioStreamMp3DecoderRefill()). Writes decoded samples to `out` (sized
|
||||
* for at least AUDIO_MP3_MAX_SAMPLES_PER_FRAME int16_t values) and sets
|
||||
* *outFrames to how many frames (not samples) were produced - 0 once the
|
||||
* compressed stream is genuinely exhausted, never negative (a corrupt/
|
||||
* truncated stream is treated the same as a clean end, not an error,
|
||||
* matching audioStreamPcmRead()'s own tolerance for a short/truncated
|
||||
* asset).
|
||||
*
|
||||
* @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @param out Destination buffer for decoded samples.
|
||||
* @param outFrames Set to the number of frames actually decoded.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderDecodeFrame(
|
||||
audiostream_t *stream,
|
||||
int16_t *out,
|
||||
size_t *outFrames
|
||||
);
|
||||
Reference in New Issue
Block a user