diff --git a/cmake/modules/Findlibmad.cmake b/cmake/modules/Findlibmad.cmake new file mode 100644 index 00000000..b0c3d020 --- /dev/null +++ b/cmake/modules/Findlibmad.cmake @@ -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() diff --git a/cmake/modules/Findminimp3.cmake b/cmake/modules/Findminimp3.cmake deleted file mode 100644 index afa42644..00000000 --- a/cmake/modules/Findminimp3.cmake +++ /dev/null @@ -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) diff --git a/cmake/targets/dolphin.cmake b/cmake/targets/dolphin.cmake index 5e84a6ec..e7e813ca 100644 --- a/cmake/targets/dolphin.cmake +++ b/cmake/targets/dolphin.cmake @@ -62,6 +62,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE z lzma ansnd + mad ) if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") diff --git a/src/dusk/asset/loader/mp3/assetmp3loader.c b/src/dusk/asset/loader/mp3/assetmp3loader.c index 75262724..0c82082e 100644 --- a/src/dusk/asset/loader/mp3/assetmp3loader.c +++ b/src/dusk/asset/loader/mp3/assetmp3loader.c @@ -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 diff --git a/src/dusk/audio/stream/audiostreammp3.h b/src/dusk/audio/stream/audiostreammp3.h index 92651126..a7b5563f 100644 --- a/src/dusk/audio/stream/audiostreammp3.h +++ b/src/dusk/audio/stream/audiostreammp3.h @@ -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 diff --git a/src/dusk/audio/stream/audiostreammp3decodersw.c b/src/dusk/audio/stream/audiostreammp3decodersw.c index 5f5d62b8..c99b922f 100644 --- a/src/dusk/audio/stream/audiostreammp3decodersw.c +++ b/src/dusk/audio/stream/audiostreammp3decodersw.c @@ -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(); } } diff --git a/src/dusk/audio/stream/audiostreammp3decodersw.h b/src/dusk/audio/stream/audiostreammp3decodersw.h index b691e130..4f4eb58e 100644 --- a/src/dusk/audio/stream/audiostreammp3decodersw.h +++ b/src/dusk/audio/stream/audiostreammp3decodersw.h @@ -7,48 +7,49 @@ #pragma once #include "error/error.h" -#include +#include 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. diff --git a/src/duskdolphin/audio/CMakeLists.txt b/src/duskdolphin/audio/CMakeLists.txt index af649f1c..2fc45395 100644 --- a/src/duskdolphin/audio/CMakeLists.txt +++ b/src/duskdolphin/audio/CMakeLists.txt @@ -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" diff --git a/src/duskdolphin/audio/audiostreammp3decoder.h b/src/duskdolphin/audio/audiostreammp3decoder.h index a3e48f4f..ec9c0f76 100644 --- a/src/duskdolphin/audio/audiostreammp3decoder.h +++ b/src/duskdolphin/audio/audiostreammp3decoder.h @@ -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" diff --git a/src/dusklinux/audio/CMakeLists.txt b/src/dusklinux/audio/CMakeLists.txt index f7ae6cca..c6c014d7 100644 --- a/src/dusklinux/audio/CMakeLists.txt +++ b/src/dusklinux/audio/CMakeLists.txt @@ -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" diff --git a/src/dusklinux/audio/audiostreammp3decoder.h b/src/dusklinux/audio/audiostreammp3decoder.h index 06dad99d..8a268837 100644 --- a/src/dusklinux/audio/audiostreammp3decoder.h +++ b/src/dusklinux/audio/audiostreammp3decoder.h @@ -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"