Split mixer channel logic into its own module, persist volume settings, add channel callbacks

- Extracted src/dusk/audio/mixer/audiomixerchannel.{h,c}: the channel enum,
  command/state structs, and all per-channel logic (StopStream,
  ApplyVolume, ChannelUpdateEarly/Late) now live there; audiomixer.{h,c}
  just holds the channel array and the public Play/PlayLooped/Stop API.
- Removed audioMixerSetChannelVolume()/audioMixerSetMasterVolume() - volume
  is written directly (SAVE.settings.audioChannelVolume[channel] = 0.3f,
  SAVE.settings.audioMasterVolume = 0.3f) and only ever validated where
  it's read, in audioMixerChannelApplyVolume().
- Moved master/channel volume into savesettings_t (new writeFloatArray/
  readFloatArray save-JSON macros) so they persist as user preferences;
  `fade` moved from an unused mixer-wide array to a real per-channel
  multiplier that's now actually folded into the volume calculation.
- audioMixerUpdateEarly()/Late() now catch and log a single channel's
  error instead of letting it abort every other channel's update for that
  frame (and cascade into skipping the rest of that frame's engine
  update), matching the existing per-stream error-isolation pattern.
- Added persistent per-channel onLoop/onEnd callbacks + a user pointer
  (audiomixerchannelstate_t), wired onto every stream a channel starts via
  a trampoline (audioMixerChannelOnStreamLoop/OnStreamEnd) so they survive
  across multiple plays on the same channel, unlike the underlying stream
  instance itself.
- General cleanup: for loops -> while loops, if/else cascades -> guard
  clauses, dropped unnecessary const on locals, wrapped to 80 columns.

Built and verified on Linux, PSP (Docker), GameCube (Docker) and Wii
(Docker).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-02 16:07:42 -05:00
co-authored by Claude Sonnet 5
parent d5078b978c
commit 9c28d5a3b7
9 changed files with 491 additions and 266 deletions
+1
View File
@@ -7,4 +7,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
audiomixer.c
audiomixerchannel.c
)
+27 -120
View File
@@ -6,12 +6,9 @@
*/
#include "audiomixer.h"
#include "audio/audio.h"
#include "audio/stream/audiostream.h"
#include "audio/stream/audiostreamtype.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/math.h"
#include "assert/assert.h"
audiomixer_t AUDIO_MIXER;
@@ -19,117 +16,39 @@ audiomixer_t AUDIO_MIXER;
errorret_t audioMixerInit() {
memoryZero(&AUDIO_MIXER, sizeof(audiomixer_t));
AUDIO_MIXER.masterVolume = 1.0f;
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
AUDIO_MIXER.channels[channel].volume = 1.0f;
audiomixerchannel_t channel = 0;
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
AUDIO_MIXER.channels[channel].fade = 1.0f;
channel++;
}
errorOk();
}
errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state) {
assertNotNull(state, "State cannot be NULL.");
if(state->stream != NULL) {
errorChain(audioStreamDispose(state->stream));
state->stream = NULL;
}
errorOk();
}
void audioMixerChannelApplyVolume(audiomixerchannelstate_t *state) {
assertNotNull(state, "State cannot be NULL.");
if(state->stream == NULL) return;
// Recomputed and reapplied every frame (not just when a PLAY command
// starts state->stream) so a live audioMixerSetChannelVolume()/
// audioMixerSetMasterVolume() call affects whatever's already playing,
// not just sounds started afterward.
const float_t volume = mathClamp(
state->baseVolume * state->volume * AUDIO_MIXER.masterVolume, 0.0f, 1.0f
);
audioStreamSetVolume(state->stream, volume);
}
errorret_t audioMixerUpdateEarly() {
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
audiomixerchannelstate_t *state = &AUDIO_MIXER.channels[channel];
audiomixercommand_t *command = &state->pending;
if(command->command == AUDIO_MIXER_COMMAND_STOP) {
errorChain(audioMixerChannelStopStream(state));
command->command = AUDIO_MIXER_COMMAND_NONE;
} else if(command->command == AUDIO_MIXER_COMMAND_PLAY && command->loadingAsset != NULL) {
// Blocks here if audioMixerUpdateLate()'s load (kicked off at the end
// of the previous frame) hasn't finished yet - see this function's
// own doc comment on why that's accepted for now.
errorret_t loadRet = assetRequireLoaded(command->loadingAsset);
if(errorIsNotOk(loadRet)) {
assetUnlockEntry(command->loadingAsset);
command->loadingAsset = NULL;
command->command = AUDIO_MIXER_COMMAND_NONE;
errorChain(loadRet);
}
errorChain(audioMixerChannelStopStream(state));
audiostream_t *stream = audioAquireStream(command->loadingAsset);
assertNotNull(stream, "No free audio stream slots available.");
errorret_t initRet = audioStreamInit(stream, command->loadingAsset);
// The stream holds its own lock on the asset once initialized (or,
// on failure, audioStreamInit() has already released it itself) -
// either way this call's own lock is no longer needed.
assetUnlockEntry(command->loadingAsset);
command->loadingAsset = NULL;
command->command = AUDIO_MIXER_COMMAND_NONE;
errorChain(initRet);
audioStreamSetDirectionality(stream, command->pan);
if(command->looping) {
audioStreamSetLooping(stream, true);
audioStreamSetLoopPoints(stream, command->loopStart, command->loopTo);
audioStreamSetLoopLimit(stream, command->loopCount);
}
audioStreamPlay(stream);
state->stream = stream;
state->baseVolume = command->volume;
}
// A PLAY command with no loadingAsset yet (command->loadingAsset ==
// NULL) was queued too late for the audioMixerUpdateLate() that just
// ran immediately before this call to have locked it - the only way
// that happens is a command queued outside the normal per-frame cycle,
// e.g. during engine startup before any audioMixerUpdateLate() has run
// yet. Left alone (still AUDIO_MIXER_COMMAND_PLAY) rather than treated
// as an error: the very next audioMixerUpdateLate() will lock it, and
// the frame after that will reach this branch and apply it normally.
// Determines the actual volume passed to audioStreamSetVolume() from
// this sound's own volume (just set above, for a fresh PLAY) and this
// channel's/the mixer's - see its own comment for why this runs every
// frame regardless of whether a command was queued this frame.
audioMixerChannelApplyVolume(state);
audiomixerchannel_t channel = 0;
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
// One channel's error (e.g. a failed asset load) must not stop every
// other channel from being updated this frame - caught and logged
// rather than propagated, same as audioStream*IsFinished()'s own
// per-stream error handling.
errorret_t ret = audioMixerChannelUpdateEarly(
channel, &AUDIO_MIXER.channels[channel]
);
if(errorIsNotOk(ret)) errorCatch(errorPrint(ret));
channel++;
}
errorOk();
}
errorret_t audioMixerUpdateLate() {
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
if(command->command != AUDIO_MIXER_COMMAND_PLAY) continue;
const assetloadertype_t assetType = audioStreamAssetTypeForPath(command->file);
assertTrue(assetType != ASSET_LOADER_TYPE_NULL, "Unsupported audio file extension.");
// Begins loading now - kicked off at the end of this frame so it has
// until the start of the next (audioMixerUpdateEarly()) to finish
// before that has to block on it.
command->loadingAsset = assetLock(command->file, assetType, NULL);
audiomixerchannel_t channel = 0;
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
errorret_t ret =
audioMixerChannelUpdateLate(&AUDIO_MIXER.channels[channel]);
if(errorIsNotOk(ret)) errorCatch(errorPrint(ret));
channel++;
}
errorOk();
@@ -153,7 +72,9 @@ void audioMixerPlayLooped(
const float_t loopStart,
const float_t loopTo
) {
audioMixerQueuePlay(file, channel, volume, pan, true, loopCount, loopStart, loopTo);
audioMixerQueuePlay(
file, channel, volume, pan, true, loopCount, loopStart, loopTo
);
}
void audioMixerQueuePlay(
@@ -196,25 +117,11 @@ void audioMixerStop(const audiomixerchannel_t channel) {
AUDIO_MIXER.channels[channel].pending.command = AUDIO_MIXER_COMMAND_STOP;
}
void audioMixerSetChannelVolume(
const audiomixerchannel_t channel,
const float_t volume
) {
assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range");
assertTrue(volume >= 0.0f && volume <= 1.0f, "Volume is out of range");
AUDIO_MIXER.channels[channel].volume = volume;
}
void audioMixerSetMasterVolume(const float_t volume) {
assertTrue(volume >= 0.0f && volume <= 1.0f, "Volume is out of range");
AUDIO_MIXER.masterVolume = volume;
}
errorret_t audioMixerDispose() {
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
audiomixerchannel_t channel = 0;
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
errorChain(audioMixerChannelStopStream(&AUDIO_MIXER.channels[channel]));
channel++;
}
errorOk();
+5 -143
View File
@@ -6,86 +6,10 @@
*/
#pragma once
#include "asset/asset.h"
typedef struct audiostream_s audiostream_t;
typedef enum {
AUDIO_MIXER_CHANNEL_BGM_0,
AUDIO_MIXER_CHANNEL_VOICE_0,
AUDIO_MIXER_CHANNEL_VOICE_1,
AUDIO_MIXER_CHANNEL_SFX_0,
AUDIO_MIXER_CHANNEL_SFX_1,
AUDIO_MIXER_CHANNEL_SFX_2,
AUDIO_MIXER_CHANNEL_SFX_3,
AUDIO_MIXER_CHANNEL_COUNT
} audiomixerchannel_t;
#define AUDIO_PATH_MAX 256
typedef enum {
AUDIO_MIXER_COMMAND_NONE,
AUDIO_MIXER_COMMAND_PLAY,
AUDIO_MIXER_COMMAND_STOP
} audiomixercommandtype_t;
typedef struct {
// What audioMixerUpdateEarly() should do to this channel at the start of
// the next frame - AUDIO_MIXER_COMMAND_NONE if nothing's queued. Set by
// audioMixerPlay()/audioMixerPlayLooped()/audioMixerStop(); queuing a new
// command on a channel replaces, rather than stacks on top of, whatever
// was already queued for it this frame - only the most recent survives.
audiomixercommandtype_t command;
// AUDIO_MIXER_COMMAND_PLAY only, below.
char_t file[AUDIO_PATH_MAX];
float_t volume;
float_t pan;
bool_t looping;
uint8_t loopCount;
float_t loopStart;
float_t loopTo;
// Set by audioMixerUpdateLate() (the end of the frame this PLAY command
// was queued on), once it locks the requested file and kicks off its
// load. audioMixerUpdateEarly() (the start of the next frame) blocks on
// this being fully loaded before actually starting playback - see its
// own comment. NULL except in that in-between window.
assetentry_t *loadingAsset;
} audiomixercommand_t;
typedef struct {
// Queued action for the next audioMixerUpdateEarly() to apply.
audiomixercommand_t pending;
// This channel's currently playing stream, or NULL if nothing is -
// acquired from AUDIO.streams by audioMixerUpdateEarly() when it applies
// an AUDIO_MIXER_COMMAND_PLAY, disposed the same way on
// AUDIO_MIXER_COMMAND_STOP or before a new AUDIO_MIXER_COMMAND_PLAY
// replaces it.
audiostream_t *stream;
// The per-sound volume `stream` was last started with (the `volume`
// argument to whichever audioMixerPlay()/audioMixerPlayLooped() call
// started it) - combined with this channel's own volume and the mixer's
// master volume, every frame, to get the volume actually passed to
// audioStreamSetVolume(). Meaningless while `stream` is NULL.
float_t baseVolume;
// This channel's own volume, from 0.0 (silent) to 1.0 (loudest, the
// default) - independent of any individual sound's own volume. Set via
// audioMixerSetChannelVolume().
float_t volume;
} audiomixerchannelstate_t;
#include "audio/mixer/audiomixerchannel.h"
typedef struct {
audiomixerchannelstate_t channels[AUDIO_MIXER_CHANNEL_COUNT];
uint8_t fade[AUDIO_MIXER_CHANNEL_COUNT];
// Global volume multiplier applied on top of every channel's own volume
// and every sound's own volume - from 0.0 (silent) to 1.0 (loudest, the
// default). Set via audioMixerSetMasterVolume().
float_t masterVolume;
} audiomixer_t;
extern audiomixer_t AUDIO_MIXER;
@@ -95,52 +19,16 @@ extern audiomixer_t AUDIO_MIXER;
*/
errorret_t audioMixerInit();
/**
* Stops and disposes the given channel's currently playing stream, if any -
* a safe no-op if the channel is already idle. Shared by
* audioMixerUpdateEarly() (AUDIO_MIXER_COMMAND_STOP, and replacing a
* channel's stream before a new AUDIO_MIXER_COMMAND_PLAY starts) and
* audioMixerDispose().
*
* @param state The channel state to stop.
*/
errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state);
/**
* Computes the given channel's actual playback volume - its currently
* playing sound's own volume (baseVolume) combined with the channel's own
* volume and the mixer's master volume - and applies it via
* audioStreamSetVolume(). A safe no-op if the channel isn't playing
* anything. Called every audioMixerUpdateEarly(), not just when a PLAY
* command starts a new stream - see audioMixerUpdateEarly()'s own comment.
*
* @param state The channel state to update.
*/
void audioMixerChannelApplyVolume(audiomixerchannelstate_t *state);
/**
* Updates the audio mixer, called before rendering (the start of the
* frame): applies whatever command (play, stop - including everything a
* play carries, pan/looping) audioMixerUpdateLate() queued for loading at
* the end of the previous frame. If that load hasn't finished yet, this
* blocks (via assetRequireLoaded()) until it has - a deliberate stall for
* now rather than skipping a frame, since there's currently no way to only
* partially apply a channel's command. Also recomputes and reapplies every
* playing channel's mixed volume (see audioMixerChannelApplyVolume()) every
* time, regardless of whether a command was queued this frame, so
* audioMixerSetChannelVolume()/audioMixerSetMasterVolume() take effect on
* whatever's already playing too.
* frame) - see audioMixerChannelUpdateEarly()'s own comment, run for every
* channel.
*/
errorret_t audioMixerUpdateEarly();
/**
* Updates the audio mixer, called at the end of each frame, once every
* other system has had a chance to queue a command via
* audioMixerPlay()/audioMixerPlayLooped()/audioMixerStop(). For a queued
* AUDIO_MIXER_COMMAND_PLAY, locks its file and begins loading it (see
* assetLock()) - giving it from now until the next audioMixerUpdateEarly()
* to finish before that has to block on it. Actually starting/stopping
* playback happens there, not here.
* Updates the audio mixer, called at the end of each frame - see
* audioMixerChannelUpdateLate()'s own comment, run for every channel.
*/
errorret_t audioMixerUpdateLate();
@@ -228,32 +116,6 @@ void audioMixerQueuePlay(
*/
void audioMixerStop(const audiomixerchannel_t channel);
/**
* Sets the given channel's own volume, independent of whatever individual
* sound is (or will be) playing on it - combined with that sound's own
* volume and the mixer's master volume on the very next
* audioMixerUpdateEarly(), so it takes effect immediately even if nothing
* new is being played.
*
* @param channel Channel to update.
* @param volume The channel's new volume, from 0.0 (silent) to 1.0
* (loudest, the default).
*/
void audioMixerSetChannelVolume(
const audiomixerchannel_t channel,
const float_t volume
);
/**
* Sets the mixer's master volume, combined with every channel's own volume
* and every sound's own volume on the very next audioMixerUpdateEarly(), so
* it takes effect immediately for whatever's already playing.
*
* @param volume The new master volume, from 0.0 (silent) to 1.0 (loudest,
* the default).
*/
void audioMixerSetMasterVolume(const float_t volume);
/**
* Disposes the audio mixer.
*/
+174
View File
@@ -0,0 +1,174 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "audiomixerchannel.h"
#include "audio/audio.h"
#include "audio/stream/audiostream.h"
#include "audio/stream/audiostreamtype.h"
#include "save/save.h"
#include "assert/assert.h"
errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state) {
assertNotNull(state, "State cannot be NULL.");
if(state->stream == NULL) errorOk();
errorChain(audioStreamDispose(state->stream));
state->stream = NULL;
errorOk();
}
void audioMixerChannelApplyVolume(
const audiomixerchannel_t channel,
audiomixerchannelstate_t *state
) {
assertNotNull(state, "State cannot be NULL.");
if(state->stream == NULL) return;
// state->fade and both SAVE.settings volumes have no setter - written
// directly by callers - so this is the only place any of them ever gets
// validated, rather than at the point they're set.
assertTrue(
state->fade >= 0.0f && state->fade <= 1.0f,
"Channel fade is out of range."
);
assertTrue(
SAVE.settings.audioChannelVolume[channel] >= 0.0f &&
SAVE.settings.audioChannelVolume[channel] <= 1.0f,
"Channel volume is out of range."
);
assertTrue(
SAVE.settings.audioMasterVolume >= 0.0f &&
SAVE.settings.audioMasterVolume <= 1.0f,
"Master volume is out of range."
);
// Final computed volume.
float_t volume = (
state->baseVolume *
state->fade *
SAVE.settings.audioChannelVolume[channel] *
SAVE.settings.audioMasterVolume
);
audioStreamSetVolume(state->stream, volume);
}
errorret_t audioMixerChannelUpdateEarly(
const audiomixerchannel_t channel,
audiomixerchannelstate_t *state
) {
assertNotNull(state, "State cannot be NULL.");
audiomixercommand_t *command = &state->pending;
if(command->command == AUDIO_MIXER_COMMAND_STOP) {
errorChain(audioMixerChannelStopStream(state));
command->command = AUDIO_MIXER_COMMAND_NONE;
audioMixerChannelApplyVolume(channel, state);
errorOk();
}
if(
command->command != AUDIO_MIXER_COMMAND_PLAY ||
command->loadingAsset == NULL
) {
// A PLAY command with no loadingAsset yet (command->loadingAsset ==
// NULL) was queued too late for the audioMixerChannelUpdateLate() that
// just ran immediately before this call to have locked it - the only
// way that happens is a command queued outside the normal per-frame
// cycle, e.g. during engine startup before any
// audioMixerChannelUpdateLate() has run yet. Left alone (still
// AUDIO_MIXER_COMMAND_PLAY) rather than treated as an error: the very
// next audioMixerChannelUpdateLate() will lock it, and the frame after
// that will reach here and apply it normally.
audioMixerChannelApplyVolume(channel, state);
errorOk();
}
// Blocks here if audioMixerChannelUpdateLate()'s load (kicked off at the
// end of the previous frame) hasn't finished yet - see this function's
// own doc comment on why that's accepted for now.
errorret_t loadRet = assetRequireLoaded(command->loadingAsset);
if(errorIsNotOk(loadRet)) {
assetUnlockEntry(command->loadingAsset);
command->loadingAsset = NULL;
command->command = AUDIO_MIXER_COMMAND_NONE;
errorChain(loadRet);
}
errorChain(audioMixerChannelStopStream(state));
audiostream_t *stream = audioAquireStream(command->loadingAsset);
assertNotNull(stream, "No free audio stream slots available.");
errorret_t initRet = audioStreamInit(stream, command->loadingAsset);
// The stream holds its own lock on the asset once initialized (or, on
// failure, audioStreamInit() has already released it itself) - either
// way this call's own lock is no longer needed.
assetUnlockEntry(command->loadingAsset);
command->loadingAsset = NULL;
command->command = AUDIO_MIXER_COMMAND_NONE;
errorChain(initRet);
// Forwards to this channel's own onLoop/onEnd (persistent across every
// play on this channel) rather than this one stream instance's own -
// see audiomixerchannelstate_t.onLoop's own comment.
stream->user = state;
stream->onLoop = audioMixerChannelOnStreamLoop;
stream->onEnd = audioMixerChannelOnStreamEnd;
audioStreamSetDirectionality(stream, command->pan);
if(command->looping) {
audioStreamSetLooping(stream, true);
audioStreamSetLoopPoints(stream, command->loopStart, command->loopTo);
audioStreamSetLoopLimit(stream, command->loopCount);
}
audioStreamPlay(stream);
state->stream = stream;
state->baseVolume = command->volume;
audioMixerChannelApplyVolume(channel, state);
errorOk();
}
errorret_t audioMixerChannelUpdateLate(audiomixerchannelstate_t *state) {
assertNotNull(state, "State cannot be NULL.");
audiomixercommand_t *command = &state->pending;
if(command->command != AUDIO_MIXER_COMMAND_PLAY) errorOk();
assetloadertype_t assetType = audioStreamAssetTypeForPath(command->file);
assertTrue(
assetType != ASSET_LOADER_TYPE_NULL, "Unsupported audio file extension."
);
// Begins loading now - kicked off at the end of this frame so it has
// until the start of the next (audioMixerChannelUpdateEarly()) to finish
// before that has to block on it.
command->loadingAsset = assetLock(command->file, assetType, NULL);
errorOk();
}
void audioMixerChannelOnStreamLoop(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
audiomixerchannelstate_t *state = (audiomixerchannelstate_t *) stream->user;
if(state->onLoop != NULL) state->onLoop(state);
}
void audioMixerChannelOnStreamEnd(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
audiomixerchannelstate_t *state = (audiomixerchannelstate_t *) stream->user;
if(state->onEnd != NULL) state->onEnd(state);
}
+188
View File
@@ -0,0 +1,188 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/asset.h"
typedef struct audiostream_s audiostream_t;
#define AUDIO_PATH_MAX 256
typedef enum {
AUDIO_MIXER_CHANNEL_BGM_0,
AUDIO_MIXER_CHANNEL_VOICE_0,
AUDIO_MIXER_CHANNEL_VOICE_1,
AUDIO_MIXER_CHANNEL_SFX_0,
AUDIO_MIXER_CHANNEL_SFX_1,
AUDIO_MIXER_CHANNEL_SFX_2,
AUDIO_MIXER_CHANNEL_SFX_3,
AUDIO_MIXER_CHANNEL_COUNT
} audiomixerchannel_t;
typedef enum {
AUDIO_MIXER_COMMAND_NONE,
AUDIO_MIXER_COMMAND_PLAY,
AUDIO_MIXER_COMMAND_STOP
} audiomixercommandtype_t;
typedef struct {
// What audioMixerChannelUpdateEarly() should do to this channel at the
// start of the next frame - AUDIO_MIXER_COMMAND_NONE if nothing's
// queued. Set by audioMixerPlay()/audioMixerPlayLooped()/
// audioMixerStop(); queuing a new command on a channel replaces, rather
// than stacks on top of, whatever was already queued for it this frame -
// only the most recent survives.
audiomixercommandtype_t command;
// AUDIO_MIXER_COMMAND_PLAY only, below.
char_t file[AUDIO_PATH_MAX];
float_t volume;
float_t pan;
bool_t looping;
uint8_t loopCount;
float_t loopStart;
float_t loopTo;
// Set by audioMixerChannelUpdateLate() (the end of the frame this PLAY
// command was queued on), once it locks the requested file and kicks off
// its load. audioMixerChannelUpdateEarly() (the start of the next frame)
// blocks on this being fully loaded before actually starting playback -
// see its own comment. NULL except in that in-between window.
assetentry_t *loadingAsset;
} audiomixercommand_t;
typedef struct audiomixerchannelstate_s audiomixerchannelstate_t;
typedef struct audiomixerchannelstate_s {
// Queued action for the next audioMixerChannelUpdateEarly() to apply.
audiomixercommand_t pending;
// This channel's currently playing stream, or NULL if nothing is -
// acquired from AUDIO.streams by audioMixerChannelUpdateEarly() when it
// applies an AUDIO_MIXER_COMMAND_PLAY, disposed the same way on
// AUDIO_MIXER_COMMAND_STOP or before a new AUDIO_MIXER_COMMAND_PLAY
// replaces it.
audiostream_t *stream;
// The per-sound volume `stream` was last started with (the `volume`
// argument to whichever audioMixerPlay()/audioMixerPlayLooped() call
// started it) - combined with this channel's fade, its own volume
// (SAVE.settings.audioChannelVolume) and the mixer's master volume
// (SAVE.settings.audioMasterVolume), every frame, to get the volume
// actually passed to audioStreamSetVolume(). Meaningless while `stream`
// is NULL.
float_t baseVolume;
// This channel's own fade multiplier, from 0.0 (fully faded out) to 1.0
// (no fade applied, the default) - transient runtime state, unlike the
// channel/master volumes below (which are persisted user preferences).
// No setter - write it directly; audioMixerChannelApplyVolume() asserts
// it's in range the next time it reads it, rather than validating at the
// point it's set.
float_t fade;
// Arbitrary caller data, handed back as-is to onLoop/onEnd below - never
// read or written by the mixer itself. NULL by default.
void *user;
// Fired (from the main thread - safe to do arbitrary work, same
// guarantee as audiostream_t's own onLoop/onEnd) whenever this channel's
// currently playing stream loops, or reaches its natural end (including
// hitting its loop limit - see audioStreamSetLoopLimit()) and stops.
// Wired onto every stream this channel starts via
// audioMixerChannelOnStreamLoop()/OnStreamEnd() - not called directly by
// anything else. No setter - write these fields directly; NULL (the
// default) means not notified. Unlike `stream`/`pending`, these persist
// across every play on this channel, not just one.
void (*onLoop)(audiomixerchannelstate_t *state);
void (*onEnd)(audiomixerchannelstate_t *state);
} audiomixerchannelstate_t;
/**
* Stops and disposes the given channel's currently playing stream, if any -
* a safe no-op if the channel is already idle. Shared by
* audioMixerChannelUpdateEarly() (AUDIO_MIXER_COMMAND_STOP, and replacing a
* channel's stream before a new AUDIO_MIXER_COMMAND_PLAY starts) and
* audioMixerDispose().
*
* @param state The channel state to stop.
*/
errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state);
/**
* Computes the given channel's actual playback volume - its currently
* playing sound's own volume (baseVolume), this channel's fade, this
* channel's own volume (SAVE.settings.audioChannelVolume[channel]) and the
* mixer's master volume (SAVE.settings.audioMasterVolume) - and applies it
* via audioStreamSetVolume(). Asserts fade and both save-settings volumes
* are each within [0.0, 1.0] - the only validation any of them ever gets,
* since none of them have a setter. A safe no-op if the channel isn't
* playing anything. Called every audioMixerChannelUpdateEarly(), not just
* when a PLAY command starts a new stream - see its own comment.
*
* @param channel Which channel `state` is, to index
* SAVE.settings.audioChannelVolume with.
* @param state The channel state to update.
*/
void audioMixerChannelApplyVolume(
const audiomixerchannel_t channel,
audiomixerchannelstate_t *state
);
/**
* Updates a single mixer channel, called before rendering (the start of
* the frame): applies whatever command (play, stop - including everything
* a play carries, pan/looping) audioMixerChannelUpdateLate() queued for
* loading at the end of the previous frame. If that load hasn't finished
* yet, this blocks (via assetRequireLoaded()) until it has - a deliberate
* stall for now rather than skipping a frame, since there's currently no
* way to only partially apply a channel's command. Also recomputes and
* reapplies this channel's mixed volume (see
* audioMixerChannelApplyVolume()) every time, regardless of whether a
* command was queued this frame, so a live change to this channel's fade,
* its own volume, or the master volume takes effect on whatever's already
* playing too.
*
* @param channel Which channel `state` is - see
* audioMixerChannelApplyVolume()'s own comment.
* @param state The channel state to update.
*/
errorret_t audioMixerChannelUpdateEarly(
const audiomixerchannel_t channel,
audiomixerchannelstate_t *state
);
/**
* Updates a single mixer channel, called at the end of each frame, once
* every other system has had a chance to queue a command via
* audioMixerPlay()/audioMixerPlayLooped()/audioMixerStop(). For a queued
* AUDIO_MIXER_COMMAND_PLAY, locks its file and begins loading it (see
* assetLock()) - giving it from now until the next
* audioMixerChannelUpdateEarly() to finish before that has to block on it.
* Actually starting/stopping playback happens there, not here.
*
* @param state The channel state to update.
*/
errorret_t audioMixerChannelUpdateLate(audiomixerchannelstate_t *state);
/**
* Trampoline wired onto every stream a channel starts (as that stream's
* onLoop) - forwards to the channel's own onLoop (see
* audiomixerchannelstate_t.onLoop's own comment) via stream->user, which
* audioMixerChannelUpdateEarly() points at the channel's own state.
*
* @param stream The stream that looped.
*/
void audioMixerChannelOnStreamLoop(audiostream_t *stream);
/**
* Same as audioMixerChannelOnStreamLoop() but wired onto onEnd, forwarding
* to the channel's own onEnd.
*
* @param stream The stream that ended.
*/
void audioMixerChannelOnStreamEnd(audiostream_t *stream);
+5 -3
View File
@@ -55,9 +55,11 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
// Smoke-tests the audio mixer end to end (asset loading -> PCM/MP3
// streaming -> platform playback) against a real MP3 asset rather than
// synthesizing PCM data at runtime.
audioMixerPlayLooped(
"audio/boa.mp3", AUDIO_MIXER_CHANNEL_BGM_0, 1.0f, AUDIO_STREAM_CENTER,
0, -1.0f, 0.0f
audioMixerPlay(
"audio/boa.mp3",
AUDIO_MIXER_CHANNEL_BGM_0,
1.0f,
AUDIO_STREAM_CENTER
);
consolePrint("Engine initialized");
+68
View File
@@ -256,6 +256,74 @@
(dest) = hasFloat(key) ? \
(float_t)yyjson_get_real(yyjson_obj_get(object, key)) : (float_t)(def)
/**
* Checks if the given key exists on the current JSON object.
*
* @param key The key to check for.
*/
#define hasFloatArray(key) (yyjson_obj_get(object, key) != NULL)
/**
* Errors if the given key does not exist on the current JSON object.
*
* @param key The key that must exist.
*/
#define requireFloatArray(key) \
if(!hasFloatArray(key)) errorThrow("Save JSON missing '%s' key", key)
/**
* Writes a float_t array to the current JSON object.
*
* @param key The key to write to.
* @param value The array to write.
* @param count Number of elements in the array.
*/
#define writeFloatArray(key, value, count) { \
yyjson_mut_val *saveJsonArr = yyjson_mut_arr(doc); \
size_t saveJsonArrIdx = 0; \
while(saveJsonArrIdx < (size_t)(count)) { \
yyjson_mut_arr_add_real(doc, saveJsonArr, (double)(value)[saveJsonArrIdx]); \
saveJsonArrIdx++; \
} \
yyjson_mut_obj_add_val(doc, object, key, saveJsonArr); \
}
/**
* Reads a float_t array from the current JSON object into dest, falling
* back to filling every element with def if the key is missing. Errors if
* the JSON array's length doesn't exactly match count.
*
* @param key The key to read from.
* @param dest The destination array to assign to.
* @param count Number of elements dest holds.
* @param def The default value to fill every element with if the key is
* missing.
*/
#define readFloatArray(key, dest, count, def) { \
yyjson_val *saveJsonArr = yyjson_obj_get(object, key); \
if(saveJsonArr != NULL) { \
if(yyjson_arr_size(saveJsonArr) != (size_t)(count)) { \
errorThrow( \
"Save JSON array '%s' has wrong length, expected %d", \
key, (int)(count) \
); \
} \
size_t saveJsonArrIdx, saveJsonArrLen; \
yyjson_val *saveJsonArrElem; \
yyjson_arr_foreach( \
saveJsonArr, saveJsonArrIdx, saveJsonArrLen, saveJsonArrElem \
) { \
(dest)[saveJsonArrIdx] = (float_t)yyjson_get_real(saveJsonArrElem); \
} \
} else { \
size_t saveJsonArrIdx = 0; \
while(saveJsonArrIdx < (size_t)(count)) { \
(dest)[saveJsonArrIdx] = (float_t)(def); \
saveJsonArrIdx++; \
} \
} \
}
/**
* Checks if the given key exists on the current JSON object.
*
+17
View File
@@ -14,6 +14,13 @@ void saveSettingsInit(savesettings_t *settings) {
assertNotNull(settings, "Settings cannot be null");
memorySet(settings, 0, sizeof(savesettings_t));
settings->audioMasterVolume = 1.0f;
audiomixerchannel_t channel = 0;
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
settings->audioChannelVolume[channel] = 1.0f;
channel++;
}
}
errorret_t saveSettingsWriteJSON(
@@ -26,6 +33,11 @@ errorret_t saveSettingsWriteJSON(
assertNotNull(object, "Object cannot be null");
writeInt32("someSetting", settings->someSetting);
writeFloat("audioMasterVolume", settings->audioMasterVolume);
writeFloatArray(
"audioChannelVolume", settings->audioChannelVolume,
AUDIO_MIXER_CHANNEL_COUNT
);
errorOk();
}
@@ -35,6 +47,11 @@ errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object) {
assertNotNull(object, "Object cannot be null");
readInt32("someSetting", settings->someSetting, 0);
readFloat("audioMasterVolume", settings->audioMasterVolume, 1.0f);
readFloatArray(
"audioChannelVolume", settings->audioChannelVolume,
AUDIO_MIXER_CHANNEL_COUNT, 1.0f
);
errorOk();
}
+6
View File
@@ -9,9 +9,15 @@
#include "time/timeepoch.h"
#include "savedevice.h"
#include "yyjson.h"
#include "audio/mixer/audiomixerchannel.h"
typedef struct savesettings_s {
int32_t someSetting;
// Persisted mixer volume preferences - see audiomixerchannelstate_t's own
// comment on why these live here rather than on the mixer itself.
float_t audioMasterVolume;
float_t audioChannelVolume[AUDIO_MIXER_CHANNEL_COUNT];
} savesettings_t;
/**