/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #pragma once #include "error/error.h" #include typedef struct audiostream_s audiostream_t; // Size of the sliding compressed-byte window libmad decodes from - just // needs to comfortably exceed one MPEG frame's worth of bytes plus // whatever bit-reservoir carryover libmad itself buffers internally // (MAD_BUFFER_MDLEN), with room to refill in reasonably-sized chunks // rather than one frame at a time. Unlike this file's previous minimp3- // based implementation, libmad reports "not enough data" (MAD_ERROR_BUFLEN) // and "genuinely bad data" (every other error code) as distinct, explicit // signals rather than one overloaded return value - so, verified against a // real ~236s VBR file, this window doesn't need minimp3's 256KB (itself // only ~99.7% accurate) to decode every frame correctly. #define AUDIO_MP3_SW_BUFFER_SIZE (32 * 1024) /** * Software MP3 decoder state, shared by every platform that doesn't have * (or doesn't use) a hardware MP3 decoder - currently Linux and Dolphin, * both via this same libmad-based implementation * (audiostreammp3decodersw.c). See audiostreammp3.h for how this plugs * into the shared MP3 stream layer, and audiostreammp3.h's own * documentation of audioStreamMp3DecoderInit()/Dispose()/Rewind()/ * DecodeFrame() for the interface this and the PSP hardware backend * (src/duskpsp/audio/audiostreammp3decoder.c) both implement. * * Note this is libmad (https://www.underbit.com/products/mad/), used here * for its bitstream-accurate frame sync (see AUDIO_MP3_SW_BUFFER_SIZE's own * comment) - unlike the rest of this project, libmad is GPL-licensed, not * MIT. Any binary linking this file's compiled output must comply with the * GPL; a deliberate tradeoff made after minimp3 proved unable to decode a * real-world VBR file without audible, unresolvable data loss. */ typedef struct { struct mad_stream stream; struct mad_frame frame; struct mad_synth synth; // libmad reads up to MAD_BUFFER_GUARD bytes past whatever it's given as // the end of valid data while decoding the final real frame(s) in a // buffer - the trailing MAD_BUFFER_GUARD bytes here are that padding // (zeroed once endOfFile is set), never counted in bufferFilled. uint8_t buffer[AUDIO_MP3_SW_BUFFER_SIZE + MAD_BUFFER_GUARD]; size_t bufferFilled; // Set once assetFileRead() returns fewer bytes than requested - there's // no more compressed data to refill `buffer` with, though whatever's // still in it may still decode into one or more final frames. bool_t endOfFile; } audiostreammp3decoder_t; /** * Initializes the software MP3 decoder for the given stream - resets * libmad's stream/frame/synth state and the compressed-byte window (empty, * not yet filled from the asset). * * @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3. * @return Error indicating success or failure. */ errorret_t audioStreamMp3DecoderInit(audiostream_t *stream); /** * Disposes the software MP3 decoder for the given stream: tears down * libmad's stream/frame state (mad_stream_finish()/mad_frame_finish()) - * mad_synth carries no allocated state of its own, so there's nothing to * release for it. * * @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3. * @return Error indicating success or failure. */ errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream); /** * Resets the decoder to the very start of the compressed stream: rewinds * stream->mp3.file back to the asset's parsed data offset, resets libmad's * stream/frame/synth state and the compressed-byte window, so the next * DecodeFrame() call starts decoding from the first MPEG frame again. See * audioStreamMp3Seek()'s own comment on why every seek goes through here. * * @param stream The audio stream to rewind. Must be AUDIO_STREAM_TYPE_MP3. * @return Error indicating success or failure. */ errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream); /** * Tops up the compressed-byte window from stream->mp3.file: shifts * whatever libmad hasn't consumed yet (from its own stream.next_frame * cursor) to the front of the window, reads as much more as there's room * for, and re-buffers libmad's stream (mad_stream_buffer()) over the * result - padding with MAD_BUFFER_GUARD zero bytes once the file's * genuinely exhausted, since libmad may read slightly past the end of * valid data while decoding a final frame. * * @param stream The audio stream to refill. Must be AUDIO_STREAM_TYPE_MP3. * @return Error indicating success or failure. */ errorret_t audioStreamMp3DecoderRefill(audiostream_t *stream); /** * Converts one libmad fixed-point PCM sample to a clamped 16-bit sample - * the standard scale() conversion from libmad's own reference examples * (minimad.c et al.), rounding to nearest and clamping to +/-1.0 rather * than wrapping on overflow. * * @param sample The fixed-point sample to convert (mad_fixed_t, * Q1.MAD_F_FRACBITS). * @return The converted 16-bit signed sample. */ int16_t audioStreamMp3DecoderScale(mad_fixed_t sample); /** * Decodes the next MPEG frame's worth of PCM samples, topping up the * compressed-byte window from stream->mp3.file as needed * (audioStreamMp3DecoderRefill()). Writes decoded samples to `out` (sized * for at least AUDIO_MP3_MAX_SAMPLES_PER_FRAME int16_t values) and sets * *outFrames to how many frames (not samples) were produced - 0 once the * compressed stream is genuinely exhausted, never negative (a corrupt/ * truncated stream is treated the same as a clean end, not an error, * matching audioStreamPcmRead()'s own tolerance for a short/truncated * asset). * * @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3. * @param out Destination buffer for decoded samples. * @param outFrames Set to the number of frames actually decoded. * @return Error indicating success or failure. */ errorret_t audioStreamMp3DecoderDecodeFrame( audiostream_t *stream, int16_t *out, size_t *outFrames );