f8f8a80a21
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>
311 lines
11 KiB
C
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); |