Files
dusk/src/dusk/audio/audiostream.h
T
YourWishes 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 <noreply@anthropic.com>
2026-09-01 08:21:30 -05:00

311 lines
11 KiB
C

/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "audio/audiostreampcm.h"
#include "audio/audiostreammp3.h"
#include "audio/audiostreamplatform.h"
#include "asset/loader/assetentry.h"
#ifndef audioStreamPlatformInit
#error "audioStreamPlatformInit is not defined"
#endif
#ifndef audioStreamPlatformDispose
#error "audioStreamPlatformDispose is not defined"
#endif
#ifndef audioStreamPlatformBuffer
#error "audioStreamPlatformBuffer is not defined"
#endif
#ifndef audioStreamPlatformIsFinished
#error "audioStreamPlatformIsFinished is not defined"
#endif
#define AUDIO_STREAMS_MAX 8
#define AUDIO_STREAM_STATE_PLAYING (1 << 0)
#define AUDIO_STREAM_STATE_LOOPING (1 << 1)
#define AUDIO_STREAM_STATE_BUFFERED (1 << 2)
#define AUDIO_STREAM_CENTER 0
#define AUDIO_STREAM_LEFT -128
#define AUDIO_STREAM_RIGHT 127
typedef enum {
AUDIO_STREAM_TYPE_NULL,
AUDIO_STREAM_TYPE_PCM,
AUDIO_STREAM_TYPE_MP3,
AUDIO_STREAM_TYPE_COUNT
} audistreamtype_t;
typedef struct audiostream_s audiostream_t;
typedef struct audiostream_s {
// Used for aquiring new data.
audistreamtype_t type;
// What state the stream is in.
uint8_t state;
// The asset backing this stream's data (e.g. a WAV file) - determines
// `type` (see audioStreamInit()) and is read from on demand by the
// type-specific module (e.g. audiostreampcm.h) rather than ever being
// fully decoded into memory up front. The stream holds its own lock on
// this entry (assetEntryLock()/assetEntryUnlock()) for as long as it's
// in use, independent of whatever lock(s) the caller that requested
// playback may also be holding.
assetentry_t *asset;
// Loudness. Can only be 0 to 0xFF
uint8_t volume;
// In stereo space, where do we send the audio.
// TODO: Can we use a 3D vector + Pro Logic II?
int8_t directionality;
// In seconds, where the loop segment ends and playback jumps back to
// loopTo - -1 (the default) means "at the end of the stream", i.e. loop
// the whole buffer. Set via audioStreamSetLoopPoints().
float_t loopStart;
// In seconds, where a loop jumps back to once it reaches loopStart.
// Defaults to 0 (start of stream). Set via audioStreamSetLoopPoints().
float_t loopTo;
// Cached duration of the stream in seconds.
float_t duration;
// Frame offset the next platform Buffer() call should start playback
// from - set by audioStreamSetPosition() (an explicit seek) and by
// audioStreamUpdate() itself (to loopTo's frame offset, for platforms
// that re-enter Buffer() on every loop restart rather than looping
// natively). Each platform's Buffer()-invoking entry point must read and
// reset this to 0 synchronously, in the same call that decided to
// (re)buffer - not later/asynchronously (e.g. from a feeder thread),
// since audioStreamUpdate() may already have moved on to something else
// that touches this field by the time an async reader gets to it.
size_t startFrame;
// True when startFrame came from an explicit audioStreamSetPosition()
// seek rather than a natural loop restart. Platforms whose Buffer() call
// can leave previously-queued audio still playing underneath the new
// pass (currently just Linux's SDL queue, kept deliberately overlapping
// across a loop restart to avoid a gap) need this to tell "jump now,
// discarding whatever's still playing" (seek) apart from "let the old
// tail keep playing while the new pass queues underneath it" (loop
// restart) - both look identical as just "a pending startFrame"
// otherwise. Consumed (reset to false) the same way as startFrame.
bool_t seeking;
// Callbacks
void *user;
void (*onLoop)(audiostream_t *stream);
void (*onEnd)(audiostream_t *stream);
// Incremented by platform code (from whatever thread/context it runs in)
// each time a loop happens, instead of calling onLoop directly - onLoop
// may do arbitrary, possibly-slow work (console printing, game logic),
// which is only safe to run from the main thread inside audioStreamUpdate().
// Calling it straight from a real-time audio thread risks starving the
// hardware buffer if it takes too long - confirmed as the real cause of
// a loud crackle on PSP once its feeder thread called onLoop inline.
volatile uint32_t loopCount;
// audioStreamUpdate()'s own record of the last loopCount it fired
// onLoop for - only ever touched from the main thread.
uint32_t lastLoopCount;
// PCM format of the stream's decoded output - read identically by every
// platform backend regardless of stream type (PCM decodes straight
// through; MP3 decodes to this same format), so it lives here rather
// than duplicated in both audiostreampcm_t and audiostreammp3_t. Set by
// whichever type-specific Init function runs (audioStreamPcmInit() /
// audioStreamMp3Init()).
uint32_t sampleRate;
uint8_t channels;
// Stream type specific data.
union {
audiostreampcm_t pcm;
audiostreammp3_t mp3;
};
// Platform-specific playback state (e.g. SDL2 device, PSP channel, ansnd
// voice). Defined by each platform's audiostreamplatform.h.
audiostreamplatform_t platform;
} audiostream_t;
/**
* Initializes an audio stream to play the given asset, determining the
* stream's type from the asset's loader type (e.g. a WAV asset becomes an
* AUDIO_STREAM_TYPE_PCM stream) and dispatching to that type's own setup
* (e.g. audioStreamPcmInit()). Does not begin playback; call
* audioStreamPlay() once this returns.
*
* The stream takes its own lock on `asset` (see audiostream_t.asset's own
* comment), released by audioStreamDispose() - the asset must already be
* loaded (ASSET_ENTRY_STATE_LOADED) when this is called.
*
* @param stream The audio stream to initialize.
* @param asset The loaded asset to play - its type must be one this
* function supports (ASSET_LOADER_TYPE_WAV or
* ASSET_LOADER_TYPE_MP3).
* @return Error indicating success or failure.
*/
errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset);
/**
* Returns the total number of decoded PCM frames (one sample per channel)
* available in the stream's underlying data, regardless of its type -
* dispatches to audioStreamPcmGetTotalFrames() or
* audioStreamMp3GetTotalFrames(). Platform backends should use this rather
* than a type-specific function directly, so they don't need to know or
* care which type they're driving.
*
* @param stream The audio stream to query.
* @return The total number of frames available.
*/
size_t audioStreamGetTotalFrames(const audiostream_t *stream);
/**
* Seeks the stream's decode position to the given frame offset, regardless
* of its type - dispatches to audioStreamPcmSeek() or audioStreamMp3Seek().
*
* @param stream The audio stream to seek.
* @param frame Frame offset to seek to, relative to the start of the
* stream's decoded data.
* @return Error indicating success or failure.
*/
errorret_t audioStreamSeek(audiostream_t *stream, const size_t frame);
/**
* Reads up to frameCount frames of decoded PCM sample data from the
* stream's current position, regardless of its type - dispatches to
* audioStreamPcmRead() or audioStreamMp3Read(). See either for the exact
* short-read-at-end-of-stream contract, which is identical for both.
*
* @param stream The audio stream to read from.
* @param buffer Destination buffer, sized for at least frameCount frames.
* @param frameCount Maximum number of frames to read.
* @param outFramesRead Set to the number of frames actually read.
* @return Error indicating success or failure.
*/
errorret_t audioStreamRead(
audiostream_t *stream,
int16_t *buffer,
const size_t frameCount,
size_t *outFramesRead
);
/**
* Begins or resumes playback of the given audio stream.
*
* @param stream The audio stream to play.
*/
void audioStreamPlay(audiostream_t *stream);
/**
* Pauses playback of the given audio stream. Playback position is retained,
* so audioStreamPlay() resumes from the same point.
*
* @param stream The audio stream to pause.
*/
void audioStreamPause(audiostream_t *stream);
/**
* Seeks the given audio stream to the given position, in seconds, wrapping
* into range if the position is outside [0, duration). Takes effect on the
* next audioStreamUpdate() call for platforms that re-buffer per frame
* (e.g. Linux); on PSP, which manages an entire playback pass on its own
* feeder thread once started, a seek only takes effect the next time the
* stream begins playing from a stopped state, not instantaneously mid-pass.
*
* @param stream The audio stream to seek.
* @param position The new playback position, in seconds.
*/
void audioStreamSetPosition(audiostream_t *stream, const float_t position);
/**
* Sets the loop region for the given audio stream: once playback reaches
* loopStart, it jumps back to loopTo instead of continuing (or stopping,
* if not looping). Has no effect unless looping is also enabled via
* audioStreamSetLooping().
*
* @param stream The audio stream to update.
* @param loopStart Where the loop segment ends, in seconds, or -1 to loop
* the whole stream (the default).
* @param loopTo Where the loop segment starts, in seconds.
*/
void audioStreamSetLoopPoints(
audiostream_t *stream,
const float_t loopStart,
const float_t loopTo
);
/**
* Computes normalized left/right pan factors (0..1) for the given audio
* stream's directionality. Callers multiply these by their own
* platform-specific base volume representation.
*
* @param stream The audio stream to read directionality from.
* @param outLeft Set to the left channel's pan factor.
* @param outRight Set to the right channel's pan factor.
*/
void audioStreamGetPanFactors(
const audiostream_t *stream,
float_t *outLeft,
float_t *outRight
);
/**
* Sets the playback volume of the given audio stream.
*
* @param stream The audio stream to update.
* @param volume The new volume, from 0 (silent) to 0xFF (loudest).
*/
void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume);
/**
* Sets the stereo directionality (panning) of the given audio stream.
*
* @param stream The audio stream to update.
* @param directionality The new panning value, from AUDIO_STREAM_LEFT to
* AUDIO_STREAM_RIGHT.
*/
void audioStreamSetDirectionality(
audiostream_t *stream,
const int8_t directionality
);
/**
* Sets whether the given audio stream loops back to loopTo (or the start
* of the buffer, by default - see audioStreamSetLoopPoints()) when it
* reaches loopStart (or the end of the buffer), rather than stopping and
* firing onEnd.
*
* @param stream The audio stream to update.
* @param looping Whether the stream should loop.
*/
void audioStreamSetLooping(audiostream_t *stream, const bool_t looping);
/**
* Updates the given audio stream, decoding new data and advancing playback
* as needed. Should be called every frame for every active stream.
*
* @param stream The audio stream to update.
* @return Error indicating success or failure.
*/
errorret_t audioStreamUpdate(audiostream_t *stream);
/**
* Disposes the audio stream, stopping playback and releasing any resources
* associated with it.
*
* @param stream The audio stream to dispose.
* @return Error indicating success or failure.
*/
errorret_t audioStreamDispose(audiostream_t *stream);