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>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# minimp3 has no tagged releases - pinned to a specific commit instead of a
|
||||
# branch for reproducibility.
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
minimp3
|
||||
GIT_REPOSITORY https://github.com/lieff/minimp3.git
|
||||
GIT_TAG ea99364f61c14656440e8d77e9c233ccf3124633
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(minimp3)
|
||||
|
||||
set(MINIMP3_INCLUDE_DIR "${minimp3_SOURCE_DIR}")
|
||||
set(MINIMP3_HEADER "${minimp3_SOURCE_DIR}/minimp3.h")
|
||||
|
||||
if(EXISTS "${MINIMP3_HEADER}")
|
||||
add_library(minimp3 INTERFACE)
|
||||
target_include_directories(minimp3 INTERFACE "${MINIMP3_INCLUDE_DIR}")
|
||||
set(minimp3_FOUND TRUE)
|
||||
else()
|
||||
set(minimp3_FOUND FALSE)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(MINIMP3_INCLUDE_DIR MINIMP3_HEADER)
|
||||
@@ -30,6 +30,7 @@ target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
pspgu
|
||||
pspaudio
|
||||
pspaudiolib
|
||||
pspmp3
|
||||
psputility
|
||||
pspvfpu
|
||||
pspvram
|
||||
|
||||
@@ -19,3 +19,4 @@ add_subdirectory(chunk)
|
||||
add_subdirectory(dmf)
|
||||
add_subdirectory(cutscene)
|
||||
add_subdirectory(wav)
|
||||
add_subdirectory(mp3)
|
||||
@@ -63,4 +63,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.loadAsync = assetWavLoaderAsync,
|
||||
.dispose = assetWavDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_MP3] = {
|
||||
.loadSync = assetMp3LoaderSync,
|
||||
.loadAsync = assetMp3LoaderAsync,
|
||||
.dispose = assetMp3Dispose
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "asset/loader/chunk/assetchunkloader.h"
|
||||
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
||||
#include "asset/loader/wav/assetwavloader.h"
|
||||
#include "asset/loader/mp3/assetmp3loader.h"
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
@@ -28,6 +29,7 @@ typedef enum {
|
||||
ASSET_LOADER_TYPE_CHUNK,
|
||||
ASSET_LOADER_TYPE_CUTSCENE,
|
||||
ASSET_LOADER_TYPE_WAV,
|
||||
ASSET_LOADER_TYPE_MP3,
|
||||
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
@@ -42,6 +44,7 @@ typedef union {
|
||||
assetchunkloaderloading_t chunk;
|
||||
assetcutsceneloaderloading_t cutscene;
|
||||
assetwavloaderloading_t wav;
|
||||
assetmp3loaderloading_t mp3;
|
||||
} assetloaderloading_t;
|
||||
|
||||
typedef union {
|
||||
@@ -54,6 +57,7 @@ typedef union {
|
||||
assetchunkoutput_t chunk;
|
||||
assetcutsceneoutput_t cutscene;
|
||||
assetwavoutput_t wav;
|
||||
assetmp3output_t mp3;
|
||||
} assetloaderoutput_t;
|
||||
|
||||
typedef union {
|
||||
@@ -64,6 +68,7 @@ typedef union {
|
||||
assetchunkloaderinput_t chunk;
|
||||
assetcutsceneloaderinput_t cutscene;
|
||||
assetwavloaderinput_t wav;
|
||||
assetmp3loaderinput_t mp3;
|
||||
} assetloaderinput_t;
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
assetmp3loader.c
|
||||
)
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetmp3loader.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
|
||||
// How far into the file (after any ID3v2 tag) assetMp3ParseHeader() will
|
||||
// search for the first valid MPEG frame sync. Real encoders write it
|
||||
// within the first handful of bytes; this is a generous sane upper bound
|
||||
// (matching assetwavloader.c's own approach to bounding a stack buffer)
|
||||
// rather than a real expectation of needing that much.
|
||||
#define ASSET_MP3_HEADER_SEARCH_MAX 8192
|
||||
|
||||
// MPEG-1 Layer III bitrates (kbps), indexed by the header's 4-bit bitrate
|
||||
// index. Index 0 ("free format") and 15 (reserved) aren't supported.
|
||||
static const uint16_t ASSET_MP3_BITRATE_MPEG1_L3[16] = {
|
||||
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0
|
||||
};
|
||||
|
||||
// MPEG-2/2.5 Layer III bitrates (kbps) - a different table from MPEG-1's.
|
||||
static const uint16_t ASSET_MP3_BITRATE_MPEG2_L3[16] = {
|
||||
0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0
|
||||
};
|
||||
|
||||
static const uint32_t ASSET_MP3_SAMPLE_RATE_MPEG1[4] = { 44100, 48000, 32000, 0 };
|
||||
static const uint32_t ASSET_MP3_SAMPLE_RATE_MPEG2[4] = { 22050, 24000, 16000, 0 };
|
||||
static const uint32_t ASSET_MP3_SAMPLE_RATE_MPEG25[4] = { 11025, 12000, 8000, 0 };
|
||||
|
||||
errorret_t assetMp3LoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Async loader should not be on main thread.");
|
||||
|
||||
if(loading->loading.mp3.state != ASSET_MP3_LOADER_STATE_READ_HEADER) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assetmp3file_t *mp3File = &loading->entry->data.mp3;
|
||||
memoryZero(mp3File, sizeof(assetmp3file_t));
|
||||
|
||||
assetfile_t file;
|
||||
assetLoaderErrorChain(loading, assetFileInit(
|
||||
&file, loading->entry->name, NULL, NULL
|
||||
));
|
||||
assetLoaderErrorChain(loading, assetFileOpen(&file));
|
||||
assetLoaderErrorChain(loading, assetMp3ParseHeader(&file, mp3File));
|
||||
assetLoaderErrorChain(loading, assetFileClose(&file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(&file));
|
||||
|
||||
loading->loading.mp3.state = ASSET_MP3_LOADER_STATE_DONE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetMp3LoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_MP3, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
switch(loading->loading.mp3.state) {
|
||||
case ASSET_MP3_LOADER_STATE_INITIAL:
|
||||
loading->loading.mp3.state = ASSET_MP3_LOADER_STATE_READ_HEADER;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
break;
|
||||
|
||||
case ASSET_MP3_LOADER_STATE_DONE:
|
||||
break;
|
||||
|
||||
default:
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetMp3Dispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Asset entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_MP3, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetMp3ParseHeader(assetfile_t *file, assetmp3file_t *mp3File) {
|
||||
assertNotNull(file, "Asset file cannot be NULL.");
|
||||
assertNotNull(mp3File, "Mp3 file cannot be NULL.");
|
||||
|
||||
memoryZero(mp3File, sizeof(assetmp3file_t));
|
||||
|
||||
// Skip a leading ID3v2 tag if present: "ID3" + version(2) + flags(1) + a
|
||||
// 4-byte syncsafe (7 usable bits/byte) size covering everything after
|
||||
// this 10-byte header.
|
||||
uint8_t id3Header[10];
|
||||
errorChain(assetFileRead(file, id3Header, sizeof(id3Header)));
|
||||
|
||||
size_t searchBase;
|
||||
if(memoryCompare(id3Header, "ID3", 3) == 0) {
|
||||
const uint32_t tagSize = (
|
||||
((uint32_t) (id3Header[6] & 0x7F) << 21) |
|
||||
((uint32_t) (id3Header[7] & 0x7F) << 14) |
|
||||
((uint32_t) (id3Header[8] & 0x7F) << 7) |
|
||||
((uint32_t) (id3Header[9] & 0x7F))
|
||||
);
|
||||
errorChain(assetFileRead(file, NULL, tagSize));
|
||||
searchBase = sizeof(id3Header) + tagSize;
|
||||
} else {
|
||||
// No ID3v2 tag - rewind the probe bytes just consumed so the frame
|
||||
// sync search below starts from the true beginning of the file.
|
||||
errorChain(assetFileRewind(file));
|
||||
searchBase = 0;
|
||||
}
|
||||
|
||||
const size_t searchWindowSize = mathMin(
|
||||
(size_t) ASSET_MP3_HEADER_SEARCH_MAX,
|
||||
(size_t) file->size > searchBase ? (size_t) file->size - searchBase : 0
|
||||
);
|
||||
if(searchWindowSize < 4) {
|
||||
errorThrow("MP3 file is too short to contain a frame header: %s", file->filename);
|
||||
}
|
||||
|
||||
uint8_t *window = (uint8_t *) memoryAllocate(searchWindowSize);
|
||||
errorret_t readRet = assetFileRead(file, window, searchWindowSize);
|
||||
if(errorIsNotOk(readRet)) {
|
||||
memoryFree(window);
|
||||
errorChain(readRet);
|
||||
}
|
||||
|
||||
bool_t found = false;
|
||||
size_t frameIndex = 0;
|
||||
|
||||
for(size_t i = 0; i + 4 <= searchWindowSize; i++) {
|
||||
if(window[i] != 0xFF || (window[i + 1] & 0xE0) != 0xE0) continue;
|
||||
|
||||
const uint8_t versionBits = (window[i + 1] >> 3) & 0x3;
|
||||
const uint8_t layerBits = (window[i + 1] >> 1) & 0x3;
|
||||
if(versionBits == 1 || layerBits != 1) continue; // reserved version, or not Layer III
|
||||
|
||||
const uint8_t bitrateIndex = (window[i + 2] >> 4) & 0xF;
|
||||
const uint8_t sampleRateIndex = (window[i + 2] >> 2) & 0x3;
|
||||
if(bitrateIndex == 0 || bitrateIndex == 15 || sampleRateIndex == 3) continue;
|
||||
|
||||
const uint8_t channelMode = (window[i + 3] >> 6) & 0x3;
|
||||
const bool_t isMpeg1 = versionBits == 3;
|
||||
|
||||
mp3File->bitrateKbps = isMpeg1
|
||||
? ASSET_MP3_BITRATE_MPEG1_L3[bitrateIndex]
|
||||
: ASSET_MP3_BITRATE_MPEG2_L3[bitrateIndex];
|
||||
mp3File->sampleRate = isMpeg1
|
||||
? ASSET_MP3_SAMPLE_RATE_MPEG1[sampleRateIndex]
|
||||
: (versionBits == 2
|
||||
? ASSET_MP3_SAMPLE_RATE_MPEG2[sampleRateIndex]
|
||||
: ASSET_MP3_SAMPLE_RATE_MPEG25[sampleRateIndex]);
|
||||
mp3File->samplesPerFrame = isMpeg1 ? 1152 : 576;
|
||||
mp3File->channels = channelMode == 3 ? 1 : 2;
|
||||
|
||||
// A Xing/Info header, if present, sits right after the side info -
|
||||
// whose size depends on MPEG version and channel mode - immediately
|
||||
// following the 4-byte header (plus a 2-byte CRC, if the protection
|
||||
// bit says one follows).
|
||||
const bool_t hasCrc = (window[i + 1] & 0x1) == 0;
|
||||
const size_t sideInfoSize = isMpeg1
|
||||
? (mp3File->channels == 1 ? 17 : 32)
|
||||
: (mp3File->channels == 1 ? 9 : 17);
|
||||
const size_t xingOffset = i + 4 + (hasCrc ? 2 : 0) + sideInfoSize;
|
||||
|
||||
if(
|
||||
xingOffset + 8 <= searchWindowSize &&
|
||||
(
|
||||
memoryCompare(window + xingOffset, "Xing", 4) == 0 ||
|
||||
memoryCompare(window + xingOffset, "Info", 4) == 0
|
||||
)
|
||||
) {
|
||||
const uint8_t *flagsBytes = window + xingOffset + 4;
|
||||
const uint32_t flags = (
|
||||
((uint32_t) flagsBytes[0] << 24) | ((uint32_t) flagsBytes[1] << 16) |
|
||||
((uint32_t) flagsBytes[2] << 8) | (uint32_t) flagsBytes[3]
|
||||
);
|
||||
if((flags & 0x1) != 0 && xingOffset + 12 <= searchWindowSize) {
|
||||
const uint8_t *framesBytes = window + xingOffset + 8;
|
||||
const uint32_t xingFrameCount = (
|
||||
((uint32_t) framesBytes[0] << 24) | ((uint32_t) framesBytes[1] << 16) |
|
||||
((uint32_t) framesBytes[2] << 8) | (uint32_t) framesBytes[3]
|
||||
);
|
||||
// The Xing/Info header's frame count includes the header's own
|
||||
// frame (this one, at `i`) - which minimp3 (and sceMp3) will
|
||||
// attempt to decode like any other, but which doesn't contribute a
|
||||
// real decodable frame of audio the way every other one does.
|
||||
// Confirmed against a real LAME-encoded file: its Xing header
|
||||
// declared 40 frames, but decoding actually produced exactly 39
|
||||
// frames' worth of real PCM - without this -1, playback would
|
||||
// permanently stall one frame short of the declared total (audible
|
||||
// as "never loops"), since real decode output can never catch up
|
||||
// to an overstated total.
|
||||
if(xingFrameCount > 0) {
|
||||
mp3File->totalFrames = (size_t) (xingFrameCount - 1) * mp3File->samplesPerFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frameIndex = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
memoryFree(window);
|
||||
|
||||
if(!found) {
|
||||
errorThrow(
|
||||
"MP3 file has no valid MPEG-1/2/2.5 Layer III frame within the first %u bytes: %s",
|
||||
(uint32_t) searchWindowSize, file->filename
|
||||
);
|
||||
}
|
||||
|
||||
mp3File->dataOffset = searchBase + frameIndex;
|
||||
mp3File->dataSize = (size_t) file->size - mp3File->dataOffset;
|
||||
|
||||
// No Xing/Info header found - estimate assuming constant bitrate (the
|
||||
// common case when one's genuinely absent; a real VBR file without one
|
||||
// is rare and will just get an approximate duration/loop point instead
|
||||
// of an exact one).
|
||||
if(mp3File->totalFrames == 0) {
|
||||
const float_t durationSeconds = (
|
||||
((float_t) mp3File->dataSize * 8.0f) / ((float_t) mp3File->bitrateKbps * 1000.0f)
|
||||
);
|
||||
mp3File->totalFrames = (size_t) (durationSeconds * (float_t) mp3File->sampleRate);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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);
|
||||
@@ -26,7 +26,7 @@ audiostream_t * audioAquireStream(assetentry_t *asset) {
|
||||
// too catches the mistake as early as possible, before any stream slot
|
||||
// is handed out for it.
|
||||
assertTrue(
|
||||
asset->type == ASSET_LOADER_TYPE_WAV,
|
||||
asset->type == ASSET_LOADER_TYPE_WAV || asset->type == ASSET_LOADER_TYPE_MP3,
|
||||
"Unsupported asset type for an audio stream."
|
||||
);
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
|
||||
asset->state == ASSET_ENTRY_STATE_LOADED,
|
||||
"Asset must be loaded before it can back an audio stream."
|
||||
);
|
||||
// Only WAV (-> PCM) assets can back an audio stream today - a caller
|
||||
// Only WAV (-> PCM) and MP3 assets can back an audio stream - a caller
|
||||
// passing the wrong kind of asset entry is a programmer error, not
|
||||
// something that can happen from untrusted data, so this is an assert
|
||||
// rather than an errorThrow (see feedback_assert_vs_error convention).
|
||||
assertTrue(
|
||||
asset->type == ASSET_LOADER_TYPE_WAV,
|
||||
asset->type == ASSET_LOADER_TYPE_WAV || asset->type == ASSET_LOADER_TYPE_MP3,
|
||||
"Unsupported asset type for an audio stream."
|
||||
);
|
||||
|
||||
@@ -43,10 +43,12 @@ errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
|
||||
stream->asset = asset;
|
||||
assetEntryLock(asset);
|
||||
|
||||
// Type-specific setup (audioStreamPcmInit() / audioStreamMp3Init(), once
|
||||
// MP3 exists) determines stream->type and asks the platform
|
||||
// implementation to set up its state.
|
||||
errorret_t ret = audioStreamPcmInit(stream);
|
||||
// Type-specific setup (audioStreamPcmInit() / audioStreamMp3Init())
|
||||
// determines stream->type, sets stream->sampleRate/channels, and asks
|
||||
// the platform implementation to set up its state.
|
||||
errorret_t ret = asset->type == ASSET_LOADER_TYPE_MP3
|
||||
? audioStreamMp3Init(stream)
|
||||
: audioStreamPcmInit(stream);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetEntryUnlock(asset);
|
||||
stream->asset = NULL;
|
||||
@@ -56,6 +58,41 @@ errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
size_t audioStreamGetTotalFrames(const audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
return stream->type == AUDIO_STREAM_TYPE_MP3
|
||||
? audioStreamMp3GetTotalFrames(stream)
|
||||
: audioStreamPcmGetTotalFrames(stream);
|
||||
}
|
||||
|
||||
errorret_t audioStreamSeek(audiostream_t *stream, const size_t frame) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
errorChain(
|
||||
stream->type == AUDIO_STREAM_TYPE_MP3
|
||||
? audioStreamMp3Seek(stream, frame)
|
||||
: audioStreamPcmSeek(stream, frame)
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamRead(
|
||||
audiostream_t *stream,
|
||||
int16_t *buffer,
|
||||
const size_t frameCount,
|
||||
size_t *outFramesRead
|
||||
) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
errorChain(
|
||||
stream->type == AUDIO_STREAM_TYPE_MP3
|
||||
? audioStreamMp3Read(stream, buffer, frameCount, outFramesRead)
|
||||
: audioStreamPcmRead(stream, buffer, frameCount, outFramesRead)
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void audioStreamPlay(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
stream->state |= AUDIO_STREAM_STATE_PLAYING;
|
||||
@@ -74,7 +111,7 @@ void audioStreamSetPosition(audiostream_t *stream, const float_t position) {
|
||||
while(t < 0) t += stream->duration;
|
||||
while(t >= stream->duration) t -= stream->duration;
|
||||
|
||||
stream->startFrame = (size_t) (t * stream->pcm.sampleRate);
|
||||
stream->startFrame = (size_t) (t * stream->sampleRate);
|
||||
stream->seeking = true;
|
||||
|
||||
// Force the next audioStreamUpdate() to re-buffer from startFrame instead
|
||||
@@ -183,7 +220,7 @@ errorret_t audioStreamUpdate(audiostream_t *stream) {
|
||||
// matters for platforms that reach this generic restart path at all;
|
||||
// PSP/Dolphin loop entirely on their own (thread/hardware) and never
|
||||
// report "finished" while looping, so in practice this is Linux-only.
|
||||
stream->startFrame = (size_t) (stream->loopTo * stream->pcm.sampleRate);
|
||||
stream->startFrame = (size_t) (stream->loopTo * stream->sampleRate);
|
||||
|
||||
if(stream->onLoop != NULL) {
|
||||
stream->onLoop(stream);
|
||||
@@ -217,6 +254,8 @@ errorret_t audioStreamDispose(audiostream_t *stream) {
|
||||
|
||||
if(stream->type == AUDIO_STREAM_TYPE_PCM) {
|
||||
errorChain(audioStreamPcmDispose(stream));
|
||||
} else if(stream->type == AUDIO_STREAM_TYPE_MP3) {
|
||||
errorChain(audioStreamMp3Dispose(stream));
|
||||
}
|
||||
|
||||
if(stream->asset != NULL) {
|
||||
|
||||
@@ -119,6 +119,15 @@ typedef struct audiostream_s {
|
||||
// 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;
|
||||
@@ -143,11 +152,55 @@ typedef struct audiostream_s {
|
||||
*
|
||||
* @param stream The audio stream to initialize.
|
||||
* @param asset The loaded asset to play - its type must be one this
|
||||
* function supports (currently only ASSET_LOADER_TYPE_WAV).
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -6,3 +6,156 @@
|
||||
*/
|
||||
|
||||
#include "audiostreammp3.h"
|
||||
#include "audiostream.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/math.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t audioStreamMp3Init(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertNotNull(stream->asset, "Stream must have an asset assigned.");
|
||||
assertTrue(
|
||||
stream->asset->type == ASSET_LOADER_TYPE_MP3,
|
||||
"Asset is not an MP3 file."
|
||||
);
|
||||
|
||||
const assetmp3file_t *mp3 = &stream->asset->data.mp3;
|
||||
|
||||
stream->type = AUDIO_STREAM_TYPE_MP3;
|
||||
stream->sampleRate = mp3->sampleRate;
|
||||
stream->channels = mp3->channels;
|
||||
stream->duration = (float_t) mp3->totalFrames / (float_t) mp3->sampleRate;
|
||||
|
||||
stream->mp3.pendingFrames = 0;
|
||||
stream->mp3.pendingPosition = 0;
|
||||
stream->mp3.position = 0;
|
||||
stream->mp3.totalFrames = mp3->totalFrames;
|
||||
|
||||
// Each stream opens its own independent handle to the same underlying
|
||||
// asset file - see audiostreampcm.c's audioStreamPcmInit() for why.
|
||||
errorChain(assetFileInit(
|
||||
&stream->mp3.file, stream->asset->name, NULL, NULL
|
||||
));
|
||||
errorChain(assetFileOpen(&stream->mp3.file));
|
||||
errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset));
|
||||
|
||||
errorChain(audioStreamMp3DecoderInit(stream));
|
||||
|
||||
errorChain(audioStreamPlatformInit(stream));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3Dispose(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
errorChain(audioStreamMp3DecoderDispose(stream));
|
||||
errorChain(assetFileClose(&stream->mp3.file));
|
||||
errorChain(assetFileDispose(&stream->mp3.file));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
size_t audioStreamMp3GetTotalFrames(const audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
return stream->mp3.totalFrames;
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3Seek(audiostream_t *stream, const size_t frame) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
if(frame == stream->mp3.position) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Every seek - forward or backward - restarts decoding from the very
|
||||
// start of the compressed stream and discards frames until reaching the
|
||||
// target; see this file's own header comment for why MP3 has no cheaper
|
||||
// option, unlike audioStreamPcmSeek()'s direct byte-offset seek.
|
||||
errorChain(audioStreamMp3DecoderRewind(stream));
|
||||
stream->mp3.position = 0;
|
||||
stream->mp3.pendingFrames = 0;
|
||||
stream->mp3.pendingPosition = 0;
|
||||
|
||||
const size_t channels = stream->channels;
|
||||
int16_t discard[AUDIO_MP3_MAX_SAMPLES_PER_FRAME];
|
||||
|
||||
while(stream->mp3.position < frame) {
|
||||
size_t decodedFrames = 0;
|
||||
errorChain(audioStreamMp3DecoderDecodeFrame(stream, discard, &decodedFrames));
|
||||
if(decodedFrames == 0) {
|
||||
// Ran out of stream before reaching the target - clamp rather than
|
||||
// erroring, same tolerance audioStreamPcmRead() has for a
|
||||
// shorter-than-declared asset.
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t framesNeeded = frame - stream->mp3.position;
|
||||
if(decodedFrames <= framesNeeded) {
|
||||
stream->mp3.position += decodedFrames;
|
||||
continue;
|
||||
}
|
||||
|
||||
// This frame overshoots the target - keep the excess as pending so
|
||||
// the next Read() starts exactly at `frame` instead of skipping past
|
||||
// it.
|
||||
const size_t keepFrames = decodedFrames - framesNeeded;
|
||||
memoryMove(
|
||||
discard, discard + (framesNeeded * channels),
|
||||
keepFrames * channels * sizeof(int16_t)
|
||||
);
|
||||
memoryCopy(stream->mp3.pending, discard, keepFrames * channels * sizeof(int16_t));
|
||||
stream->mp3.pendingFrames = keepFrames;
|
||||
stream->mp3.pendingPosition = 0;
|
||||
stream->mp3.position = frame;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3Read(
|
||||
audiostream_t *stream,
|
||||
int16_t *buffer,
|
||||
const size_t frameCount,
|
||||
size_t *outFramesRead
|
||||
) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertNotNull(buffer, "Buffer cannot be NULL.");
|
||||
assertNotNull(outFramesRead, "outFramesRead cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
const size_t channels = stream->channels;
|
||||
size_t framesWritten = 0;
|
||||
|
||||
while(framesWritten < frameCount) {
|
||||
if(stream->mp3.pendingPosition >= stream->mp3.pendingFrames) {
|
||||
size_t decodedFrames = 0;
|
||||
errorChain(audioStreamMp3DecoderDecodeFrame(
|
||||
stream, stream->mp3.pending, &decodedFrames
|
||||
));
|
||||
stream->mp3.pendingFrames = decodedFrames;
|
||||
stream->mp3.pendingPosition = 0;
|
||||
if(decodedFrames == 0) break; // Genuine end of stream.
|
||||
}
|
||||
|
||||
const size_t framesAvailable = stream->mp3.pendingFrames - stream->mp3.pendingPosition;
|
||||
const size_t framesToCopy = mathMin(frameCount - framesWritten, framesAvailable);
|
||||
|
||||
memoryCopy(
|
||||
buffer + (framesWritten * channels),
|
||||
stream->mp3.pending + (stream->mp3.pendingPosition * channels),
|
||||
framesToCopy * channels * sizeof(int16_t)
|
||||
);
|
||||
|
||||
stream->mp3.pendingPosition += framesToCopy;
|
||||
framesWritten += framesToCopy;
|
||||
}
|
||||
|
||||
stream->mp3.position += framesWritten;
|
||||
*outFramesRead = framesWritten;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,133 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "asset/assetfile.h"
|
||||
#include "audio/audiostreammp3decoder.h"
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
// How many interleaved PCM samples (not frames - samples, i.e. frames *
|
||||
// channels) a single audioStreamMp3DecoderDecodeFrame() call is ever
|
||||
// allowed to produce. Sizes the pending-sample buffer below, which every
|
||||
// decoder backend writes directly into - this must be sized to the
|
||||
// largest of them, not just "one MPEG frame":
|
||||
// - minimp3 (software, Linux/Dolphin): always exactly one frame, 1152
|
||||
// samples/channel for MPEG-1 Layer III, doubled here for stereo.
|
||||
// - sceMp3 (hardware, PSP): its own pcmBuf is provisioned at double that
|
||||
// (see AUDIO_MP3_PSP_PCM_BUF_SIZE) - i.e. sceMp3Decode() can
|
||||
// legitimately hand back more than one frame's worth in a single call.
|
||||
// Sizing this for only one frame silently overflowed stream->mp3.pending
|
||||
// on real hardware whenever that happened - confirmed as the cause of
|
||||
// very intermittent audio corruption/clicking, since it only bit when
|
||||
// sceMp3 actually returned the larger amount.
|
||||
// So this covers the larger (PSP) case; minimp3's decode is safely well
|
||||
// within it.
|
||||
#define AUDIO_MP3_MAX_SAMPLES_PER_FRAME (1152 * 2 * 2)
|
||||
|
||||
typedef struct {
|
||||
void *empty;
|
||||
// This stream's own private handle into its asset's underlying file -
|
||||
// see audiostreampcm_t.file's own comment for why this isn't shared
|
||||
// across streams playing the same asset. Holds compressed MP3 bytes;
|
||||
// the decoder backend (audiostreammp3decoder.h) reads from it directly.
|
||||
assetfile_t file;
|
||||
|
||||
// Opaque per-platform decoder state - a reserved sceMp3 handle plus its
|
||||
// buffers on PSP, or an mp3dec_t plus a sliding compressed-byte window on
|
||||
// the minimp3-based software backend (Linux/Dolphin). Defined by
|
||||
// whichever audiostreammp3decoder.h is actually visible when this file
|
||||
// is compiled - see that header's own comment.
|
||||
audiostreammp3decoder_t decoder;
|
||||
|
||||
// One decoded MPEG frame's worth of PCM, held here across Read() calls
|
||||
// since a frame (576 or 1152 samples/channel) rarely lines up exactly
|
||||
// with whatever frameCount a caller asks for. pendingPosition marks how
|
||||
// many of the first pendingFrames frames have already been consumed.
|
||||
int16_t pending[AUDIO_MP3_MAX_SAMPLES_PER_FRAME];
|
||||
size_t pendingFrames;
|
||||
size_t pendingPosition;
|
||||
|
||||
// Current logical PCM frame position, relative to the start of the
|
||||
// decoded stream - decoding is push-forward-only (see
|
||||
// audioStreamMp3Seek()'s own comment on why a backward seek re-decodes
|
||||
// from the start rather than truly random-accessing).
|
||||
size_t position;
|
||||
|
||||
// Total decoded PCM frame count for the whole clip - copied from the
|
||||
// asset's parsed metadata at Init (see assetmp3file_t.totalFrames),
|
||||
// exact if a Xing/Info header was present, otherwise an estimate.
|
||||
size_t totalFrames;
|
||||
} audiostreammp3_t;
|
||||
|
||||
/**
|
||||
* Configures the given MP3 audio stream from its already-assigned asset
|
||||
* (see audiostream_t.asset - set by audioStreamInit(), which is what
|
||||
* should be calling this, not application code directly): opens this
|
||||
* stream's own private file handle onto the asset's compressed MPEG data,
|
||||
* and initializes the platform decoder backend
|
||||
* (audioStreamMp3DecoderInit()).
|
||||
*
|
||||
* Must be called after stream->asset is set and before audioStreamPlay().
|
||||
*
|
||||
* @param stream The audio stream to configure.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3Init(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Disposes the given MP3 audio stream's decoder backend and private file
|
||||
* handle.
|
||||
*
|
||||
* @param stream The audio stream to dispose.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3Dispose(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Returns the total number of frames (one sample per channel) available in
|
||||
* the stream's underlying MP3 data - see assetmp3file_t.totalFrames's own
|
||||
* comment on why this is sometimes an estimate rather than an exact count,
|
||||
* unlike audioStreamPcmGetTotalFrames().
|
||||
*
|
||||
* @param stream The audio stream to query. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return The total number of frames available.
|
||||
*/
|
||||
size_t audioStreamMp3GetTotalFrames(const audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Seeks the stream's decode position to the given frame offset. Unlike
|
||||
* audioStreamPcmSeek(), this is never cheap: MPEG frames aren't
|
||||
* independently decodable (each one's bit reservoir can depend on data
|
||||
* carried over from prior frames), so there's no equivalent of PCM's
|
||||
* direct byte-offset seek - every call, forward or backward, rewinds the
|
||||
* decoder to the very start of the compressed stream and decodes (and
|
||||
* discards) frames until reaching the target. Acceptable for this engine's
|
||||
* actual use (looping background music, where loopTo is typically near
|
||||
* the start anyway), but a real, non-constant cost for a seek deep into a
|
||||
* long track.
|
||||
*
|
||||
* @param stream The audio stream to seek. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @param frame Frame offset to seek to, relative to the start of the
|
||||
* decoded stream.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3Seek(audiostream_t *stream, const size_t frame);
|
||||
|
||||
/**
|
||||
* Reads up to frameCount frames of PCM sample data from the stream's
|
||||
* current position, advancing it by however many frames were actually
|
||||
* read. Reads fewer than frameCount (down to zero) once the underlying
|
||||
* MP3 data is exhausted, rather than erroring - same contract as
|
||||
* audioStreamPcmRead().
|
||||
*
|
||||
* @param stream The audio stream to read from. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @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 audioStreamMp3Read(
|
||||
audiostream_t *stream,
|
||||
int16_t *buffer,
|
||||
const size_t frameCount,
|
||||
size_t *outFramesRead
|
||||
);
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#define MINIMP3_IMPLEMENTATION
|
||||
#include "audiostreammp3decodersw.h"
|
||||
#include "audio/audiostream.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
mp3dec_init(&decoder->decoder);
|
||||
decoder->bufferFilled = 0;
|
||||
decoder->endOfFile = false;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
const assetmp3file_t *mp3 = &stream->asset->data.mp3;
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
|
||||
errorChain(assetFileRewind(&stream->mp3.file));
|
||||
errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset));
|
||||
|
||||
mp3dec_init(&decoder->decoder);
|
||||
decoder->bufferFilled = 0;
|
||||
decoder->endOfFile = false;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderDecodeFrame(
|
||||
audiostream_t *stream,
|
||||
int16_t *out,
|
||||
size_t *outFrames
|
||||
) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertNotNull(out, "Out buffer cannot be NULL.");
|
||||
assertNotNull(outFrames, "outFrames cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
|
||||
for(;;) {
|
||||
mp3dec_frame_info_t info;
|
||||
memoryZero(&info, sizeof(info));
|
||||
const int samples = mp3dec_decode_frame(
|
||||
&decoder->decoder, decoder->buffer, (int) decoder->bufferFilled, out, &info
|
||||
);
|
||||
|
||||
if(samples > 0) {
|
||||
// Shift the consumed bytes (the decoded frame, and any garbage
|
||||
// minimp3 skipped before it) out of the front of the window - unless
|
||||
// it consumed the whole thing, in which case there's nothing left to
|
||||
// shift (memoryMove() disallows a 0-byte move).
|
||||
const size_t remaining = decoder->bufferFilled - (size_t) info.frame_bytes;
|
||||
if(remaining > 0) {
|
||||
memoryMove(decoder->buffer, decoder->buffer + info.frame_bytes, remaining);
|
||||
}
|
||||
decoder->bufferFilled = remaining;
|
||||
*outFrames = (size_t) samples;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(info.frame_bytes > 0) {
|
||||
// Garbage bytes skipped (e.g. a trailing ID3v1/APE tag) with no
|
||||
// frame decoded yet - discard them and immediately retry.
|
||||
const size_t remaining = decoder->bufferFilled - (size_t) info.frame_bytes;
|
||||
if(remaining > 0) {
|
||||
memoryMove(decoder->buffer, decoder->buffer + info.frame_bytes, remaining);
|
||||
}
|
||||
decoder->bufferFilled = remaining;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not enough data buffered to decide anything either way - top up
|
||||
// from the file, unless there's genuinely nothing left to add.
|
||||
if(decoder->endOfFile) {
|
||||
*outFrames = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
const size_t room = AUDIO_MP3_SW_BUFFER_SIZE - decoder->bufferFilled;
|
||||
if(room == 0) {
|
||||
// The window is already full and minimp3 still can't make a
|
||||
// decision from it - the stream is malformed (or this buffer is
|
||||
// pathologically small for its content). Treat as exhausted rather
|
||||
// than looping forever.
|
||||
decoder->endOfFile = true;
|
||||
*outFrames = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorChain(assetFileRead(
|
||||
&stream->mp3.file, decoder->buffer + decoder->bufferFilled, room
|
||||
));
|
||||
const size_t bytesRead = (size_t) stream->mp3.file.lastRead;
|
||||
decoder->bufferFilled += bytesRead;
|
||||
if(bytesRead < room) decoder->endOfFile = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 <minimp3.h>
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
// Size of the sliding compressed-byte window minimp3 decodes from. Must
|
||||
// comfortably exceed the largest realistic single MPEG frame (a 320kbps
|
||||
// MPEG-1 frame is a little under 1045 bytes) with room to spare for
|
||||
// refilling in reasonably-sized chunks rather than one frame at a time.
|
||||
#define AUDIO_MP3_SW_BUFFER_SIZE (16 * 1024)
|
||||
|
||||
/**
|
||||
* Software MP3 decoder state, shared by every platform that doesn't have
|
||||
* (or doesn't use) a hardware MP3 decoder - currently Linux and Dolphin,
|
||||
* both via this same minimp3-based implementation
|
||||
* (audiostreammp3decodersw.c). See audiostreammp3.h for how this plugs
|
||||
* into the shared MP3 stream layer, and audiostreammp3.h's own
|
||||
* documentation of audioStreamMp3DecoderInit()/Dispose()/Rewind()/
|
||||
* DecodeFrame() for the interface this and the PSP hardware backend
|
||||
* (src/duskpsp/audio/audiostreammp3decoder.c) both implement.
|
||||
*/
|
||||
typedef struct {
|
||||
mp3dec_t decoder;
|
||||
|
||||
// minimp3 decides how many compressed bytes one frame consumed only
|
||||
// after attempting to decode it, so this holds however much hasn't been
|
||||
// consumed yet, refilled from the stream's assetfile_t as it drains.
|
||||
uint8_t buffer[AUDIO_MP3_SW_BUFFER_SIZE];
|
||||
size_t bufferFilled;
|
||||
|
||||
// Set once assetFileRead() returns fewer bytes than requested - there's
|
||||
// no more compressed data to refill `buffer` with, though whatever's
|
||||
// still in it may still decode into one or more final frames.
|
||||
bool_t endOfFile;
|
||||
} audiostreammp3decoder_t;
|
||||
|
||||
/**
|
||||
* Initializes the software MP3 decoder for the given stream - resets
|
||||
* minimp3's internal state and the compressed-byte window (empty, not yet
|
||||
* filled from the asset).
|
||||
*
|
||||
* @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Disposes the software MP3 decoder for the given stream. A no-op beyond
|
||||
* that - minimp3 allocates nothing itself, and the compressed-byte window
|
||||
* lives inline in audiostreammp3decoder_t.
|
||||
*
|
||||
* @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Resets the decoder to the very start of the compressed stream: rewinds
|
||||
* stream->mp3.file back to the asset's parsed data offset, clears minimp3's
|
||||
* internal state and the compressed-byte window, so the next
|
||||
* DecodeFrame() call starts decoding from the first MPEG frame again. See
|
||||
* audioStreamMp3Seek()'s own comment on why every seek goes through here.
|
||||
*
|
||||
* @param stream The audio stream to rewind. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Decodes the next MPEG frame's worth of PCM samples, topping up the
|
||||
* compressed-byte window from stream->mp3.file as needed. Writes decoded
|
||||
* samples to `out` (sized for at least AUDIO_MP3_MAX_SAMPLES_PER_FRAME
|
||||
* int16_t values) and sets *outFrames to how many frames (not samples)
|
||||
* were produced - 0 once the compressed stream is genuinely exhausted,
|
||||
* never negative (a corrupt/truncated stream is treated the same as a
|
||||
* clean end, not an error, matching audioStreamPcmRead()'s own tolerance
|
||||
* for a short/truncated asset).
|
||||
*
|
||||
* @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @param out Destination buffer for decoded samples.
|
||||
* @param outFrames Set to the number of frames actually decoded.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderDecodeFrame(
|
||||
audiostream_t *stream,
|
||||
int16_t *out,
|
||||
size_t *outFrames
|
||||
);
|
||||
@@ -22,8 +22,8 @@ errorret_t audioStreamPcmInit(audiostream_t *stream) {
|
||||
assetwavfile_t *wav = &stream->asset->data.wav;
|
||||
|
||||
stream->type = AUDIO_STREAM_TYPE_PCM;
|
||||
stream->pcm.sampleRate = wav->sampleRate;
|
||||
stream->pcm.channels = wav->channels;
|
||||
stream->sampleRate = wav->sampleRate;
|
||||
stream->channels = wav->channels;
|
||||
stream->duration = (
|
||||
(float_t) audioStreamPcmGetTotalFrames(stream) /
|
||||
(float_t) wav->sampleRate
|
||||
@@ -64,7 +64,7 @@ size_t audioStreamPcmGetTotalFrames(const audiostream_t *stream) {
|
||||
|
||||
const assetwavfile_t *wav = &stream->asset->data.wav;
|
||||
const size_t sourceFrameSize = (
|
||||
stream->pcm.channels * (wav->bitsPerSample / 8)
|
||||
stream->channels * (wav->bitsPerSample / 8)
|
||||
);
|
||||
return wav->dataSize / sourceFrameSize;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ errorret_t audioStreamPcmSeek(audiostream_t *stream, const size_t frame) {
|
||||
|
||||
assetwavfile_t *wav = &stream->asset->data.wav;
|
||||
const size_t sourceFrameSize = (
|
||||
stream->pcm.channels * (wav->bitsPerSample / 8)
|
||||
stream->channels * (wav->bitsPerSample / 8)
|
||||
);
|
||||
const size_t targetByte = wav->dataOffset + (frame * sourceFrameSize);
|
||||
|
||||
@@ -113,7 +113,7 @@ errorret_t audioStreamPcmRead(
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM.");
|
||||
|
||||
assetwavfile_t *wav = &stream->asset->data.wav;
|
||||
const size_t channels = stream->pcm.channels;
|
||||
const size_t channels = stream->channels;
|
||||
const size_t sourceSampleBytes = wav->bitsPerSample / 8;
|
||||
const size_t sourceFrameSize = channels * sourceSampleBytes;
|
||||
const size_t dataEndByte = wav->dataOffset + wav->dataSize;
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
typedef struct {
|
||||
// Sample rate of the stream's data, in Hz.
|
||||
uint32_t sampleRate;
|
||||
|
||||
// Number of interleaved channels in the stream's data.
|
||||
uint8_t channels;
|
||||
|
||||
// This stream's own private handle into its asset's underlying file -
|
||||
// deliberately not shared with any other stream reading the same asset
|
||||
// (see audioStreamPcmInit's own comment for why). Its read cursor is
|
||||
|
||||
@@ -63,8 +63,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
// streaming -> platform playback) against a real WAV asset rather than
|
||||
// synthesizing PCM data at runtime.
|
||||
assetentry_t *testToneEntry = assetLock(
|
||||
"audio/pepsiman.wav", ASSET_LOADER_TYPE_WAV, NULL
|
||||
// "audio/audiotest.wav", ASSET_LOADER_TYPE_WAV, NULL
|
||||
"audio/audiotest.wav", ASSET_LOADER_TYPE_WAV, NULL
|
||||
);
|
||||
errorChain(assetRequireLoaded(testToneEntry));
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
@@ -9,3 +9,16 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
audiolinux.c
|
||||
audiostreamlinux.c
|
||||
)
|
||||
|
||||
# No hardware MP3 decoder on Linux - use the shared minimp3-based software
|
||||
# backend (also used by Dolphin), 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()
|
||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC minimp3)
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
"${DUSK_SOURCES_DIR}/dusk/audio/audiostreammp3decodersw.c"
|
||||
)
|
||||
|
||||
@@ -29,9 +29,9 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream) {
|
||||
|
||||
SDL_AudioSpec desired;
|
||||
memoryZero(&desired, sizeof(SDL_AudioSpec));
|
||||
desired.freq = (int) stream->pcm.sampleRate;
|
||||
desired.freq = (int) stream->sampleRate;
|
||||
desired.format = AUDIO_S16SYS;
|
||||
desired.channels = stream->pcm.channels;
|
||||
desired.channels = stream->channels;
|
||||
desired.samples = AUDIO_LINUX_LEAD_FRAMES;
|
||||
|
||||
stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
|
||||
@@ -53,8 +53,8 @@ errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
|
||||
errorret_t audioStreamLinuxBuffer(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);
|
||||
|
||||
// Consumed synchronously (right here, in the same call that decided to
|
||||
// (re)buffer) rather than left for later - see startFrame's own comment.
|
||||
@@ -66,7 +66,7 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
size_t endFrame = totalFrames;
|
||||
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
|
||||
endFrame = mathMin(
|
||||
(size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames
|
||||
(size_t) (stream->loopStart * stream->sampleRate), totalFrames
|
||||
);
|
||||
}
|
||||
// A seek (or a loop restart landing exactly on the loop end) can put
|
||||
@@ -84,7 +84,7 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
if(seeking) {
|
||||
SDL_ClearQueuedAudio(stream->platform.device);
|
||||
}
|
||||
errorChain(audioStreamPcmSeek(stream, startFrame));
|
||||
errorChain(audioStreamSeek(stream, startFrame));
|
||||
|
||||
stream->platform.position = startFrame;
|
||||
stream->platform.endFrame = endFrame;
|
||||
@@ -99,7 +99,7 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
|
||||
const size_t frameSize = stream->channels * sizeof(int16_t);
|
||||
const size_t framesRemaining = (
|
||||
stream->platform.position < stream->platform.endFrame
|
||||
? stream->platform.endFrame - stream->platform.position
|
||||
@@ -112,7 +112,7 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
|
||||
|
||||
int16_t *chunk = memoryAllocate(framesToRead * frameSize);
|
||||
size_t framesRead = 0;
|
||||
errorret_t ret = audioStreamPcmRead(stream, chunk, framesToRead, &framesRead);
|
||||
errorret_t ret = audioStreamRead(stream, chunk, framesToRead, &framesRead);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(chunk);
|
||||
errorChain(ret);
|
||||
@@ -156,7 +156,7 @@ bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
|
||||
// empty guarantees a gap by definition: silence has already started by
|
||||
// the time "empty" can be observed.
|
||||
const Uint32 leadBytes = (Uint32) (
|
||||
AUDIO_LINUX_LEAD_FRAMES * stream->pcm.channels * sizeof(int16_t)
|
||||
AUDIO_LINUX_LEAD_FRAMES * stream->channels * sizeof(int16_t)
|
||||
);
|
||||
|
||||
if(SDL_GetQueuedAudioSize(stream->platform.device) > leadBytes) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// Linux has no hardware MP3 decoder to prefer, so it uses the shared
|
||||
// minimp3-based software backend (also used by Dolphin) - see that
|
||||
// header's own documentation for the actual struct/interface.
|
||||
#include "audio/audiostreammp3decodersw.h"
|
||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
audiopsp.c
|
||||
audiostreampsp.c
|
||||
audiostreammp3decoder.c
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "audiopsp.h"
|
||||
#include "audiostreammp3decoder.h"
|
||||
|
||||
errorret_t audioPSPInit() {
|
||||
errorOk();
|
||||
@@ -16,5 +17,7 @@ errorret_t audioPSPUpdate() {
|
||||
}
|
||||
|
||||
errorret_t audioPSPDispose() {
|
||||
errorChain(audioStreamMp3DecoderGlobalDispose());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "audiostreammp3decoder.h"
|
||||
#include "audio/audiostream.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include <pspmp3.h>
|
||||
#include <psputility.h>
|
||||
|
||||
// Per the PSP SDK's mp3 sample and pspmp3.h's own documentation - these
|
||||
// are hard minimums sceMp3ReserveMp3Handle() requires, not tunable.
|
||||
#define AUDIO_MP3_PSP_STREAM_BUF_SIZE (16 * 1024)
|
||||
#define AUDIO_MP3_PSP_PCM_BUF_SIZE (16 * (1152 / 2))
|
||||
|
||||
// sceMp3 manages its own internal ring buffer of compressed bytes rather
|
||||
// than being handed a whole file/buffer up front (unlike, say,
|
||||
// zip_source_buffer_create elsewhere in this codebase) - it tells the
|
||||
// caller exactly where in the source stream it wants more data from
|
||||
// (sceMp3GetInfoToAddStreamData()'s srcpos) and how much room is free to
|
||||
// receive it into, and the caller is expected to fetch exactly that and
|
||||
// call sceMp3NotifyAddStreamData(). This decoder only ever feeds it
|
||||
// forward, sequentially, from stream->mp3.file (never anything sceMp3
|
||||
// itself wouldn't naturally request next), so despite the API being built
|
||||
// for arbitrary positioning, this driver never needs to seek that file
|
||||
// out of sequence - see audioStreamMp3DecoderFillStream()'s own assertion.
|
||||
|
||||
// End-of-stream sentinel returned by sceMp3Decode() once nothing more can
|
||||
// be decoded - not documented in pspmp3.h, but confirmed by the PSP SDK's
|
||||
// own mp3 sample (main.c), which explicitly excludes it from its generic
|
||||
// "sceMp3Decode failed" error path.
|
||||
#define AUDIO_MP3_PSP_DECODE_END_OF_STREAM 0x80671402
|
||||
|
||||
// Set once audioStreamMp3DecoderInit() has loaded sceMp3's firmware
|
||||
// modules and initialized its resource pool - lazily, on the first MP3
|
||||
// stream ever created, rather than unconditionally at engine startup, so
|
||||
// a game that never plays an MP3 never pays for it. Guards
|
||||
// audioStreamMp3DecoderGlobalDispose() so it only tears down what was
|
||||
// actually set up.
|
||||
static bool_t AUDIO_MP3_PSP_RESOURCES_READY = false;
|
||||
|
||||
errorret_t audioStreamMp3DecoderFillStream(audiostream_t *stream) {
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
|
||||
SceUChar8 *dst = NULL;
|
||||
SceInt32 toWrite = 0;
|
||||
SceInt32 srcPos = 0;
|
||||
SceInt32 status = sceMp3GetInfoToAddStreamData(
|
||||
decoder->handle, &dst, &toWrite, &srcPos
|
||||
);
|
||||
if(status < 0) {
|
||||
errorThrow("sceMp3GetInfoToAddStreamData failed: 0x%08X", status);
|
||||
}
|
||||
|
||||
if(toWrite <= 0) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
(size_t) srcPos == decoder->streamPosition,
|
||||
"sceMp3 requested stream data out of the sequence this decoder feeds it in."
|
||||
);
|
||||
|
||||
errorChain(assetFileRead(&stream->mp3.file, dst, (size_t) toWrite));
|
||||
const size_t bytesRead = (size_t) stream->mp3.file.lastRead;
|
||||
|
||||
status = sceMp3NotifyAddStreamData(decoder->handle, (SceInt32) bytesRead);
|
||||
if(status < 0) {
|
||||
errorThrow("sceMp3NotifyAddStreamData failed: 0x%08X", status);
|
||||
}
|
||||
|
||||
decoder->streamPosition += bytesRead;
|
||||
if(bytesRead < (size_t) toWrite) decoder->endOfFile = true;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
if(!AUDIO_MP3_PSP_RESOURCES_READY) {
|
||||
int status = sceUtilityLoadModule(PSP_MODULE_AV_AVCODEC);
|
||||
if(status < 0) {
|
||||
errorThrow("Failed to load PSP_MODULE_AV_AVCODEC: 0x%08X", status);
|
||||
}
|
||||
status = sceUtilityLoadModule(PSP_MODULE_AV_MP3);
|
||||
if(status < 0) {
|
||||
errorThrow("Failed to load PSP_MODULE_AV_MP3: 0x%08X", status);
|
||||
}
|
||||
status = sceMp3InitResource();
|
||||
if(status < 0) {
|
||||
errorThrow("sceMp3InitResource failed: 0x%08X", status);
|
||||
}
|
||||
AUDIO_MP3_PSP_RESOURCES_READY = true;
|
||||
}
|
||||
|
||||
const assetmp3file_t *mp3 = &stream->asset->data.mp3;
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
|
||||
decoder->mp3Buf = memoryAlign(64, AUDIO_MP3_PSP_STREAM_BUF_SIZE);
|
||||
decoder->pcmBuf = memoryAlign(64, AUDIO_MP3_PSP_PCM_BUF_SIZE);
|
||||
decoder->streamPosition = mp3->dataOffset;
|
||||
decoder->endOfFile = false;
|
||||
|
||||
SceMp3InitArg initArg;
|
||||
memoryZero(&initArg, sizeof(initArg));
|
||||
initArg.mp3StreamStart = (SceOff) mp3->dataOffset;
|
||||
initArg.mp3StreamEnd = (SceOff) (mp3->dataOffset + mp3->dataSize);
|
||||
initArg.mp3Buf = decoder->mp3Buf;
|
||||
initArg.mp3BufSize = AUDIO_MP3_PSP_STREAM_BUF_SIZE;
|
||||
initArg.pcmBuf = decoder->pcmBuf;
|
||||
initArg.pcmBufSize = AUDIO_MP3_PSP_PCM_BUF_SIZE;
|
||||
|
||||
const SceInt32 handle = sceMp3ReserveMp3Handle(&initArg);
|
||||
if(handle < 0) {
|
||||
memoryFree(decoder->mp3Buf);
|
||||
memoryFree(decoder->pcmBuf);
|
||||
errorThrow("sceMp3ReserveMp3Handle failed: 0x%08X", handle);
|
||||
}
|
||||
decoder->handle = handle;
|
||||
|
||||
// sceMp3Init() needs some stream data already fed before it can
|
||||
// determine the stream's format - see the PSP SDK's own mp3 sample.
|
||||
errorChain(audioStreamMp3DecoderFillStream(stream));
|
||||
|
||||
const SceInt32 initStatus = sceMp3Init(decoder->handle);
|
||||
if(initStatus < 0) {
|
||||
sceMp3ReleaseMp3Handle(decoder->handle);
|
||||
memoryFree(decoder->mp3Buf);
|
||||
memoryFree(decoder->pcmBuf);
|
||||
errorThrow("sceMp3Init failed: 0x%08X", initStatus);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
|
||||
sceMp3ReleaseMp3Handle(decoder->handle);
|
||||
memoryFree(decoder->mp3Buf);
|
||||
decoder->mp3Buf = NULL;
|
||||
memoryFree(decoder->pcmBuf);
|
||||
decoder->pcmBuf = NULL;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
const assetmp3file_t *mp3 = &stream->asset->data.mp3;
|
||||
|
||||
errorChain(assetFileRewind(&stream->mp3.file));
|
||||
errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset));
|
||||
|
||||
// Deliberately not sceMp3ResetPlayPosition(): the SDK doesn't document
|
||||
// precisely enough whether it fully clears sceMp3's own internal
|
||||
// compressed-data ring buffer or just resets a playback cursor, leaving
|
||||
// room for it to disagree with the fresh stream position we just fed it
|
||||
// from - a mismatch that would only be audible as an occasional click
|
||||
// right at the loop point, depending on how much of sceMp3's internal
|
||||
// buffer happened to still hold stale data at reset time. Fully tearing
|
||||
// down and recreating the decoder instead guarantees a clean state by
|
||||
// construction, via the exact same path already proven correct for the
|
||||
// stream's very first Init - one extra handle release/reserve cycle per
|
||||
// loop wrap is negligible next to how rarely loops actually happen.
|
||||
errorChain(audioStreamMp3DecoderDispose(stream));
|
||||
errorChain(audioStreamMp3DecoderInit(stream));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderDecodeFrame(
|
||||
audiostream_t *stream,
|
||||
int16_t *out,
|
||||
size_t *outFrames
|
||||
) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
assertNotNull(out, "Out buffer cannot be NULL.");
|
||||
assertNotNull(outFrames, "outFrames cannot be NULL.");
|
||||
assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3.");
|
||||
|
||||
audiostreammp3decoder_t *decoder = &stream->mp3.decoder;
|
||||
const size_t frameSize = stream->channels * sizeof(int16_t);
|
||||
|
||||
for(;;) {
|
||||
if(sceMp3CheckStreamDataNeeded(decoder->handle) > 0 && !decoder->endOfFile) {
|
||||
errorChain(audioStreamMp3DecoderFillStream(stream));
|
||||
}
|
||||
|
||||
SceShort16 *pcm = NULL;
|
||||
const SceInt32 bytesDecoded = sceMp3Decode(decoder->handle, &pcm);
|
||||
|
||||
if(bytesDecoded > 0) {
|
||||
// `out` (stream->mp3.pending) is sized for
|
||||
// AUDIO_MP3_MAX_SAMPLES_PER_FRAME samples - sized specifically to
|
||||
// cover sceMp3's pcmBuf provisioning (see this decoder's own
|
||||
// AUDIO_MP3_PSP_PCM_BUF_SIZE comment). Asserted rather than trusted:
|
||||
// a real hardware overflow here was previously silent, corrupting
|
||||
// whatever followed stream->mp3.pending in memory, and only showed
|
||||
// up as very intermittent audio clicking.
|
||||
assertTrue(
|
||||
(size_t) bytesDecoded <= AUDIO_MP3_MAX_SAMPLES_PER_FRAME * sizeof(int16_t),
|
||||
"sceMp3Decode returned more PCM than the destination buffer can hold."
|
||||
);
|
||||
const size_t frames = (size_t) bytesDecoded / frameSize;
|
||||
memoryCopy(out, pcm, (size_t) bytesDecoded);
|
||||
*outFrames = frames;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(
|
||||
bytesDecoded < 0 &&
|
||||
(uint32_t) bytesDecoded != AUDIO_MP3_PSP_DECODE_END_OF_STREAM
|
||||
) {
|
||||
errorThrow("sceMp3Decode failed: 0x%08X", bytesDecoded);
|
||||
}
|
||||
|
||||
// Decoded nothing this call - if sceMp3 doesn't need more data either,
|
||||
// it's genuinely out of frames to produce.
|
||||
if(sceMp3CheckStreamDataNeeded(decoder->handle) <= 0) {
|
||||
*outFrames = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(decoder->endOfFile) {
|
||||
// sceMp3 wants more data but the file has none left - one last
|
||||
// decode attempt already happened above with whatever was fed;
|
||||
// nothing further will ever arrive.
|
||||
*outFrames = 0;
|
||||
errorOk();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t audioStreamMp3DecoderGlobalDispose(void) {
|
||||
if(!AUDIO_MP3_PSP_RESOURCES_READY) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
const SceInt32 status = sceMp3TermResource();
|
||||
if(status < 0) {
|
||||
errorThrow("sceMp3TermResource failed: 0x%08X", status);
|
||||
}
|
||||
|
||||
sceUtilityUnloadModule(PSP_MODULE_AV_MP3);
|
||||
sceUtilityUnloadModule(PSP_MODULE_AV_AVCODEC);
|
||||
AUDIO_MP3_PSP_RESOURCES_READY = false;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 <psptypes.h>
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
/**
|
||||
* Hardware MP3 decoder state (sceMp3) for a single stream. Firmware-side
|
||||
* decode, off the CPU entirely - see audiostreammp3decoder.c's top-of-file
|
||||
* comment for the streaming model this drives (sceMp3 manages its own
|
||||
* internal compressed-byte ring buffer, pulling more from
|
||||
* stream->mp3.file on demand via audioStreamMp3DecoderFillStream()).
|
||||
*/
|
||||
typedef struct {
|
||||
// Handle returned by sceMp3ReserveMp3Handle(), valid for this stream's
|
||||
// whole lifetime.
|
||||
SceInt32 handle;
|
||||
|
||||
// Buffers owned by this decoder and registered with sceMp3 via
|
||||
// SceMp3InitArg - sized to the SDK's documented minimums (mp3Buf must be
|
||||
// >= 8192 bytes, pcmBuf >= 9216 bytes). Allocated once in
|
||||
// audioStreamMp3DecoderInit(), freed in audioStreamMp3DecoderDispose().
|
||||
uint8_t *mp3Buf;
|
||||
uint8_t *pcmBuf;
|
||||
|
||||
// This decoder's own record of how many compressed bytes have been fed
|
||||
// to sceMp3 so far (relative to the start of the whole file, matching
|
||||
// stream->mp3.file's own position) - sceMp3GetInfoToAddStreamData()
|
||||
// reports back the absolute source position it wants read from next,
|
||||
// and since this decoder only ever feeds it forward, sequentially, that
|
||||
// position should always match this - asserted rather than silently
|
||||
// trusted, in audioStreamMp3DecoderFillStream().
|
||||
size_t streamPosition;
|
||||
|
||||
// Set once assetFileRead() returns fewer bytes than requested while
|
||||
// filling sceMp3's stream buffer - there's no more compressed data to
|
||||
// add, though sceMp3 may still have enough already-fed data to decode
|
||||
// one or more final frames.
|
||||
bool_t endOfFile;
|
||||
} audiostreammp3decoder_t;
|
||||
|
||||
/**
|
||||
* Feeds sceMp3 more compressed bytes if (and only as much as) it currently
|
||||
* wants: asks it where in the stream and how much (sceMp3GetInfoToAddStreamData()),
|
||||
* reads exactly that from stream->mp3.file, and hands it back
|
||||
* (sceMp3NotifyAddStreamData()). A no-op if sceMp3 doesn't currently want
|
||||
* more. See this file's own top-of-file comment on why the source
|
||||
* position sceMp3 requests is asserted rather than actually used to seek.
|
||||
*
|
||||
* @param stream The audio stream to feed. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderFillStream(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Initializes the hardware MP3 decoder for the given stream: lazily loads
|
||||
* the sceMp3/AVCODEC firmware modules and initializes sceMp3's resource
|
||||
* pool the first time any stream ever needs it (see
|
||||
* audioStreamMp3DecoderGlobalDispose()'s own comment), reserves a handle
|
||||
* sized from the stream's asset metadata, and feeds it enough initial
|
||||
* compressed data for sceMp3Init() to succeed.
|
||||
*
|
||||
* @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderInit(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Disposes the hardware MP3 decoder for the given stream: releases its
|
||||
* sceMp3 handle and frees its buffers. Does not tear down sceMp3's global
|
||||
* resource pool - see audioStreamMp3DecoderGlobalDispose().
|
||||
*
|
||||
* @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Resets the decoder to the very start of the compressed stream: rewinds
|
||||
* stream->mp3.file back to the asset's parsed data offset, resets this
|
||||
* decoder's own streamPosition to match, and calls
|
||||
* sceMp3ResetPlayPosition() so sceMp3 requests stream data from the start
|
||||
* again. See audioStreamMp3Seek()'s own comment on why every seek goes
|
||||
* through here.
|
||||
*
|
||||
* @param stream The audio stream to rewind. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream);
|
||||
|
||||
/**
|
||||
* Decodes the next MPEG frame's worth of PCM samples, feeding sceMp3's
|
||||
* internal stream buffer from stream->mp3.file as it requests more.
|
||||
* Writes decoded samples to `out` (sized for at least
|
||||
* AUDIO_MP3_MAX_SAMPLES_PER_FRAME int16_t values) and sets *outFrames to
|
||||
* how many frames (not samples) were produced - 0 once sceMp3 reports it
|
||||
* both has nothing left to decode and needs no more stream data (a
|
||||
* genuine end of stream).
|
||||
*
|
||||
* @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3.
|
||||
* @param out Destination buffer for decoded samples.
|
||||
* @param outFrames Set to the number of frames actually decoded.
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderDecodeFrame(
|
||||
audiostream_t *stream,
|
||||
int16_t *out,
|
||||
size_t *outFrames
|
||||
);
|
||||
|
||||
/**
|
||||
* Tears down sceMp3's global resource pool and unloads its firmware
|
||||
* modules, if audioStreamMp3DecoderInit() ever actually initialized them -
|
||||
* a no-op otherwise. Called once from audioPSPDispose(), regardless of
|
||||
* whether any MP3 stream was ever created.
|
||||
*
|
||||
* @return Error indicating success or failure.
|
||||
*/
|
||||
errorret_t audioStreamMp3DecoderGlobalDispose(void);
|
||||
@@ -42,24 +42,24 @@
|
||||
errorret_t audioStreamPSPInit(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
if(stream->pcm.channels != 1 && stream->pcm.channels != 2) {
|
||||
if(stream->channels != 1 && stream->channels != 2) {
|
||||
errorThrow(
|
||||
"PSP audio only supports mono or stereo PCM, got %d channels.",
|
||||
stream->pcm.channels
|
||||
stream->channels
|
||||
);
|
||||
}
|
||||
|
||||
// sceAudioChReserve's hardware channels always run at the PSP's native
|
||||
// 44100Hz; there is no per-channel sample rate. Arbitrary rates would need
|
||||
// sceAudioSRCChReserve's single exclusive channel instead.
|
||||
if(stream->pcm.sampleRate != 44100) {
|
||||
if(stream->sampleRate != 44100) {
|
||||
errorThrow(
|
||||
"PSP audio channels are fixed at 44100Hz, got %uHz.",
|
||||
stream->pcm.sampleRate
|
||||
stream->sampleRate
|
||||
);
|
||||
}
|
||||
|
||||
const int format = stream->pcm.channels == 1
|
||||
const int format = stream->channels == 1
|
||||
? PSP_AUDIO_FORMAT_MONO
|
||||
: PSP_AUDIO_FORMAT_STEREO;
|
||||
|
||||
@@ -75,7 +75,7 @@ errorret_t audioStreamPSPInit(audiostream_t *stream) {
|
||||
stream->platform.playRequested = false;
|
||||
|
||||
const size_t ringSize =
|
||||
AUDIO_PSP_RING_FRAMES * stream->pcm.channels * sizeof(int16_t);
|
||||
AUDIO_PSP_RING_FRAMES * stream->channels * sizeof(int16_t);
|
||||
stream->platform.ring = memoryAllocate(ringSize);
|
||||
stream->platform.scratch = memoryAllocate(ringSize);
|
||||
threadMutexInit(&stream->platform.ringLock);
|
||||
@@ -109,7 +109,7 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
|
||||
stream->platform.readReachedEnd = false;
|
||||
stream->platform.readFailed = false;
|
||||
|
||||
stream->platform.totalFrames = audioStreamPcmGetTotalFrames(stream);
|
||||
stream->platform.totalFrames = audioStreamGetTotalFrames(stream);
|
||||
|
||||
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
|
||||
// loopEndFrame) that a looping pass wraps within, once it's reached -
|
||||
@@ -117,12 +117,12 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
|
||||
// behaviour is unchanged when no explicit loop points are configured.
|
||||
stream->platform.loopEndFrame = stream->loopStart >= 0
|
||||
? mathMin(
|
||||
(size_t) (stream->loopStart * stream->pcm.sampleRate),
|
||||
(size_t) (stream->loopStart * stream->sampleRate),
|
||||
stream->platform.totalFrames
|
||||
)
|
||||
: stream->platform.totalFrames;
|
||||
stream->platform.loopToFrame = mathMin(
|
||||
(size_t) (stream->loopTo * stream->pcm.sampleRate),
|
||||
(size_t) (stream->loopTo * stream->sampleRate),
|
||||
stream->platform.loopEndFrame
|
||||
);
|
||||
|
||||
@@ -130,7 +130,7 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
|
||||
stream->startFrame = 0;
|
||||
stream->seeking = false;
|
||||
|
||||
errorChain(audioStreamPcmSeek(stream, startFrame));
|
||||
errorChain(audioStreamSeek(stream, startFrame));
|
||||
stream->platform.readPosition = startFrame;
|
||||
|
||||
threadMutexLock(&stream->platform.ringLock);
|
||||
@@ -158,20 +158,28 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
|
||||
threadMutexUnlock(&stream->platform.ringLock);
|
||||
if(filled >= AUDIO_PSP_LEAD_FRAMES) return;
|
||||
|
||||
const size_t channels = stream->pcm.channels;
|
||||
const size_t channels = stream->channels;
|
||||
const size_t frameSize = channels * sizeof(int16_t);
|
||||
|
||||
// Fill however much room the ring actually has in this one call, not
|
||||
// just a small fixed step - otherwise a temporary engine frame-rate dip
|
||||
// (below roughly one hardware chunk's worth of playback time per frame)
|
||||
// would let production permanently fall behind consumption, since a
|
||||
// fixed-size top-up per call can never make up lost ground. Bounded by
|
||||
// the ring's own physical capacity, and by loopEndFrame (the loop
|
||||
// segment's end) - except a seek can legitimately land past it (e.g.
|
||||
// into an outro after the loop point), in which case read out to the
|
||||
// true end of the clip once instead of underflowing
|
||||
// framesRemainingInSegment.
|
||||
const size_t room = AUDIO_PSP_RING_FRAMES - filled;
|
||||
// Fill however much room the ring has, up to one bounded step, not just
|
||||
// whatever's needed to reach LEAD in a single shot - AUDIO_PSP_RING_FRAMES
|
||||
// is now sized to absorb a slow MP3 loop-wrap (see its own comment), and
|
||||
// greedily trying to fill all of it in one call right after a fresh
|
||||
// Buffer() (when the ring starts empty) would turn the very first
|
||||
// top-up into one long blocking decode burst on the main thread instead
|
||||
// of many small ones - worse for startup smoothness, not better. Still
|
||||
// uncapped per se: reaching LEAD just takes a few calls (a few engine
|
||||
// frames) instead of one, which comfortably keeps up with real-time
|
||||
// consumption the same way an unbounded fill would, since a temporary
|
||||
// frame-rate dip only slows how fast the ring tops up, never how much
|
||||
// room is left to fill on the next call. Bounded by the ring's own
|
||||
// physical capacity, and by loopEndFrame (the loop segment's end) -
|
||||
// except a seek can legitimately land past it (e.g. into an outro after
|
||||
// the loop point), in which case read out to the true end of the clip
|
||||
// once instead of underflowing framesRemainingInSegment.
|
||||
const size_t room = mathMin(
|
||||
AUDIO_PSP_RING_FRAMES - filled, (size_t) AUDIO_PSP_TOPUP_STEP_FRAMES
|
||||
);
|
||||
const size_t currentEndFrame = stream->platform.readPosition < stream->platform.loopEndFrame
|
||||
? stream->platform.loopEndFrame
|
||||
: stream->platform.totalFrames;
|
||||
@@ -186,7 +194,7 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
|
||||
|
||||
size_t framesRead = 0;
|
||||
if(errorIsNotOk(
|
||||
audioStreamPcmRead(stream, scratch, framesToRead, &framesRead)
|
||||
audioStreamRead(stream, scratch, framesToRead, &framesRead)
|
||||
)) {
|
||||
stream->platform.readFailed = true;
|
||||
return;
|
||||
@@ -219,7 +227,7 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
|
||||
|
||||
if(reachesSegmentEnd) {
|
||||
if(willLoop) {
|
||||
if(errorIsNotOk(audioStreamPcmSeek(stream, stream->platform.loopToFrame))) {
|
||||
if(errorIsNotOk(audioStreamSeek(stream, stream->platform.loopToFrame))) {
|
||||
stream->platform.readFailed = true;
|
||||
return;
|
||||
}
|
||||
@@ -258,7 +266,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
||||
sceKernelChangeThreadPriority(sceKernelGetThreadId(), AUDIO_PSP_THREAD_PRIORITY);
|
||||
|
||||
audiostream_t *stream = (audiostream_t *) thread->data;
|
||||
const size_t channels = stream->pcm.channels;
|
||||
const size_t channels = stream->channels;
|
||||
const size_t frameSize = channels * sizeof(int16_t);
|
||||
int16_t *chunk = memoryAllocate(AUDIO_PSP_CHUNK_FRAMES * frameSize);
|
||||
|
||||
|
||||
@@ -16,13 +16,32 @@ typedef struct audiostream_s audiostream_t;
|
||||
// Physical capacity of the ring; must comfortably exceed
|
||||
// AUDIO_PSP_LEAD_FRAMES plus one top-up step so a single top-up call can
|
||||
// never overwrite data the output thread hasn't consumed yet.
|
||||
#define AUDIO_PSP_RING_FRAMES 16384
|
||||
//
|
||||
// Sized well above what a PCM stream would ever need on its own, because
|
||||
// an MP3 stream's loop-wrap (audioStreamMp3DecoderRewind() on PSP) fully
|
||||
// tears down and recreates the hardware decoder - a real handle release/
|
||||
// reserve round-trip plus a fresh ~16KB Memory Stick read to re-prime
|
||||
// sceMp3's stream buffer - all synchronously, inside the same TopUp() call
|
||||
// that's supposed to be keeping this ring fed. That's slow enough,
|
||||
// occasionally, to eat into a tighter lead margin and click right at the
|
||||
// loop point (confirmed on real hardware) - this gives it generous room to
|
||||
// do so without the output thread ever catching up to empty.
|
||||
#define AUDIO_PSP_RING_FRAMES 49152
|
||||
|
||||
// Top-up threshold - audioStreamPSPTopUp() (called once per engine
|
||||
// Update(), see audioStreamPSPIsFinished()) only reads more data once the
|
||||
// ring drops below this, same trigger dusklinux uses for its own lead
|
||||
// margin.
|
||||
#define AUDIO_PSP_LEAD_FRAMES 4096
|
||||
// margin. See AUDIO_PSP_RING_FRAMES's own comment for why this is sized
|
||||
// well beyond dusklinux's equivalent.
|
||||
#define AUDIO_PSP_LEAD_FRAMES 12288
|
||||
|
||||
// Max frames audioStreamPSPTopUp() will read in one call, regardless of
|
||||
// how much ring room is actually free - keeps each call's worst-case
|
||||
// blocking time on the main thread bounded and roughly constant even
|
||||
// though AUDIO_PSP_RING_FRAMES/AUDIO_PSP_LEAD_FRAMES are large; reaching
|
||||
// LEAD from an empty ring just takes a few calls (a few engine frames)
|
||||
// instead of one long one. See AUDIO_PSP_RING_FRAMES's own comment.
|
||||
#define AUDIO_PSP_TOPUP_STEP_FRAMES 4096
|
||||
|
||||
// Max number of pending loop-wrap events (see audiostreampsploopmarker_t
|
||||
// below) the ring can remember at once. Sized generously relative to how
|
||||
|
||||
Reference in New Issue
Block a user