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]>
114 lines
4.1 KiB
C
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);
|