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
+107 -34
View File
@@ -6,7 +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 "assert/assert.h" #include "assert/assert.h"
@@ -19,32 +21,87 @@ errorret_t audioMixerInit() {
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();
}
errorret_t audioMixerUpdateEarly() { 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(); errorOk();
} }
errorret_t audioMixerUpdateLate() { errorret_t audioMixerUpdateLate() {
// For each channel. for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
audiomixerchannel_t channel = 0; audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
do {
audiomixerchanneldata_t *queue = &AUDIO_MIXER.queue[channel];
// File queued? if(command->command != AUDIO_MIXER_COMMAND_PLAY) continue;
if(queue->file[0] == '\0') {
channel++; const assetloadertype_t assetType = audioStreamAssetTypeForPath(command->file);
continue; 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);
} }
// Stop this channel
audioMixerStop(channel);
// Get the file
// TODO: Loading?
// assetentry_t *entry = assetLock(queue->file, ASSET_LOADER_TYPE_WAV, NULL);
printf("Playing %s on channel %d\n", queue->file, channel);
} while(channel < AUDIO_MIXER_CHANNEL_COUNT);
errorOk(); errorOk();
} }
@@ -54,7 +111,7 @@ void audioMixerPlay(
const float_t volume, const float_t volume,
const float_t pan 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( void audioMixerPlayLooped(
@@ -65,6 +122,19 @@ void audioMixerPlayLooped(
const uint8_t loopCount, const uint8_t loopCount,
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);
}
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"); 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");
@@ -73,30 +143,33 @@ void audioMixerPlayLooped(
pan >= AUDIO_STREAM_LEFT && pan <= AUDIO_STREAM_RIGHT, pan >= AUDIO_STREAM_LEFT && pan <= AUDIO_STREAM_RIGHT,
"Pan is out of range" "Pan is out of range"
); );
assertTrue(loopStart >= 0.0f, "Loop start incorrect"); assertTrue(
assertTrue(loopTo >= 0.0f, "Loop to incorrect"); !looping || loopStart < 0.0f || loopTo < loopStart,
"Loop points incorrect"
);
audiomixerchanneldata_t *queue = &AUDIO_MIXER.queue[channel]; audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
stringCopy(queue->file, file, AUDIO_PATH_MAX); command->command = AUDIO_MIXER_COMMAND_PLAY;
queue->volume = volume; stringCopy(command->file, file, AUDIO_PATH_MAX);
queue->pan = pan; command->volume = volume;
queue->loopCount = loopCount; command->pan = pan;
queue->loopStart = loopStart; command->looping = looping;
queue->loopTo = loopTo; command->loopCount = loopCount;
command->loopStart = loopStart;
command->loopTo = loopTo;
} }
void audioMixerStop(const audiomixerchannel_t channel) { void audioMixerStop(const audiomixerchannel_t channel) {
assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); 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() { errorret_t audioMixerDispose() {
audiomixerchannel_t channel = 0; for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
do { errorChain(audioMixerChannelStopStream(&AUDIO_MIXER.channels[channel]));
audioMixerStop(channel); }
channel++;
} while(channel < AUDIO_MIXER_CHANNEL_COUNT);
errorOk(); errorOk();
} }
+93 -10
View File
@@ -8,6 +8,8 @@
#pragma once #pragma once
#include "asset/asset.h" #include "asset/asset.h"
typedef struct audiostream_s audiostream_t;
typedef enum { typedef enum {
AUDIO_MIXER_CHANNEL_BGM_0, AUDIO_MIXER_CHANNEL_BGM_0,
AUDIO_MIXER_CHANNEL_VOICE_0, AUDIO_MIXER_CHANNEL_VOICE_0,
@@ -21,19 +23,51 @@ typedef enum {
#define AUDIO_PATH_MAX 256 #define AUDIO_PATH_MAX 256
typedef struct { typedef enum {
assetentry_t *asset; 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]; char_t file[AUDIO_PATH_MAX];
float_t volume; float_t volume;
float_t pan; float_t pan;
bool_t looping;
uint8_t loopCount; uint8_t loopCount;
float_t loopStart; float_t loopStart;
float_t loopTo; 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 { 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]; uint8_t fade[AUDIO_MIXER_CHANNEL_COUNT];
} audiomixer_t; } audiomixer_t;
@@ -45,16 +79,35 @@ extern audiomixer_t AUDIO_MIXER;
errorret_t audioMixerInit(); errorret_t audioMixerInit();
/** /**
* Updates the audio mixer, called before rendering. Reserved for * Stops and disposes the given channel's currently playing stream, if any -
* time-sensitive mixer state (e.g. volume fades) that should be reflected * a safe no-op if the channel is already idle. Shared by
* before this frame's streams are buffered - nothing uses it yet. * 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(); errorret_t audioMixerUpdateEarly();
/** /**
* Updates the audio mixer, called at the end of each frame - starts any * Updates the audio mixer, called at the end of each frame, once every
* sound queued this frame via audioMixerPlay()/audioMixerPlayLooped(), once * other system has had a chance to queue a command via
* every other system has had a chance to queue one. * 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();
@@ -105,6 +158,36 @@ void audioMixerPlayLooped(
const float_t loopTo 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. * Stops playing sound on the given channel.
* *