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 "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();
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user