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:
@@ -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()
|
||||
@@ -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)
|
||||
@@ -62,6 +62,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||
z
|
||||
lzma
|
||||
ansnd
|
||||
mad
|
||||
)
|
||||
|
||||
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
||||
|
||||
@@ -191,7 +191,7 @@ errorret_t assetMp3ParseHeader(assetfile_t *file, assetmp3file_t *mp3File) {
|
||||
((uint32_t) framesBytes[2] << 8) | (uint32_t) framesBytes[3]
|
||||
);
|
||||
// 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
|
||||
// real decodable frame of audio the way every other one does.
|
||||
// Confirmed against a real LAME-encoded file: its Xing header
|
||||
|
||||
@@ -17,7 +17,7 @@ typedef struct audiostream_s audiostream_t;
|
||||
// allowed to produce. Sizes the pending-sample buffer below, which every
|
||||
// decoder backend writes directly into - this must be sized to the
|
||||
// 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.
|
||||
// - sceMp3 (hardware, PSP): its own pcmBuf is provisioned at double that
|
||||
// (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
|
||||
// very intermittent audio corruption/clicking, since it only bit when
|
||||
// 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.
|
||||
#define AUDIO_MP3_MAX_SAMPLES_PER_FRAME (1152 * 2 * 2)
|
||||
|
||||
@@ -38,10 +38,11 @@ typedef struct {
|
||||
assetfile_t file;
|
||||
|
||||
// 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
|
||||
// the minimp3-based software backend (Linux/Dolphin). Defined by
|
||||
// whichever audiostreammp3decoder.h is actually visible when this file
|
||||
// is compiled - see that header's own comment.
|
||||
// buffers on PSP, or libmad's mad_stream/mad_frame/mad_synth state plus a
|
||||
// sliding compressed-byte window on the libmad-based software backend
|
||||
// (Linux/Dolphin). Defined by whichever audiostreammp3decoder.h is
|
||||
// actually visible when this file is compiled - see that header's own
|
||||
// comment.
|
||||
audiostreammp3decoder_t decoder;
|
||||
|
||||
// One decoded MPEG frame's worth of PCM, held here across Read() calls
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#define MINIMP3_IMPLEMENTATION
|
||||
#include "audiostreammp3decodersw.h"
|
||||
#include "audiostream.h"
|
||||
#include "audiostreammp3.h"
|
||||
#include "assert/assert.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.");
|
||||
|
||||
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->endOfFile = false;
|
||||
|
||||
@@ -27,6 +29,10 @@ errorret_t audioStreamMp3DecoderDispose(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;
|
||||
mad_frame_finish(&decoder->frame);
|
||||
mad_stream_finish(&decoder->stream);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -35,18 +41,77 @@ errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream) {
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
const assetmp3file_t *mp3 = &stream->asset->data.mp3;
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
|
||||
errorChain(assetFileRewind(&stream->mp3.file));
|
||||
errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset));
|
||||
|
||||
mp3dec_init(&decoder->decoder);
|
||||
decoder->bufferFilled = 0;
|
||||
decoder->endOfFile = false;
|
||||
errorChain(audioStreamMp3DecoderDispose(stream));
|
||||
errorChain(audioStreamMp3DecoderInit(stream));
|
||||
|
||||
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(
|
||||
audiostream_t *stream,
|
||||
int16_t *out,
|
||||
@@ -59,64 +124,67 @@ errorret_t audioStreamMp3DecoderDecodeFrame(
|
||||
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
|
||||
for(;;) {
|
||||
mp3dec_frame_info_t info;
|
||||
memoryZero(&info, sizeof(info));
|
||||
const int samples = mp3dec_decode_frame(
|
||||
&decoder->decoder, decoder->buffer, (int) decoder->bufferFilled, out, &info
|
||||
);
|
||||
|
||||
if(samples > 0) {
|
||||
// Shift the consumed bytes (the decoded frame, and any garbage
|
||||
// minimp3 skipped before it) out of the front of the window - unless
|
||||
// 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;
|
||||
// Prime the window on this stream's very first-ever call - mad_frame_
|
||||
// decode() assumes mad_stream_buffer() has already been called at least
|
||||
// once. Every subsequent refill is triggered reactively below, only when
|
||||
// a decode attempt itself reports it ran out of buffered data
|
||||
// (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(decoder->stream.buffer == NULL) {
|
||||
errorChain(audioStreamMp3DecoderRefill(stream));
|
||||
if(decoder->bufferFilled == 0 && decoder->endOfFile) {
|
||||
*outFrames = 0;
|
||||
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);
|
||||
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;
|
||||
}
|
||||
decoder->bufferFilled = remaining;
|
||||
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;
|
||||
}
|
||||
|
||||
// Nothing to skip and nothing left to add - genuinely exhausted.
|
||||
*outFrames = 0;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,48 +7,49 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include <minimp3.h>
|
||||
#include <mad.h>
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
// Size of the sliding compressed-byte window minimp3 decodes from. Must
|
||||
// comfortably exceed the largest realistic single MPEG frame (a 320kbps
|
||||
// MPEG-1 frame is a little under 1045 bytes) with room to spare for
|
||||
// refilling in reasonably-sized chunks rather than one frame at a time -
|
||||
// but also, empirically, much bigger than that: minimp3 only reports a
|
||||
// frame as decoded once it can also confirm the *next* frame's header
|
||||
// past it (mp3d_find_frame's lookahead), so a frame sitting near the tail
|
||||
// of a window that's otherwise full can be indistinguishable from genuine
|
||||
// garbage purely for lack of trailing bytes to confirm against - nothing
|
||||
// to do with frame size. Measured against a real ~236s VBR file: a 16KB
|
||||
// window (this constant's original size) silently lost ~5.5% of frames
|
||||
// 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)
|
||||
// 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 minimp3-based implementation
|
||||
* 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 {
|
||||
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
|
||||
// after attempting to decode it, so this holds however much hasn't been
|
||||
// consumed yet, refilled from the stream's assetfile_t as it drains.
|
||||
uint8_t buffer[AUDIO_MP3_SW_BUFFER_SIZE];
|
||||
// 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
|
||||
@@ -59,8 +60,8 @@ typedef struct {
|
||||
|
||||
/**
|
||||
* Initializes the software MP3 decoder for the given stream - resets
|
||||
* minimp3's internal state and the compressed-byte window (empty, not yet
|
||||
* filled from the asset).
|
||||
* 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.
|
||||
@@ -68,9 +69,10 @@ typedef struct {
|
||||
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Disposes the software MP3 decoder for the given stream. A no-op beyond
|
||||
* that - minimp3 allocates nothing itself, and the compressed-byte window
|
||||
* lives inline in audiostreammp3decoder_t.
|
||||
* 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.
|
||||
@@ -79,8 +81,8 @@ 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, clears minimp3's
|
||||
* internal state and the compressed-byte window, so the next
|
||||
* 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.
|
||||
*
|
||||
@@ -89,15 +91,41 @@ errorret_t audioStreamMp3DecoderDispose(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
|
||||
* compressed-byte window from stream->mp3.file as needed. 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).
|
||||
* 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.
|
||||
|
||||
@@ -12,17 +12,13 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# CMakeLists.txt so platforms with a hardware decoder (PSP) never pull
|
||||
# minimp3 in at all.
|
||||
if(NOT minimp3_FOUND)
|
||||
find_package(minimp3 REQUIRED)
|
||||
endif()
|
||||
# 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)
|
||||
# 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"
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
// architecture (libogc's MP3Player wraps libmad but drives its own
|
||||
// internal audio output, bypassing the ansnd-based pipeline
|
||||
// audiostreamdolphin.c already owns - not a fit here), so Dolphin uses the
|
||||
// same shared minimp3-based software backend as Linux - see that header's
|
||||
// own documentation for the actual struct/interface.
|
||||
// same shared libmad-based software backend as Linux, driven directly at
|
||||
// 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"
|
||||
|
||||
@@ -10,14 +10,16 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
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
|
||||
# rather than through its own unconditional CMakeLists.txt so platforms
|
||||
# with a hardware decoder (PSP) never pull minimp3 in at all.
|
||||
if(NOT minimp3_FOUND)
|
||||
find_package(minimp3 REQUIRED)
|
||||
# with a hardware decoder (PSP) never pull libmad in at all. See
|
||||
# audiostreammp3decodersw.h's own comment on why libmad (GPL-licensed)
|
||||
# replaced this project's original minimp3-based implementation.
|
||||
if(NOT libmad_FOUND)
|
||||
find_package(libmad REQUIRED)
|
||||
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}
|
||||
PUBLIC
|
||||
"${DUSK_SOURCES_DIR}/dusk/audio/stream/audiostreammp3decodersw.c"
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#pragma once
|
||||
|
||||
// Linux has no hardware MP3 decoder to prefer, so it uses the shared
|
||||
// minimp3-based software backend (also used by Dolphin) - see that
|
||||
// header's own documentation for the actual struct/interface.
|
||||
// libmad-based software backend (also used by Dolphin) - see that
|
||||
// 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"
|
||||
|
||||
Reference in New Issue
Block a user