Redesign audio mixer around a two-phase command queue instead of synchronous main-thread calls

audioMixerPlay()/audioMixerPlayLooped()/audioMixerStop() now just queue a
command per channel instead of taking effect immediately:
- audioMixerUpdateLate() (end of frame) locks and begins loading any newly
  queued PLAY command's asset, giving it until the start of the next frame
  to finish.
- audioMixerUpdateEarly() (start of the next frame) applies the queued
  command - blocking (assetRequireLoaded()) if that load hasn't finished
  yet - actually starting/stopping playback and setting volume,
  directionality and looping (reusing this session's new
  audioStreamSetLoopLimit()) on a real audiostream_t acquired from the
  shared stream pool.

Previously this was an unwired stub (never initialized/updated/disposed
from audio.c, and audioMixerUpdateLate() only printf'd). Built and verified
on Linux, PSP (Docker) and GameCube (Docker).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-02 14:48:14 -05:00
co-authored by Claude Sonnet 5
parent 619ce932f9
commit d49594d3fb
2 changed files with 208 additions and 52 deletions
+109 -36
View File
@@ -1,12 +1,14 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#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 "assert/assert.h"
@@ -19,31 +21,86 @@ errorret_t audioMixerInit() {
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();
}
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));
} else if(command->command == AUDIO_MIXER_COMMAND_PLAY) {
assertNotNull(
command->loadingAsset,
"PLAY command reached UpdateEarly without a loadingAsset - "
"UpdateLate should always have locked one by now."
);
// 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);
audioStreamSetVolume(stream, command->volume);
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;
}
}
errorOk();
}
errorret_t audioMixerUpdateLate() {
// For each channel.
audiomixerchannel_t channel = 0;
do {
audiomixerchanneldata_t *queue = &AUDIO_MIXER.queue[channel];
// File queued?
if(queue->file[0] == '\0') {
channel++;
continue;
}
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
// Stop this channel
audioMixerStop(channel);
if(command->command != AUDIO_MIXER_COMMAND_PLAY) continue;
// Get the file
// TODO: Loading?
// assetentry_t *entry = assetLock(queue->file, ASSET_LOADER_TYPE_WAV, NULL);
const assetloadertype_t assetType = audioStreamAssetTypeForPath(command->file);
assertTrue(assetType != ASSET_LOADER_TYPE_NULL, "Unsupported audio file extension.");
printf("Playing %s on channel %d\n", queue->file, channel);
} while(channel < AUDIO_MIXER_CHANNEL_COUNT);
// 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();
}
@@ -54,7 +111,7 @@ void audioMixerPlay(
const float_t volume,
const float_t pan
) {
audioMixerPlayLooped(file, channel, volume, pan, 0, 0.0f, 0.0f);
audioMixerQueuePlay(file, channel, volume, pan, false, 0, -1.0f, 0.0f);
}
void audioMixerPlayLooped(
@@ -65,6 +122,19 @@ void audioMixerPlayLooped(
const uint8_t loopCount,
const float_t loopStart,
const float_t loopTo
) {
audioMixerQueuePlay(file, channel, volume, pan, true, loopCount, loopStart, loopTo);
}
void audioMixerQueuePlay(
const char_t *file,
const audiomixerchannel_t channel,
const float_t volume,
const float_t pan,
const bool_t looping,
const uint8_t loopCount,
const float_t loopStart,
const float_t loopTo
) {
assertStrLenMin(file, 1, "File is required, to stop use audioMixerStop");
assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range");
@@ -73,30 +143,33 @@ void audioMixerPlayLooped(
pan >= AUDIO_STREAM_LEFT && pan <= AUDIO_STREAM_RIGHT,
"Pan is out of range"
);
assertTrue(loopStart >= 0.0f, "Loop start incorrect");
assertTrue(loopTo >= 0.0f, "Loop to incorrect");
assertTrue(
!looping || loopStart < 0.0f || loopTo < loopStart,
"Loop points incorrect"
);
audiomixerchanneldata_t *queue = &AUDIO_MIXER.queue[channel];
stringCopy(queue->file, file, AUDIO_PATH_MAX);
queue->volume = volume;
queue->pan = pan;
queue->loopCount = loopCount;
queue->loopStart = loopStart;
queue->loopTo = loopTo;
audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
command->command = AUDIO_MIXER_COMMAND_PLAY;
stringCopy(command->file, file, AUDIO_PATH_MAX);
command->volume = volume;
command->pan = pan;
command->looping = looping;
command->loopCount = loopCount;
command->loopStart = loopStart;
command->loopTo = loopTo;
}
void audioMixerStop(const audiomixerchannel_t channel) {
assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range");
printf("Stopping channel %d\n", channel);
AUDIO_MIXER.channels[channel].pending.command = AUDIO_MIXER_COMMAND_STOP;
}
errorret_t audioMixerDispose() {
audiomixerchannel_t channel = 0;
do {
audioMixerStop(channel);
channel++;
} while(channel < AUDIO_MIXER_CHANNEL_COUNT);
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
errorChain(audioMixerChannelStopStream(&AUDIO_MIXER.channels[channel]));
}
errorOk();
}
}
+99 -16
View File
@@ -1,6 +1,6 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
@@ -8,6 +8,8 @@
#pragma once
#include "asset/asset.h"
typedef struct audiostream_s audiostream_t;
typedef enum {
AUDIO_MIXER_CHANNEL_BGM_0,
AUDIO_MIXER_CHANNEL_VOICE_0,
@@ -21,19 +23,51 @@ typedef enum {
#define AUDIO_PATH_MAX 256
typedef struct {
assetentry_t *asset;
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;
} audiomixerchanneldata_t;
// 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 {
audiomixerchanneldata_t queue[AUDIO_MIXER_CHANNEL_COUNT];
// 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;
} audiomixerchannelstate_t;
typedef struct {
audiomixerchannelstate_t channels[AUDIO_MIXER_CHANNEL_COUNT];
uint8_t fade[AUDIO_MIXER_CHANNEL_COUNT];
} audiomixer_t;
@@ -45,22 +79,41 @@ extern audiomixer_t AUDIO_MIXER;
errorret_t audioMixerInit();
/**
* 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.
* 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);
/**
* Updates the audio mixer, called before rendering (the start of the
* frame): applies whatever command (play, stop - including everything a
* play carries, volume/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.
*/
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.
* 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.
*/
errorret_t audioMixerUpdateLate();
/**
* Same as audioMixerPlayLooped() but with no looping.
*
*
* @param file File to be played.
* @param channel Channel you want to play this sound on.
* @param volume Volume of the sound, from 0.0 (silent) to 1.0 (loudest).
@@ -77,11 +130,11 @@ void audioMixerPlay(
/**
* Queues a sound to be played on the next audio mixer update, which occurs at
* the end of each frame.
*
*
* Mixer channels can only hold one voice (audio stream) at a time. If you try
* 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.
*
*
* Volume is also mixed based on the channel, so supplying volume 1.0 is then
* multiplied by the channel's volume, e.g. 0.5*1.0 = 0.5.
*
@@ -105,9 +158,39 @@ void audioMixerPlayLooped(
const float_t loopTo
);
/**
* Shared implementation behind audioMixerPlay()/audioMixerPlayLooped() -
* queues an AUDIO_MIXER_COMMAND_PLAY for the given channel, to be loaded at
* the end of this frame and started at the start of the next (see
* audioMixerUpdateLate()/audioMixerUpdateEarly()).
*
* @param file File to be played.
* @param channel Channel you want to play this sound on.
* @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
* AUDIO_STREAM_RIGHT.
* @param looping Whether the stream should loop once it finishes.
* @param loopCount How many times to loop the sound, 0 for infinite looping.
* Ignored unless looping is true.
* @param loopStart Where the loop segment ends, in seconds, or -1 to loop
* the whole stream. Ignored unless looping is true.
* @param loopTo Where the loop segment starts, in seconds. Ignored unless
* looping is true.
*/
void audioMixerQueuePlay(
const char_t *file,
const audiomixerchannel_t channel,
const float_t volume,
const float_t pan,
const bool_t looping,
const uint8_t loopCount,
const float_t loopStart,
const float_t loopTo
);
/**
* Stops playing sound on the given channel.
*
*
* @param channel Channel to stop.
*/
void audioMixerStop(const audiomixerchannel_t channel);
@@ -115,4 +198,4 @@ void audioMixerStop(const audiomixerchannel_t channel);
/**
* Disposes the audio mixer.
*/
errorret_t audioMixerDispose();
errorret_t audioMixerDispose();