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:
@@ -6,7 +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 "assert/assert.h"
|
||||
@@ -19,32 +21,87 @@ 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];
|
||||
for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) {
|
||||
audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
|
||||
|
||||
// File queued?
|
||||
if(queue->file[0] == '\0') {
|
||||
channel++;
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending;
|
||||
|
||||
stringCopy(queue->file, file, AUDIO_PATH_MAX);
|
||||
queue->volume = volume;
|
||||
queue->pan = pan;
|
||||
queue->loopCount = loopCount;
|
||||
queue->loopStart = loopStart;
|
||||
queue->loopTo = loopTo;
|
||||
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();
|
||||
}
|
||||
@@ -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,16 +79,35 @@ 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();
|
||||
|
||||
@@ -105,6 +158,36 @@ 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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user