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:
2026-09-02 14:10:36 -05:00
co-authored by Claude Sonnet 5
parent fc1fc9f170
commit b564d0b1c1
17 changed files with 822 additions and 70 deletions
+139
View File
@@ -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
);