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]>
This commit is contained in:
2026-09-01 08:21:30 -05:00
co-authored by Claude Sonnet 5
parent ac8023d50f
commit f8f8a80a21
31 changed files with 1530 additions and 72 deletions
+18
View File
@@ -9,3 +9,21 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
audiodolphin.c
audiostreamdolphin.c
)
# 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
# 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)
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
"${DUSK_SOURCES_DIR}/dusk/audio/audiostreammp3decodersw.c"
)
+8 -8
View File
@@ -47,8 +47,8 @@ errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
const size_t totalFrames = audioStreamPcmGetTotalFrames(stream);
const size_t frameSize = stream->channels * sizeof(int16_t);
const size_t totalFrames = audioStreamGetTotalFrames(stream);
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) the DSP wraps within, once looping is enabled -
@@ -56,10 +56,10 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
// behaviour is unchanged when no explicit loop points are configured.
// Same math as PSP/Linux - see their own comments.
const size_t loopEndFrame = stream->loopStart >= 0
? mathMin((size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames)
? mathMin((size_t) (stream->loopStart * stream->sampleRate), totalFrames)
: totalFrames;
const size_t loopToFrame = mathMin(
(size_t) (stream->loopTo * stream->pcm.sampleRate), loopEndFrame
(size_t) (stream->loopTo * stream->sampleRate), loopEndFrame
);
const size_t startFrame = mathMin(stream->startFrame, totalFrames);
@@ -90,10 +90,10 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
stream->platform.buffer = NULL;
}
errorChain(audioStreamPcmSeek(stream, 0));
errorChain(audioStreamSeek(stream, 0));
uint8_t *buffer = (uint8_t *) memoryAllocate(bufferFrames * frameSize);
size_t framesRead = 0;
errorret_t readRet = audioStreamPcmRead(
errorret_t readRet = audioStreamRead(
stream, (int16_t *) buffer, bufferFrames, &framesRead
);
if(errorIsNotOk(readRet)) {
@@ -111,9 +111,9 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
ansnd_pcm_voice_config_t config;
memoryZero(&config, sizeof(ansnd_pcm_voice_config_t));
config.samplerate = stream->pcm.sampleRate;
config.samplerate = stream->sampleRate;
config.format = ANSND_VOICE_PCM_FORMAT_SIGNED_16_PCM;
config.channels = stream->pcm.channels;
config.channels = stream->channels;
config.pitch = 1.0f;
config.left_volume = baseVolume * leftFactor;
config.right_volume = baseVolume * rightFactor;
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
// GameCube/Wii have no usable hardware MP3 decoder for this engine's
// 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.
#include "audio/audiostreammp3decodersw.h"