Fixed PSP trying to load entirely into memory
This commit is contained in:
Binary file not shown.
@@ -119,6 +119,15 @@ errorret_t assetRequireLoaded(assetentry_t *entry) {
|
|||||||
assetEntryLock(entry);
|
assetEntryLock(entry);
|
||||||
|
|
||||||
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
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);
|
usleep(1000);
|
||||||
errorret_t ret = assetUpdate();
|
errorret_t ret = assetUpdate();
|
||||||
if(errorIsNotOk(ret)) {
|
if(errorIsNotOk(ret)) {
|
||||||
|
|||||||
@@ -55,6 +55,19 @@ errorret_t assetDskOpenFromPath(
|
|||||||
const char_t *path,
|
const char_t *path,
|
||||||
zip_t **outCompressed,
|
zip_t **outCompressed,
|
||||||
zip_t **outStored
|
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(path, "Path cannot be NULL.");
|
||||||
assertNotNull(outCompressed, "Out compressed 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);
|
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];
|
uint8_t headerBytes[ASSET_DSK_HEADER_SIZE];
|
||||||
size_t headerRead = fread(headerBytes, 1, sizeof(headerBytes), headerFile);
|
size_t headerRead = fread(headerBytes, 1, sizeof(headerBytes), headerFile);
|
||||||
fclose(headerFile);
|
fclose(headerFile);
|
||||||
@@ -78,11 +96,21 @@ errorret_t assetDskOpenFromPath(
|
|||||||
assetdskheader_t header;
|
assetdskheader_t header;
|
||||||
errorChain(assetDskParseHeader(headerBytes, sizeof(headerBytes), &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_t zipError;
|
||||||
zip_error_init(&zipError);
|
zip_error_init(&zipError);
|
||||||
|
|
||||||
zip_source_t *compressedSource = zip_source_file_create(
|
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) {
|
if(compressedSource == NULL) {
|
||||||
errorThrow(
|
errorThrow(
|
||||||
@@ -99,7 +127,10 @@ errorret_t assetDskOpenFromPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
zip_source_t *storedSource = zip_source_file_create(
|
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) {
|
if(storedSource == NULL) {
|
||||||
zip_close(*outCompressed);
|
zip_close(*outCompressed);
|
||||||
@@ -132,7 +163,7 @@ errorret_t assetDskOpenFromPath(
|
|||||||
*outCompressed = NULL;
|
*outCompressed = NULL;
|
||||||
errorThrow("Failed to re-open dusk.dsk to verify stored checksum: %s", path);
|
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);
|
size_t storedRead = fread(storedBytes, 1, header.storedSize, storedFile);
|
||||||
fclose(storedFile);
|
fclose(storedFile);
|
||||||
if(storedRead != header.storedSize) {
|
if(storedRead != header.storedSize) {
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ errorret_t assetDskParseHeader(
|
|||||||
* archive's checksum is intentionally not verified here since doing so
|
* 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.
|
* 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 path Filesystem path to the dusk.dsk file.
|
||||||
* @param outCompressed Set to the opened compressed archive on success.
|
* @param outCompressed Set to the opened compressed archive on success.
|
||||||
* @param outStored Set to the opened stored archive on success.
|
* @param outStored Set to the opened stored archive on success.
|
||||||
@@ -76,6 +79,30 @@ errorret_t assetDskOpenFromPath(
|
|||||||
zip_t **outStored
|
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
|
* 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
|
* (e.g. a PSAR embedded in an EBOOT.PBP, or an ISO-embedded file already
|
||||||
|
|||||||
@@ -100,8 +100,10 @@ errorret_t assetFileRead(
|
|||||||
uint8_t tempBuffer[256];
|
uint8_t tempBuffer[256];
|
||||||
while(bytesRemaining > 0) {
|
while(bytesRemaining > 0) {
|
||||||
size_t chunkSize = mathMin(bytesRemaining, sizeof(tempBuffer));
|
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));
|
errorChain(assetFileRead(file, tempBuffer, chunkSize));
|
||||||
file->position += chunkSize;
|
|
||||||
bytesRemaining -= chunkSize;
|
bytesRemaining -= chunkSize;
|
||||||
}
|
}
|
||||||
file->lastRead = bufferSize;
|
file->lastRead = bufferSize;
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ add_subdirectory(locale)
|
|||||||
add_subdirectory(json)
|
add_subdirectory(json)
|
||||||
add_subdirectory(chunk)
|
add_subdirectory(chunk)
|
||||||
add_subdirectory(dmf)
|
add_subdirectory(dmf)
|
||||||
add_subdirectory(cutscene)
|
add_subdirectory(cutscene)
|
||||||
|
add_subdirectory(wav)
|
||||||
@@ -57,4 +57,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
|||||||
.loadAsync = assetCutsceneLoaderAsync,
|
.loadAsync = assetCutsceneLoaderAsync,
|
||||||
.dispose = assetCutsceneDispose
|
.dispose = assetCutsceneDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ASSET_LOADER_TYPE_WAV] = {
|
||||||
|
.loadSync = assetWavLoaderSync,
|
||||||
|
.loadAsync = assetWavLoaderAsync,
|
||||||
|
.dispose = assetWavDispose
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "asset/loader/json/assetjsonloader.h"
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "asset/loader/chunk/assetchunkloader.h"
|
#include "asset/loader/chunk/assetchunkloader.h"
|
||||||
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
||||||
|
#include "asset/loader/wav/assetwavloader.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_LOADER_TYPE_NULL,
|
ASSET_LOADER_TYPE_NULL,
|
||||||
@@ -26,6 +27,7 @@ typedef enum {
|
|||||||
ASSET_LOADER_TYPE_JSON,
|
ASSET_LOADER_TYPE_JSON,
|
||||||
ASSET_LOADER_TYPE_CHUNK,
|
ASSET_LOADER_TYPE_CHUNK,
|
||||||
ASSET_LOADER_TYPE_CUTSCENE,
|
ASSET_LOADER_TYPE_CUTSCENE,
|
||||||
|
ASSET_LOADER_TYPE_WAV,
|
||||||
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
ASSET_LOADER_TYPE_COUNT
|
||||||
} assetloadertype_t;
|
} assetloadertype_t;
|
||||||
@@ -39,6 +41,7 @@ typedef union {
|
|||||||
assetjsonloaderloading_t json;
|
assetjsonloaderloading_t json;
|
||||||
assetchunkloaderloading_t chunk;
|
assetchunkloaderloading_t chunk;
|
||||||
assetcutsceneloaderloading_t cutscene;
|
assetcutsceneloaderloading_t cutscene;
|
||||||
|
assetwavloaderloading_t wav;
|
||||||
} assetloaderloading_t;
|
} assetloaderloading_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -50,6 +53,7 @@ typedef union {
|
|||||||
assetjsonoutput_t json;
|
assetjsonoutput_t json;
|
||||||
assetchunkoutput_t chunk;
|
assetchunkoutput_t chunk;
|
||||||
assetcutsceneoutput_t cutscene;
|
assetcutsceneoutput_t cutscene;
|
||||||
|
assetwavoutput_t wav;
|
||||||
} assetloaderoutput_t;
|
} assetloaderoutput_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -59,6 +63,7 @@ typedef union {
|
|||||||
assetjsonloaderinput_t json;
|
assetjsonloaderinput_t json;
|
||||||
assetchunkloaderinput_t chunk;
|
assetchunkloaderinput_t chunk;
|
||||||
assetcutsceneloaderinput_t cutscene;
|
assetcutsceneloaderinput_t cutscene;
|
||||||
|
assetwavloaderinput_t wav;
|
||||||
} assetloaderinput_t;
|
} assetloaderinput_t;
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "audio.h"
|
#include "audio.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
audio_t AUDIO;
|
audio_t AUDIO;
|
||||||
|
|
||||||
@@ -18,7 +19,17 @@ errorret_t audioInit() {
|
|||||||
errorOk();
|
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++) {
|
for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) {
|
||||||
audiostream_t *stream = &AUDIO.streams[i];
|
audiostream_t *stream = &AUDIO.streams[i];
|
||||||
if(stream->type == AUDIO_STREAM_TYPE_NULL) {
|
if(stream->type == AUDIO_STREAM_TYPE_NULL) {
|
||||||
|
|||||||
+11
-5
@@ -36,12 +36,18 @@ extern audio_t AUDIO;
|
|||||||
errorret_t audioInit();
|
errorret_t audioInit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aquires an available audio stream, can return NULL if there is no available
|
* Aquires an available audio stream for playing the given asset. Can return
|
||||||
* stream.
|
* NULL if there is no available stream slot.
|
||||||
*
|
*
|
||||||
* @return Pointer to an available audio stream.
|
* 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
|
* Updates the audio subsystem, updating every active stream. Should be
|
||||||
|
|||||||
@@ -8,12 +8,23 @@
|
|||||||
#include "audiostream.h"
|
#include "audiostream.h"
|
||||||
#include "assert/assert.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(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->state = 0;
|
||||||
stream->data = NULL;
|
|
||||||
stream->dataSize = 0;
|
|
||||||
stream->volume = 0xFF;
|
stream->volume = 0xFF;
|
||||||
stream->directionality = AUDIO_STREAM_CENTER;
|
stream->directionality = AUDIO_STREAM_CENTER;
|
||||||
stream->loopStart = -1;
|
stream->loopStart = -1;
|
||||||
@@ -27,8 +38,20 @@ errorret_t audioStreamInit(audiostream_t *stream) {
|
|||||||
stream->loopCount = 0;
|
stream->loopCount = 0;
|
||||||
stream->lastLoopCount = 0;
|
stream->lastLoopCount = 0;
|
||||||
|
|
||||||
// Type-specific setup (audioStreamPcmInit() / audioStreamMp3Init()) sets
|
// Locked for as long as the stream is in use (see audiostream_t.asset's
|
||||||
// stream->type and asks the platform implementation to set up its state.
|
// 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();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -192,6 +215,15 @@ errorret_t audioStreamDispose(audiostream_t *stream) {
|
|||||||
errorChain(audioStreamPlatformDispose(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;
|
stream->type = AUDIO_STREAM_TYPE_NULL;
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "audio/audiostreampcm.h"
|
#include "audio/audiostreampcm.h"
|
||||||
#include "audio/audiostreammp3.h"
|
#include "audio/audiostreammp3.h"
|
||||||
#include "audio/audiostreamplatform.h"
|
#include "audio/audiostreamplatform.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
|
||||||
#ifndef audioStreamPlatformInit
|
#ifndef audioStreamPlatformInit
|
||||||
#error "audioStreamPlatformInit is not defined"
|
#error "audioStreamPlatformInit is not defined"
|
||||||
@@ -50,11 +51,14 @@ typedef struct audiostream_s {
|
|||||||
// What state the stream is in.
|
// What state the stream is in.
|
||||||
uint8_t state;
|
uint8_t state;
|
||||||
|
|
||||||
// Raw PCM Data, already decoded. Not owned by the stream.
|
// The asset backing this stream's data (e.g. a WAV file) - determines
|
||||||
uint8_t *data;
|
// `type` (see audioStreamInit()) and is read from on demand by the
|
||||||
|
// type-specific module (e.g. audiostreampcm.h) rather than ever being
|
||||||
// Size of the buffer pointed to by data, in bytes.
|
// fully decoded into memory up front. The stream holds its own lock on
|
||||||
size_t dataSize;
|
// 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
|
// Loudness. Can only be 0 to 0xFF
|
||||||
uint8_t volume;
|
uint8_t volume;
|
||||||
@@ -127,15 +131,22 @@ typedef struct audiostream_s {
|
|||||||
} audiostream_t;
|
} audiostream_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an audio stream in preparation for playback. Does not begin
|
* Initializes an audio stream to play the given asset, determining the
|
||||||
* playback; call audioStreamPlay() once the stream is configured (see
|
* stream's type from the asset's loader type (e.g. a WAV asset becomes an
|
||||||
* audioStreamPcmInit() / audioStreamMp3Init() for the type-specific setup
|
* AUDIO_STREAM_TYPE_PCM stream) and dispatching to that type's own setup
|
||||||
* this must be followed by).
|
* (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 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.
|
* @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.
|
* 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
|
* Sets whether the given audio stream loops back to loopTo (or the start
|
||||||
* reaches the end, rather than stopping and firing onEnd. Only looping
|
* of the buffer, by default - see audioStreamSetLoopPoints()) when it
|
||||||
* back to the very start of the buffer is currently supported - loopTo is
|
* reaches loopStart (or the end of the buffer), rather than stopping and
|
||||||
* not yet honored.
|
* firing onEnd.
|
||||||
*
|
*
|
||||||
* @param stream The audio stream to update.
|
* @param stream The audio stream to update.
|
||||||
* @param looping Whether the stream should loop.
|
* @param looping Whether the stream should loop.
|
||||||
|
|||||||
+148
-15
@@ -8,29 +8,162 @@
|
|||||||
#include "audiostreampcm.h"
|
#include "audiostreampcm.h"
|
||||||
#include "audiostream.h"
|
#include "audiostream.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
errorret_t audioStreamPcmInit(
|
errorret_t audioStreamPcmInit(audiostream_t *stream) {
|
||||||
audiostream_t *stream,
|
|
||||||
uint8_t *data,
|
|
||||||
const size_t dataSize,
|
|
||||||
const uint32_t sampleRate,
|
|
||||||
const uint8_t channels
|
|
||||||
) {
|
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
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->type = AUDIO_STREAM_TYPE_PCM;
|
||||||
stream->data = data;
|
stream->pcm.sampleRate = wav->sampleRate;
|
||||||
stream->dataSize = dataSize;
|
stream->pcm.channels = wav->channels;
|
||||||
stream->pcm.sampleRate = sampleRate;
|
|
||||||
stream->pcm.channels = channels;
|
|
||||||
stream->duration = (
|
stream->duration = (
|
||||||
(float_t) dataSize /
|
(float_t) audioStreamPcmGetTotalFrames(stream) /
|
||||||
(float_t) (channels * sizeof(int16_t)) /
|
(float_t) wav->sampleRate
|
||||||
(float_t) 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));
|
errorChain(audioStreamPlatformInit(stream));
|
||||||
|
|
||||||
errorOk();
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "asset/assetfile.h"
|
||||||
|
|
||||||
typedef struct audiostream_s audiostream_t;
|
typedef struct audiostream_s audiostream_t;
|
||||||
|
|
||||||
@@ -16,25 +17,89 @@ typedef struct {
|
|||||||
|
|
||||||
// Number of interleaved channels in the stream's data.
|
// Number of interleaved channels in the stream's data.
|
||||||
uint8_t channels;
|
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;
|
} audiostreampcm_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the given audio stream to play raw, already-decoded 16-bit
|
* Configures the given PCM audio stream from its already-assigned asset
|
||||||
* signed PCM data. Must be called after audioStreamInit() and before
|
* (see audiostream_t.asset - set by audioStreamInit(), which is what
|
||||||
* audioStreamPlay().
|
* 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 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.
|
* @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,
|
audiostream_t *stream,
|
||||||
uint8_t *data,
|
int16_t *buffer,
|
||||||
const size_t dataSize,
|
const size_t frameCount,
|
||||||
const uint32_t sampleRate,
|
size_t *outFramesRead
|
||||||
const uint8_t channels
|
|
||||||
);
|
);
|
||||||
|
|||||||
+13
-27
@@ -23,23 +23,9 @@
|
|||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "audio/audio.h"
|
#include "audio/audio.h"
|
||||||
#include "util/math.h"
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
engine_t ENGINE;
|
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) {
|
void engineTestToneOnEnd(audiostream_t *stream) {
|
||||||
consolePrint("Test tone finished playing");
|
consolePrint("Test tone finished playing");
|
||||||
}
|
}
|
||||||
@@ -73,20 +59,20 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
#endif
|
#endif
|
||||||
errorChain(sceneInit());
|
errorChain(sceneInit());
|
||||||
|
|
||||||
int16_t *toneSamples = (int16_t *) AUDIO_TEST_TONE;
|
// Smoke-tests the audio subsystem end to end (asset loading -> PCM
|
||||||
for(uint32_t i = 0; i < AUDIO_TEST_TONE_SAMPLES; i++) {
|
// streaming -> platform playback) against a real WAV asset rather than
|
||||||
float_t t = (float_t) i / AUDIO_TEST_TONE_SAMPLE_RATE;
|
// synthesizing PCM data at runtime.
|
||||||
toneSamples[i] = (int16_t) (
|
assetentry_t *testToneEntry = assetLock(
|
||||||
sinf(2.0f * MATH_PI * AUDIO_TEST_TONE_FREQUENCY * t) * INT16_MAX
|
"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->onEnd = engineTestToneOnEnd;
|
||||||
stream->onLoop = engineTestToneOnLoop;
|
stream->onLoop = engineTestToneOnLoop;
|
||||||
audioStreamSetLooping(stream, true);
|
audioStreamSetLooping(stream, true);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "audio/audiostream.h"
|
#include "audio/audiostream.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
#include <ansndlib.h>
|
#include <ansndlib.h>
|
||||||
#include <ogc/cache.h>
|
#include <ogc/cache.h>
|
||||||
#include <ogc/system.h>
|
#include <ogc/system.h>
|
||||||
@@ -25,6 +26,7 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream) {
|
|||||||
|
|
||||||
stream->platform.voiceId = voiceId;
|
stream->platform.voiceId = voiceId;
|
||||||
stream->platform.finished = false;
|
stream->platform.finished = false;
|
||||||
|
stream->platform.buffer = NULL;
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -34,6 +36,11 @@ errorret_t audioStreamDolphinDispose(audiostream_t *stream) {
|
|||||||
|
|
||||||
ansnd_deallocate_voice((u32) stream->platform.voiceId);
|
ansnd_deallocate_voice((u32) stream->platform.voiceId);
|
||||||
|
|
||||||
|
if(stream->platform.buffer != NULL) {
|
||||||
|
memoryFree(stream->platform.buffer);
|
||||||
|
stream->platform.buffer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +48,28 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
|
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;
|
float_t leftFactor, rightFactor;
|
||||||
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||||
@@ -49,6 +78,37 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
|
|
||||||
stream->platform.finished = false;
|
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;
|
ansnd_pcm_voice_config_t config;
|
||||||
memoryZero(&config, sizeof(ansnd_pcm_voice_config_t));
|
memoryZero(&config, sizeof(ansnd_pcm_voice_config_t));
|
||||||
config.samplerate = stream->pcm.sampleRate;
|
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.
|
// ANSND_ERROR_INVALID_MEMORY) - it needs the physical address instead.
|
||||||
// Flush first so the DMA sees what the CPU actually wrote, not stale
|
// Flush first so the DMA sees what the CPU actually wrote, not stale
|
||||||
// memory contents.
|
// memory contents.
|
||||||
DCFlushRange(stream->data, stream->dataSize);
|
DCFlushRange(buffer, bufferFrames * frameSize);
|
||||||
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(stream->data);
|
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(buffer);
|
||||||
config.frame_count = (u32) (stream->dataSize / frameSize);
|
config.frame_count = (u32) bufferFrames;
|
||||||
config.start_offset = (u32) stream->startFrame;
|
config.start_offset = (u32) startFrame;
|
||||||
stream->startFrame = 0;
|
|
||||||
stream->seeking = false;
|
|
||||||
config.voice_callback = audioStreamDolphinVoiceCallback;
|
config.voice_callback = audioStreamDolphinVoiceCallback;
|
||||||
config.stream_callback = NULL; // single-buffer playback
|
config.stream_callback = NULL; // single-buffer playback
|
||||||
config.user_pointer = stream;
|
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
|
// loopTo's (true today only because the shared 441Hz test tone was
|
||||||
// deliberately chosen to divide evenly into the sample rate).
|
// deliberately chosen to divide evenly into the sample rate).
|
||||||
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
||||||
config.loop_start_offset = (u32) (stream->loopTo * stream->pcm.sampleRate);
|
config.loop_start_offset = (u32) loopToFrame;
|
||||||
config.loop_end_offset = stream->loopStart >= 0
|
config.loop_end_offset = (u32) loopEndFrame - 1;
|
||||||
? (u32) (stream->loopStart * stream->pcm.sampleRate) - 1
|
|
||||||
: config.frame_count - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s32 result = ansnd_configure_pcm_voice((u32) stream->platform.voiceId, &config);
|
s32 result = ansnd_configure_pcm_voice((u32) stream->platform.voiceId, &config);
|
||||||
|
|||||||
@@ -19,6 +19,19 @@ typedef struct {
|
|||||||
// itself (unlike PSP/Linux), so this flag is how audioStreamDolphinIsFinished()
|
// itself (unlike PSP/Linux), so this flag is how audioStreamDolphinIsFinished()
|
||||||
// answers without a callback of its own.
|
// answers without a callback of its own.
|
||||||
volatile bool_t finished;
|
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;
|
} audiostreamdolphin_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +53,8 @@ errorret_t audioStreamDolphinInit(audiostream_t *stream);
|
|||||||
errorret_t audioStreamDolphinDispose(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.
|
* allocated voice.
|
||||||
*
|
*
|
||||||
* @param stream The audio stream to output.
|
* @param stream The audio stream to output.
|
||||||
|
|||||||
@@ -10,12 +10,20 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
// How many frames of lead time to keep queued ahead of playback. Matches
|
// 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
|
// 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."
|
// this is "never let the queue run drier than one SDL-internal buffer."
|
||||||
#define AUDIO_LINUX_LEAD_FRAMES 4096
|
#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) {
|
errorret_t audioStreamLinuxInit(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
@@ -46,7 +54,7 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
|||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
|
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
|
// Consumed synchronously (right here, in the same call that decided to
|
||||||
// (re)buffer) rather than left for later - see startFrame's own comment.
|
// (re)buffer) rather than left for later - see startFrame's own comment.
|
||||||
@@ -67,57 +75,106 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
|||||||
// as PSP.
|
// as PSP.
|
||||||
if(startFrame >= endFrame) endFrame = totalFrames;
|
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 -
|
// Only an explicit seek discards whatever's still queued and jumps -
|
||||||
// a natural loop restart deliberately leaves the previous pass's tail
|
// a natural loop restart deliberately leaves the previous pass's tail
|
||||||
// (AUDIO_LINUX_LEAD_FRAMES worth) queued and appends the new pass after
|
// (AUDIO_LINUX_LEAD_FRAMES worth) queued and appends the new pass after
|
||||||
// it, which is what makes looping gapless (see IsFinished()'s comment).
|
// 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.
|
// reintroduce the exact gap this was built to avoid.
|
||||||
if(seeking) {
|
if(seeking) {
|
||||||
SDL_ClearQueuedAudio(stream->platform.device);
|
SDL_ClearQueuedAudio(stream->platform.device);
|
||||||
}
|
}
|
||||||
|
errorChain(audioStreamPcmSeek(stream, startFrame));
|
||||||
|
|
||||||
int queued;
|
stream->platform.position = startFrame;
|
||||||
if(stream->volume == 0xFF) {
|
stream->platform.endFrame = endFrame;
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(queued != 0) {
|
errorChain(audioStreamLinuxFeed(stream));
|
||||||
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_PauseAudioDevice(stream->platform.device, 0);
|
SDL_PauseAudioDevice(stream->platform.device, 0);
|
||||||
|
|
||||||
errorOk();
|
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) {
|
bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
// Reports "finished" (ready to be re-buffered) with AUDIO_LINUX_LEAD_FRAMES
|
// Reports "finished" (ready to loop/end) with AUDIO_LINUX_LEAD_FRAMES of
|
||||||
// of margin still queued, rather than waiting for the queue to actually
|
// margin still queued, rather than waiting for the queue to actually run
|
||||||
// run dry. SDL_QueueAudio only ever appends to a FIFO, so queuing the next
|
// dry - queuing the next pass this early never causes overlap (SDL's
|
||||||
// loop this early never causes overlap - it just avoids ever going silent
|
// queue is a plain FIFO), it just avoids ever going silent while our
|
||||||
// while our once-per-frame Update() notices and catches up. Waiting for
|
// once-per-frame Update() notices and catches up. Waiting for truly
|
||||||
// truly empty (as this used to) guarantees a gap by definition: silence
|
// empty guarantees a gap by definition: silence has already started by
|
||||||
// has already started by the time "empty" can be observed.
|
// the time "empty" can be observed.
|
||||||
const Uint32 leadBytes = (Uint32) (
|
const Uint32 leadBytes = (Uint32) (
|
||||||
AUDIO_LINUX_LEAD_FRAMES * stream->pcm.channels * sizeof(int16_t)
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ typedef struct audiostream_s audiostream_t;
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
SDL_AudioDeviceID device;
|
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;
|
} audiostreamlinux_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,17 +45,39 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream);
|
|||||||
errorret_t audioStreamLinuxDispose(audiostream_t *stream);
|
errorret_t audioStreamLinuxDispose(audiostream_t *stream);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the stream's currently staged PCM data (stream->data) to its SDL2
|
* Starts a new playback pass: seeks the stream's PCM read cursor to
|
||||||
* audio device and starts/resumes playback.
|
* 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.
|
* @param stream The audio stream to output.
|
||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
*/
|
*/
|
||||||
errorret_t audioStreamLinuxBuffer(audiostream_t *stream);
|
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
|
* 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.
|
* @param stream The audio stream to check.
|
||||||
* @return true if playback has finished, false otherwise.
|
* @return true if playback has finished, false otherwise.
|
||||||
|
|||||||
@@ -9,129 +9,75 @@
|
|||||||
#include "asset/assetdsk.h"
|
#include "asset/assetdsk.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/math.h"
|
|
||||||
|
|
||||||
errorret_t assetInitPBP(const char_t *pbpPath) {
|
errorret_t assetInitPBP(const char_t *pbpPath) {
|
||||||
assertNotNull(pbpPath, "PBP path cannot be null.");
|
assertNotNull(pbpPath, "PBP path cannot be null.");
|
||||||
assertStrLenMin(pbpPath, 1, "PBP path cannot be empty.");
|
assertStrLenMin(pbpPath, 1, "PBP path cannot be empty.");
|
||||||
assertStrLenMax(pbpPath, ASSET_PBP_PATH_MAX, "PBP path is too long.");
|
assertStrLenMax(pbpPath, ASSET_PBP_PATH_MAX, "PBP path is too long.");
|
||||||
|
|
||||||
ASSET.platform.pbpFile = fopen(pbpPath, "rb");
|
FILE *pbpFile = fopen(pbpPath, "rb");
|
||||||
if(ASSET.platform.pbpFile == NULL) {
|
if(pbpFile == NULL) {
|
||||||
errorThrow("Failed to open PBP file: %s", pbpPath);
|
errorThrow("Failed to open PBP file: %s", pbpPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get size of PBP file.
|
// Get size of PBP file.
|
||||||
if(fseek(ASSET.platform.pbpFile, 0, SEEK_END) != 0) {
|
if(fseek(pbpFile, 0, SEEK_END) != 0) {
|
||||||
fclose(ASSET.platform.pbpFile);
|
fclose(pbpFile);
|
||||||
errorThrow("Failed to seek to end of PBP file : %s", pbpPath);
|
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) {
|
if(pbpSize == -1L) {
|
||||||
fclose(ASSET.platform.pbpFile);
|
fclose(pbpFile);
|
||||||
errorThrow("Failed to get size of PBP file : %s", pbpPath);
|
errorThrow("Failed to get size of PBP file : %s", pbpPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(pbpSize < sizeof(assetpbpheader_t)) {
|
if(pbpSize < sizeof(assetpbpheader_t)) {
|
||||||
fclose(ASSET.platform.pbpFile);
|
fclose(pbpFile);
|
||||||
errorThrow("PBP file is too small to be valid: %s", pbpPath);
|
errorThrow("PBP file is too small to be valid: %s", pbpPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewind to start
|
// Rewind to start
|
||||||
if(fseek(ASSET.platform.pbpFile, 0, SEEK_SET) != 0) {
|
if(fseek(pbpFile, 0, SEEK_SET) != 0) {
|
||||||
fclose(ASSET.platform.pbpFile);
|
fclose(pbpFile);
|
||||||
errorThrow("Failed to seek to start of PBP file : %s", pbpPath);
|
errorThrow("Failed to seek to start of PBP file : %s", pbpPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the PBP header
|
// Read the PBP header
|
||||||
size_t read = fread(
|
size_t read = fread(
|
||||||
&ASSET.platform.pbpHeader,
|
&ASSET.platform.pbpHeader, 1, sizeof(assetpbpheader_t), pbpFile
|
||||||
1,
|
|
||||||
sizeof(assetpbpheader_t),
|
|
||||||
ASSET.platform.pbpFile
|
|
||||||
);
|
);
|
||||||
|
fclose(pbpFile);
|
||||||
if(read != sizeof(assetpbpheader_t)) {
|
if(read != sizeof(assetpbpheader_t)) {
|
||||||
fclose(ASSET.platform.pbpFile);
|
|
||||||
errorThrow("Failed to read PBP header", pbpPath);
|
errorThrow("Failed to read PBP header", pbpPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(memoryCompare(
|
if(memoryCompare(
|
||||||
ASSET.platform.pbpHeader.signature,
|
ASSET.platform.pbpHeader.signature,
|
||||||
ASSET_PBP_SIGNATURE,
|
ASSET_PBP_SIGNATURE,
|
||||||
sizeof(ASSET_PBP_SIGNATURE)
|
sizeof(ASSET_PBP_SIGNATURE)
|
||||||
) != 0) {
|
) != 0) {
|
||||||
fclose(ASSET.platform.pbpFile);
|
|
||||||
errorThrow("Invalid PBP signature in file: %s", pbpPath);
|
errorThrow("Invalid PBP signature in file: %s", pbpPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we seek to the PSAR offset, we can read the WAD file from there.
|
// The PSAR region holds the embedded dusk.dsk (DSK2) archive - I'm not
|
||||||
// I'm not sure what PSAR was intended for, but it holds any user data we
|
// sure what PSAR was intended for, but it holds any user data we want,
|
||||||
// want, so I shoved the entire dusk wad there.
|
// so I shoved the entire dusk.dsk there. Opened lazily/on-demand
|
||||||
if(fseek(
|
// straight from the PBP file by path+offset (see
|
||||||
ASSET.platform.pbpFile, ASSET.platform.pbpHeader.psarOffset, SEEK_SET
|
// assetDskOpenFromPathRange), the same way every other platform opens
|
||||||
) != 0) {
|
// dusk.dsk, rather than reading the whole PSAR into memory up front.
|
||||||
fclose(ASSET.platform.pbpFile);
|
|
||||||
errorThrow("Failed to seek to PSAR offset in PBP file: %s", pbpPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t psarSize = pbpSize - ASSET.platform.pbpHeader.psarOffset;
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t assetDisposePBP(void) {
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -28,13 +28,7 @@ typedef struct {
|
|||||||
} assetpbpheader_t;
|
} assetpbpheader_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
FILE *pbpFile;
|
|
||||||
assetpbpheader_t pbpHeader;
|
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;
|
} assetpbp_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
}
|
}
|
||||||
stream->platform.playRequested = false;
|
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/loopToFrame define the loop segment [loopToFrame,
|
||||||
// loopEndFrame) that a looping pass wraps within, once it's reached -
|
// 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;
|
size_t position = stream->platform.startFrame;
|
||||||
bool_t reachedEnd = false;
|
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
|
// Every call always sends a full, constant-size AUDIO_PSP_CHUNK_FRAMES
|
||||||
// buffer - the channel is never re-declared to a different length, to
|
// buffer - the channel is never re-declared to a different length, to
|
||||||
// avoid relying on sceAudioSetChannelDataLen's undocumented behavior
|
// 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
|
// engine's finished+looping path is designed for platforms with no
|
||||||
// better option (see audiostream.c), but here the thread can just
|
// better option (see audiostream.c), but here the thread can just
|
||||||
// keep going and call onLoop itself instead.
|
// 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
|
// Normally bounded by loopEndFrame (the loop segment's end), but a
|
||||||
// seek can legitimately land past it (e.g. into an outro after the
|
// 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
|
// 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 &&
|
const bool_t willLoop = reachesEndThisChunk &&
|
||||||
(stream->state & AUDIO_STREAM_STATE_LOOPING);
|
(stream->state & AUDIO_STREAM_STATE_LOOPING);
|
||||||
|
|
||||||
memoryCopy(
|
size_t framesRead = 0;
|
||||||
chunk, stream->data + (position * frameSize), framesThisChunk * frameSize
|
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;
|
const size_t remainderFrames = AUDIO_PSP_CHUNK_FRAMES - framesThisChunk;
|
||||||
size_t wrapFrames = 0;
|
size_t wrapFrames = 0;
|
||||||
@@ -205,15 +226,22 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
wrapFrames = remainderFrames < loopSegmentFrames
|
wrapFrames = remainderFrames < loopSegmentFrames
|
||||||
? remainderFrames
|
? remainderFrames
|
||||||
: loopSegmentFrames;
|
: loopSegmentFrames;
|
||||||
memoryCopy(
|
|
||||||
chunk + (framesThisChunk * channels),
|
if(errorIsNotOk(audioStreamPcmSeek(stream, loopToFrame))) {
|
||||||
stream->data + (loopToFrame * frameSize),
|
readFailed = true;
|
||||||
wrapFrames * frameSize
|
break;
|
||||||
);
|
}
|
||||||
if(wrapFrames < remainderFrames) {
|
size_t wrapRead = 0;
|
||||||
|
if(errorIsNotOk(audioStreamPcmRead(
|
||||||
|
stream, chunk + (framesThisChunk * channels), wrapFrames, &wrapRead
|
||||||
|
))) {
|
||||||
|
readFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(wrapRead < remainderFrames) {
|
||||||
memoryZero(
|
memoryZero(
|
||||||
chunk + ((framesThisChunk + wrapFrames) * channels),
|
chunk + ((framesThisChunk + wrapRead) * channels),
|
||||||
(remainderFrames - wrapFrames) * frameSize
|
(remainderFrames - wrapRead) * frameSize
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if(reachesEndThisChunk) {
|
} else if(reachesEndThisChunk) {
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ errorret_t audioStreamPSPInit(audiostream_t *stream);
|
|||||||
errorret_t audioStreamPSPDispose(audiostream_t *stream);
|
errorret_t audioStreamPSPDispose(audiostream_t *stream);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wakes the stream's persistent feeder thread to stream stream->data to
|
* Wakes the stream's persistent feeder thread to stream PCM data (read on
|
||||||
* its reserved hardware output channel in small chunks until exhausted.
|
* 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.
|
* @param stream The audio stream to output.
|
||||||
* @return Error state if any.
|
* @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.
|
* Feeder thread entry point, run once for the stream's whole lifetime.
|
||||||
* Idles (polling playRequested) until woken by audioStreamPSPBuffer(),
|
* Idles (polling playRequested) until woken by audioStreamPSPBuffer(),
|
||||||
* then streams stream->data (passed via thread->data) to its hardware
|
* then reads and streams the stream's asset PCM data (via the audiostream_t
|
||||||
* channel in fixed-size chunks, blocking naturally on each
|
* passed as thread->data) to its hardware channel in fixed-size chunks,
|
||||||
|
* blocking naturally on each
|
||||||
* sceAudioOutputPannedBlocking() call, until the whole buffer has been
|
* sceAudioOutputPannedBlocking() call, until the whole buffer has been
|
||||||
* sent - then goes back to idling, ready for the next play request, until
|
* sent - then goes back to idling, ready for the next play request, until
|
||||||
* the thread is asked to stop.
|
* the thread is asked to stop.
|
||||||
|
|||||||
Reference in New Issue
Block a user