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
|
z
|
||||||
lzma
|
lzma
|
||||||
ansnd
|
ansnd
|
||||||
|
mad
|
||||||
)
|
)
|
||||||
|
|
||||||
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
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]
|
((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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user