Files
dusk/src/dusk/asset/loader/mp3/assetmp3loader.h
T
YourWishesandClaude Sonnet 5 f8f8a80a21 Add MP3 audio stream support with hardware/software decoder backends
New ASSET_LOADER_TYPE_MP3 (hand-rolled MPEG-1/2/2.5 Layer III header
parser - no third-party dependency needed just for metadata, since PSP's
hardware path doesn't need one at all) plus a shared audiostreammp3.c
stream layer mirroring audiostreampcm.c's shape. Generalized the stream
dispatch (hoisted sampleRate/channels onto audiostream_t, added
audioStreamGetTotalFrames()/Seek()/Read()) so all three platform audio
backends keep working unchanged, just calling the generic names instead
of PCM-specific ones.

Two decoder backends behind one interface: PSP uses the real sceMp3
hardware decoder (firmware-offloaded, lazily initialized on first use);
Linux and Dolphin share one minimp3-based software decoder (public
domain, vendored via CMake FetchContent) - libogc's own MP3Player wraps
libmad (GPL) and drives its own output pipeline, not a fit for the
ansnd-based architecture already in place, so skipped in favor of the
shared minimp3 path.

Fixed three real bugs found via hardware/runtime testing along the way:
- LAME's Xing header counts its own placeholder frame in the declared
  total, which made playback stall permanently one frame short of the
  declared end (looked like "never loops") - fixed by subtracting it.
- sceMp3Decode() can return more PCM than one MPEG frame's worth in a
  single call (PSP's pcmBuf is provisioned for 2x), overflowing the
  shared per-frame decode buffer with no bound check - very intermittent
  corruption/clicking on real hardware. Widened the buffer to the real
  worst case and added an assertion.
- sceMp3ResetPlayPosition()'s exact internal reset semantics aren't
  documented precisely enough to trust for looping - occasionally
  disagreed with the fresh stream position fed right after, clicking at
  the loop boundary about 1 in 3-4 loops. Rewind now fully tears down and
  recreates the decoder instead, the same path already proven correct at
  first Init. Also widened the PSP ring buffer to absorb that now-heavier
  operation, capping each top-up call's own work so the bigger buffer
  doesn't turn into one long blocking decode burst instead.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-01 08:21:30 -05:00

114 lines
4.1 KiB
C

/**
* 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 "asset/assetfile.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetmp3loaderinput_t;
typedef enum {
ASSET_MP3_LOADER_STATE_INITIAL,
ASSET_MP3_LOADER_STATE_READ_HEADER,
ASSET_MP3_LOADER_STATE_DONE
} assetmp3loaderstate_t;
typedef struct {
assetmp3loaderstate_t state;
} assetmp3loaderloading_t;
/**
* Parsed metadata for an MP3 asset - deliberately just enough to configure
* a decoder and know the clip's total length, not anything about its
* actual compressed content (which is read/decoded on demand at playback
* time - see audiostreammp3.c). Only MPEG-1/2/2.5 Layer III ("MP3" in the
* everyday sense) is supported; Layers I/II are rejected.
*/
typedef struct {
uint32_t sampleRate;
uint8_t channels;
// 1152 for MPEG-1, 576 for MPEG-2/2.5 - how many PCM samples (per
// channel) one compressed MPEG frame decodes to.
uint16_t samplesPerFrame;
// The first frame's bitrate, in kbps. Used only as a fallback duration
// estimate (assuming CBR) when no Xing/Info header is found - see
// assetMp3ParseHeader()'s own comment.
uint32_t bitrateKbps;
// Byte offset (from the start of the file) of the first real MPEG
// frame, i.e. right after any leading ID3v2 tag.
size_t dataOffset;
// Bytes of compressed MPEG data, from dataOffset to the end of the file.
size_t dataSize;
// Decoded PCM frame count for the whole clip - exact if a Xing/Info
// header was found (the common case for anything encoded with a modern
// tool), otherwise estimated from bitrateKbps/dataSize assuming CBR (see
// assetMp3ParseHeader()'s own comment) - unlike assetwavfile_t.dataSize,
// MP3 has no header field that gives this directly.
size_t totalFrames;
} assetmp3file_t;
typedef assetmp3file_t assetmp3output_t;
/**
* Async (background-thread) half of the MP3 loader - opens the asset file,
* parses its header (see assetMp3ParseHeader()), and closes it again. Never
* buffers the compressed MPEG data itself; that's read on demand at
* playback time by audiostreammp3.c via its own independent file handle.
*
* @param loading The asset loading slot.
* @return Error indicating success or failure.
*/
errorret_t assetMp3LoaderAsync(assetloading_t *loading);
/**
* Sync (main-thread) half of the MP3 loader - a simple state machine that
* schedules the async header parse and marks the entry loaded once it's
* done, mirroring assetWavLoaderSync()'s shape exactly.
*
* @param loading The asset loading slot.
* @return Error indicating success or failure.
*/
errorret_t assetMp3LoaderSync(assetloading_t *loading);
/**
* Disposes an MP3 asset entry. A no-op - an assetmp3file_t owns no heap
* allocations, and playback streams open their own independent file
* handles (see audiostreammp3.c).
*
* @param entry The asset entry to dispose.
* @return Error indicating success or failure.
*/
errorret_t assetMp3Dispose(assetentry_t *entry);
/**
* Parses an MP3 file's metadata from an already-opened asset file
* positioned at its very start: skips a leading ID3v2 tag if present,
* scans forward for the first valid MPEG-1/2/2.5 Layer III frame header
* (rejecting anything else - other layers, or no valid frame found within
* a sane search window), and checks that frame for a Xing/Info header
* (found in the vast majority of real-world MP3s, VBR or CBR) to get an
* exact total-frame count; falls back to a CBR estimate from the first
* frame's bitrate and the file's remaining size if absent.
*
* Never reads more than a bounded lookahead window into memory - the bulk
* of the file (the actual compressed audio, following the parsed frame's
* position) is left untouched, exactly like assetWavParseHeader().
*
* @param file Asset file to parse, positioned at offset 0.
* @param mp3File Filled with the parsed metadata on success.
* @return Error indicating success or failure.
*/
errorret_t assetMp3ParseHeader(assetfile_t *file, assetmp3file_t *mp3File);