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:
@@ -7,4 +7,5 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
audiomixer.c
|
audiomixer.c
|
||||||
|
audiomixerchannel.c
|
||||||
)
|
)
|
||||||
@@ -6,12 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "audiomixer.h"
|
#include "audiomixer.h"
|
||||||
#include "audio/audio.h"
|
|
||||||
#include "audio/stream/audiostream.h"
|
#include "audio/stream/audiostream.h"
|
||||||
#include "audio/stream/audiostreamtype.h"
|
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "util/math.h"
|
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
|
||||||
audiomixer_t AUDIO_MIXER;
|
audiomixer_t AUDIO_MIXER;
|
||||||
@@ -19,117 +16,39 @@ audiomixer_t AUDIO_MIXER;
|
|||||||
errorret_t audioMixerInit() {
|
errorret_t audioMixerInit() {
|
||||||
memoryZero(&AUDIO_MIXER, sizeof(audiomixer_t));
|
memoryZero(&AUDIO_MIXER, sizeof(audiomixer_t));
|
||||||
|
|
||||||
AUDIO_MIXER.masterVolume = 1.0f;
|
audiomixerchannel_t channel = 0;
|
||||||
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
|
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
|
||||||
AUDIO_MIXER.channels[channel].volume = 1.0f;
|
AUDIO_MIXER.channels[channel].fade = 1.0f;
|
||||||
|
channel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
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() {
|
errorret_t audioMixerUpdateEarly() {
|
||||||
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
|
audiomixerchannel_t channel = 0;
|
||||||
audiomixerchannelstate_t *state = &AUDIO_MIXER.channels[channel];
|
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
|
||||||
audiomixercommand_t *command = &state->pending;
|
// One channel's error (e.g. a failed asset load) must not stop every
|
||||||
|
// other channel from being updated this frame - caught and logged
|
||||||
if(command->command == AUDIO_MIXER_COMMAND_STOP) {
|
// rather than propagated, same as audioStream*IsFinished()'s own
|
||||||
errorChain(audioMixerChannelStopStream(state));
|
// per-stream error handling.
|
||||||
command->command = AUDIO_MIXER_COMMAND_NONE;
|
errorret_t ret = audioMixerChannelUpdateEarly(
|
||||||
} else if(command->command == AUDIO_MIXER_COMMAND_PLAY && command->loadingAsset != NULL) {
|
channel, &AUDIO_MIXER.channels[channel]
|
||||||
// Blocks here if audioMixerUpdateLate()'s load (kicked off at the end
|
);
|
||||||
// of the previous frame) hasn't finished yet - see this function's
|
if(errorIsNotOk(ret)) errorCatch(errorPrint(ret));
|
||||||
// own doc comment on why that's accepted for now.
|
channel++;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t audioMixerUpdateLate() {
|
errorret_t audioMixerUpdateLate() {
|
||||||
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
|
audiomixerchannel_t channel = 0;
|
||||||
audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
|
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
|
||||||
|
errorret_t ret =
|
||||||
if(command->command != AUDIO_MIXER_COMMAND_PLAY) continue;
|
audioMixerChannelUpdateLate(&AUDIO_MIXER.channels[channel]);
|
||||||
|
if(errorIsNotOk(ret)) errorCatch(errorPrint(ret));
|
||||||
const assetloadertype_t assetType = audioStreamAssetTypeForPath(command->file);
|
channel++;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
@@ -153,7 +72,9 @@ void audioMixerPlayLooped(
|
|||||||
const float_t loopStart,
|
const float_t loopStart,
|
||||||
const float_t loopTo
|
const float_t loopTo
|
||||||
) {
|
) {
|
||||||
audioMixerQueuePlay(file, channel, volume, pan, true, loopCount, loopStart, loopTo);
|
audioMixerQueuePlay(
|
||||||
|
file, channel, volume, pan, true, loopCount, loopStart, loopTo
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void audioMixerQueuePlay(
|
void audioMixerQueuePlay(
|
||||||
@@ -196,25 +117,11 @@ void audioMixerStop(const audiomixerchannel_t channel) {
|
|||||||
AUDIO_MIXER.channels[channel].pending.command = AUDIO_MIXER_COMMAND_STOP;
|
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() {
|
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]));
|
errorChain(audioMixerChannelStopStream(&AUDIO_MIXER.channels[channel]));
|
||||||
|
channel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -6,86 +6,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "asset/asset.h"
|
#include "audio/mixer/audiomixerchannel.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;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
audiomixerchannelstate_t channels[AUDIO_MIXER_CHANNEL_COUNT];
|
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;
|
} audiomixer_t;
|
||||||
|
|
||||||
extern audiomixer_t AUDIO_MIXER;
|
extern audiomixer_t AUDIO_MIXER;
|
||||||
@@ -95,52 +19,16 @@ extern audiomixer_t AUDIO_MIXER;
|
|||||||
*/
|
*/
|
||||||
errorret_t audioMixerInit();
|
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
|
* Updates the audio mixer, called before rendering (the start of the
|
||||||
* frame): applies whatever command (play, stop - including everything a
|
* frame) - see audioMixerChannelUpdateEarly()'s own comment, run for every
|
||||||
* play carries, pan/looping) audioMixerUpdateLate() queued for loading at
|
* channel.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
errorret_t audioMixerUpdateEarly();
|
errorret_t audioMixerUpdateEarly();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the audio mixer, called at the end of each frame, once every
|
* Updates the audio mixer, called at the end of each frame - see
|
||||||
* other system has had a chance to queue a command via
|
* audioMixerChannelUpdateLate()'s own comment, run for every channel.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
errorret_t audioMixerUpdateLate();
|
errorret_t audioMixerUpdateLate();
|
||||||
|
|
||||||
@@ -228,32 +116,6 @@ void audioMixerQueuePlay(
|
|||||||
*/
|
*/
|
||||||
void audioMixerStop(const audiomixerchannel_t channel);
|
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.
|
* Disposes the audio mixer.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
|
// Smoke-tests the audio mixer end to end (asset loading -> PCM/MP3
|
||||||
// streaming -> platform playback) against a real MP3 asset rather than
|
// streaming -> platform playback) against a real MP3 asset rather than
|
||||||
// synthesizing PCM data at runtime.
|
// synthesizing PCM data at runtime.
|
||||||
audioMixerPlayLooped(
|
audioMixerPlay(
|
||||||
"audio/boa.mp3", AUDIO_MIXER_CHANNEL_BGM_0, 1.0f, AUDIO_STREAM_CENTER,
|
"audio/boa.mp3",
|
||||||
0, -1.0f, 0.0f
|
AUDIO_MIXER_CHANNEL_BGM_0,
|
||||||
|
1.0f,
|
||||||
|
AUDIO_STREAM_CENTER
|
||||||
);
|
);
|
||||||
|
|
||||||
consolePrint("Engine initialized");
|
consolePrint("Engine initialized");
|
||||||
|
|||||||
@@ -256,6 +256,74 @@
|
|||||||
(dest) = hasFloat(key) ? \
|
(dest) = hasFloat(key) ? \
|
||||||
(float_t)yyjson_get_real(yyjson_obj_get(object, key)) : (float_t)(def)
|
(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.
|
* Checks if the given key exists on the current JSON object.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ void saveSettingsInit(savesettings_t *settings) {
|
|||||||
assertNotNull(settings, "Settings cannot be null");
|
assertNotNull(settings, "Settings cannot be null");
|
||||||
|
|
||||||
memorySet(settings, 0, sizeof(savesettings_t));
|
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(
|
errorret_t saveSettingsWriteJSON(
|
||||||
@@ -26,6 +33,11 @@ errorret_t saveSettingsWriteJSON(
|
|||||||
assertNotNull(object, "Object cannot be null");
|
assertNotNull(object, "Object cannot be null");
|
||||||
|
|
||||||
writeInt32("someSetting", settings->someSetting);
|
writeInt32("someSetting", settings->someSetting);
|
||||||
|
writeFloat("audioMasterVolume", settings->audioMasterVolume);
|
||||||
|
writeFloatArray(
|
||||||
|
"audioChannelVolume", settings->audioChannelVolume,
|
||||||
|
AUDIO_MIXER_CHANNEL_COUNT
|
||||||
|
);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -35,6 +47,11 @@ errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object) {
|
|||||||
assertNotNull(object, "Object cannot be null");
|
assertNotNull(object, "Object cannot be null");
|
||||||
|
|
||||||
readInt32("someSetting", settings->someSetting, 0);
|
readInt32("someSetting", settings->someSetting, 0);
|
||||||
|
readFloat("audioMasterVolume", settings->audioMasterVolume, 1.0f);
|
||||||
|
readFloatArray(
|
||||||
|
"audioChannelVolume", settings->audioChannelVolume,
|
||||||
|
AUDIO_MIXER_CHANNEL_COUNT, 1.0f
|
||||||
|
);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,15 @@
|
|||||||
#include "time/timeepoch.h"
|
#include "time/timeepoch.h"
|
||||||
#include "savedevice.h"
|
#include "savedevice.h"
|
||||||
#include "yyjson.h"
|
#include "yyjson.h"
|
||||||
|
#include "audio/mixer/audiomixerchannel.h"
|
||||||
|
|
||||||
typedef struct savesettings_s {
|
typedef struct savesettings_s {
|
||||||
int32_t someSetting;
|
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;
|
} savesettings_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user