Split audio update into early/late phases, add asset<->stream type map, switch volume/pan to float, add stream loop limit
- audioUpdate()/audioMixerUpdate() split into Early (before rendering, per-stream buffering) and Late (end of frame, starts newly-queued mixer sounds) phases, wired into engineUpdate() around displayUpdate(); mixer is now actually initialized/disposed/updated from audio.c.
- New src/dusk/audio/stream/audiostreamtype.{h,c}: asset-loader-type <-> audio-stream-type map (with file extension), plus audioStreamAssetTypeForPath()/audioStreamTypeForAssetType()/audioStreamAssetTypeForStreamType() lookups.
- audiostream_t/audiomixerchanneldata_t volume and directionality/pan switched from integer (0-0xFF / -128..127) to float (0.0-1.0 / -1.0..1.0), removing the manual scaling each platform backend had to do.
- audiostream_t gains loopLimit/loopRestartCount so a stream can be told to stop (fire onEnd) after a fixed number of loop restarts instead of looping forever; wired into the shared restart path (Linux/Dolphin) and PSP's own read-ahead loop decision.
Built and verified on Linux, PSP (Docker) and GameCube (Docker).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
+12
-2
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "audio.h"
|
#include "audio.h"
|
||||||
|
#include "audio/mixer/audiomixer.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ errorret_t audioInit() {
|
|||||||
memoryZero(&AUDIO, sizeof(audio_t));
|
memoryZero(&AUDIO, sizeof(audio_t));
|
||||||
|
|
||||||
errorChain(audioPlatformInit());
|
errorChain(audioPlatformInit());
|
||||||
|
errorChain(audioMixerInit());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -26,7 +28,7 @@ audiostream_t * audioAquireStream(assetentry_t *asset) {
|
|||||||
// too catches the mistake as early as possible, before any stream slot
|
// too catches the mistake as early as possible, before any stream slot
|
||||||
// is handed out for it.
|
// is handed out for it.
|
||||||
assertTrue(
|
assertTrue(
|
||||||
asset->type == ASSET_LOADER_TYPE_WAV || asset->type == ASSET_LOADER_TYPE_MP3,
|
audioStreamTypeForAssetType(asset->type) != AUDIO_STREAM_TYPE_NULL,
|
||||||
"Unsupported asset type for an audio stream."
|
"Unsupported asset type for an audio stream."
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -40,8 +42,9 @@ audiostream_t * audioAquireStream(assetentry_t *asset) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t audioUpdate() {
|
errorret_t audioUpdateEarly() {
|
||||||
errorChain(audioPlatformUpdate());
|
errorChain(audioPlatformUpdate());
|
||||||
|
errorChain(audioMixerUpdateEarly());
|
||||||
|
|
||||||
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];
|
||||||
@@ -51,12 +54,19 @@ errorret_t audioUpdate() {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t audioUpdateLate() {
|
||||||
|
errorChain(audioMixerUpdateLate());
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t audioDispose() {
|
errorret_t audioDispose() {
|
||||||
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];
|
||||||
errorChain(audioStreamDispose(stream));
|
errorChain(audioStreamDispose(stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorChain(audioMixerDispose());
|
||||||
errorChain(audioPlatformDispose());
|
errorChain(audioPlatformDispose());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
+14
-2
@@ -51,11 +51,23 @@ 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
|
||||||
* called once per frame.
|
* called once per frame, before rendering - keeps stream buffering/decode
|
||||||
|
* work off the render thread's critical path (see duskmad's MP3 decode
|
||||||
|
* ring for why this matters).
|
||||||
*
|
*
|
||||||
* @return Error indicating success or failure of the operation.
|
* @return Error indicating success or failure of the operation.
|
||||||
*/
|
*/
|
||||||
errorret_t audioUpdate();
|
errorret_t audioUpdateEarly();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the audio subsystem's end-of-frame work - currently just starting
|
||||||
|
* whatever the audio mixer queued this frame (see
|
||||||
|
* audioMixerUpdateLate()'s own comment). Should be called once per frame,
|
||||||
|
* after rendering.
|
||||||
|
*
|
||||||
|
* @return Error indicating success or failure of the operation.
|
||||||
|
*/
|
||||||
|
errorret_t audioUpdateLate();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes the audio subsystem, stopping and disposing every active stream.
|
* Disposes the audio subsystem, stopping and disposing every active stream.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "audiomixer.h"
|
#include "audiomixer.h"
|
||||||
|
#include "audio/stream/audiostream.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
@@ -18,7 +19,11 @@ errorret_t audioMixerInit() {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t audioMixerUpdate() {
|
errorret_t audioMixerUpdateEarly() {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t audioMixerUpdateLate() {
|
||||||
// For each channel.
|
// For each channel.
|
||||||
audiomixerchannel_t channel = 0;
|
audiomixerchannel_t channel = 0;
|
||||||
do {
|
do {
|
||||||
@@ -46,8 +51,8 @@ errorret_t audioMixerUpdate() {
|
|||||||
void audioMixerPlay(
|
void audioMixerPlay(
|
||||||
const char_t *file,
|
const char_t *file,
|
||||||
const audiomixerchannel_t channel,
|
const audiomixerchannel_t channel,
|
||||||
const uint8_t volume,
|
const float_t volume,
|
||||||
const int8_t pan
|
const float_t pan
|
||||||
) {
|
) {
|
||||||
audioMixerPlayLooped(file, channel, volume, pan, 0, 0.0f, 0.0f);
|
audioMixerPlayLooped(file, channel, volume, pan, 0, 0.0f, 0.0f);
|
||||||
}
|
}
|
||||||
@@ -55,16 +60,19 @@ void audioMixerPlay(
|
|||||||
void audioMixerPlayLooped(
|
void audioMixerPlayLooped(
|
||||||
const char_t *file,
|
const char_t *file,
|
||||||
const audiomixerchannel_t channel,
|
const audiomixerchannel_t channel,
|
||||||
const uint8_t volume,
|
const float_t volume,
|
||||||
const int8_t pan,
|
const float_t pan,
|
||||||
const uint8_t loopCount,
|
const uint8_t loopCount,
|
||||||
const float_t loopStart,
|
const float_t loopStart,
|
||||||
const float_t loopTo
|
const float_t loopTo
|
||||||
) {
|
) {
|
||||||
assertStrLenMin(file, 1, "File is required, to stop use audioMixerStop");
|
assertStrLenMin(file, 1, "File is required, to stop use audioMixerStop");
|
||||||
assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range");
|
assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range");
|
||||||
assertTrue(volume <= 0xFF, "Volume is out of range");
|
assertTrue(volume > 0.0f && volume <= 1.0f, "Volume is out of range");
|
||||||
assertTrue(volume > 0, "Volume must be greater than 0");
|
assertTrue(
|
||||||
|
pan >= AUDIO_STREAM_LEFT && pan <= AUDIO_STREAM_RIGHT,
|
||||||
|
"Pan is out of range"
|
||||||
|
);
|
||||||
assertTrue(loopStart >= 0.0f, "Loop start incorrect");
|
assertTrue(loopStart >= 0.0f, "Loop start incorrect");
|
||||||
assertTrue(loopTo >= 0.0f, "Loop to incorrect");
|
assertTrue(loopTo >= 0.0f, "Loop to incorrect");
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ typedef struct {
|
|||||||
assetentry_t *asset;
|
assetentry_t *asset;
|
||||||
|
|
||||||
char_t file[AUDIO_PATH_MAX];
|
char_t file[AUDIO_PATH_MAX];
|
||||||
uint8_t volume;
|
float_t volume;
|
||||||
int8_t pan;
|
float_t pan;
|
||||||
uint8_t loopCount;
|
uint8_t loopCount;
|
||||||
float_t loopStart;
|
float_t loopStart;
|
||||||
float_t loopTo;
|
float_t loopTo;
|
||||||
@@ -45,24 +45,33 @@ extern audiomixer_t AUDIO_MIXER;
|
|||||||
errorret_t audioMixerInit();
|
errorret_t audioMixerInit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the audio mixer.
|
* Updates the audio mixer, called before rendering. Reserved for
|
||||||
|
* time-sensitive mixer state (e.g. volume fades) that should be reflected
|
||||||
|
* before this frame's streams are buffered - nothing uses it yet.
|
||||||
*/
|
*/
|
||||||
errorret_t audioMixerUpdate();
|
errorret_t audioMixerUpdateEarly();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the audio mixer, called at the end of each frame - starts any
|
||||||
|
* sound queued this frame via audioMixerPlay()/audioMixerPlayLooped(), once
|
||||||
|
* every other system has had a chance to queue one.
|
||||||
|
*/
|
||||||
|
errorret_t audioMixerUpdateLate();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as audioMixerPlayLooped() but with no looping.
|
* Same as audioMixerPlayLooped() but with no looping.
|
||||||
*
|
*
|
||||||
* @param file File to be played.
|
* @param file File to be played.
|
||||||
* @param channel Channel you want to play this sound on.
|
* @param channel Channel you want to play this sound on.
|
||||||
* @param volume Volume of the sound, from 0 (silent) to 0xFF (loudest).
|
* @param volume Volume of the sound, from 0.0 (silent) to 1.0 (loudest).
|
||||||
* @param pan Stereo panning of the sound, from AUDIO_STREAM_LEFT to
|
* @param pan Stereo panning of the sound, from AUDIO_STREAM_LEFT to
|
||||||
* AUDIO_STREAM_RIGHT.
|
* AUDIO_STREAM_RIGHT.
|
||||||
*/
|
*/
|
||||||
void audioMixerPlay(
|
void audioMixerPlay(
|
||||||
const char_t *file,
|
const char_t *file,
|
||||||
const audiomixerchannel_t channel,
|
const audiomixerchannel_t channel,
|
||||||
const uint8_t volume,
|
const float_t volume,
|
||||||
const int8_t pan
|
const float_t pan
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,12 +82,12 @@ void audioMixerPlay(
|
|||||||
* to queue multiple sounds on the same channel only the last one will be played
|
* to queue multiple sounds on the same channel only the last one will be played
|
||||||
* and will stop any actively playing on that channel currently.
|
* and will stop any actively playing on that channel currently.
|
||||||
*
|
*
|
||||||
* Volume is also mixed based on the channel, so supplying volume 0xFF is then
|
* Volume is also mixed based on the channel, so supplying volume 1.0 is then
|
||||||
* multiplied by the channel's volume, e.g. 0xDD*0xFF = 0xDD.
|
* multiplied by the channel's volume, e.g. 0.5*1.0 = 0.5.
|
||||||
*
|
*
|
||||||
* @param file File to be played.
|
* @param file File to be played.
|
||||||
* @param channel Channel you want to play this sound on.
|
* @param channel Channel you want to play this sound on.
|
||||||
* @param volume Volume of the sound, from 0 (silent) to 0xFF (loudest).
|
* @param volume Volume of the sound, from 0.0 (silent) to 1.0 (loudest).
|
||||||
* @param pan Stereo panning of the sound, from AUDIO_STREAM_LEFT to
|
* @param pan Stereo panning of the sound, from AUDIO_STREAM_LEFT to
|
||||||
* AUDIO_STREAM_RIGHT.
|
* AUDIO_STREAM_RIGHT.
|
||||||
* @param loopCount How many times to loop the sound, 0 for infinite looping.
|
* @param loopCount How many times to loop the sound, 0 for infinite looping.
|
||||||
@@ -89,8 +98,8 @@ void audioMixerPlay(
|
|||||||
void audioMixerPlayLooped(
|
void audioMixerPlayLooped(
|
||||||
const char_t *file,
|
const char_t *file,
|
||||||
const audiomixerchannel_t channel,
|
const audiomixerchannel_t channel,
|
||||||
const uint8_t volume,
|
const float_t volume,
|
||||||
const int8_t pan,
|
const float_t pan,
|
||||||
const uint8_t loopCount,
|
const uint8_t loopCount,
|
||||||
const float_t loopStart,
|
const float_t loopStart,
|
||||||
const float_t loopTo
|
const float_t loopTo
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
audiostream.c
|
audiostream.c
|
||||||
audiostreampcm.c
|
audiostreampcm.c
|
||||||
audiostreammp3.c
|
audiostreammp3.c
|
||||||
|
audiostreamtype.c
|
||||||
)
|
)
|
||||||
@@ -16,17 +16,17 @@ errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
|
|||||||
asset->state == ASSET_ENTRY_STATE_LOADED,
|
asset->state == ASSET_ENTRY_STATE_LOADED,
|
||||||
"Asset must be loaded before it can back an audio stream."
|
"Asset must be loaded before it can back an audio stream."
|
||||||
);
|
);
|
||||||
// Only WAV (-> PCM) and MP3 assets can back an audio stream - a caller
|
// A caller passing an asset type that can't back an audio stream is a
|
||||||
// passing the wrong kind of asset entry is a programmer error, not
|
// programmer error, not something that can happen from untrusted data, so
|
||||||
// something that can happen from untrusted data, so this is an assert
|
// this is an assert rather than an errorThrow (see
|
||||||
// rather than an errorThrow (see feedback_assert_vs_error convention).
|
// feedback_assert_vs_error convention).
|
||||||
assertTrue(
|
assertTrue(
|
||||||
asset->type == ASSET_LOADER_TYPE_WAV || asset->type == ASSET_LOADER_TYPE_MP3,
|
audioStreamTypeForAssetType(asset->type) != AUDIO_STREAM_TYPE_NULL,
|
||||||
"Unsupported asset type for an audio stream."
|
"Unsupported asset type for an audio stream."
|
||||||
);
|
);
|
||||||
|
|
||||||
stream->state = 0;
|
stream->state = 0;
|
||||||
stream->volume = 0xFF;
|
stream->volume = 1.0f;
|
||||||
stream->directionality = AUDIO_STREAM_CENTER;
|
stream->directionality = AUDIO_STREAM_CENTER;
|
||||||
stream->loopStart = -1;
|
stream->loopStart = -1;
|
||||||
stream->loopTo = 0;
|
stream->loopTo = 0;
|
||||||
@@ -38,6 +38,8 @@ errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) {
|
|||||||
stream->onEnd = NULL;
|
stream->onEnd = NULL;
|
||||||
stream->loopCount = 0;
|
stream->loopCount = 0;
|
||||||
stream->lastLoopCount = 0;
|
stream->lastLoopCount = 0;
|
||||||
|
stream->loopLimit = 0;
|
||||||
|
stream->loopRestartCount = 0;
|
||||||
|
|
||||||
// Locked for as long as the stream is in use (see audiostream_t.asset's
|
// Locked for as long as the stream is in use (see audiostream_t.asset's
|
||||||
// own comment) - released in audioStreamDispose().
|
// own comment) - released in audioStreamDispose().
|
||||||
@@ -146,10 +148,11 @@ void audioStreamGetPanFactors(
|
|||||||
assertNotNull(outLeft, "outLeft cannot be NULL.");
|
assertNotNull(outLeft, "outLeft cannot be NULL.");
|
||||||
assertNotNull(outRight, "outRight cannot be NULL.");
|
assertNotNull(outRight, "outRight cannot be NULL.");
|
||||||
|
|
||||||
// directionality is an int8_t clamped to AUDIO_STREAM_LEFT..RIGHT
|
// directionality is already a pan factor in AUDIO_STREAM_LEFT..RIGHT
|
||||||
// (-128..127) by its own type, so pan is always within [-1.0, 0.992] -
|
// (-1.0..1.0) - assertTrue'd here rather than clamped, since a caller
|
||||||
// no further clamping needed.
|
// passing something outside that range is a programmer error (see
|
||||||
const float_t pan = (float_t) stream->directionality / 128.0f;
|
// audioStreamSetDirectionality()'s own assert).
|
||||||
|
const float_t pan = stream->directionality;
|
||||||
|
|
||||||
*outLeft = pan > 0 ? (1.0f - pan) : 1.0f;
|
*outLeft = pan > 0 ? (1.0f - pan) : 1.0f;
|
||||||
*outRight = pan < 0 ? (1.0f + pan) : 1.0f;
|
*outRight = pan < 0 ? (1.0f + pan) : 1.0f;
|
||||||
@@ -173,17 +176,22 @@ size_t audioStreamComputeEndFrame(
|
|||||||
return endFrame;
|
return endFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume) {
|
void audioStreamSetVolume(audiostream_t *stream, const float_t volume) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(volume >= 0.0f && volume <= 1.0f, "Volume is out of range.");
|
||||||
stream->volume = volume;
|
stream->volume = volume;
|
||||||
// TODO: Do I need to update the device output? PSP may require this
|
// TODO: Do I need to update the device output? PSP may require this
|
||||||
}
|
}
|
||||||
|
|
||||||
void audioStreamSetDirectionality(
|
void audioStreamSetDirectionality(
|
||||||
audiostream_t *stream,
|
audiostream_t *stream,
|
||||||
const int8_t directionality
|
const float_t directionality
|
||||||
) {
|
) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(
|
||||||
|
directionality >= AUDIO_STREAM_LEFT && directionality <= AUDIO_STREAM_RIGHT,
|
||||||
|
"Directionality is out of range."
|
||||||
|
);
|
||||||
stream->directionality = directionality;
|
stream->directionality = directionality;
|
||||||
// TODO: Need to update internal decoder?
|
// TODO: Need to update internal decoder?
|
||||||
}
|
}
|
||||||
@@ -198,6 +206,12 @@ void audioStreamSetLooping(audiostream_t *stream, const bool_t looping) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void audioStreamSetLoopLimit(audiostream_t *stream, const uint32_t loopLimit) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
stream->loopLimit = loopLimit;
|
||||||
|
stream->loopRestartCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t audioStreamUpdate(audiostream_t *stream) {
|
errorret_t audioStreamUpdate(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
@@ -232,13 +246,22 @@ errorret_t audioStreamUpdate(audiostream_t *stream) {
|
|||||||
(stream->state & AUDIO_STREAM_STATE_BUFFERED) &&
|
(stream->state & AUDIO_STREAM_STATE_BUFFERED) &&
|
||||||
audioStreamPlatformIsFinished(stream)
|
audioStreamPlatformIsFinished(stream)
|
||||||
) {
|
) {
|
||||||
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
// A limit of 0 means unlimited; otherwise this pass's restart only goes
|
||||||
|
// ahead while fewer than loopLimit restarts have already happened -
|
||||||
|
// once it's reached, fall through to the same stop/onEnd path as a
|
||||||
|
// non-looping stream instead of restarting again.
|
||||||
|
const bool_t loopLimitReached = stream->loopLimit > 0 &&
|
||||||
|
stream->loopRestartCount >= stream->loopLimit;
|
||||||
|
|
||||||
|
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && !loopLimitReached) {
|
||||||
stream->state &= ~AUDIO_STREAM_STATE_BUFFERED;
|
stream->state &= ~AUDIO_STREAM_STATE_BUFFERED;
|
||||||
|
stream->loopRestartCount++;
|
||||||
|
|
||||||
// Resume from loopTo rather than the very start of the buffer - only
|
// Resume from loopTo rather than the very start of the buffer - only
|
||||||
// matters for platforms that reach this generic restart path at all;
|
// matters for platforms that reach this generic restart path at all
|
||||||
// PSP/Dolphin loop entirely on their own (thread/hardware) and never
|
// (Linux and Dolphin, both of which re-buffer per pass and report
|
||||||
// report "finished" while looping, so in practice this is Linux-only.
|
// "finished" at every loop boundary); PSP loops entirely on its own
|
||||||
|
// read-ahead ring instead (see loopRestartCount's own comment).
|
||||||
stream->startFrame = (size_t) (stream->loopTo * stream->sampleRate);
|
stream->startFrame = (size_t) (stream->loopTo * stream->sampleRate);
|
||||||
|
|
||||||
if(stream->onLoop != NULL) {
|
if(stream->onLoop != NULL) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "audio/stream/audiostreamtype.h"
|
||||||
#include "audio/stream/audiostreampcm.h"
|
#include "audio/stream/audiostreampcm.h"
|
||||||
#include "audio/stream/audiostreammp3.h"
|
#include "audio/stream/audiostreammp3.h"
|
||||||
#include "audio/audiostreamplatform.h"
|
#include "audio/audiostreamplatform.h"
|
||||||
@@ -31,16 +32,9 @@
|
|||||||
#define AUDIO_STREAM_STATE_PLAYING (1 << 0)
|
#define AUDIO_STREAM_STATE_PLAYING (1 << 0)
|
||||||
#define AUDIO_STREAM_STATE_LOOPING (1 << 1)
|
#define AUDIO_STREAM_STATE_LOOPING (1 << 1)
|
||||||
#define AUDIO_STREAM_STATE_BUFFERED (1 << 2)
|
#define AUDIO_STREAM_STATE_BUFFERED (1 << 2)
|
||||||
#define AUDIO_STREAM_CENTER 0
|
#define AUDIO_STREAM_CENTER 0.0f
|
||||||
#define AUDIO_STREAM_LEFT -128
|
#define AUDIO_STREAM_LEFT -1.0f
|
||||||
#define AUDIO_STREAM_RIGHT 127
|
#define AUDIO_STREAM_RIGHT 1.0f
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
AUDIO_STREAM_TYPE_NULL,
|
|
||||||
AUDIO_STREAM_TYPE_PCM,
|
|
||||||
AUDIO_STREAM_TYPE_MP3,
|
|
||||||
AUDIO_STREAM_TYPE_COUNT
|
|
||||||
} audistreamtype_t;
|
|
||||||
|
|
||||||
typedef struct audiostream_s audiostream_t;
|
typedef struct audiostream_s audiostream_t;
|
||||||
|
|
||||||
@@ -60,12 +54,14 @@ typedef struct audiostream_s {
|
|||||||
// playback may also be holding.
|
// playback may also be holding.
|
||||||
assetentry_t *asset;
|
assetentry_t *asset;
|
||||||
|
|
||||||
// Loudness. Can only be 0 to 0xFF
|
// Loudness, from 0.0 (silent) to 1.0 (loudest).
|
||||||
uint8_t volume;
|
float_t volume;
|
||||||
|
|
||||||
// In stereo space, where do we send the audio.
|
// In stereo space, where do we send the audio - from AUDIO_STREAM_LEFT
|
||||||
|
// (-1.0) to AUDIO_STREAM_RIGHT (1.0), AUDIO_STREAM_CENTER (0.0) by
|
||||||
|
// default.
|
||||||
// TODO: Can we use a 3D vector + Pro Logic II?
|
// TODO: Can we use a 3D vector + Pro Logic II?
|
||||||
int8_t directionality;
|
float_t directionality;
|
||||||
|
|
||||||
// In seconds, where the loop segment ends and playback jumps back to
|
// In seconds, where the loop segment ends and playback jumps back to
|
||||||
// loopTo - -1 (the default) means "at the end of the stream", i.e. loop
|
// loopTo - -1 (the default) means "at the end of the stream", i.e. loop
|
||||||
@@ -119,6 +115,22 @@ typedef struct audiostream_s {
|
|||||||
// onLoop for - only ever touched from the main thread.
|
// onLoop for - only ever touched from the main thread.
|
||||||
uint32_t lastLoopCount;
|
uint32_t lastLoopCount;
|
||||||
|
|
||||||
|
// Maximum number of times this stream is allowed to loop before stopping
|
||||||
|
// (firing onEnd) instead of restarting again - 0 (the default) means loop
|
||||||
|
// forever. Has no effect unless looping is also enabled via
|
||||||
|
// audioStreamSetLooping(). Set via audioStreamSetLoopLimit().
|
||||||
|
uint32_t loopLimit;
|
||||||
|
|
||||||
|
// How many loop restarts this stream has already committed to - compared
|
||||||
|
// against loopLimit each time a restart is considered, then incremented
|
||||||
|
// if it goes ahead; reset to 0 by audioStreamSetLoopLimit(). Distinct
|
||||||
|
// from loopCount (which tracks when a loop becomes audible, purely for
|
||||||
|
// onLoop notification, and is PSP-only) - this counts decided restarts,
|
||||||
|
// main-thread-only on every platform (PSP decides whether to loop from
|
||||||
|
// its own read-ahead ring, itself driven from the main thread via
|
||||||
|
// audioStreamPSPIsFinished() - see its own comment).
|
||||||
|
uint32_t loopRestartCount;
|
||||||
|
|
||||||
// PCM format of the stream's decoded output - read identically by every
|
// PCM format of the stream's decoded output - read identically by every
|
||||||
// platform backend regardless of stream type (PCM decodes straight
|
// platform backend regardless of stream type (PCM decodes straight
|
||||||
// through; MP3 decodes to this same format), so it lives here rather
|
// through; MP3 decodes to this same format), so it lives here rather
|
||||||
@@ -286,9 +298,9 @@ size_t audioStreamComputeEndFrame(
|
|||||||
* Sets the playback volume of the given audio stream.
|
* Sets the playback volume of the given audio stream.
|
||||||
*
|
*
|
||||||
* @param stream The audio stream to update.
|
* @param stream The audio stream to update.
|
||||||
* @param volume The new volume, from 0 (silent) to 0xFF (loudest).
|
* @param volume The new volume, from 0.0 (silent) to 1.0 (loudest).
|
||||||
*/
|
*/
|
||||||
void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume);
|
void audioStreamSetVolume(audiostream_t *stream, const float_t volume);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the stereo directionality (panning) of the given audio stream.
|
* Sets the stereo directionality (panning) of the given audio stream.
|
||||||
@@ -299,7 +311,7 @@ void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume);
|
|||||||
*/
|
*/
|
||||||
void audioStreamSetDirectionality(
|
void audioStreamSetDirectionality(
|
||||||
audiostream_t *stream,
|
audiostream_t *stream,
|
||||||
const int8_t directionality
|
const float_t directionality
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,6 +325,19 @@ void audioStreamSetDirectionality(
|
|||||||
*/
|
*/
|
||||||
void audioStreamSetLooping(audiostream_t *stream, const bool_t looping);
|
void audioStreamSetLooping(audiostream_t *stream, const bool_t looping);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum number of times the given audio stream is allowed to
|
||||||
|
* loop before stopping (firing onEnd) instead of restarting again. Has no
|
||||||
|
* effect unless looping is also enabled via audioStreamSetLooping(). Resets
|
||||||
|
* the stream's current loop-restart progress back to 0, so the stream gets
|
||||||
|
* the full count from this call onward.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to update.
|
||||||
|
* @param loopLimit Maximum number of loop restarts, or 0 (the default) to
|
||||||
|
* loop forever.
|
||||||
|
*/
|
||||||
|
void audioStreamSetLoopLimit(audiostream_t *stream, const uint32_t loopLimit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the given audio stream, decoding new data and advancing playback
|
* Updates the given audio stream, decoding new data and advancing playback
|
||||||
* as needed. Should be called every frame for every active stream.
|
* as needed. Should be called every frame for every active stream.
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "audiostreamtype.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
|
||||||
|
const audiostreamassettypeinfo_t AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[ASSET_LOADER_TYPE_COUNT] = {
|
||||||
|
[ASSET_LOADER_TYPE_WAV] = { .type = AUDIO_STREAM_TYPE_PCM, .extension = ".wav" },
|
||||||
|
[ASSET_LOADER_TYPE_MP3] = { .type = AUDIO_STREAM_TYPE_MP3, .extension = ".mp3" }
|
||||||
|
};
|
||||||
|
|
||||||
|
assetloadertype_t audioStreamAssetTypeForPath(const char_t *path) {
|
||||||
|
for(assetloadertype_t i = 0; i < ASSET_LOADER_TYPE_COUNT; i++) {
|
||||||
|
const char_t *extension = AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[i].extension;
|
||||||
|
if(extension != NULL && stringEndsWithCaseInsensitive(path, extension)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ASSET_LOADER_TYPE_NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
audistreamtype_t audioStreamTypeForAssetType(const assetloadertype_t assetType) {
|
||||||
|
return AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[assetType].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
assetloadertype_t audioStreamAssetTypeForStreamType(const audistreamtype_t streamType) {
|
||||||
|
if(streamType == AUDIO_STREAM_TYPE_NULL) return ASSET_LOADER_TYPE_NULL;
|
||||||
|
|
||||||
|
for(assetloadertype_t i = 0; i < ASSET_LOADER_TYPE_COUNT; i++) {
|
||||||
|
if(AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[i].type == streamType) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ASSET_LOADER_TYPE_NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
#include "asset/loader/assetloader.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_STREAM_TYPE_NULL,
|
||||||
|
AUDIO_STREAM_TYPE_PCM,
|
||||||
|
AUDIO_STREAM_TYPE_MP3,
|
||||||
|
AUDIO_STREAM_TYPE_COUNT
|
||||||
|
} audistreamtype_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// The audistreamtype_t an audio stream backed by this asset type should
|
||||||
|
// become - AUDIO_STREAM_TYPE_NULL (the zero-initialized default) for any
|
||||||
|
// asset type that can't back an audio stream.
|
||||||
|
audistreamtype_t type;
|
||||||
|
|
||||||
|
// This asset type's file extension, including the leading '.' (e.g.
|
||||||
|
// ".wav"). NULL for any asset type that can't back an audio stream.
|
||||||
|
const char_t *extension;
|
||||||
|
} audiostreamassettypeinfo_t;
|
||||||
|
|
||||||
|
// Maps an assetloadertype_t to the info describing the audio stream it
|
||||||
|
// backs. Indexed directly by assetentry_t.type; see audioStreamInit()'s own
|
||||||
|
// use of this for the canonical "is this asset type supported" check. Use
|
||||||
|
// the functions below rather than indexing this directly, except where
|
||||||
|
// you already have an assetloadertype_t in hand.
|
||||||
|
extern const audiostreamassettypeinfo_t AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[ASSET_LOADER_TYPE_COUNT];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines which asset loader type a file path's extension corresponds
|
||||||
|
* to, by matching it against AUDIO_STREAM_TYPE_FOR_ASSET_TYPE's own
|
||||||
|
* extensions (case-insensitively).
|
||||||
|
*
|
||||||
|
* @param path The file path to inspect, e.g. "audio/boa.mp3".
|
||||||
|
* @return The matching assetloadertype_t, or ASSET_LOADER_TYPE_NULL if the
|
||||||
|
* path's extension doesn't match any audio asset type.
|
||||||
|
*/
|
||||||
|
assetloadertype_t audioStreamAssetTypeForPath(const char_t *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up the audistreamtype_t an audio stream backed by the given asset
|
||||||
|
* type should become.
|
||||||
|
*
|
||||||
|
* @param assetType The asset loader type to look up.
|
||||||
|
* @return The matching audistreamtype_t, or AUDIO_STREAM_TYPE_NULL if
|
||||||
|
* assetType can't back an audio stream.
|
||||||
|
*/
|
||||||
|
audistreamtype_t audioStreamTypeForAssetType(const assetloadertype_t assetType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up which asset loader type backs audio streams of the given type -
|
||||||
|
* the reverse of audioStreamTypeForAssetType(). Scans
|
||||||
|
* AUDIO_STREAM_TYPE_FOR_ASSET_TYPE for the first entry whose type matches,
|
||||||
|
* since it isn't itself indexed by audistreamtype_t.
|
||||||
|
*
|
||||||
|
* @param streamType The audio stream type to look up.
|
||||||
|
* @return The matching assetloadertype_t, or ASSET_LOADER_TYPE_NULL if
|
||||||
|
* streamType is AUDIO_STREAM_TYPE_NULL or otherwise unmapped.
|
||||||
|
*/
|
||||||
|
assetloadertype_t audioStreamAssetTypeForStreamType(const audistreamtype_t streamType);
|
||||||
@@ -99,7 +99,7 @@ errorret_t engineUpdate(void) {
|
|||||||
timeUpdate();
|
timeUpdate();
|
||||||
inputUpdate();
|
inputUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
errorChain(audioUpdate());
|
errorChain(audioUpdateEarly());
|
||||||
errorChain(rpgUpdate());
|
errorChain(rpgUpdate());
|
||||||
errorChain(sceneUpdate());
|
errorChain(sceneUpdate());
|
||||||
errorChain(assetUpdate());
|
errorChain(assetUpdate());
|
||||||
@@ -107,6 +107,7 @@ errorret_t engineUpdate(void) {
|
|||||||
|
|
||||||
// Render
|
// Render
|
||||||
errorChain(displayUpdate());
|
errorChain(displayUpdate());
|
||||||
|
errorChain(audioUpdateLate());
|
||||||
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
|
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
|
|
||||||
float_t leftFactor, rightFactor;
|
float_t leftFactor, rightFactor;
|
||||||
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||||
const float_t baseVolume = (float_t) stream->volume / 255.0f;
|
const float_t baseVolume = stream->volume;
|
||||||
stream->platform.volumeLeft = (s32) mathClamp(baseVolume * leftFactor * MAX_VOLUME, 0, MAX_VOLUME);
|
stream->platform.volumeLeft = (s32) mathClamp(baseVolume * leftFactor * MAX_VOLUME, 0, MAX_VOLUME);
|
||||||
stream->platform.volumeRight = (s32) mathClamp(baseVolume * rightFactor * MAX_VOLUME, 0, MAX_VOLUME);
|
stream->platform.volumeRight = (s32) mathClamp(baseVolume * rightFactor * MAX_VOLUME, 0, MAX_VOLUME);
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
|
|||||||
const size_t bytesRead = framesRead * frameSize;
|
const size_t bytesRead = framesRead * frameSize;
|
||||||
int queued;
|
int queued;
|
||||||
|
|
||||||
if(stream->volume == 0xFF) {
|
if(stream->volume >= 1.0f) {
|
||||||
// Nothing to mix at full volume - queue the window directly instead of
|
// Nothing to mix at full volume - queue the window directly instead of
|
||||||
// allocating/zeroing a same-size scratch buffer just to copy it in.
|
// allocating/zeroing a same-size scratch buffer just to copy it in.
|
||||||
queued = SDL_QueueAudio(stream->platform.device, chunk, (Uint32) bytesRead);
|
queued = SDL_QueueAudio(stream->platform.device, chunk, (Uint32) bytesRead);
|
||||||
@@ -200,7 +200,7 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
|
|||||||
memoryZero(mixed, bytesRead);
|
memoryZero(mixed, bytesRead);
|
||||||
SDL_MixAudioFormat(
|
SDL_MixAudioFormat(
|
||||||
mixed, (uint8_t *) chunk, AUDIO_S16SYS, (Uint32) bytesRead,
|
mixed, (uint8_t *) chunk, AUDIO_S16SYS, (Uint32) bytesRead,
|
||||||
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
|
(int) (stream->volume * SDL_MIX_MAXVOLUME)
|
||||||
);
|
);
|
||||||
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytesRead);
|
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytesRead);
|
||||||
memoryFree(mixed);
|
memoryFree(mixed);
|
||||||
|
|||||||
@@ -232,9 +232,15 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
|
|||||||
// detection ("racing through content"). A short read that isn't
|
// detection ("racing through content"). A short read that isn't
|
||||||
// genuinely the end just means less got queued this call; the next
|
// genuinely the end just means less got queued this call; the next
|
||||||
// TopUp() call (next engine frame) naturally retries.
|
// TopUp() call (next engine frame) naturally retries.
|
||||||
|
|
||||||
|
// A limit of 0 means unlimited - see loopRestartCount's own comment for
|
||||||
|
// why counting decided (not yet necessarily audible) restarts here is
|
||||||
|
// still the right thing to compare against loopLimit.
|
||||||
|
const bool_t loopLimitReached = stream->loopLimit > 0 &&
|
||||||
|
stream->loopRestartCount >= stream->loopLimit;
|
||||||
const bool_t reachesSegmentEnd = stream->platform.readPosition >= currentEndFrame;
|
const bool_t reachesSegmentEnd = stream->platform.readPosition >= currentEndFrame;
|
||||||
const bool_t willLoop = reachesSegmentEnd &&
|
const bool_t willLoop = reachesSegmentEnd &&
|
||||||
(stream->state & AUDIO_STREAM_STATE_LOOPING);
|
(stream->state & AUDIO_STREAM_STATE_LOOPING) && !loopLimitReached;
|
||||||
|
|
||||||
if(reachesSegmentEnd) {
|
if(reachesSegmentEnd) {
|
||||||
if(willLoop) {
|
if(willLoop) {
|
||||||
@@ -243,6 +249,7 @@ void audioStreamPSPTopUp(audiostream_t *stream) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream->platform.readPosition = stream->platform.loopToFrame;
|
stream->platform.readPosition = stream->platform.loopToFrame;
|
||||||
|
stream->loopRestartCount++;
|
||||||
|
|
||||||
threadMutexLock(&stream->platform.ringLock);
|
threadMutexLock(&stream->platform.ringLock);
|
||||||
if(stream->platform.loopMarkerCount < AUDIO_PSP_LOOP_MARKER_MAX) {
|
if(stream->platform.loopMarkerCount < AUDIO_PSP_LOOP_MARKER_MAX) {
|
||||||
@@ -379,7 +386,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
float_t leftFactor, rightFactor;
|
float_t leftFactor, rightFactor;
|
||||||
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||||
|
|
||||||
const int baseVolume = (stream->volume * PSP_AUDIO_VOLUME_MAX) / 0xFF;
|
const int baseVolume = (int) (stream->volume * PSP_AUDIO_VOLUME_MAX);
|
||||||
const int leftVolume = (int) (baseVolume * leftFactor);
|
const int leftVolume = (int) (baseVolume * leftFactor);
|
||||||
const int rightVolume = (int) (baseVolume * rightFactor);
|
const int rightVolume = (int) (baseVolume * rightFactor);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user