Cache PSP volume/pan computation, mix channel + master volume into mixer playback, wire boa.mp3 as looping BGM
- PSP output thread now caches the last computed leftVolume/rightVolume and only recomputes them when stream->volume/directionality actually changed since the previous chunk, instead of recomputing every ~23ms chunk unconditionally. - Mixer channels and the mixer itself now carry their own volume (audioMixerSetChannelVolume()/audioMixerSetMasterVolume()), multiplied with each sound's own volume every audioMixerUpdateEarly() and applied via audioStreamSetVolume() - so changing a channel's or the master volume affects whatever's already playing, not just future sounds. Also fixed audioMixerUpdateEarly() to tolerate a PLAY command queued outside the normal per-frame cycle (no loadingAsset yet from a preceding audioMixerUpdateLate(), e.g. during engine startup) by leaving it queued for the following frame instead of asserting. - engine.c now plays boa.mp3 on AUDIO_MIXER_CHANNEL_BGM_0 on loop through the mixer instead of driving a raw audiostream_t directly, exercising the new mixer pipeline end to end. Built and verified on Linux, PSP (Docker) and GameCube (Docker). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -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]));
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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.
|
||||
// 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);
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user