diff --git a/src/dusk/audio/mixer/audiomixer.c b/src/dusk/audio/mixer/audiomixer.c index 412da449..5a424bf0 100644 --- a/src/dusk/audio/mixer/audiomixer.c +++ b/src/dusk/audio/mixer/audiomixer.c @@ -11,6 +11,7 @@ #include "audio/stream/audiostreamtype.h" #include "util/memory.h" #include "util/string.h" +#include "util/math.h" #include "assert/assert.h" audiomixer_t AUDIO_MIXER; @@ -18,6 +19,11 @@ audiomixer_t AUDIO_MIXER; errorret_t audioMixerInit() { memoryZero(&AUDIO_MIXER, sizeof(audiomixer_t)); + AUDIO_MIXER.masterVolume = 1.0f; + for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) { + AUDIO_MIXER.channels[channel].volume = 1.0f; + } + errorOk(); } @@ -32,6 +38,21 @@ errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state) { errorOk(); } +void audioMixerChannelApplyVolume(audiomixerchannelstate_t *state) { + assertNotNull(state, "State cannot be NULL."); + + if(state->stream == NULL) return; + + // Recomputed and reapplied every frame (not just when a PLAY command + // starts state->stream) so a live audioMixerSetChannelVolume()/ + // audioMixerSetMasterVolume() call affects whatever's already playing, + // not just sounds started afterward. + const float_t volume = mathClamp( + state->baseVolume * state->volume * AUDIO_MIXER.masterVolume, 0.0f, 1.0f + ); + audioStreamSetVolume(state->stream, volume); +} + errorret_t audioMixerUpdateEarly() { for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) { audiomixerchannelstate_t *state = &AUDIO_MIXER.channels[channel]; @@ -39,13 +60,8 @@ errorret_t audioMixerUpdateEarly() { 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." - ); - + command->command = AUDIO_MIXER_COMMAND_NONE; + } else if(command->command == AUDIO_MIXER_COMMAND_PLAY && command->loadingAsset != NULL) { // 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. @@ -71,7 +87,6 @@ errorret_t audioMixerUpdateEarly() { command->command = AUDIO_MIXER_COMMAND_NONE; errorChain(initRet); - audioStreamSetVolume(stream, command->volume); audioStreamSetDirectionality(stream, command->pan); if(command->looping) { audioStreamSetLooping(stream, true); @@ -81,7 +96,22 @@ errorret_t audioMixerUpdateEarly() { audioStreamPlay(stream); state->stream = stream; + state->baseVolume = command->volume; } + // A PLAY command with no loadingAsset yet (command->loadingAsset == + // NULL) was queued too late for the audioMixerUpdateLate() that just + // ran immediately before this call to have locked it - the only way + // that happens is a command queued outside the normal per-frame cycle, + // e.g. during engine startup before any audioMixerUpdateLate() has run + // yet. Left alone (still AUDIO_MIXER_COMMAND_PLAY) rather than treated + // as an error: the very next audioMixerUpdateLate() will lock it, and + // the frame after that will reach this branch and apply it normally. + + // Determines the actual volume passed to audioStreamSetVolume() from + // this sound's own volume (just set above, for a fresh PLAY) and this + // channel's/the mixer's - see its own comment for why this runs every + // frame regardless of whether a command was queued this frame. + audioMixerChannelApplyVolume(state); } errorOk(); @@ -166,6 +196,22 @@ void audioMixerStop(const audiomixerchannel_t channel) { AUDIO_MIXER.channels[channel].pending.command = AUDIO_MIXER_COMMAND_STOP; } +void audioMixerSetChannelVolume( + const audiomixerchannel_t channel, + const float_t volume +) { + assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); + assertTrue(volume >= 0.0f && volume <= 1.0f, "Volume is out of range"); + + AUDIO_MIXER.channels[channel].volume = volume; +} + +void audioMixerSetMasterVolume(const float_t volume) { + assertTrue(volume >= 0.0f && volume <= 1.0f, "Volume is out of range"); + + AUDIO_MIXER.masterVolume = volume; +} + errorret_t audioMixerDispose() { for(audiomixerchannel_t channel = 0; channel < AUDIO_MIXER_CHANNEL_COUNT; channel++) { errorChain(audioMixerChannelStopStream(&AUDIO_MIXER.channels[channel])); diff --git a/src/dusk/audio/mixer/audiomixer.h b/src/dusk/audio/mixer/audiomixer.h index 6ca943b0..6f842277 100644 --- a/src/dusk/audio/mixer/audiomixer.h +++ b/src/dusk/audio/mixer/audiomixer.h @@ -64,11 +64,28 @@ typedef struct { // AUDIO_MIXER_COMMAND_STOP or before a new AUDIO_MIXER_COMMAND_PLAY // replaces it. audiostream_t *stream; + + // The per-sound volume `stream` was last started with (the `volume` + // argument to whichever audioMixerPlay()/audioMixerPlayLooped() call + // started it) - combined with this channel's own volume and the mixer's + // master volume, every frame, to get the volume actually passed to + // audioStreamSetVolume(). Meaningless while `stream` is NULL. + float_t baseVolume; + + // This channel's own volume, from 0.0 (silent) to 1.0 (loudest, the + // default) - independent of any individual sound's own volume. Set via + // audioMixerSetChannelVolume(). + float_t volume; } audiomixerchannelstate_t; typedef struct { audiomixerchannelstate_t channels[AUDIO_MIXER_CHANNEL_COUNT]; uint8_t fade[AUDIO_MIXER_CHANNEL_COUNT]; + + // Global volume multiplier applied on top of every channel's own volume + // and every sound's own volume - from 0.0 (silent) to 1.0 (loudest, the + // default). Set via audioMixerSetMasterVolume(). + float_t masterVolume; } audiomixer_t; extern audiomixer_t AUDIO_MIXER; @@ -89,14 +106,30 @@ errorret_t audioMixerInit(); */ errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state); +/** + * Computes the given channel's actual playback volume - its currently + * playing sound's own volume (baseVolume) combined with the channel's own + * volume and the mixer's master volume - and applies it via + * audioStreamSetVolume(). A safe no-op if the channel isn't playing + * anything. Called every audioMixerUpdateEarly(), not just when a PLAY + * command starts a new stream - see audioMixerUpdateEarly()'s own comment. + * + * @param state The channel state to update. + */ +void audioMixerChannelApplyVolume(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. + * play carries, 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. Also recomputes and reapplies every + * playing channel's mixed volume (see audioMixerChannelApplyVolume()) every + * time, regardless of whether a command was queued this frame, so + * audioMixerSetChannelVolume()/audioMixerSetMasterVolume() take effect on + * whatever's already playing too. */ errorret_t audioMixerUpdateEarly(); @@ -195,6 +228,32 @@ void audioMixerQueuePlay( */ void audioMixerStop(const audiomixerchannel_t channel); +/** + * Sets the given channel's own volume, independent of whatever individual + * sound is (or will be) playing on it - combined with that sound's own + * volume and the mixer's master volume on the very next + * audioMixerUpdateEarly(), so it takes effect immediately even if nothing + * new is being played. + * + * @param channel Channel to update. + * @param volume The channel's new volume, from 0.0 (silent) to 1.0 + * (loudest, the default). + */ +void audioMixerSetChannelVolume( + const audiomixerchannel_t channel, + const float_t volume +); + +/** + * Sets the mixer's master volume, combined with every channel's own volume + * and every sound's own volume on the very next audioMixerUpdateEarly(), so + * it takes effect immediately for whatever's already playing. + * + * @param volume The new master volume, from 0.0 (silent) to 1.0 (loudest, + * the default). + */ +void audioMixerSetMasterVolume(const float_t volume); + /** * Disposes the audio mixer. */ diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 856c2e51..ebb99e35 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -23,17 +23,10 @@ #include "console/console.h" #include "save/save.h" #include "audio/audio.h" +#include "audio/mixer/audiomixer.h" engine_t ENGINE; -void engineTestToneOnEnd(audiostream_t *stream) { - consolePrint("Test tone finished playing"); -} - -void engineTestToneOnLoop(audiostream_t *stream) { - consolePrint("Test tone looped"); -} - errorret_t engineInit(const int32_t argc, const char_t **argv) { assertInit(); memoryZero(&ENGINE, sizeof(engine_t)); @@ -59,23 +52,13 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { #endif errorChain(sceneInit()); - // Smoke-tests the audio subsystem end to end (asset loading -> PCM - // streaming -> platform playback) against a real WAV asset rather than + // Smoke-tests the audio mixer end to end (asset loading -> PCM/MP3 + // streaming -> platform playback) against a real MP3 asset rather than // synthesizing PCM data at runtime. - assetentry_t *testToneEntry = assetLock( - "audio/audiotest.wav", ASSET_LOADER_TYPE_WAV, NULL + audioMixerPlayLooped( + "audio/boa.mp3", AUDIO_MIXER_CHANNEL_BGM_0, 1.0f, AUDIO_STREAM_CENTER, + 0, -1.0f, 0.0f ); - errorChain(assetRequireLoaded(testToneEntry)); - - audiostream_t *stream = audioAquireStream(testToneEntry); - assertNotNull(stream, "No free audio stream slots available."); - errorChain(audioStreamInit(stream, testToneEntry)); - assetUnlockEntry(testToneEntry); // The stream now holds its own lock. - - stream->onEnd = engineTestToneOnEnd; - stream->onLoop = engineTestToneOnLoop; - audioStreamSetLooping(stream, true); - audioStreamPlay(stream); consolePrint("Engine initialized"); diff --git a/src/duskpsp/audio/audiostreampsp.c b/src/duskpsp/audio/audiostreampsp.c index 56ff7a12..4ec781a3 100644 --- a/src/duskpsp/audio/audiostreampsp.c +++ b/src/duskpsp/audio/audiostreampsp.c @@ -73,6 +73,7 @@ errorret_t audioStreamPSPInit(audiostream_t *stream) { stream->platform.channel = channel; stream->platform.finished = false; stream->platform.playRequested = false; + stream->platform.hasLastVolume = false; const size_t ringSize = AUDIO_PSP_RING_FRAMES * stream->channels * sizeof(int16_t); @@ -381,17 +382,33 @@ void audioStreamPSPThreadFeed(thread_t *thread) { } } - // Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality - // take effect mid-playback, unlike the platform's other one-shot calls. - float_t leftFactor, rightFactor; - audioStreamGetPanFactors(stream, &leftFactor, &rightFactor); + // Checked every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality + // take effect mid-playback, unlike the platform's other one-shot + // calls - but only actually recomputed when one of them has changed + // since the last chunk (see lastVolume's own comment); otherwise the + // previous chunk's already-computed leftVolume/rightVolume are reused + // as-is. + if( + !stream->platform.hasLastVolume || + stream->platform.lastVolume != stream->volume || + stream->platform.lastDirectionality != stream->directionality + ) { + float_t leftFactor, rightFactor; + audioStreamGetPanFactors(stream, &leftFactor, &rightFactor); - const int baseVolume = (int) (stream->volume * PSP_AUDIO_VOLUME_MAX); - const int leftVolume = (int) (baseVolume * leftFactor); - const int rightVolume = (int) (baseVolume * rightFactor); + const int baseVolume = (int) (stream->volume * PSP_AUDIO_VOLUME_MAX); + stream->platform.lastLeftVolume = (int) (baseVolume * leftFactor); + stream->platform.lastRightVolume = (int) (baseVolume * rightFactor); + stream->platform.lastVolume = stream->volume; + stream->platform.lastDirectionality = stream->directionality; + stream->platform.hasLastVolume = true; + } sceAudioOutputPannedBlocking( - stream->platform.channel, leftVolume, rightVolume, chunk + stream->platform.channel, + stream->platform.lastLeftVolume, + stream->platform.lastRightVolume, + chunk ); stream->platform.framesOutput += framesThisChunk; diff --git a/src/duskpsp/audio/audiostreampsp.h b/src/duskpsp/audio/audiostreampsp.h index 70b7fa49..80f4d730 100644 --- a/src/duskpsp/audio/audiostreampsp.h +++ b/src/duskpsp/audio/audiostreampsp.h @@ -141,6 +141,21 @@ typedef struct { // pass's last, possibly-partial chunk. bool_t readReachedEnd; bool_t readFailed; + + // Cache of the last stream->volume/directionality the output thread + // computed lastLeftVolume/lastRightVolume from - recomputing + // audioStreamGetPanFactors() and both multiplications is wasted work on + // every single chunk (~23ms) whenever neither has actually changed since + // the previous one, which is the common case outside of an active fade/ + // pan. PSP itself has no persistent per-channel volume to cache this + // for us (see sceAudioOutputPannedBlocking's own call site) - hence + // caching it ourselves. Output-thread-only, like the rest of this + // section - see sceAudioOutputPannedBlocking's own call site. + float_t lastVolume; + float_t lastDirectionality; + int lastLeftVolume; + int lastRightVolume; + bool_t hasLastVolume; } audiostreampsp_t; /**