diff --git a/src/dusk/audio/mixer/audiomixer.c b/src/dusk/audio/mixer/audiomixer.c index 5e4aa893..412da449 100644 --- a/src/dusk/audio/mixer/audiomixer.c +++ b/src/dusk/audio/mixer/audiomixer.c @@ -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(); -} \ No newline at end of file +} diff --git a/src/dusk/audio/mixer/audiomixer.h b/src/dusk/audio/mixer/audiomixer.h index 69e10cc9..6ca943b0 100644 --- a/src/dusk/audio/mixer/audiomixer.h +++ b/src/dusk/audio/mixer/audiomixer.h @@ -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(); \ No newline at end of file +errorret_t audioMixerDispose();