Switch Linux/Dolphin MP3 decode from minimp3 to libmad

minimp3's sliding-window frame confirmation could spuriously discard real
frames near the tail of a not-yet-full window, losing ~5.5% of a real
VBR file's content even at a tuned 256KB window (~0.3% residual) -
audible as the whole track finishing early with stutters at each drop.

libmad's mad_stream/mad_frame/mad_synth API reports "need more data"
(MAD_ERROR_BUFLEN) and "genuinely bad data" separately rather than
overloading one return value, which was the actual ambiguity minimp3
couldn't resolve. Verified against the same real file with a 32KB window
(vs minimp3's 256KB): frame count and duration match the Xing header
exactly. Confirmed building for Linux, GameCube, and Wii (the latter two
via the project's real devkitPPC/libogc Docker toolchain) - libmad ships
inside libogc itself on Dolphin, no separate fetch needed there.

libmad is GPL-licensed, unlike the rest of this MIT project - a
deliberate tradeoff, noted at each site that pulls it in.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-02 09:47:15 -05:00
co-authored by Claude Sonnet 5
parent 3ee0a53688
commit fd5bcfae90
11 changed files with 256 additions and 151 deletions
+32
View File
@@ -0,0 +1,32 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# NOTE: libmad itself is GPL-licensed (see https://www.underbit.com/products/mad/),
# not MIT like the rest of this project. Any binary linking it must comply
# with the GPL - a deliberate tradeoff accepted for MP3 decode correctness
# (see audiostreammp3decodersw.c's own comment on why minimp3 wasn't enough).
find_path(LIBMAD_INCLUDE_DIR NAMES mad.h)
mark_as_advanced(LIBMAD_INCLUDE_DIR)
find_library(LIBMAD_LIBRARY NAMES mad)
mark_as_advanced(LIBMAD_LIBRARY)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
libmad
REQUIRED_VARS
LIBMAD_LIBRARY
LIBMAD_INCLUDE_DIR
)
if(libmad_FOUND AND NOT TARGET libmad::mad)
add_library(libmad::mad UNKNOWN IMPORTED)
set_target_properties(libmad::mad
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${LIBMAD_INCLUDE_DIR}
IMPORTED_LOCATION ${LIBMAD_LIBRARY}
)
endif()
-28
View File
@@ -1,28 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# minimp3 has no tagged releases - pinned to a specific commit instead of a
# branch for reproducibility.
include(FetchContent)
FetchContent_Declare(
minimp3
GIT_REPOSITORY https://github.com/lieff/minimp3.git
GIT_TAG ea99364f61c14656440e8d77e9c233ccf3124633
)
FetchContent_MakeAvailable(minimp3)
set(MINIMP3_INCLUDE_DIR "${minimp3_SOURCE_DIR}")
set(MINIMP3_HEADER "${minimp3_SOURCE_DIR}/minimp3.h")
if(EXISTS "${MINIMP3_HEADER}")
add_library(minimp3 INTERFACE)
target_include_directories(minimp3 INTERFACE "${MINIMP3_INCLUDE_DIR}")
set(minimp3_FOUND TRUE)
else()
set(minimp3_FOUND FALSE)
endif()
mark_as_advanced(MINIMP3_INCLUDE_DIR MINIMP3_HEADER)
+1
View File
@@ -62,6 +62,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
z z
lzma lzma
ansnd ansnd
mad
) )
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
+1 -1
View File
@@ -191,7 +191,7 @@ errorret_t assetMp3ParseHeader(assetfile_t *file, assetmp3file_t *mp3File) {
((uint32_t) framesBytes[2] << 8) | (uint32_t) framesBytes[3] ((uint32_t) framesBytes[2] << 8) | (uint32_t) framesBytes[3]
); );
// The Xing/Info header's frame count includes the header's own // The Xing/Info header's frame count includes the header's own
// frame (this one, at `i`) - which minimp3 (and sceMp3) will // frame (this one, at `i`) - which libmad (and sceMp3) will
// attempt to decode like any other, but which doesn't contribute a // attempt to decode like any other, but which doesn't contribute a
// real decodable frame of audio the way every other one does. // real decodable frame of audio the way every other one does.
// Confirmed against a real LAME-encoded file: its Xing header // Confirmed against a real LAME-encoded file: its Xing header
+7 -6
View File
@@ -17,7 +17,7 @@ typedef struct audiostream_s audiostream_t;
// allowed to produce. Sizes the pending-sample buffer below, which every // allowed to produce. Sizes the pending-sample buffer below, which every
// decoder backend writes directly into - this must be sized to the // decoder backend writes directly into - this must be sized to the
// largest of them, not just "one MPEG frame": // largest of them, not just "one MPEG frame":
// - minimp3 (software, Linux/Dolphin): always exactly one frame, 1152 // - libmad (software, Linux/Dolphin): always exactly one frame, 1152
// samples/channel for MPEG-1 Layer III, doubled here for stereo. // samples/channel for MPEG-1 Layer III, doubled here for stereo.
// - sceMp3 (hardware, PSP): its own pcmBuf is provisioned at double that // - sceMp3 (hardware, PSP): its own pcmBuf is provisioned at double that
// (see AUDIO_MP3_PSP_PCM_BUF_SIZE) - i.e. sceMp3Decode() can // (see AUDIO_MP3_PSP_PCM_BUF_SIZE) - i.e. sceMp3Decode() can
@@ -26,7 +26,7 @@ typedef struct audiostream_s audiostream_t;
// on real hardware whenever that happened - confirmed as the cause of // on real hardware whenever that happened - confirmed as the cause of
// very intermittent audio corruption/clicking, since it only bit when // very intermittent audio corruption/clicking, since it only bit when
// sceMp3 actually returned the larger amount. // sceMp3 actually returned the larger amount.
// So this covers the larger (PSP) case; minimp3's decode is safely well // So this covers the larger (PSP) case; libmad's decode is safely well
// within it. // within it.
#define AUDIO_MP3_MAX_SAMPLES_PER_FRAME (1152 * 2 * 2) #define AUDIO_MP3_MAX_SAMPLES_PER_FRAME (1152 * 2 * 2)
@@ -38,10 +38,11 @@ typedef struct {
assetfile_t file; assetfile_t file;
// Opaque per-platform decoder state - a reserved sceMp3 handle plus its // Opaque per-platform decoder state - a reserved sceMp3 handle plus its
// buffers on PSP, or an mp3dec_t plus a sliding compressed-byte window on // buffers on PSP, or libmad's mad_stream/mad_frame/mad_synth state plus a
// the minimp3-based software backend (Linux/Dolphin). Defined by // sliding compressed-byte window on the libmad-based software backend
// whichever audiostreammp3decoder.h is actually visible when this file // (Linux/Dolphin). Defined by whichever audiostreammp3decoder.h is
// is compiled - see that header's own comment. // actually visible when this file is compiled - see that header's own
// comment.
audiostreammp3decoder_t decoder; audiostreammp3decoder_t decoder;
// One decoded MPEG frame's worth of PCM, held here across Read() calls // One decoded MPEG frame's worth of PCM, held here across Read() calls
+131 -63
View File
@@ -5,9 +5,9 @@
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
#define MINIMP3_IMPLEMENTATION
#include "audiostreammp3decodersw.h" #include "audiostreammp3decodersw.h"
#include "audiostream.h" #include "audiostream.h"
#include "audiostreammp3.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
@@ -16,7 +16,9 @@ errorret_t audioStreamMp3DecoderInit(audiostream_t *stream) {
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
audiostreammp3decoder_t *decoder = &stream->mp3.decoder; audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
mp3dec_init(&decoder->decoder); mad_stream_init(&decoder->stream);
mad_frame_init(&decoder->frame);
mad_synth_init(&decoder->synth);
decoder->bufferFilled = 0; decoder->bufferFilled = 0;
decoder->endOfFile = false; decoder->endOfFile = false;
@@ -27,6 +29,10 @@ errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL."); assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
mad_frame_finish(&decoder->frame);
mad_stream_finish(&decoder->stream);
errorOk(); errorOk();
} }
@@ -35,18 +41,77 @@ errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream) {
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
const assetmp3file_t *mp3 = &stream->asset->data.mp3; const assetmp3file_t *mp3 = &stream->asset->data.mp3;
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
errorChain(assetFileRewind(&stream->mp3.file)); errorChain(assetFileRewind(&stream->mp3.file));
errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset)); errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset));
mp3dec_init(&decoder->decoder); errorChain(audioStreamMp3DecoderDispose(stream));
decoder->bufferFilled = 0; errorChain(audioStreamMp3DecoderInit(stream));
decoder->endOfFile = false;
errorOk(); errorOk();
} }
errorret_t audioStreamMp3DecoderRefill(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
// Shift whatever libmad hasn't consumed yet (it tracks this itself via
// next_frame, since it never copies our buffer - just holds pointers
// into it) to the front before appending more.
size_t leftover = 0;
if(decoder->stream.next_frame != NULL) {
leftover = (size_t) (decoder->stream.bufend - decoder->stream.next_frame);
if(leftover > 0) {
memoryMove(decoder->buffer, decoder->stream.next_frame, leftover);
}
}
decoder->bufferFilled = leftover;
if(!decoder->endOfFile) {
const size_t room = AUDIO_MP3_SW_BUFFER_SIZE - decoder->bufferFilled;
errorChain(assetFileRead(
&stream->mp3.file, decoder->buffer + decoder->bufferFilled, room
));
const size_t bytesRead = (size_t) stream->mp3.file.lastRead;
decoder->bufferFilled += bytesRead;
if(bytesRead < room) decoder->endOfFile = true;
}
if(decoder->bufferFilled == 0 && decoder->endOfFile) {
// Genuinely nothing left, not even a leftover partial frame - don't
// bother re-buffering an empty (plus guard) window.
errorOk();
}
size_t bufferLength = decoder->bufferFilled;
if(decoder->endOfFile) {
// libmad may read up to MAD_BUFFER_GUARD bytes past the end of valid
// data while decoding the last real frame(s) - pad with zeros so that
// never reads past this array.
memoryZero(decoder->buffer + decoder->bufferFilled, MAD_BUFFER_GUARD);
bufferLength += MAD_BUFFER_GUARD;
}
mad_stream_buffer(&decoder->stream, decoder->buffer, bufferLength);
errorOk();
}
int16_t audioStreamMp3DecoderScale(mad_fixed_t sample) {
// Round to nearest, then clamp to +/-1.0 rather than letting an
// out-of-range sample wrap - the standard scale() conversion from
// libmad's own reference examples (minimad.c et al.).
sample += (1L << (MAD_F_FRACBITS - 16));
if(sample >= MAD_F_ONE) {
sample = MAD_F_ONE - 1;
} else if(sample < -MAD_F_ONE) {
sample = -MAD_F_ONE;
}
return (int16_t) (sample >> (MAD_F_FRACBITS + 1 - 16));
}
errorret_t audioStreamMp3DecoderDecodeFrame( errorret_t audioStreamMp3DecoderDecodeFrame(
audiostream_t *stream, audiostream_t *stream,
int16_t *out, int16_t *out,
@@ -59,64 +124,67 @@ errorret_t audioStreamMp3DecoderDecodeFrame(
audiostreammp3decoder_t *decoder = &stream->mp3.decoder; audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
for(;;) { // Prime the window on this stream's very first-ever call - mad_frame_
mp3dec_frame_info_t info; // decode() assumes mad_stream_buffer() has already been called at least
memoryZero(&info, sizeof(info)); // once. Every subsequent refill is triggered reactively below, only when
const int samples = mp3dec_decode_frame( // a decode attempt itself reports it ran out of buffered data
&decoder->decoder, decoder->buffer, (int) decoder->bufferFilled, out, &info // (MAD_ERROR_BUFLEN) - checking decoder->stream.error as a general loop
); // guard instead isn't reliable, since nothing guarantees a successful
// decode leaves it holding any particular value.
if(samples > 0) { if(decoder->stream.buffer == NULL) {
// Shift the consumed bytes (the decoded frame, and any garbage errorChain(audioStreamMp3DecoderRefill(stream));
// minimp3 skipped before it) out of the front of the window - unless if(decoder->bufferFilled == 0 && decoder->endOfFile) {
// it consumed the whole thing, in which case there's nothing left to
// shift (memoryMove() disallows a 0-byte move).
const size_t remaining = decoder->bufferFilled - (size_t) info.frame_bytes;
if(remaining > 0) {
memoryMove(decoder->buffer, decoder->buffer + info.frame_bytes, remaining);
}
decoder->bufferFilled = remaining;
*outFrames = (size_t) samples;
errorOk();
}
// minimp3 didn't decode anything this call - either it found genuine
// garbage ahead of a frame (info.frame_bytes > 0, e.g. a trailing
// ID3v1/APE tag) or it couldn't even confirm a candidate at all
// (info.frame_bytes == 0). Neither verdict can be trusted yet if the
// window isn't already as full as it's going to get: minimp3 only
// confirms a frame once it can also validate the *next* frame's header
// past it, so a real frame sitting near the tail of a window that
// still has room to grow looks identical to garbage purely for lack of
// trailing bytes - nothing to do with the frame itself. Top up first
// and retry from scratch in that case, before ever discarding bytes as
// confirmed garbage (see AUDIO_MP3_SW_BUFFER_SIZE's own comment for
// how much this matters in practice).
if(decoder->bufferFilled < AUDIO_MP3_SW_BUFFER_SIZE && !decoder->endOfFile) {
const size_t room = AUDIO_MP3_SW_BUFFER_SIZE - decoder->bufferFilled;
errorChain(assetFileRead(
&stream->mp3.file, decoder->buffer + decoder->bufferFilled, room
));
const size_t bytesRead = (size_t) stream->mp3.file.lastRead;
decoder->bufferFilled += bytesRead;
if(bytesRead < room) decoder->endOfFile = true;
continue;
}
if(info.frame_bytes > 0) {
// The window is as full as it'll ever get (or the file is
// exhausted) and minimp3 still couldn't confirm a frame here -
// genuinely garbage. Discard and retry.
const size_t remaining = decoder->bufferFilled - (size_t) info.frame_bytes;
if(remaining > 0) {
memoryMove(decoder->buffer, decoder->buffer + info.frame_bytes, remaining);
}
decoder->bufferFilled = remaining;
continue;
}
// Nothing to skip and nothing left to add - genuinely exhausted.
*outFrames = 0; *outFrames = 0;
errorOk(); errorOk();
} }
} }
for(;;) {
if(mad_frame_decode(&decoder->frame, &decoder->stream) == -1) {
if(decoder->stream.error == MAD_ERROR_BUFLEN) {
// Needs more data than is currently buffered - if the file's also
// exhausted, there will never be more, so this is a genuine end of
// stream; otherwise refill and retry.
if(decoder->endOfFile) {
*outFrames = 0;
errorOk();
}
errorChain(audioStreamMp3DecoderRefill(stream));
if(decoder->bufferFilled == 0 && decoder->endOfFile) {
*outFrames = 0;
errorOk();
}
continue;
}
if(!MAD_RECOVERABLE(decoder->stream.error)) {
errorThrow("mad_frame_decode failed: 0x%04X", decoder->stream.error);
}
// Recoverable (e.g. MAD_ERROR_LOSTSYNC, a genuinely corrupt/non-frame
// byte run) - libmad already advanced stream.next_frame past the
// offending bytes internally; retry decoding from there.
continue;
}
mad_synth_frame(&decoder->synth, &decoder->frame);
const size_t frames = (size_t) decoder->synth.pcm.length;
const size_t channels = (size_t) MAD_NCHANNELS(&decoder->frame.header);
// `out` (stream->mp3.pending) is sized for
// AUDIO_MP3_MAX_SAMPLES_PER_FRAME samples - one MPEG frame is at most
// 1152 samples/channel, well within it.
assertTrue(
frames * channels <= AUDIO_MP3_MAX_SAMPLES_PER_FRAME,
"mad_synth_frame produced more PCM than the destination buffer can hold."
);
for(size_t i = 0; i < frames; i++) {
for(size_t c = 0; c < channels; c++) {
out[i * channels + c] = audioStreamMp3DecoderScale(
decoder->synth.pcm.samples[c][i]
);
}
}
*outFrames = frames;
errorOk();
}
}
+69 -41
View File
@@ -7,48 +7,49 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include <minimp3.h> #include <mad.h>
typedef struct audiostream_s audiostream_t; typedef struct audiostream_s audiostream_t;
// Size of the sliding compressed-byte window minimp3 decodes from. Must // Size of the sliding compressed-byte window libmad decodes from - just
// comfortably exceed the largest realistic single MPEG frame (a 320kbps // needs to comfortably exceed one MPEG frame's worth of bytes plus
// MPEG-1 frame is a little under 1045 bytes) with room to spare for // whatever bit-reservoir carryover libmad itself buffers internally
// refilling in reasonably-sized chunks rather than one frame at a time - // (MAD_BUFFER_MDLEN), with room to refill in reasonably-sized chunks
// but also, empirically, much bigger than that: minimp3 only reports a // rather than one frame at a time. Unlike this file's previous minimp3-
// frame as decoded once it can also confirm the *next* frame's header // based implementation, libmad reports "not enough data" (MAD_ERROR_BUFLEN)
// past it (mp3d_find_frame's lookahead), so a frame sitting near the tail // and "genuinely bad data" (every other error code) as distinct, explicit
// of a window that's otherwise full can be indistinguishable from genuine // signals rather than one overloaded return value - so, verified against a
// garbage purely for lack of trailing bytes to confirm against - nothing // real ~236s VBR file, this window doesn't need minimp3's 256KB (itself
// to do with frame size. Measured against a real ~236s VBR file: a 16KB // only ~99.7% accurate) to decode every frame correctly.
// window (this constant's original size) silently lost ~5.5% of frames #define AUDIO_MP3_SW_BUFFER_SIZE (32 * 1024)
// this way (heard as the whole stream finishing early/"racing", with a
// stutter at each drop); doubling the window roughly halves the loss, and
// it converges towards (but never quite reaches) zero. 256KB was chosen
// as landing past the point of diminishing returns (<0.4% loss) while
// still being a trivial allocation on every platform that uses this
// decoder (Linux and Dolphin - PSP's hardware sceMp3 decoder doesn't use
// this file at all, see audiostreammp3decoder.c's own top-of-file
// comment).
#define AUDIO_MP3_SW_BUFFER_SIZE (256 * 1024)
/** /**
* Software MP3 decoder state, shared by every platform that doesn't have * Software MP3 decoder state, shared by every platform that doesn't have
* (or doesn't use) a hardware MP3 decoder - currently Linux and Dolphin, * (or doesn't use) a hardware MP3 decoder - currently Linux and Dolphin,
* both via this same minimp3-based implementation * both via this same libmad-based implementation
* (audiostreammp3decodersw.c). See audiostreammp3.h for how this plugs * (audiostreammp3decodersw.c). See audiostreammp3.h for how this plugs
* into the shared MP3 stream layer, and audiostreammp3.h's own * into the shared MP3 stream layer, and audiostreammp3.h's own
* documentation of audioStreamMp3DecoderInit()/Dispose()/Rewind()/ * documentation of audioStreamMp3DecoderInit()/Dispose()/Rewind()/
* DecodeFrame() for the interface this and the PSP hardware backend * DecodeFrame() for the interface this and the PSP hardware backend
* (src/duskpsp/audio/audiostreammp3decoder.c) both implement. * (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 { typedef struct {
mp3dec_t decoder; struct mad_stream stream;
struct mad_frame frame;
struct mad_synth synth;
// minimp3 decides how many compressed bytes one frame consumed only // libmad reads up to MAD_BUFFER_GUARD bytes past whatever it's given as
// after attempting to decode it, so this holds however much hasn't been // the end of valid data while decoding the final real frame(s) in a
// consumed yet, refilled from the stream's assetfile_t as it drains. // buffer - the trailing MAD_BUFFER_GUARD bytes here are that padding
uint8_t buffer[AUDIO_MP3_SW_BUFFER_SIZE]; // (zeroed once endOfFile is set), never counted in bufferFilled.
uint8_t buffer[AUDIO_MP3_SW_BUFFER_SIZE + MAD_BUFFER_GUARD];
size_t bufferFilled; size_t bufferFilled;
// Set once assetFileRead() returns fewer bytes than requested - there's // Set once assetFileRead() returns fewer bytes than requested - there's
@@ -59,8 +60,8 @@ typedef struct {
/** /**
* Initializes the software MP3 decoder for the given stream - resets * Initializes the software MP3 decoder for the given stream - resets
* minimp3's internal state and the compressed-byte window (empty, not yet * libmad's stream/frame/synth state and the compressed-byte window (empty,
* filled from the asset). * not yet filled from the asset).
* *
* @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3. * @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3.
* @return Error indicating success or failure. * @return Error indicating success or failure.
@@ -68,9 +69,10 @@ typedef struct {
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream); errorret_t audioStreamMp3DecoderInit(audiostream_t *stream);
/** /**
* Disposes the software MP3 decoder for the given stream. A no-op beyond * Disposes the software MP3 decoder for the given stream: tears down
* that - minimp3 allocates nothing itself, and the compressed-byte window * libmad's stream/frame state (mad_stream_finish()/mad_frame_finish()) -
* lives inline in audiostreammp3decoder_t. * 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. * @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3.
* @return Error indicating success or failure. * @return Error indicating success or failure.
@@ -79,8 +81,8 @@ errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream);
/** /**
* Resets the decoder to the very start of the compressed stream: rewinds * Resets the decoder to the very start of the compressed stream: rewinds
* stream->mp3.file back to the asset's parsed data offset, clears minimp3's * stream->mp3.file back to the asset's parsed data offset, resets libmad's
* internal state and the compressed-byte window, so the next * stream/frame/synth state and the compressed-byte window, so the next
* DecodeFrame() call starts decoding from the first MPEG frame again. See * DecodeFrame() call starts decoding from the first MPEG frame again. See
* audioStreamMp3Seek()'s own comment on why every seek goes through here. * audioStreamMp3Seek()'s own comment on why every seek goes through here.
* *
@@ -89,15 +91,41 @@ errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream);
*/ */
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream); 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 * Decodes the next MPEG frame's worth of PCM samples, topping up the
* compressed-byte window from stream->mp3.file as needed. Writes decoded * compressed-byte window from stream->mp3.file as needed
* samples to `out` (sized for at least AUDIO_MP3_MAX_SAMPLES_PER_FRAME * (audioStreamMp3DecoderRefill()). Writes decoded samples to `out` (sized
* int16_t values) and sets *outFrames to how many frames (not samples) * for at least AUDIO_MP3_MAX_SAMPLES_PER_FRAME int16_t values) and sets
* were produced - 0 once the compressed stream is genuinely exhausted, * *outFrames to how many frames (not samples) were produced - 0 once the
* never negative (a corrupt/truncated stream is treated the same as a * compressed stream is genuinely exhausted, never negative (a corrupt/
* clean end, not an error, matching audioStreamPcmRead()'s own tolerance * truncated stream is treated the same as a clean end, not an error,
* for a short/truncated asset). * matching audioStreamPcmRead()'s own tolerance for a short/truncated
* asset).
* *
* @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3. * @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3.
* @param out Destination buffer for decoded samples. * @param out Destination buffer for decoded samples.
+5 -9
View File
@@ -12,17 +12,13 @@ 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) - use the
# same shared minimp3-based software backend as Linux, sourced directly # same shared libmad-based software backend as Linux, sourced directly
# from src/dusk/audio rather than through its own unconditional # from src/dusk/audio rather than through its own unconditional
# CMakeLists.txt so platforms with a hardware decoder (PSP) never pull # CMakeLists.txt so platforms with a hardware decoder (PSP) never pull
# minimp3 in at all. # libmad in at all. Unlike Linux, libmad here comes bundled with the
if(NOT minimp3_FOUND) # devkitPPC/libogc toolchain itself (see cmake/targets/dolphin.cmake's
find_package(minimp3 REQUIRED) # own "mad" link entry) rather than via find_package() - unnecessary
endif() # for, and not reliable when cross-compiling.
# PRIVATE to match this toolchain's own convention (see cmake/targets/
# dolphin.cmake) of avoiding PUBLIC library visibility, which has tripped
# up the PPC linker for other dependencies in the past.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE minimp3)
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
"${DUSK_SOURCES_DIR}/dusk/audio/stream/audiostreammp3decodersw.c" "${DUSK_SOURCES_DIR}/dusk/audio/stream/audiostreammp3decodersw.c"
@@ -11,6 +11,9 @@
// architecture (libogc's MP3Player wraps libmad but drives its own // architecture (libogc's MP3Player wraps libmad but drives its own
// internal audio output, bypassing the ansnd-based pipeline // internal audio output, bypassing the ansnd-based pipeline
// audiostreamdolphin.c already owns - not a fit here), so Dolphin uses the // audiostreamdolphin.c already owns - not a fit here), so Dolphin uses the
// same shared minimp3-based software backend as Linux - see that header's // same shared libmad-based software backend as Linux, driven directly at
// own documentation for the actual struct/interface. // the mad_stream/mad_frame/mad_synth level instead of through MP3Player -
// see that header's own documentation for the actual struct/interface.
// Conveniently, libmad ships as part of libogc itself (gc/mad.h) rather
// than needing to be fetched separately, unlike on Linux.
#include "audio/stream/audiostreammp3decodersw.h" #include "audio/stream/audiostreammp3decodersw.h"
+7 -5
View File
@@ -10,14 +10,16 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
audiostreamlinux.c audiostreamlinux.c
) )
# No hardware MP3 decoder on Linux - use the shared minimp3-based software # No hardware MP3 decoder on Linux - use the shared libmad-based software
# backend (also used by Dolphin), sourced directly from src/dusk/audio # backend (also used by Dolphin), sourced directly from src/dusk/audio
# rather than through its own unconditional CMakeLists.txt so platforms # rather than through its own unconditional CMakeLists.txt so platforms
# with a hardware decoder (PSP) never pull minimp3 in at all. # with a hardware decoder (PSP) never pull libmad in at all. See
if(NOT minimp3_FOUND) # audiostreammp3decodersw.h's own comment on why libmad (GPL-licensed)
find_package(minimp3 REQUIRED) # replaced this project's original minimp3-based implementation.
if(NOT libmad_FOUND)
find_package(libmad REQUIRED)
endif() endif()
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC minimp3) target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC libmad::mad)
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
"${DUSK_SOURCES_DIR}/dusk/audio/stream/audiostreammp3decodersw.c" "${DUSK_SOURCES_DIR}/dusk/audio/stream/audiostreammp3decodersw.c"
+4 -2
View File
@@ -8,6 +8,8 @@
#pragma once #pragma once
// Linux has no hardware MP3 decoder to prefer, so it uses the shared // Linux has no hardware MP3 decoder to prefer, so it uses the shared
// minimp3-based software backend (also used by Dolphin) - see that // libmad-based software backend (also used by Dolphin) - see that
// header's own documentation for the actual struct/interface. // 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 "audio/stream/audiostreammp3decodersw.h"