Fixed PSP trying to load entirely into memory

This commit is contained in:
2026-08-31 19:07:51 -05:00
parent 15bd9fc43c
commit d37109f5f7
26 changed files with 995 additions and 232 deletions
Binary file not shown.
+9
View File
@@ -119,6 +119,15 @@ errorret_t assetRequireLoaded(assetentry_t *entry) {
assetEntryLock(entry);
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
// A failed load transitions to ERROR, not LOADED - without this check
// this loop spins forever on any load failure (assetUpdate() itself
// still returns OK, since a single asset failing isn't meant to halt
// the whole update loop - see its ASSET_ENTRY_STATE_ERROR handling).
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
assetEntryUnlock(entry);
errorThrow("Failed to load asset: %s", entry->name);
}
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
+34 -3
View File
@@ -55,6 +55,19 @@ errorret_t assetDskOpenFromPath(
const char_t *path,
zip_t **outCompressed,
zip_t **outStored
) {
errorChain(assetDskOpenFromPathRange(
path, 0, SIZE_MAX, outCompressed, outStored
));
errorOk();
}
errorret_t assetDskOpenFromPathRange(
const char_t *path,
const size_t baseOffset,
const size_t baseSize,
zip_t **outCompressed,
zip_t **outStored
) {
assertNotNull(path, "Path cannot be NULL.");
assertNotNull(outCompressed, "Out compressed cannot be NULL.");
@@ -68,6 +81,11 @@ errorret_t assetDskOpenFromPath(
errorThrow("Failed to open dusk.dsk: %s", path);
}
if(fseek(headerFile, (long) baseOffset, SEEK_SET) != 0) {
fclose(headerFile);
errorThrow("Failed to seek to dusk.dsk range in file: %s", path);
}
uint8_t headerBytes[ASSET_DSK_HEADER_SIZE];
size_t headerRead = fread(headerBytes, 1, sizeof(headerBytes), headerFile);
fclose(headerFile);
@@ -78,11 +96,21 @@ errorret_t assetDskOpenFromPath(
assetdskheader_t header;
errorChain(assetDskParseHeader(headerBytes, sizeof(headerBytes), &header));
if(
(size_t) header.compressedOffset + header.compressedSize > baseSize ||
(size_t) header.storedOffset + header.storedSize > baseSize
) {
errorThrow("dusk.dsk header describes ranges beyond its containing file.");
}
zip_error_t zipError;
zip_error_init(&zipError);
zip_source_t *compressedSource = zip_source_file_create(
path, header.compressedOffset, (zip_int64_t) header.compressedSize, &zipError
path,
(zip_uint64_t) (baseOffset + header.compressedOffset),
(zip_int64_t) header.compressedSize,
&zipError
);
if(compressedSource == NULL) {
errorThrow(
@@ -99,7 +127,10 @@ errorret_t assetDskOpenFromPath(
}
zip_source_t *storedSource = zip_source_file_create(
path, header.storedOffset, (zip_int64_t) header.storedSize, &zipError
path,
(zip_uint64_t) (baseOffset + header.storedOffset),
(zip_int64_t) header.storedSize,
&zipError
);
if(storedSource == NULL) {
zip_close(*outCompressed);
@@ -132,7 +163,7 @@ errorret_t assetDskOpenFromPath(
*outCompressed = NULL;
errorThrow("Failed to re-open dusk.dsk to verify stored checksum: %s", path);
}
fseek(storedFile, (long) header.storedOffset, SEEK_SET);
fseek(storedFile, (long) (baseOffset + header.storedOffset), SEEK_SET);
size_t storedRead = fread(storedBytes, 1, header.storedSize, storedFile);
fclose(storedFile);
if(storedRead != header.storedSize) {
+27
View File
@@ -64,6 +64,9 @@ errorret_t assetDskParseHeader(
* archive's checksum is intentionally not verified here since doing so
* would require reading the bulk of the game's assets just to compute it.
*
* See assetDskOpenFromPathRange() for the case where the DSK2 blob isn't
* the whole file (e.g. embedded inside another container).
*
* @param path Filesystem path to the dusk.dsk file.
* @param outCompressed Set to the opened compressed archive on success.
* @param outStored Set to the opened stored archive on success.
@@ -76,6 +79,30 @@ errorret_t assetDskOpenFromPath(
zip_t **outStored
);
/**
* Same as assetDskOpenFromPath(), but the DSK2 blob doesn't start at the
* beginning of `path` - it's a byte range embedded inside a larger
* container file (e.g. the PSAR region of an EBOOT.PBP). Every offset the
* header describes is relative to `baseOffset`; `baseSize` bounds them
* (the size of the embedded blob, not the whole container file) - pass
* SIZE_MAX to skip that check when the caller doesn't know/care.
*
* @param path Filesystem path to the container file.
* @param baseOffset Byte offset within `path` where the DSK2 blob starts.
* @param baseSize Number of bytes available at `baseOffset`.
* @param outCompressed Set to the opened compressed archive on success.
* @param outStored Set to the opened stored archive on success.
* @return OK on success, error if the file is missing, too short, has a
* bad header, or either archive fails to open/verify.
*/
errorret_t assetDskOpenFromPathRange(
const char_t *path,
const size_t baseOffset,
const size_t baseSize,
zip_t **outCompressed,
zip_t **outStored
);
/**
* Opens both archives of a dusk.dsk file already fully resident in memory
* (e.g. a PSAR embedded in an EBOOT.PBP, or an ISO-embedded file already
+3 -1
View File
@@ -100,8 +100,10 @@ errorret_t assetFileRead(
uint8_t tempBuffer[256];
while(bytesRemaining > 0) {
size_t chunkSize = mathMin(bytesRemaining, sizeof(tempBuffer));
// The recursive call below already advances file->position by
// chunkSize (the non-NULL branch does this itself) - do not also
// advance it here, or every skip ends up double-counted.
errorChain(assetFileRead(file, tempBuffer, chunkSize));
file->position += chunkSize;
bytesRemaining -= chunkSize;
}
file->lastRead = bufferSize;
+2 -1
View File
@@ -17,4 +17,5 @@ add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(chunk)
add_subdirectory(dmf)
add_subdirectory(cutscene)
add_subdirectory(cutscene)
add_subdirectory(wav)
+6
View File
@@ -57,4 +57,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.loadAsync = assetCutsceneLoaderAsync,
.dispose = assetCutsceneDispose
},
[ASSET_LOADER_TYPE_WAV] = {
.loadSync = assetWavLoaderSync,
.loadAsync = assetWavLoaderAsync,
.dispose = assetWavDispose
},
};
+5
View File
@@ -14,6 +14,7 @@
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/chunk/assetchunkloader.h"
#include "asset/loader/cutscene/assetcutsceneloader.h"
#include "asset/loader/wav/assetwavloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
@@ -26,6 +27,7 @@ typedef enum {
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_CHUNK,
ASSET_LOADER_TYPE_CUTSCENE,
ASSET_LOADER_TYPE_WAV,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
@@ -39,6 +41,7 @@ typedef union {
assetjsonloaderloading_t json;
assetchunkloaderloading_t chunk;
assetcutsceneloaderloading_t cutscene;
assetwavloaderloading_t wav;
} assetloaderloading_t;
typedef union {
@@ -50,6 +53,7 @@ typedef union {
assetjsonoutput_t json;
assetchunkoutput_t chunk;
assetcutsceneoutput_t cutscene;
assetwavoutput_t wav;
} assetloaderoutput_t;
typedef union {
@@ -59,6 +63,7 @@ typedef union {
assetjsonloaderinput_t json;
assetchunkloaderinput_t chunk;
assetcutsceneloaderinput_t cutscene;
assetwavloaderinput_t wav;
} assetloaderinput_t;
typedef struct assetloading_s assetloading_t;
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetwavloader.c
)
+181
View File
@@ -0,0 +1,181 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetwavloader.h"
#include "util/memory.h"
#include "util/endian.h"
#include "assert/assert.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
// "RIFF" + chunkSize + "WAVE", before any sub-chunks begin.
#define ASSET_WAV_RIFF_HEADER_SIZE 12
// A sub-chunk header is a 4-byte id followed by a 4-byte little-endian size.
#define ASSET_WAV_CHUNK_HEADER_SIZE 8
#define ASSET_WAV_FMT_CHUNK_SIZE_MIN 16
// Sane upper bound for a stack buffer - real fmt chunks (PCM or otherwise)
// never come close to this; anything bigger is treated as malformed.
#define ASSET_WAV_FMT_CHUNK_SIZE_MAX 64
errorret_t assetWavLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.wav.state != ASSET_WAV_LOADER_STATE_READ_HEADER) {
errorOk();
}
assetwavfile_t *wavFile = &loading->entry->data.wav;
memoryZero(wavFile, sizeof(assetwavfile_t));
assetfile_t file;
assetLoaderErrorChain(loading, assetFileInit(
&file, loading->entry->name, NULL, NULL
));
assetLoaderErrorChain(loading, assetFileOpen(&file));
assetLoaderErrorChain(loading, assetWavParseHeader(&file, wavFile));
assetLoaderErrorChain(loading, assetFileClose(&file));
assetLoaderErrorChain(loading, assetFileDispose(&file));
loading->loading.wav.state = ASSET_WAV_LOADER_STATE_DONE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetWavLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_WAV, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.wav.state) {
case ASSET_WAV_LOADER_STATE_INITIAL:
loading->loading.wav.state = ASSET_WAV_LOADER_STATE_READ_HEADER;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_WAV_LOADER_STATE_DONE:
break;
default:
errorOk();
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetWavDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_WAV, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
errorOk();
}
errorret_t assetWavParseHeader(
assetfile_t *file,
assetwavfile_t *wavFile
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(wavFile, "Wav file cannot be NULL.");
uint8_t riffHeader[ASSET_WAV_RIFF_HEADER_SIZE];
errorChain(assetFileRead(file, riffHeader, sizeof(riffHeader)));
if(memoryCompare(riffHeader, "RIFF", 4) != 0) {
errorThrow("WAV file has an invalid RIFF header: %s", file->filename);
}
if(memoryCompare(riffHeader + 8, "WAVE", 4) != 0) {
errorThrow("WAV file has an invalid WAVE header: %s", file->filename);
}
bool_t foundFormat = false;
bool_t foundData = false;
uint16_t audioFormat = 0;
// Walk the chunk list, reading only chunk headers (and the small "fmt "
// body) - every other chunk, "data"'s sample bytes included, is skipped
// via a NULL-buffer read rather than buffered into memory. Stops as soon
// as "data" is found; nothing after it matters here.
while(!foundData && (size_t) file->position < (size_t) file->size) {
uint8_t chunkHeader[ASSET_WAV_CHUNK_HEADER_SIZE];
errorChain(assetFileRead(file, chunkHeader, sizeof(chunkHeader)));
uint32_t chunkSizeLE;
memoryCopy(&chunkSizeLE, chunkHeader + 4, sizeof(chunkSizeLE));
const uint32_t chunkSize = endianLittleToHost32(chunkSizeLE);
// Chunks are padded to an even total size - the pad byte (if any)
// isn't included in chunkSize but still needs to be skipped over.
const uint32_t chunkSizePadded = chunkSize + (chunkSize % 2);
if(memoryCompare(chunkHeader, "fmt ", 4) == 0) {
if(
chunkSize < ASSET_WAV_FMT_CHUNK_SIZE_MIN ||
chunkSize > ASSET_WAV_FMT_CHUNK_SIZE_MAX
) {
errorThrow("WAV 'fmt ' chunk has an unsupported size: %u", chunkSize);
}
uint8_t fmtBuffer[ASSET_WAV_FMT_CHUNK_SIZE_MAX];
errorChain(assetFileRead(file, fmtBuffer, chunkSize));
if(chunkSize % 2 != 0) {
errorChain(assetFileRead(file, NULL, 1));
}
uint16_t u16;
uint32_t u32;
memoryCopy(&u16, fmtBuffer + 0, sizeof(u16));
audioFormat = endianLittleToHost16(u16);
memoryCopy(&u16, fmtBuffer + 2, sizeof(u16));
wavFile->channels = (uint8_t) endianLittleToHost16(u16);
memoryCopy(&u32, fmtBuffer + 4, sizeof(u32));
wavFile->sampleRate = endianLittleToHost32(u32);
memoryCopy(&u16, fmtBuffer + 14, sizeof(u16));
wavFile->bitsPerSample = (uint8_t) endianLittleToHost16(u16);
foundFormat = true;
} else if(memoryCompare(chunkHeader, "data", 4) == 0) {
wavFile->dataOffset = (size_t) file->position;
wavFile->dataSize = chunkSize;
foundData = true;
} else {
errorChain(assetFileRead(file, NULL, chunkSizePadded));
}
}
if(!foundFormat) {
errorThrow("WAV file is missing its 'fmt ' chunk: %s", file->filename);
}
if(!foundData) {
errorThrow("WAV file is missing its 'data' chunk: %s", file->filename);
}
if(audioFormat != 1) {
errorThrow(
"Unsupported WAV audio format: %u (only PCM is supported): %s",
audioFormat, file->filename
);
}
if(wavFile->bitsPerSample != 16 && wavFile->bitsPerSample != 24) {
errorThrow(
"Unsupported WAV bits per sample: %u (only 16/24-bit is supported): %s",
wavFile->bitsPerSample, file->filename
);
}
if(wavFile->channels == 0) {
errorThrow("WAV file declares 0 channels: %s", file->filename);
}
errorOk();
}
+117
View File
@@ -0,0 +1,117 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
/** Input passed to the wav loader - currently unused. */
typedef struct { void *nothing; } assetwavloaderinput_t;
typedef enum {
ASSET_WAV_LOADER_STATE_INITIAL,
ASSET_WAV_LOADER_STATE_READ_HEADER,
ASSET_WAV_LOADER_STATE_DONE
} assetwavloaderstate_t;
/** Per-slot scratch data used while the wav file is loading. */
typedef struct {
assetwavloaderstate_t state;
} assetwavloaderloading_t;
/**
* Parsed metadata for a WAV asset - only the RIFF/WAVE/fmt/data chunk
* headers are ever read; the PCM sample data itself is never loaded here
* (see @ref assetWavParseHeader). Playback reads sample data directly
* from the archive on demand instead - see audiostreampcm.h.
*/
typedef struct {
/** Sample rate of the PCM data, in Hz. */
uint32_t sampleRate;
/** Number of interleaved channels in the PCM data. */
uint8_t channels;
/**
* Bits per sample of the *source* file data - 16 or 24 (see
* assetWavParseHeader). Playback always reads 16-bit samples out via
* audioStreamPcmRead() regardless of this - a 24-bit source is
* truncated to 16-bit there, since none of this project's audio
* backends (PSP/Dolphin hardware, SDL2 on Linux) accept anything wider.
*/
uint8_t bitsPerSample;
/** Byte offset from the start of the file to the first PCM sample. */
size_t dataOffset;
/** Size of the PCM data, in bytes. */
size_t dataSize;
} assetwavfile_t;
/** Convenience alias - the loaded output type of a wav asset entry. */
typedef assetwavfile_t assetwavoutput_t;
/**
* Asynchronous loader callback. Opens the WAV file and reads just enough of
* it to locate and parse the `fmt ` chunk and locate (not read) the `data`
* chunk - see @ref assetWavParseHeader. All I/O happens here so the main
* thread is not blocked. Sets entry state to `ASSET_ENTRY_STATE_PENDING_SYNC`
* on success or `ASSET_ENTRY_STATE_ERROR` on failure.
*
* @param loading The loading slot for this asset entry.
* @return OK on success, error otherwise.
*/
errorret_t assetWavLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader callback. Confirms the async phase completed and marks
* the entry as `ASSET_ENTRY_STATE_LOADED`.
*
* @param loading The loading slot for this asset entry.
* @return OK on success, error otherwise.
*/
errorret_t assetWavLoaderSync(assetloading_t *loading);
/**
* Dispose callback. The wav asset owns no allocations of its own (its data
* chunk is read directly from the archive by whichever streams are playing
* it, each through their own handle - see audiostreampcm.h), so this is
* currently a no-op beyond the standard asserts.
*
* @param entry The asset entry to dispose.
* @return OK on success, error otherwise.
*/
errorret_t assetWavDispose(assetentry_t *entry);
/**
* Parses a RIFF/WAVE file's chunk structure from an already-open asset
* file, reading only chunk headers (and the small `fmt ` chunk body) -
* every other chunk, including `data`'s actual sample bytes, is skipped
* over via `assetFileRead(file, NULL, size)` rather than read into memory,
* so this never buffers the (potentially large) PCM payload.
*
* Stops as soon as the `data` chunk header is found, recording its file
* offset and declared size in `wavFile` without reading any of its bytes -
* the file is left positioned at the start of the PCM data.
*
* Only PCM (audio format 1), 16- or 24-bit-per-sample WAV data is
* supported - see audiostreampcm.h's audioStreamPcmRead() for how a
* 24-bit source gets truncated to the 16-bit output every platform
* backend expects. Requires the `fmt ` chunk to appear before `data`, per
* the WAV spec's recommended ordering.
*
* @param file An open asset file, positioned at the start of the WAV data.
* @param wavFile Struct whose fields will be filled in.
* @return OK on success, error if the file is malformed or an unsupported
* format.
*/
errorret_t assetWavParseHeader(
assetfile_t *file,
assetwavfile_t *wavFile
);
+12 -1
View File
@@ -7,6 +7,7 @@
#include "audio.h"
#include "util/memory.h"
#include "assert/assert.h"
audio_t AUDIO;
@@ -18,7 +19,17 @@ errorret_t audioInit() {
errorOk();
}
audiostream_t * audioAquireStream() {
audiostream_t * audioAquireStream(assetentry_t *asset) {
assertNotNull(asset, "Asset cannot be NULL.");
// Same check audioStreamInit() makes - see its own comment on why this
// is an assert (a programmer error, not untrusted data). Checking here
// too catches the mistake as early as possible, before any stream slot
// is handed out for it.
assertTrue(
asset->type == ASSET_LOADER_TYPE_WAV,
"Unsupported asset type for an audio stream."
);
for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) {
audiostream_t *stream = &AUDIO.streams[i];
if(stream->type == AUDIO_STREAM_TYPE_NULL) {
+11 -5
View File
@@ -36,12 +36,18 @@ extern audio_t AUDIO;
errorret_t audioInit();
/**
* Aquires an available audio stream, can return NULL if there is no available
* stream.
*
* @return Pointer to an available audio stream.
* Aquires an available audio stream for playing the given asset. Can return
* NULL if there is no available stream slot.
*
* The returned stream is not yet configured to play `asset` - call
* audioStreamInit(stream, asset) next.
*
* @param asset The asset the caller intends to play - validated eagerly so
* an unsupported asset type is caught here rather than only
* once audioStreamInit() is called.
* @return Pointer to an available audio stream, or NULL if none are free.
*/
audiostream_t * audioAquireStream();
audiostream_t * audioAquireStream(assetentry_t *asset);
/**
* Updates the audio subsystem, updating every active stream. Should be
+37 -5
View File
@@ -8,12 +8,23 @@
#include "audiostream.h"
#include "assert/assert.h"
errorret_t audioStreamInit(audiostream_t *stream) {
errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
assertNotNull(stream, "Stream cannot be NULL.");
assertNotNull(asset, "Asset cannot be NULL.");
assertTrue(
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
// 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,
"Unsupported asset type for an audio stream."
);
stream->state = 0;
stream->data = NULL;
stream->dataSize = 0;
stream->volume = 0xFF;
stream->directionality = AUDIO_STREAM_CENTER;
stream->loopStart = -1;
@@ -27,8 +38,20 @@ errorret_t audioStreamInit(audiostream_t *stream) {
stream->loopCount = 0;
stream->lastLoopCount = 0;
// Type-specific setup (audioStreamPcmInit() / audioStreamMp3Init()) sets
// stream->type and asks the platform implementation to set up its state.
// Locked for as long as the stream is in use (see audiostream_t.asset's
// own comment) - released in audioStreamDispose().
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);
if(errorIsNotOk(ret)) {
assetEntryUnlock(asset);
stream->asset = NULL;
errorChain(ret);
}
errorOk();
}
@@ -192,6 +215,15 @@ errorret_t audioStreamDispose(audiostream_t *stream) {
errorChain(audioStreamPlatformDispose(stream));
}
if(stream->type == AUDIO_STREAM_TYPE_PCM) {
errorChain(audioStreamPcmDispose(stream));
}
if(stream->asset != NULL) {
assetEntryUnlock(stream->asset);
stream->asset = NULL;
}
stream->type = AUDIO_STREAM_TYPE_NULL;
errorOk();
+25 -14
View File
@@ -9,6 +9,7 @@
#include "audio/audiostreampcm.h"
#include "audio/audiostreammp3.h"
#include "audio/audiostreamplatform.h"
#include "asset/loader/assetentry.h"
#ifndef audioStreamPlatformInit
#error "audioStreamPlatformInit is not defined"
@@ -50,11 +51,14 @@ typedef struct audiostream_s {
// What state the stream is in.
uint8_t state;
// Raw PCM Data, already decoded. Not owned by the stream.
uint8_t *data;
// Size of the buffer pointed to by data, in bytes.
size_t dataSize;
// 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;
@@ -127,15 +131,22 @@ typedef struct audiostream_s {
} audiostream_t;
/**
* Initializes an audio stream in preparation for playback. Does not begin
* playback; call audioStreamPlay() once the stream is configured (see
* audioStreamPcmInit() / audioStreamMp3Init() for the type-specific setup
* this must be followed by).
* 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 (currently only ASSET_LOADER_TYPE_WAV).
* @return Error indicating success or failure.
*/
errorret_t audioStreamInit(audiostream_t *stream);
errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset);
/**
* Begins or resumes playback of the given audio stream.
@@ -218,10 +229,10 @@ void audioStreamSetDirectionality(
);
/**
* Sets whether the given audio stream loops back to the start when it
* reaches the end, rather than stopping and firing onEnd. Only looping
* back to the very start of the buffer is currently supported - loopTo is
* not yet honored.
* 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.
+148 -15
View File
@@ -8,29 +8,162 @@
#include "audiostreampcm.h"
#include "audiostream.h"
#include "assert/assert.h"
#include "util/math.h"
#include "util/memory.h"
errorret_t audioStreamPcmInit(
audiostream_t *stream,
uint8_t *data,
const size_t dataSize,
const uint32_t sampleRate,
const uint8_t channels
) {
errorret_t audioStreamPcmInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
assertNotNull(data, "Data cannot be NULL.");
assertNotNull(stream->asset, "Stream must have an asset assigned.");
assertTrue(
stream->asset->type == ASSET_LOADER_TYPE_WAV,
"Asset is not a WAV file."
);
assetwavfile_t *wav = &stream->asset->data.wav;
stream->type = AUDIO_STREAM_TYPE_PCM;
stream->data = data;
stream->dataSize = dataSize;
stream->pcm.sampleRate = sampleRate;
stream->pcm.channels = channels;
stream->pcm.sampleRate = wav->sampleRate;
stream->pcm.channels = wav->channels;
stream->duration = (
(float_t) dataSize /
(float_t) (channels * sizeof(int16_t)) /
(float_t) sampleRate
(float_t) audioStreamPcmGetTotalFrames(stream) /
(float_t) wav->sampleRate
);
// Each stream opens its own independent handle to the same underlying
// asset file, rather than sharing a single handle on the asset entry -
// multiple streams playing the same asset concurrently (e.g. two
// simultaneous plays of the same sound effect) would otherwise fight
// over one shared read position. This isn't the final answer for that
// (a shared decode/cache layer would scale better than N independent
// decompression streams of the same data), but it's a correct one for
// now - a problem to revisit once it actually matters.
errorChain(assetFileInit(
&stream->pcm.file, stream->asset->name, NULL, NULL
));
errorChain(assetFileOpen(&stream->pcm.file));
errorChain(assetFileRead(&stream->pcm.file, NULL, wav->dataOffset));
errorChain(audioStreamPlatformInit(stream));
errorOk();
}
errorret_t audioStreamPcmDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM.");
errorChain(assetFileClose(&stream->pcm.file));
errorChain(assetFileDispose(&stream->pcm.file));
errorOk();
}
size_t audioStreamPcmGetTotalFrames(const audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM.");
const assetwavfile_t *wav = &stream->asset->data.wav;
const size_t sourceFrameSize = (
stream->pcm.channels * (wav->bitsPerSample / 8)
);
return wav->dataSize / sourceFrameSize;
}
errorret_t audioStreamPcmSeek(audiostream_t *stream, const size_t frame) {
assertNotNull(stream, "Stream cannot be NULL.");
assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM.");
assetwavfile_t *wav = &stream->asset->data.wav;
const size_t sourceFrameSize = (
stream->pcm.channels * (wav->bitsPerSample / 8)
);
const size_t targetByte = wav->dataOffset + (frame * sourceFrameSize);
assertTrue(
frame * sourceFrameSize <= wav->dataSize,
"Seek frame is beyond the end of the PCM data."
);
const size_t currentByte = (size_t) stream->pcm.file.position;
if(targetByte < currentByte) {
// Only a full rewind can move a read cursor earlier once it's already
// advanced past a point - a compressed archive entry can't be decoded
// backward. See this function's own doc comment for the performance
// implications of a deep loopTo.
errorChain(assetFileRewind(&stream->pcm.file));
errorChain(assetFileRead(&stream->pcm.file, NULL, targetByte));
} else if(targetByte > currentByte) {
errorChain(assetFileRead(
&stream->pcm.file, NULL, targetByte - currentByte
));
}
errorOk();
}
errorret_t audioStreamPcmRead(
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_PCM, "Stream is not PCM.");
assetwavfile_t *wav = &stream->asset->data.wav;
const size_t channels = stream->pcm.channels;
const size_t sourceSampleBytes = wav->bitsPerSample / 8;
const size_t sourceFrameSize = channels * sourceSampleBytes;
const size_t dataEndByte = wav->dataOffset + wav->dataSize;
const size_t currentByte = (size_t) stream->pcm.file.position;
const size_t bytesAvailable = (
currentByte < dataEndByte ? dataEndByte - currentByte : 0
);
const size_t framesAvailable = bytesAvailable / sourceFrameSize;
const size_t framesToRead = mathMin(frameCount, framesAvailable);
if(framesToRead == 0) {
*outFramesRead = 0;
errorOk();
}
if(sourceSampleBytes == sizeof(int16_t)) {
// Output format already matches the source - read straight through.
errorChain(assetFileRead(
&stream->pcm.file, buffer, framesToRead * sourceFrameSize
));
} else {
// 24-bit source (the only other width assetWavParseHeader() accepts) -
// read the raw 3-byte samples into a scratch buffer, then truncate
// each down to 16-bit, since none of PSP/Dolphin's audio hardware (or
// SDL2 on Linux) accepts anything wider - see assetwavfile_t's own
// comment on bitsPerSample.
assertTrue(sourceSampleBytes == 3, "Unsupported PCM sample width.");
const size_t rawBytes = framesToRead * sourceFrameSize;
uint8_t *raw = memoryAllocate(rawBytes);
errorret_t ret = assetFileRead(&stream->pcm.file, raw, rawBytes);
if(errorIsNotOk(ret)) {
memoryFree(raw);
errorChain(ret);
}
// 24-bit PCM samples are little-endian two's complement - the top two
// bytes of each (indices 1 and 2) already form that same value
// truncated to 16-bit (equivalent to an arithmetic right-shift by 8
// bits, which preserves the sign correctly since byte 2 carries it).
const size_t sampleCount = framesToRead * channels;
for(size_t i = 0; i < sampleCount; i++) {
const uint8_t *sample = raw + (i * 3);
buffer[i] = (int16_t) (sample[1] | (sample[2] << 8));
}
memoryFree(raw);
}
*outFramesRead = framesToRead;
errorOk();
}
+78 -13
View File
@@ -7,6 +7,7 @@
#pragma once
#include "error/error.h"
#include "asset/assetfile.h"
typedef struct audiostream_s audiostream_t;
@@ -16,25 +17,89 @@ typedef struct {
// 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
// what audioStreamPcmRead()/audioStreamPcmSeek() operate on.
assetfile_t file;
} audiostreampcm_t;
/**
* Configures the given audio stream to play raw, already-decoded 16-bit
* signed PCM data. Must be called after audioStreamInit() and before
* audioStreamPlay().
* Configures the given PCM 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) rather than
* taking raw decoded data: sampleRate/channels/duration are read from the
* asset's parsed WAV header, and a private file handle is opened for
* reading sample data on demand as playback consumes it (see
* audioStreamPcmRead()) - the PCM data itself is never read into memory
* all at once, so this works the same regardless of how long the
* underlying clip is.
*
* Must be called after stream->asset is set and before audioStreamPlay().
*
* @param stream The audio stream to configure.
* @param data The raw PCM data to play. Not copied; must remain valid for as
* long as the stream is playing it.
* @param dataSize The size of data, in bytes.
* @param sampleRate The sample rate of data, in Hz.
* @param channels The number of interleaved channels in data.
* @return Error indicating success or failure.
*/
errorret_t audioStreamPcmInit(
errorret_t audioStreamPcmInit(audiostream_t *stream);
/**
* Disposes the given PCM audio stream's own private file handle.
*
* @param stream The audio stream to dispose.
* @return Error indicating success or failure.
*/
errorret_t audioStreamPcmDispose(audiostream_t *stream);
/**
* Returns the total number of frames (one sample per channel) available
* in the stream's underlying PCM data - NOT simply the asset's declared
* byte size divided by a 16-bit frame size, since the source data isn't
* always 16-bit even though audioStreamPcmRead() always produces 16-bit
* output (see assetwavfile_t.bitsPerSample). Platform backends should use
* this rather than computing frame counts from dataSize themselves.
*
* @param stream The audio stream to query. Must be AUDIO_STREAM_TYPE_PCM.
* @return The total number of frames available.
*/
size_t audioStreamPcmGetTotalFrames(const audiostream_t *stream);
/**
* Seeks the stream's private read cursor to the given frame offset
* (relative to the start of the PCM data - the same units as
* audiostream_t's startFrame/loopStart/loopTo, once converted from
* seconds). Seeking backward re-reads from the start of the underlying
* asset file (see assetFileRewind()) since a compressed archive entry can
* only be decoded forward - this makes a loop with a deep loopTo more
* expensive to restart than one near the start, which is a real
* performance caveat, not just a theoretical one, for anything backed by
* a compressed (not stored) asset archive entry.
*
* @param stream The audio stream to seek. Must be AUDIO_STREAM_TYPE_PCM.
* @param frame Frame offset to seek to, relative to the start of the PCM
* data.
* @return Error indicating success or failure.
*/
errorret_t audioStreamPcmSeek(audiostream_t *stream, const size_t frame);
/**
* Reads up to frameCount frames of PCM sample data from the stream's
* current read position, advancing it by however many frames were
* actually read. Reads fewer than frameCount (down to zero) once the
* underlying asset's PCM data is exhausted, rather than erroring - it's
* up to the caller (platform code, which already knows about loop points)
* to request no more than what it wants read from within the current
* loop segment or the true end of the clip.
*
* @param stream The audio stream to read from. Must be AUDIO_STREAM_TYPE_PCM.
* @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 audioStreamPcmRead(
audiostream_t *stream,
uint8_t *data,
const size_t dataSize,
const uint32_t sampleRate,
const uint8_t channels
int16_t *buffer,
const size_t frameCount,
size_t *outFramesRead
);
+13 -27
View File
@@ -23,23 +23,9 @@
#include "console/console.h"
#include "save/save.h"
#include "audio/audio.h"
#include "util/math.h"
#include <math.h>
engine_t ENGINE;
// 44100Hz: PSP's sceAudioChReserve hardware channels are fixed at this rate.
#define AUDIO_TEST_TONE_SAMPLE_RATE 44100
// 441Hz (not 440): divides 44100Hz evenly into exactly 100 samples/cycle,
// so a whole-second buffer's last sample exactly matches its first -
// a seamless loop point with no source-data discontinuity to click on.
#define AUDIO_TEST_TONE_FREQUENCY 441
#define AUDIO_TEST_TONE_SAMPLES AUDIO_TEST_TONE_SAMPLE_RATE
#define AUDIO_TEST_TONE_SIZE (AUDIO_TEST_TONE_SAMPLES * sizeof(int16_t))
// 1 second, 16-bit signed mono PCM sine wave, used to smoke-test playback.
static uint8_t AUDIO_TEST_TONE[AUDIO_TEST_TONE_SIZE];
void engineTestToneOnEnd(audiostream_t *stream) {
consolePrint("Test tone finished playing");
}
@@ -73,20 +59,20 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
#endif
errorChain(sceneInit());
int16_t *toneSamples = (int16_t *) AUDIO_TEST_TONE;
for(uint32_t i = 0; i < AUDIO_TEST_TONE_SAMPLES; i++) {
float_t t = (float_t) i / AUDIO_TEST_TONE_SAMPLE_RATE;
toneSamples[i] = (int16_t) (
sinf(2.0f * MATH_PI * AUDIO_TEST_TONE_FREQUENCY * t) * INT16_MAX
);
}
// Smoke-tests the audio subsystem end to end (asset loading -> PCM
// 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
);
errorChain(assetRequireLoaded(testToneEntry));
audiostream_t *stream = audioAquireStream(testToneEntry);
assertNotNull(stream, "No free audio stream slots available.");
errorChain(audioStreamInit(stream, testToneEntry));
assetUnlockEntry(testToneEntry); // The stream now holds its own lock.
audiostream_t *stream = audioAquireStream();
errorChain(audioStreamInit(stream));
errorChain(audioStreamPcmInit(
stream, AUDIO_TEST_TONE, AUDIO_TEST_TONE_SIZE,
AUDIO_TEST_TONE_SAMPLE_RATE, 1
));
stream->onEnd = engineTestToneOnEnd;
stream->onLoop = engineTestToneOnLoop;
audioStreamSetLooping(stream, true);
+66 -10
View File
@@ -9,6 +9,7 @@
#include "audio/audiostream.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include <ansndlib.h>
#include <ogc/cache.h>
#include <ogc/system.h>
@@ -25,6 +26,7 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream) {
stream->platform.voiceId = voiceId;
stream->platform.finished = false;
stream->platform.buffer = NULL;
errorOk();
}
@@ -34,6 +36,11 @@ errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
ansnd_deallocate_voice((u32) stream->platform.voiceId);
if(stream->platform.buffer != NULL) {
memoryFree(stream->platform.buffer);
stream->platform.buffer = NULL;
}
errorOk();
}
@@ -41,6 +48,28 @@ 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);
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) the DSP wraps within, once looping is enabled -
// defaulting to the whole clip (loopStart == -1, loopTo == 0) so
// 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)
: totalFrames;
const size_t loopToFrame = mathMin(
(size_t) (stream->loopTo * stream->pcm.sampleRate), loopEndFrame
);
const size_t startFrame = mathMin(stream->startFrame, totalFrames);
stream->startFrame = 0;
stream->seeking = false;
// A seek can legitimately land past the loop segment's end (e.g. into an
// outro) - buffer out to the true end of the clip in that case, same as
// PSP/Linux, instead of only ever buffering the loop segment.
const size_t bufferFrames = startFrame < loopEndFrame ? loopEndFrame : totalFrames;
float_t leftFactor, rightFactor;
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
@@ -49,6 +78,37 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
stream->platform.finished = false;
// ansnd's hardware loop needs one contiguous buffer spanning everything
// the voice might play - both start_offset and the loop segment address
// the same frame_data_ptr - so (unlike PSP/Linux, which stream bounded
// windows on demand) this reads the whole segment up front. Still
// bounded to the current loop segment's length rather than the whole
// clip if the clip is longer than one loop - see this struct field's
// own comment. Re-read fresh every Buffer() call.
if(stream->platform.buffer != NULL) {
memoryFree(stream->platform.buffer);
stream->platform.buffer = NULL;
}
errorChain(audioStreamPcmSeek(stream, 0));
uint8_t *buffer = (uint8_t *) memoryAllocate(bufferFrames * frameSize);
size_t framesRead = 0;
errorret_t readRet = audioStreamPcmRead(
stream, (int16_t *) buffer, bufferFrames, &framesRead
);
if(errorIsNotOk(readRet)) {
memoryFree(buffer);
errorChain(readRet);
}
if(framesRead < bufferFrames) {
// The asset is shorter than its declared header size (corrupt or
// truncated) - pad what's missing with silence.
memoryZero(
buffer + (framesRead * frameSize), (bufferFrames - framesRead) * frameSize
);
}
stream->platform.buffer = buffer;
ansnd_pcm_voice_config_t config;
memoryZero(&config, sizeof(ansnd_pcm_voice_config_t));
config.samplerate = stream->pcm.sampleRate;
@@ -63,12 +123,10 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
// ANSND_ERROR_INVALID_MEMORY) - it needs the physical address instead.
// Flush first so the DMA sees what the CPU actually wrote, not stale
// memory contents.
DCFlushRange(stream->data, stream->dataSize);
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(stream->data);
config.frame_count = (u32) (stream->dataSize / frameSize);
config.start_offset = (u32) stream->startFrame;
stream->startFrame = 0;
stream->seeking = false;
DCFlushRange(buffer, bufferFrames * frameSize);
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(buffer);
config.frame_count = (u32) bufferFrames;
config.start_offset = (u32) startFrame;
config.voice_callback = audioStreamDolphinVoiceCallback;
config.stream_callback = NULL; // single-buffer playback
config.user_pointer = stream;
@@ -87,10 +145,8 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
// loopTo's (true today only because the shared 441Hz test tone was
// deliberately chosen to divide evenly into the sample rate).
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
config.loop_start_offset = (u32) (stream->loopTo * stream->pcm.sampleRate);
config.loop_end_offset = stream->loopStart >= 0
? (u32) (stream->loopStart * stream->pcm.sampleRate) - 1
: config.frame_count - 1;
config.loop_start_offset = (u32) loopToFrame;
config.loop_end_offset = (u32) loopEndFrame - 1;
}
s32 result = ansnd_configure_pcm_voice((u32) stream->platform.voiceId, &config);
+15 -1
View File
@@ -19,6 +19,19 @@ typedef struct {
// itself (unlike PSP/Linux), so this flag is how audioStreamDolphinIsFinished()
// answers without a callback of its own.
volatile bool_t finished;
// ansnd's DSP hardware loop (loop_start_offset/loop_end_offset) needs one
// contiguous physical buffer spanning the whole segment it may play -
// both the pre-loop lead-in and the loop segment itself, since the DSP
// addresses both as offsets into the same frame_data_ptr. Unlike
// PSP/Linux (which stream bounded windows on demand), a Dolphin voice
// can't be handed data incrementally mid-playback, so this buffer is
// read from the asset and owned here for the lifetime of one pass -
// bounded to the current loop segment's length (not the whole clip, if
// the clip is longer than one loop), freed/reallocated on each
// audioStreamDolphinBuffer() call and released for good in
// audioStreamDolphinDispose().
uint8_t *buffer;
} audiostreamdolphin_t;
/**
@@ -40,7 +53,8 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream);
errorret_t audioStreamDolphinDispose(audiostream_t *stream);
/**
* Sends the stream's currently staged PCM data (stream->data) to its
* Reads the current loop segment's PCM data from the stream's asset (see
* audiostreamdolphin_t.buffer's own comment) and sends it to its
* allocated voice.
*
* @param stream The audio stream to output.
+88 -31
View File
@@ -10,12 +10,20 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include "error/error.h"
// How many frames of lead time to keep queued ahead of playback. Matches
// SDL_AudioSpec.samples below - the device's own internal buffer size - so
// this is "never let the queue run drier than one SDL-internal buffer."
#define AUDIO_LINUX_LEAD_FRAMES 4096
// How many frames audioStreamLinuxFeed() reads and queues per call - a
// few multiples of the lead margin, so one top-up comfortably outlasts
// the time between Update() calls without ever needing to hold more than
// this much decoded audio in memory at once, regardless of how long the
// underlying clip is.
#define AUDIO_LINUX_WINDOW_FRAMES (AUDIO_LINUX_LEAD_FRAMES * 4)
errorret_t audioStreamLinuxInit(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
@@ -46,7 +54,7 @@ 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 = stream->dataSize / frameSize;
const size_t totalFrames = audioStreamPcmGetTotalFrames(stream);
// Consumed synchronously (right here, in the same call that decided to
// (re)buffer) rather than left for later - see startFrame's own comment.
@@ -67,57 +75,106 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
// as PSP.
if(startFrame >= endFrame) endFrame = totalFrames;
const size_t bytes = (endFrame - startFrame) * frameSize;
const uint8_t *segment = stream->data + (startFrame * frameSize);
// Only an explicit seek discards whatever's still queued and jumps -
// a natural loop restart deliberately leaves the previous pass's tail
// (AUDIO_LINUX_LEAD_FRAMES worth) queued and appends the new pass after
// it, which is what makes looping gapless (see IsFinished()'s comment).
// Clearing on every Buffer() call would destroy that overlap and
// Clearing here on every pass would destroy that overlap and
// reintroduce the exact gap this was built to avoid.
if(seeking) {
SDL_ClearQueuedAudio(stream->platform.device);
}
errorChain(audioStreamPcmSeek(stream, startFrame));
int queued;
if(stream->volume == 0xFF) {
// Nothing to mix at full volume - queue the segment directly instead of
// allocating/zeroing a same-size scratch buffer just to copy it in.
queued = SDL_QueueAudio(stream->platform.device, segment, (Uint32) bytes);
} else {
uint8_t *mixed = memoryAllocate(bytes);
memoryZero(mixed, bytes);
SDL_MixAudioFormat(
mixed, segment, AUDIO_S16SYS, (Uint32) bytes,
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
);
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytes);
memoryFree(mixed);
}
stream->platform.position = startFrame;
stream->platform.endFrame = endFrame;
if(queued != 0) {
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
}
errorChain(audioStreamLinuxFeed(stream));
SDL_PauseAudioDevice(stream->platform.device, 0);
errorOk();
}
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 framesRemaining = (
stream->platform.position < stream->platform.endFrame
? stream->platform.endFrame - stream->platform.position
: 0
);
const size_t framesToRead = mathMin(framesRemaining, AUDIO_LINUX_WINDOW_FRAMES);
if(framesToRead == 0) {
errorOk();
}
int16_t *chunk = memoryAllocate(framesToRead * frameSize);
size_t framesRead = 0;
errorret_t ret = audioStreamPcmRead(stream, chunk, framesToRead, &framesRead);
if(errorIsNotOk(ret)) {
memoryFree(chunk);
errorChain(ret);
}
const size_t bytesRead = framesRead * frameSize;
int queued;
if(stream->volume == 0xFF) {
// Nothing to mix at full volume - queue the window directly instead of
// allocating/zeroing a same-size scratch buffer just to copy it in.
queued = SDL_QueueAudio(stream->platform.device, chunk, (Uint32) bytesRead);
} else {
uint8_t *mixed = memoryAllocate(bytesRead);
memoryZero(mixed, bytesRead);
SDL_MixAudioFormat(
mixed, (uint8_t *) chunk, AUDIO_S16SYS, (Uint32) bytesRead,
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
);
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytesRead);
memoryFree(mixed);
}
memoryFree(chunk);
if(queued != 0) {
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
}
stream->platform.position += framesRead;
errorOk();
}
bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
// Reports "finished" (ready to be re-buffered) with AUDIO_LINUX_LEAD_FRAMES
// of margin still queued, rather than waiting for the queue to actually
// run dry. SDL_QueueAudio only ever appends to a FIFO, so queuing the next
// loop this early never causes overlap - it just avoids ever going silent
// while our once-per-frame Update() notices and catches up. Waiting for
// truly empty (as this used to) guarantees a gap by definition: silence
// has already started by the time "empty" can be observed.
// Reports "finished" (ready to loop/end) with AUDIO_LINUX_LEAD_FRAMES of
// margin still queued, rather than waiting for the queue to actually run
// dry - queuing the next pass this early never causes overlap (SDL's
// queue is a plain FIFO), it just avoids ever going silent while our
// once-per-frame Update() notices and catches up. Waiting for truly
// 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)
);
return SDL_GetQueuedAudioSize(stream->platform.device) <= leadBytes;
if(SDL_GetQueuedAudioSize(stream->platform.device) > leadBytes) {
return false;
}
// Below the lead margin - if more of the current pass is left to read,
// top up now rather than reporting finished, which would otherwise
// trigger a full loop-restart/end while still mid-pass, just because
// the queue happened to run low.
if(stream->platform.position < stream->platform.endFrame) {
errorret_t ret = audioStreamLinuxFeed(stream);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return true; // Can't recover - let the shared layer end/loop it.
}
return false;
}
return true;
}
+36 -3
View File
@@ -13,6 +13,17 @@ typedef struct audiostream_s audiostream_t;
typedef struct {
SDL_AudioDeviceID device;
// Frame offset (relative to the start of the PCM data) the next
// audioStreamLinuxFeed() call should read from - advances as each
// window is queued, so a long clip is streamed progressively rather
// than read/queued all at once.
size_t position;
// Frame offset this pass stops at (the current loop segment's end, or
// the true end of the clip if not looping) - recomputed by
// audioStreamLinuxBuffer() at the start of each pass.
size_t endFrame;
} audiostreamlinux_t;
/**
@@ -34,17 +45,39 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream);
errorret_t audioStreamLinuxDispose(audiostream_t *stream);
/**
* Sends the stream's currently staged PCM data (stream->data) to its SDL2
* audio device and starts/resumes playback.
* Starts a new playback pass: seeks the stream's PCM read cursor to
* stream->startFrame (clearing the SDL queue first if this is an explicit
* seek rather than a natural loop restart - see stream->seeking's own
* comment), determines this pass's loop-segment end, and queues the first
* window via audioStreamLinuxFeed().
*
* @param stream The audio stream to output.
* @return Error state if any.
*/
errorret_t audioStreamLinuxBuffer(audiostream_t *stream);
/**
* Reads and queues one bounded window of PCM data (AUDIO_LINUX_WINDOW_FRAMES)
* from the stream's current platform.position up to platform.endFrame,
* advancing platform.position by however many frames were actually read.
* A no-op once platform.position has reached platform.endFrame.
*
* Kept as a small, bounded read/queue operation (rather than the whole
* pass at once) specifically so a long clip is never fully resident in
* memory at once - called both by audioStreamLinuxBuffer() (the first
* window of a pass) and by audioStreamLinuxIsFinished() (subsequent
* top-ups as the SDL queue drains).
*
* @param stream The audio stream to feed.
* @return Error state if any.
*/
errorret_t audioStreamLinuxFeed(audiostream_t *stream);
/**
* Checks whether the stream's SDL2 audio device has finished playing all
* queued data.
* queued data for the current pass. As a side effect, tops up the queue
* (via audioStreamLinuxFeed()) whenever it's running low but more of the
* current pass remains to be read, rather than reporting finished early.
*
* @param stream The audio stream to check.
* @return true if playback has finished, false otherwise.
+25 -79
View File
@@ -9,129 +9,75 @@
#include "asset/assetdsk.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
errorret_t assetInitPBP(const char_t *pbpPath) {
assertNotNull(pbpPath, "PBP path cannot be null.");
assertStrLenMin(pbpPath, 1, "PBP path cannot be empty.");
assertStrLenMax(pbpPath, ASSET_PBP_PATH_MAX, "PBP path is too long.");
ASSET.platform.pbpFile = fopen(pbpPath, "rb");
if(ASSET.platform.pbpFile == NULL) {
FILE *pbpFile = fopen(pbpPath, "rb");
if(pbpFile == NULL) {
errorThrow("Failed to open PBP file: %s", pbpPath);
}
// Get size of PBP file.
if(fseek(ASSET.platform.pbpFile, 0, SEEK_END) != 0) {
fclose(ASSET.platform.pbpFile);
if(fseek(pbpFile, 0, SEEK_END) != 0) {
fclose(pbpFile);
errorThrow("Failed to seek to end of PBP file : %s", pbpPath);
}
size_t pbpSize = ftell(ASSET.platform.pbpFile);
size_t pbpSize = ftell(pbpFile);
if(pbpSize == -1L) {
fclose(ASSET.platform.pbpFile);
fclose(pbpFile);
errorThrow("Failed to get size of PBP file : %s", pbpPath);
}
if(pbpSize < sizeof(assetpbpheader_t)) {
fclose(ASSET.platform.pbpFile);
fclose(pbpFile);
errorThrow("PBP file is too small to be valid: %s", pbpPath);
}
// Rewind to start
if(fseek(ASSET.platform.pbpFile, 0, SEEK_SET) != 0) {
fclose(ASSET.platform.pbpFile);
if(fseek(pbpFile, 0, SEEK_SET) != 0) {
fclose(pbpFile);
errorThrow("Failed to seek to start of PBP file : %s", pbpPath);
}
// Read the PBP header
size_t read = fread(
&ASSET.platform.pbpHeader,
1,
sizeof(assetpbpheader_t),
ASSET.platform.pbpFile
&ASSET.platform.pbpHeader, 1, sizeof(assetpbpheader_t), pbpFile
);
fclose(pbpFile);
if(read != sizeof(assetpbpheader_t)) {
fclose(ASSET.platform.pbpFile);
errorThrow("Failed to read PBP header", pbpPath);
}
if(memoryCompare(
ASSET.platform.pbpHeader.signature,
ASSET_PBP_SIGNATURE,
sizeof(ASSET_PBP_SIGNATURE)
) != 0) {
fclose(ASSET.platform.pbpFile);
errorThrow("Invalid PBP signature in file: %s", pbpPath);
}
// If we seek to the PSAR offset, we can read the WAD file from there.
// I'm not sure what PSAR was intended for, but it holds any user data we
// want, so I shoved the entire dusk wad there.
if(fseek(
ASSET.platform.pbpFile, ASSET.platform.pbpHeader.psarOffset, SEEK_SET
) != 0) {
fclose(ASSET.platform.pbpFile);
errorThrow("Failed to seek to PSAR offset in PBP file: %s", pbpPath);
}
// The PSAR region holds the embedded dusk.dsk (DSK2) archive - I'm not
// sure what PSAR was intended for, but it holds any user data we want,
// so I shoved the entire dusk.dsk there. Opened lazily/on-demand
// straight from the PBP file by path+offset (see
// assetDskOpenFromPathRange), the same way every other platform opens
// dusk.dsk, rather than reading the whole PSAR into memory up front.
size_t psarSize = pbpSize - ASSET.platform.pbpHeader.psarOffset;
errorChain(assetDskOpenFromPathRange(
pbpPath,
ASSET.platform.pbpHeader.psarOffset,
psarSize,
&ASSET.zip,
&ASSET.zipStored
));
// Read the whole PSAR (the embedded dusk.dsk zip archive) into memory up
// front and hand libzip an in-memory buffer, instead of a lazily-seeked
// FILE source: repeated seeked reads directly against the open PBP file
// proved unreliable on PSP (zip_fread() failing with EINVAL, then with
// zlib data corruption, depending on the read chunk size used). Reading
// once, straight through, and letting libzip operate on memory from then
// on avoids that read path entirely. psarSize is small (tens of KB) so
// holding it fully in RAM is cheap.
uint8_t *psarData = (uint8_t *)malloc(psarSize);
if(psarData == NULL) {
fclose(ASSET.platform.pbpFile);
errorThrow("Failed to allocate PSAR buffer for file: %s", pbpPath);
}
size_t totalRead = 0;
while(totalRead < psarSize) {
size_t chunkSize = mathMin(
psarSize - totalRead, ASSET_FILE_READ_CHUNK_MAX
);
size_t chunkRead = fread(
psarData + totalRead, 1, chunkSize, ASSET.platform.pbpFile
);
if(chunkRead == 0) {
free(psarData);
fclose(ASSET.platform.pbpFile);
errorThrow("Failed to read PSAR data from file: %s", pbpPath);
}
totalRead += chunkRead;
}
fclose(ASSET.platform.pbpFile);
ASSET.platform.pbpFile = NULL;
errorret_t ret = assetDskOpenFromBuffer(
psarData, psarSize, &ASSET.zip, &ASSET.zipStored
);
if(errorIsNotOk(ret)) {
free(psarData);
errorChain(ret);
}
ASSET.platform.dskData = psarData;
errorOk();
}
errorret_t assetDisposePBP(void) {
if(ASSET.platform.pbpFile != NULL) {
fclose(ASSET.platform.pbpFile);
ASSET.platform.pbpFile = NULL;
}
if(ASSET.platform.dskData != NULL) {
free(ASSET.platform.dskData);
ASSET.platform.dskData = NULL;
}
errorOk();
}
-6
View File
@@ -28,13 +28,7 @@ typedef struct {
} assetpbpheader_t;
typedef struct {
FILE *pbpFile;
assetpbpheader_t pbpHeader;
// Whole dusk.dsk (PSAR) blob, kept alive for as long as ASSET.zip/
// ASSET.zipStored are open since they're non-owning windows into it (see
// assetDskOpenFromBuffer). Freed in assetDisposePBP.
uint8_t *dskData;
} assetpbp_t;
/**
+41 -13
View File
@@ -128,7 +128,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
}
stream->platform.playRequested = false;
const size_t totalFrames = stream->dataSize / frameSize;
const size_t totalFrames = audioStreamPcmGetTotalFrames(stream);
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
// loopEndFrame) that a looping pass wraps within, once it's reached -
@@ -147,6 +147,14 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
size_t position = stream->platform.startFrame;
bool_t reachedEnd = false;
// Samples are read on demand from the asset (via audioStreamPcmRead(),
// sequentially, plus an explicit audioStreamPcmSeek() whenever jumping
// backward for a loop wrap) rather than indexed out of a fully
// resident buffer - a read/seek failure here (a corrupt or truncated
// asset, an I/O error) stops playback cleanly instead of crashing the
// thread on bad data.
bool_t readFailed = errorIsNotOk(audioStreamPcmSeek(stream, position));
// Every call always sends a full, constant-size AUDIO_PSP_CHUNK_FRAMES
// buffer - the channel is never re-declared to a different length, to
// avoid relying on sceAudioSetChannelDataLen's undocumented behavior
@@ -157,7 +165,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
// engine's finished+looping path is designed for platforms with no
// better option (see audiostream.c), but here the thread can just
// keep going and call onLoop itself instead.
while(!threadShouldStop(thread) && !reachedEnd) {
while(!readFailed && !threadShouldStop(thread) && !reachedEnd) {
// Normally bounded by loopEndFrame (the loop segment's end), but a
// seek can legitimately land past it (e.g. into an outro after the
// loop point) - in that case play out to the true end of the buffer
@@ -173,9 +181,22 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
const bool_t willLoop = reachesEndThisChunk &&
(stream->state & AUDIO_STREAM_STATE_LOOPING);
memoryCopy(
chunk, stream->data + (position * frameSize), framesThisChunk * frameSize
);
size_t framesRead = 0;
if(errorIsNotOk(
audioStreamPcmRead(stream, chunk, framesThisChunk, &framesRead)
)) {
readFailed = true;
break;
}
if(framesRead < framesThisChunk) {
// The asset is shorter than its declared header size (corrupt or
// truncated) - pad what's missing with silence rather than play
// whatever was left in `chunk` from a previous pass.
memoryZero(
chunk + (framesRead * channels),
(framesThisChunk - framesRead) * frameSize
);
}
const size_t remainderFrames = AUDIO_PSP_CHUNK_FRAMES - framesThisChunk;
size_t wrapFrames = 0;
@@ -205,15 +226,22 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
wrapFrames = remainderFrames < loopSegmentFrames
? remainderFrames
: loopSegmentFrames;
memoryCopy(
chunk + (framesThisChunk * channels),
stream->data + (loopToFrame * frameSize),
wrapFrames * frameSize
);
if(wrapFrames < remainderFrames) {
if(errorIsNotOk(audioStreamPcmSeek(stream, loopToFrame))) {
readFailed = true;
break;
}
size_t wrapRead = 0;
if(errorIsNotOk(audioStreamPcmRead(
stream, chunk + (framesThisChunk * channels), wrapFrames, &wrapRead
))) {
readFailed = true;
break;
}
if(wrapRead < remainderFrames) {
memoryZero(
chunk + ((framesThisChunk + wrapFrames) * channels),
(remainderFrames - wrapFrames) * frameSize
chunk + ((framesThisChunk + wrapRead) * channels),
(remainderFrames - wrapRead) * frameSize
);
}
} else if(reachesEndThisChunk) {
+6 -4
View File
@@ -60,8 +60,9 @@ errorret_t audioStreamPSPInit(audiostream_t *stream);
errorret_t audioStreamPSPDispose(audiostream_t *stream);
/**
* Wakes the stream's persistent feeder thread to stream stream->data to
* its reserved hardware output channel in small chunks until exhausted.
* Wakes the stream's persistent feeder thread to stream PCM data (read on
* demand from the stream's asset via audioStreamPcmRead()) to its reserved
* hardware output channel in small chunks until exhausted.
*
* @param stream The audio stream to output.
* @return Error state if any.
@@ -80,8 +81,9 @@ bool_t audioStreamPSPIsFinished(audiostream_t *stream);
/**
* Feeder thread entry point, run once for the stream's whole lifetime.
* Idles (polling playRequested) until woken by audioStreamPSPBuffer(),
* then streams stream->data (passed via thread->data) to its hardware
* channel in fixed-size chunks, blocking naturally on each
* then reads and streams the stream's asset PCM data (via the audiostream_t
* passed as thread->data) to its hardware channel in fixed-size chunks,
* blocking naturally on each
* sceAudioOutputPannedBlocking() call, until the whole buffer has been
* sent - then goes back to idling, ready for the next play request, until
* the thread is asked to stop.