From 936476839773e5a7c23445635c7b2144606bba52 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Wed, 2 Sep 2026 17:44:06 -0500 Subject: [PATCH] Add cutscene controls for the audio mixer, fix PSP pause latency - Fixed PSP pause taking up to ~1.1s to actually go silent: its output thread ran independently of stream->state, draining its whole software ring regardless. audiostream_t.state is now volatile and the output thread checks AUDIO_STREAM_STATE_PLAYING before each hardware chunk, skipping output (without consuming the ring) while paused - pause now goes silent within about one chunk (~23ms) and resume has no gap. - Added audioMixerPause/Resume/SetPan/SetLoop/FadeTo/IsFading to the mixer - immediate, channel-indexed primitives for cutscenes to drive. Fade transitions (fadeFrom/To/Duration/Time/Easing) live on audiomixerchannelstate_t and advance every frame in audioMixerChannelApplyVolume(), reusing the same easingApply() interpolation uifullbox_t already uses for screen fades. - New src/dusk/rpg/cutscene/item/audio/ with 9 cutscene item types: AUDIO_PLAY (+ AUDIO_PLAY_SIMPLE/AUDIO_PLAY_LOOPED shorthands), AUDIO_STOP, AUDIO_PAUSE, AUDIO_RESUME, AUDIO_FADE (+ FADE_OUT/FADE_IN shorthands), AUDIO_FADE_WAIT, AUDIO_SET_PAN, AUDIO_SET_LOOP, and the combined AUDIO_SET - registered through the same enum/union/callback table/macro mechanism every other item type uses. - Wired the same 9 types into the JSON-based (offline JSONC -> binary .cts) cutscene asset pipeline: tools/asset/cutscene/__main__.py's encoder and assetcutsceneloader.c's decoder. Verified round-trip by hand-encoding/decoding a test file covering all 9 types, and confirmed the two existing real cutscene files re-encode byte-identical. Built and verified on Linux, PSP (Docker), GameCube (Docker) and Wii (Docker). Co-Authored-By: Claude Sonnet 5 --- .../loader/cutscene/assetcutsceneloader.c | 69 +++++++++++ src/dusk/audio/mixer/audiomixer.c | 71 +++++++++++ src/dusk/audio/mixer/audiomixer.h | 82 +++++++++++++ src/dusk/audio/mixer/audiomixerchannel.c | 49 ++++++++ src/dusk/audio/mixer/audiomixerchannel.h | 56 ++++++++- src/dusk/audio/stream/audiostream.h | 7 +- src/dusk/rpg/cutscene/cutscene.h | 111 ++++++++++++++++++ src/dusk/rpg/cutscene/item/CMakeLists.txt | 1 + .../rpg/cutscene/item/audio/CMakeLists.txt | 17 +++ .../cutscene/item/audio/cutsceneaudiofade.c | 29 +++++ .../cutscene/item/audio/cutsceneaudiofade.h | 46 ++++++++ .../item/audio/cutsceneaudiofadewait.c | 16 +++ .../item/audio/cutsceneaudiofadewait.h | 26 ++++ .../cutscene/item/audio/cutsceneaudiopause.c | 23 ++++ .../cutscene/item/audio/cutsceneaudiopause.h | 35 ++++++ .../cutscene/item/audio/cutsceneaudioplay.c | 31 +++++ .../cutscene/item/audio/cutsceneaudioplay.h | 48 ++++++++ .../cutscene/item/audio/cutsceneaudioresume.c | 23 ++++ .../cutscene/item/audio/cutsceneaudioresume.h | 35 ++++++ .../cutscene/item/audio/cutsceneaudioset.c | 30 +++++ .../cutscene/item/audio/cutsceneaudioset.h | 47 ++++++++ .../item/audio/cutsceneaudiosetloop.c | 29 +++++ .../item/audio/cutsceneaudiosetloop.h | 44 +++++++ .../cutscene/item/audio/cutsceneaudiosetpan.c | 23 ++++ .../cutscene/item/audio/cutsceneaudiosetpan.h | 41 +++++++ .../cutscene/item/audio/cutsceneaudiostop.c | 23 ++++ .../cutscene/item/audio/cutsceneaudiostop.h | 35 ++++++ src/dusk/rpg/cutscene/item/cutsceneitem.c | 44 +++++++ src/dusk/rpg/cutscene/item/cutsceneitem.h | 27 +++++ src/duskpsp/audio/audiostreampsp.c | 15 +++ tools/asset/cutscene/__main__.py | 53 +++++++++ 31 files changed, 1183 insertions(+), 3 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/audio/CMakeLists.txt create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c create mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c index 7068f148..ab3bd803 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c @@ -398,6 +398,75 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) { assetCutsceneReadPoolString(data, &offset, pool); break; + case CUTSCENE_ITEM_TYPE_AUDIO_PLAY: + assetCutsceneReadEmbeddedString( + data, &offset, item->audioPlay.file, AUDIO_PATH_MAX + ); + item->audioPlay.channel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + item->audioPlay.volume = assetCutsceneReadFloat(data, &offset); + item->audioPlay.pan = assetCutsceneReadFloat(data, &offset); + item->audioPlay.looping = assetCutsceneReadU8(data, &offset) != 0; + item->audioPlay.loopCount = assetCutsceneReadU8(data, &offset); + item->audioPlay.loopStart = assetCutsceneReadFloat(data, &offset); + item->audioPlay.loopTo = assetCutsceneReadFloat(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_STOP: + item->audioStopChannel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_PAUSE: + item->audioPauseChannel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_RESUME: + item->audioResumeChannel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_FADE: + item->audioFade.channel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + item->audioFade.from = assetCutsceneReadFloat(data, &offset); + item->audioFade.to = assetCutsceneReadFloat(data, &offset); + item->audioFade.duration = assetCutsceneReadFloat(data, &offset); + item->audioFade.easing = + (easingtype_t)assetCutsceneReadU8(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT: + item->audioFadeWaitChannel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN: + item->audioSetPan.channel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + item->audioSetPan.pan = assetCutsceneReadFloat(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP: + item->audioSetLoop.channel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + item->audioSetLoop.looping = assetCutsceneReadU8(data, &offset) != 0; + item->audioSetLoop.loopCount = assetCutsceneReadU8(data, &offset); + item->audioSetLoop.loopStart = assetCutsceneReadFloat(data, &offset); + item->audioSetLoop.loopTo = assetCutsceneReadFloat(data, &offset); + break; + + case CUTSCENE_ITEM_TYPE_AUDIO_SET: + item->audioSet.channel = + (audiomixerchannel_t)assetCutsceneReadU8(data, &offset); + item->audioSet.pan = assetCutsceneReadFloat(data, &offset); + item->audioSet.looping = assetCutsceneReadU8(data, &offset) != 0; + item->audioSet.loopCount = assetCutsceneReadU8(data, &offset); + item->audioSet.loopStart = assetCutsceneReadFloat(data, &offset); + item->audioSet.loopTo = assetCutsceneReadFloat(data, &offset); + break; + default: memoryFree(data); memoryFree(out->items); diff --git a/src/dusk/audio/mixer/audiomixer.c b/src/dusk/audio/mixer/audiomixer.c index 2024a1b1..41ec71b1 100644 --- a/src/dusk/audio/mixer/audiomixer.c +++ b/src/dusk/audio/mixer/audiomixer.c @@ -19,6 +19,9 @@ errorret_t audioMixerInit() { audiomixerchannel_t channel = 0; while(channel < AUDIO_MIXER_CHANNEL_COUNT) { AUDIO_MIXER.channels[channel].fade = 1.0f; + // -1 means "no fade in progress" - see its own comment; zero-init + // would otherwise look like an active fade at time 0. + AUDIO_MIXER.channels[channel].fadeTime = -1.0f; channel++; } @@ -117,6 +120,74 @@ void audioMixerStop(const audiomixerchannel_t channel) { AUDIO_MIXER.channels[channel].pending.command = AUDIO_MIXER_COMMAND_STOP; } +void audioMixerPause(const audiomixerchannel_t channel) { + assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); + + audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; + if(stream != NULL) audioStreamPause(stream); +} + +void audioMixerResume(const audiomixerchannel_t channel) { + assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); + + audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; + if(stream != NULL) audioStreamPlay(stream); +} + +void audioMixerSetPan(const audiomixerchannel_t channel, const float_t pan) { + assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); + assertTrue( + pan >= AUDIO_STREAM_LEFT && pan <= AUDIO_STREAM_RIGHT, + "Pan is out of range" + ); + + audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; + if(stream != NULL) audioStreamSetDirectionality(stream, pan); +} + +void audioMixerSetLoop( + const audiomixerchannel_t channel, + const bool_t looping, + const uint8_t loopCount, + const float_t loopStart, + const float_t loopTo +) { + assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); + assertTrue( + !looping || loopStart < 0.0f || loopTo < loopStart, + "Loop points incorrect" + ); + + audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; + if(stream == NULL) return; + + audioStreamSetLooping(stream, looping); + if(looping) { + audioStreamSetLoopPoints(stream, loopStart, loopTo); + audioStreamSetLoopLimit(stream, loopCount); + } +} + +void audioMixerFadeTo( + const audiomixerchannel_t channel, + const float_t from, + const float_t to, + const float_t duration, + const easingtype_t easing +) { + assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); + + audioMixerChannelFadeTo( + &AUDIO_MIXER.channels[channel], from, to, duration, easing + ); +} + +bool_t audioMixerIsFading(const audiomixerchannel_t channel) { + assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); + + return audioMixerChannelIsFading(&AUDIO_MIXER.channels[channel]); +} + errorret_t audioMixerDispose() { audiomixerchannel_t channel = 0; while(channel < AUDIO_MIXER_CHANNEL_COUNT) { diff --git a/src/dusk/audio/mixer/audiomixer.h b/src/dusk/audio/mixer/audiomixer.h index eb540404..a61f3ec9 100644 --- a/src/dusk/audio/mixer/audiomixer.h +++ b/src/dusk/audio/mixer/audiomixer.h @@ -116,6 +116,88 @@ void audioMixerQueuePlay( */ void audioMixerStop(const audiomixerchannel_t channel); +/** + * Pauses the given channel's currently playing stream, if any - a safe + * no-op if the channel isn't playing anything. Playback position is + * retained, so audioMixerResume() continues from the same point. + * + * @param channel Channel to pause. + */ +void audioMixerPause(const audiomixerchannel_t channel); + +/** + * Resumes the given channel's currently paused stream, if any - a safe + * no-op if the channel isn't playing anything. + * + * @param channel Channel to resume. + */ +void audioMixerResume(const audiomixerchannel_t channel); + +/** + * Sets the stereo panning of the given channel's currently playing stream, + * if any - a safe no-op if the channel isn't playing anything. Only + * affects what's playing right now; a later audioMixerPlay()/ + * audioMixerPlayLooped() call on this channel uses its own `pan` argument + * instead. + * + * @param channel Channel to update. + * @param pan Stereo panning, from AUDIO_STREAM_LEFT to AUDIO_STREAM_RIGHT. + */ +void audioMixerSetPan(const audiomixerchannel_t channel, const float_t pan); + +/** + * Sets the looping behaviour of the given channel's currently playing + * stream, if any - a safe no-op if the channel isn't playing anything. + * Only affects what's playing right now; a later audioMixerPlay()/ + * audioMixerPlayLooped() call on this channel uses its own loop arguments + * instead. + * + * @param channel Channel to update. + * @param looping Whether the stream should loop. + * @param loopCount How many times to loop, 0 for infinite. 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 audioMixerSetLoop( + const audiomixerchannel_t channel, + const bool_t looping, + const uint8_t loopCount, + const float_t loopStart, + const float_t loopTo +); + +/** + * Starts fading the given channel from `from` to `to` over `duration` + * seconds, eased by `easing` - see audioMixerChannelFadeTo()'s own + * comment. Continues running (advanced every audioMixerUpdateEarly()) even + * if the channel isn't currently playing anything. + * + * @param channel Channel to fade. + * @param from Starting fade value, from 0.0 (silent) to 1.0 (loudest). + * @param to Ending fade value, from 0.0 (silent) to 1.0 (loudest). + * @param duration How long the fade takes, in seconds. + * @param easing Easing curve to apply to the fade's progress. + */ +void audioMixerFadeTo( + const audiomixerchannel_t channel, + const float_t from, + const float_t to, + const float_t duration, + const easingtype_t easing +); + +/** + * Whether the given channel currently has a fade in progress (started via + * audioMixerFadeTo() and not yet finished). + * + * @param channel Channel to check. + * @return true if a fade is still in progress. + */ +bool_t audioMixerIsFading(const audiomixerchannel_t channel); + /** * Disposes the audio mixer. */ diff --git a/src/dusk/audio/mixer/audiomixerchannel.c b/src/dusk/audio/mixer/audiomixerchannel.c index f2657a74..a7946aab 100644 --- a/src/dusk/audio/mixer/audiomixerchannel.c +++ b/src/dusk/audio/mixer/audiomixerchannel.c @@ -6,10 +6,13 @@ */ #include "audiomixerchannel.h" +#include "asset/asset.h" #include "audio/audio.h" #include "audio/stream/audiostream.h" #include "audio/stream/audiostreamtype.h" #include "save/save.h" +#include "time/time.h" +#include "util/math.h" #include "assert/assert.h" errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state) { @@ -23,6 +26,37 @@ errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state) { errorOk(); } +void audioMixerChannelFadeTo( + audiomixerchannelstate_t *state, + const float_t from, + const float_t to, + const float_t duration, + const easingtype_t easing +) { + assertNotNull(state, "State cannot be NULL."); + + state->fade = from; + state->fadeFrom = from; + state->fadeTo = to; + state->fadeDuration = duration; + state->fadeEasing = easing; + // A non-positive duration finishes instantly - marked inactive right + // away (fadeTime < 0) rather than left at 0 to progress next frame, + // since there's nothing left to progress. + if(duration <= 0.0f) { + state->fade = to; + state->fadeTime = -1.0f; + } else { + state->fadeTime = 0.0f; + } +} + +bool_t audioMixerChannelIsFading(const audiomixerchannelstate_t *state) { + assertNotNull(state, "State cannot be NULL."); + + return state->fadeTime >= 0.0f; +} + void audioMixerChannelApplyVolume( const audiomixerchannel_t channel, audiomixerchannelstate_t *state @@ -31,6 +65,21 @@ void audioMixerChannelApplyVolume( if(state->stream == NULL) return; + // Advances any in-progress fade before it's read below - see + // audioMixerChannelFadeTo()'s own comment on fadeTime's meaning. + if(state->fadeTime >= 0.0f) { + state->fadeTime += TIME.delta; + const float_t t = mathClamp( + state->fadeTime / state->fadeDuration, 0.0f, 1.0f + ); + state->fade = state->fadeFrom + + (state->fadeTo - state->fadeFrom) * easingApply(state->fadeEasing, t); + if(state->fadeTime >= state->fadeDuration) { + state->fade = state->fadeTo; + state->fadeTime = -1.0f; + } + } + // state->fade and both SAVE.settings volumes have no setter - written // directly by callers - so this is the only place any of them ever gets // validated, rather than at the point they're set. diff --git a/src/dusk/audio/mixer/audiomixerchannel.h b/src/dusk/audio/mixer/audiomixerchannel.h index a10eed87..5886ab02 100644 --- a/src/dusk/audio/mixer/audiomixerchannel.h +++ b/src/dusk/audio/mixer/audiomixerchannel.h @@ -6,7 +6,18 @@ */ #pragma once -#include "asset/asset.h" +#include "dusk.h" +#include "error/error.h" +#include "animation/easing.h" + +// Forward-declared rather than including asset/asset.h - this header only +// ever needs a pointer to it, and pulling in the full asset module here +// creates a real include cycle for anything that reaches this header via +// asset/loader/cutscene/assetcutsceneloader.h (asset.h -> ... -> +// assetloader.h -> assetcutsceneloader.h -> cutscene.h -> cutsceneitem.h +// -> the audio cutscene items -> this header -> asset/asset.h again, +// mid-parse). audiomixerchannel.c itself still includes the full header. +typedef struct assetentry_s assetentry_t; typedef struct audiostream_s audiostream_t; @@ -85,6 +96,18 @@ typedef struct audiomixerchannelstate_s { // point it's set. float_t fade; + // Fade transition state, set by audioMixerChannelFadeTo() and advanced + // every frame by audioMixerChannelApplyVolume(), before it reads `fade` + // to compute the mixed volume. fadeTime < 0 means no fade is in progress + // (audioMixerChannelIsFading() is just `fadeTime >= 0`) - audioMixerInit() + // explicitly sets this to -1.0f, since zero-initializing it would + // otherwise look like an active fade at time 0. + float_t fadeFrom; + float_t fadeTo; + float_t fadeDuration; + float_t fadeTime; + easingtype_t fadeEasing; + // Arbitrary caller data, handed back as-is to onLoop/onEnd below - never // read or written by the mixer itself. NULL by default. void *user; @@ -133,6 +156,37 @@ void audioMixerChannelApplyVolume( audiomixerchannelstate_t *state ); +/** + * Starts fading the given channel's `fade` multiplier from `from` to `to` + * over `duration` seconds, eased by `easing` - snaps `fade` to `from` + * immediately, then audioMixerChannelApplyVolume() advances it every frame + * from here on. A `duration` of 0 or less snaps straight to `to` and marks + * the fade as already finished (audioMixerChannelIsFading() returns + * false on the very next call). + * + * @param state The channel state to fade. + * @param from Starting fade value, from 0.0 (silent) to 1.0 (loudest). + * @param to Ending fade value, from 0.0 (silent) to 1.0 (loudest). + * @param duration How long the fade takes, in seconds. + * @param easing Easing curve to apply to the fade's progress. + */ +void audioMixerChannelFadeTo( + audiomixerchannelstate_t *state, + const float_t from, + const float_t to, + const float_t duration, + const easingtype_t easing +); + +/** + * Whether the given channel currently has a fade in progress (started via + * audioMixerChannelFadeTo() and not yet finished). + * + * @param state The channel state to check. + * @return true if a fade is still in progress. + */ +bool_t audioMixerChannelIsFading(const audiomixerchannelstate_t *state); + /** * Updates a single mixer channel, called before rendering (the start of * the frame): applies whatever command (play, stop - including everything diff --git a/src/dusk/audio/stream/audiostream.h b/src/dusk/audio/stream/audiostream.h index 0886f5fd..d3c3889a 100644 --- a/src/dusk/audio/stream/audiostream.h +++ b/src/dusk/audio/stream/audiostream.h @@ -42,8 +42,11 @@ typedef struct audiostream_s { // Used for aquiring new data. audistreamtype_t type; - // What state the stream is in. - uint8_t state; + // What state the stream is in. volatile: PSP's output thread reads + // AUDIO_STREAM_STATE_PLAYING directly to honor a pause promptly instead + // of only reacting once the main thread's own per-frame gate notices - + // see audioStreamPSPThreadFeed()'s own comment. + volatile uint8_t state; // The asset backing this stream's data (e.g. a WAV file) - determines // `type` (see audioStreamInit()) and is read from on demand by the diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index 17643179..e51b3437 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -389,3 +389,114 @@ typedef struct cutscene_s { ), \ CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \ CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED) + +// Longhand - every audioMixerQueuePlay() parameter. See +// CUTSCENE_AUDIO_PLAY_SIMPLE/CUTSCENE_AUDIO_PLAY_LOOPED below for the +// shorthands. +#define CUTSCENE_AUDIO_PLAY( \ + FILE, CHANNEL, VOLUME, PAN, LOOPING, LOOP_COUNT, LOOP_START, LOOP_TO \ +) \ + { \ + .type = CUTSCENE_ITEM_TYPE_AUDIO_PLAY, \ + .audioPlay = { \ + .file = FILE, \ + .channel = CHANNEL, \ + .volume = VOLUME, \ + .pan = PAN, \ + .looping = LOOPING, \ + .loopCount = LOOP_COUNT, \ + .loopStart = LOOP_START, \ + .loopTo = LOOP_TO \ + } \ + } + +// Shorthand: play FILE on CHANNEL once, full volume, centered (0.0f, same +// as AUDIO_STREAM_CENTER), no loop. +#define CUTSCENE_AUDIO_PLAY_SIMPLE(FILE, CHANNEL) \ + CUTSCENE_AUDIO_PLAY(FILE, CHANNEL, 1.0f, 0.0f, false, 0, -1.0f, 0.0f) + +// Shorthand: play FILE on CHANNEL looping the whole clip forever, full +// volume, centered (0.0f, same as AUDIO_STREAM_CENTER). +#define CUTSCENE_AUDIO_PLAY_LOOPED(FILE, CHANNEL) \ + CUTSCENE_AUDIO_PLAY(FILE, CHANNEL, 1.0f, 0.0f, true, 0, -1.0f, 0.0f) + +#define CUTSCENE_AUDIO_STOP(CHANNEL) \ + { .type = CUTSCENE_ITEM_TYPE_AUDIO_STOP, .audioStopChannel = CHANNEL } + +#define CUTSCENE_AUDIO_PAUSE(CHANNEL) \ + { .type = CUTSCENE_ITEM_TYPE_AUDIO_PAUSE, .audioPauseChannel = CHANNEL } + +#define CUTSCENE_AUDIO_RESUME(CHANNEL) \ + { .type = CUTSCENE_ITEM_TYPE_AUDIO_RESUME, .audioResumeChannel = CHANNEL } + +// Longhand - starts CHANNEL fading from FROM to TO over DURATION seconds, +// eased by EASING. Doesn't wait for it - pair with +// CUTSCENE_AUDIO_FADE_WAIT(CHANNEL) to block until it finishes. See +// CUTSCENE_AUDIO_FADE_OUT/CUTSCENE_AUDIO_FADE_IN below for the shorthands; +// use this directly for a custom range, e.g. +// CUTSCENE_AUDIO_FADE(CHANNEL, 1.0f, 0.3f, 5.0f, EASING_LINEAR) to fade +// down to "playing quietly" rather than silent. +#define CUTSCENE_AUDIO_FADE(CHANNEL, FROM, TO, DURATION, EASING) \ + { \ + .type = CUTSCENE_ITEM_TYPE_AUDIO_FADE, \ + .audioFade = { \ + .channel = CHANNEL, \ + .from = FROM, \ + .to = TO, \ + .duration = DURATION, \ + .easing = EASING \ + } \ + } + +// Shorthand: fade CHANNEL from full volume to silent over DURATION seconds. +#define CUTSCENE_AUDIO_FADE_OUT(CHANNEL, DURATION) \ + CUTSCENE_AUDIO_FADE(CHANNEL, 1.0f, 0.0f, DURATION, EASING_LINEAR) + +// Shorthand: fade CHANNEL from silent to full volume over DURATION seconds. +#define CUTSCENE_AUDIO_FADE_IN(CHANNEL, DURATION) \ + CUTSCENE_AUDIO_FADE(CHANNEL, 0.0f, 1.0f, DURATION, EASING_LINEAR) + +// Waits until CHANNEL's fade (started elsewhere by CUTSCENE_AUDIO_FADE) +// has finished - a no-op wait if CHANNEL isn't fading. +#define CUTSCENE_AUDIO_FADE_WAIT(CHANNEL) \ + { \ + .type = CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT, \ + .audioFadeWaitChannel = CHANNEL \ + } + +#define CUTSCENE_AUDIO_SET_PAN(CHANNEL, PAN) \ + { \ + .type = CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN, \ + .audioSetPan = { .channel = CHANNEL, .pan = PAN } \ + } + +#define CUTSCENE_AUDIO_SET_LOOP( \ + CHANNEL, LOOPING, LOOP_COUNT, LOOP_START, LOOP_TO \ +) \ + { \ + .type = CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP, \ + .audioSetLoop = { \ + .channel = CHANNEL, \ + .looping = LOOPING, \ + .loopCount = LOOP_COUNT, \ + .loopStart = LOOP_START, \ + .loopTo = LOOP_TO \ + } \ + } + +// Combined form of CUTSCENE_AUDIO_SET_PAN + CUTSCENE_AUDIO_SET_LOOP, for +// setting both at once rather than as two separate items. +#define CUTSCENE_AUDIO_SET( \ + CHANNEL, PAN, LOOPING, LOOP_COUNT, LOOP_START, LOOP_TO \ +) \ + { \ + .type = CUTSCENE_ITEM_TYPE_AUDIO_SET, \ + .audioSet = { \ + .channel = CHANNEL, \ + .pan = PAN, \ + .looping = LOOPING, \ + .loopCount = LOOP_COUNT, \ + .loopStart = LOOP_START, \ + .loopTo = LOOP_TO \ + } \ + } diff --git a/src/dusk/rpg/cutscene/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt index 7de5c398..0f7aaed9 100755 --- a/src/dusk/rpg/cutscene/item/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/CMakeLists.txt @@ -17,3 +17,4 @@ add_subdirectory(maparea) add_subdirectory(ui) add_subdirectory(battle) add_subdirectory(save) +add_subdirectory(audio) diff --git a/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt b/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt new file mode 100644 index 00000000..2fdb6a5b --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + cutsceneaudioplay.c + cutsceneaudiostop.c + cutsceneaudiopause.c + cutsceneaudioresume.c + cutsceneaudiofade.c + cutsceneaudiofadewait.c + cutsceneaudiosetpan.c + cutsceneaudiosetloop.c + cutsceneaudioset.c +) diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c new file mode 100644 index 00000000..964cc7c0 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +void cutsceneAudioFadeStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerFadeTo( + item->audioFade.channel, + item->audioFade.from, + item->audioFade.to, + item->audioFade.duration, + item->audioFade.easing + ); +} + +bool_t cutsceneAudioFadeUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h new file mode 100644 index 00000000..7a873418 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" +#include "animation/easing.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + audiomixerchannel_t channel; + float_t from; + float_t to; + float_t duration; + easingtype_t easing; +} cutsceneaudiofade_t; + +/** + * Starts an audio fade step (starts the fade via audioMixerFadeTo() - + * doesn't wait for it; pair with CUTSCENE_AUDIO_FADE_WAIT to block on it). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioFadeStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio fade step (always completes immediately - the fade + * itself keeps running independently of cutscene progression). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioFadeUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c new file mode 100644 index 00000000..85e42b14 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +bool_t cutsceneAudioFadeWaitUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return !audioMixerIsFading(item->audioFadeWaitChannel); +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h new file mode 100644 index 00000000..918ad3a8 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** + * Updates an audio fade-wait step, completing once the watched channel's + * fade (started by a CUTSCENE_AUDIO_FADE elsewhere) finishes. Has no Start + * callback -- there's nothing to do until the fade is actually done. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true once the channel is no longer fading. + */ +bool_t cutsceneAudioFadeWaitUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c new file mode 100644 index 00000000..59738064 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +void cutsceneAudioPauseStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerPause(item->audioPauseChannel); +} + +bool_t cutsceneAudioPauseUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h new file mode 100644 index 00000000..4b283404 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** + * Starts an audio pause step (pauses the channel immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioPauseStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio pause step (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioPauseUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c new file mode 100644 index 00000000..9dd070cc --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" + +void cutsceneAudioPlayStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerQueuePlay( + item->audioPlay.file, + item->audioPlay.channel, + item->audioPlay.volume, + item->audioPlay.pan, + item->audioPlay.looping, + item->audioPlay.loopCount, + item->audioPlay.loopStart, + item->audioPlay.loopTo + ); +} + +bool_t cutsceneAudioPlayUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h new file mode 100644 index 00000000..fae99771 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixer.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + char_t file[AUDIO_PATH_MAX]; + audiomixerchannel_t channel; + float_t volume; + float_t pan; + bool_t looping; + uint8_t loopCount; + float_t loopStart; + float_t loopTo; +} cutsceneaudioplay_t; + +/** + * Starts an audio play step (queues the sound via audioMixerQueuePlay() - + * see its own comment on when it actually starts). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioPlayStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio play step (always completes immediately - the sound + * keeps loading/playing independently of cutscene progression). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioPlayUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c new file mode 100644 index 00000000..5fc2e349 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +void cutsceneAudioResumeStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerResume(item->audioResumeChannel); +} + +bool_t cutsceneAudioResumeUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h new file mode 100644 index 00000000..86ae339c --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** + * Starts an audio resume step (resumes the channel immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioResumeStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio resume step (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioResumeUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c new file mode 100644 index 00000000..83cd0ea1 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +void cutsceneAudioSetStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerSetPan(item->audioSet.channel, item->audioSet.pan); + audioMixerSetLoop( + item->audioSet.channel, + item->audioSet.looping, + item->audioSet.loopCount, + item->audioSet.loopStart, + item->audioSet.loopTo + ); +} + +bool_t cutsceneAudioSetUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h new file mode 100644 index 00000000..c33ab7c8 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + audiomixerchannel_t channel; + float_t pan; + bool_t looping; + uint8_t loopCount; + float_t loopStart; + float_t loopTo; +} cutsceneaudioset_t; + +/** + * Starts an audio set step - applies pan and looping behaviour together in + * one item (via audioMixerSetPan()/audioMixerSetLoop()), for when you want + * to change both at once rather than using CUTSCENE_AUDIO_SET_PAN/ + * CUTSCENE_AUDIO_SET_LOOP separately. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioSetStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio set step (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioSetUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c new file mode 100644 index 00000000..3a34a00d --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +void cutsceneAudioSetLoopStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerSetLoop( + item->audioSetLoop.channel, + item->audioSetLoop.looping, + item->audioSetLoop.loopCount, + item->audioSetLoop.loopStart, + item->audioSetLoop.loopTo + ); +} + +bool_t cutsceneAudioSetLoopUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h new file mode 100644 index 00000000..4ef2fd1b --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + audiomixerchannel_t channel; + bool_t looping; + uint8_t loopCount; + float_t loopStart; + float_t loopTo; +} cutsceneaudiosetloop_t; + +/** + * Starts an audio set-loop step (applies the new looping behaviour + * immediately, via audioMixerSetLoop()). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioSetLoopStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio set-loop step (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioSetLoopUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c new file mode 100644 index 00000000..ef930851 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +void cutsceneAudioSetPanStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerSetPan(item->audioSetPan.channel, item->audioSetPan.pan); +} + +bool_t cutsceneAudioSetPanUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h new file mode 100644 index 00000000..227af26d --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + audiomixerchannel_t channel; + float_t pan; +} cutsceneaudiosetpan_t; + +/** + * Starts an audio set-pan step (applies the new pan immediately, via + * audioMixerSetPan()). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioSetPanStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio set-pan step (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioSetPanUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c new file mode 100644 index 00000000..d34a488f --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/cutscene/item/cutsceneitem.h" +#include "audio/mixer/audiomixer.h" + +void cutsceneAudioStopStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + audioMixerStop(item->audioStopChannel); +} + +bool_t cutsceneAudioStopUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h new file mode 100644 index 00000000..fda1a329 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/mixer/audiomixerchannel.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +/** + * Starts an audio stop step (stops the channel immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneAudioStopStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an audio stop step (always completes immediately). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneAudioStopUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 5061c144..45476234 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -177,6 +177,50 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { [CUTSCENE_ITEM_TYPE_KEYBOARD] = { .init = cutsceneKeyboardStart, .update = cutsceneKeyboardUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_PLAY] = { + .init = cutsceneAudioPlayStart, + .update = cutsceneAudioPlayUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_STOP] = { + .init = cutsceneAudioStopStart, + .update = cutsceneAudioStopUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_PAUSE] = { + .init = cutsceneAudioPauseStart, + .update = cutsceneAudioPauseUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_RESUME] = { + .init = cutsceneAudioResumeStart, + .update = cutsceneAudioResumeUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_FADE] = { + .init = cutsceneAudioFadeStart, + .update = cutsceneAudioFadeUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT] = { + .update = cutsceneAudioFadeWaitUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN] = { + .init = cutsceneAudioSetPanStart, + .update = cutsceneAudioSetPanUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP] = { + .init = cutsceneAudioSetLoopStart, + .update = cutsceneAudioSetLoopUpdate + }, + + [CUTSCENE_ITEM_TYPE_AUDIO_SET] = { + .init = cutsceneAudioSetStart, + .update = cutsceneAudioSetUpdate } }; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 7817983b..395dfd0d 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -38,6 +38,15 @@ #include "battle/cutscenebattleforceaction.h" #include "save/cutscenesavedevicecheck.h" #include "save/cutscenesaveloadallslots.h" +#include "audio/cutsceneaudioplay.h" +#include "audio/cutsceneaudiostop.h" +#include "audio/cutsceneaudiopause.h" +#include "audio/cutsceneaudioresume.h" +#include "audio/cutsceneaudiofade.h" +#include "audio/cutsceneaudiofadewait.h" +#include "audio/cutsceneaudiosetpan.h" +#include "audio/cutsceneaudiosetloop.h" +#include "audio/cutsceneaudioset.h" typedef struct cutscene_s cutscene_t; @@ -78,6 +87,15 @@ typedef enum { CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, CUTSCENE_ITEM_TYPE_KEYBOARD, + CUTSCENE_ITEM_TYPE_AUDIO_PLAY, + CUTSCENE_ITEM_TYPE_AUDIO_STOP, + CUTSCENE_ITEM_TYPE_AUDIO_PAUSE, + CUTSCENE_ITEM_TYPE_AUDIO_RESUME, + CUTSCENE_ITEM_TYPE_AUDIO_FADE, + CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT, + CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN, + CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP, + CUTSCENE_ITEM_TYPE_AUDIO_SET, CUTSCENE_ITEM_TYPE_COUNT } cutsceneitemtype_t; @@ -118,6 +136,15 @@ struct cutsceneitem_s { cutscenesavedevicecheck_t saveDeviceCheck; cutscenesaveloadallslots_t saveLoadAllSlots; cutscenekeyboard_t keyboard; + cutsceneaudioplay_t audioPlay; + audiomixerchannel_t audioStopChannel; + audiomixerchannel_t audioPauseChannel; + audiomixerchannel_t audioResumeChannel; + cutsceneaudiofade_t audioFade; + audiomixerchannel_t audioFadeWaitChannel; + cutsceneaudiosetpan_t audioSetPan; + cutsceneaudiosetloop_t audioSetLoop; + cutsceneaudioset_t audioSet; }; }; diff --git a/src/duskpsp/audio/audiostreampsp.c b/src/duskpsp/audio/audiostreampsp.c index 4ec781a3..7994bff0 100644 --- a/src/duskpsp/audio/audiostreampsp.c +++ b/src/duskpsp/audio/audiostreampsp.c @@ -330,6 +330,21 @@ void audioStreamPSPThreadFeed(thread_t *thread) { } if(stopRequested) break; + // Paused: skip consuming from the ring and skip + // sceAudioOutputPannedBlocking() entirely - that call is the only + // thing keeping the PSP's own tiny hardware buffer fed, so simply + // not calling it silences output within about one hardware chunk + // (~23ms) instead of waiting for this thread's own much larger + // software ring (AUDIO_PSP_RING_FRAMES, ~1.1s) to drain naturally. + // The ring is left completely untouched, so resuming + // (audioStreamPlay()) continues exactly where playback left off, + // with no skip or gap. See audiostream.h's own comment on why + // reading stream->state from this thread is safe. + if(!(stream->state & AUDIO_STREAM_STATE_PLAYING)) { + sceKernelDelayThread(AUDIO_PSP_IDLE_POLL_MICROS); + continue; + } + // Production has stopped and what's left fits in one chunk (0 up to // AUDIO_PSP_CHUNK_FRAMES - it can never be more, since production // never adds more once readReachedEnd/readFailed is set) - this is diff --git a/tools/asset/cutscene/__main__.py b/tools/asset/cutscene/__main__.py index 06a57f32..53e7931e 100644 --- a/tools/asset/cutscene/__main__.py +++ b/tools/asset/cutscene/__main__.py @@ -93,6 +93,15 @@ ITEM_TYPE = { 'SCENE': 31, 'SAVE_DEVICE_CHECK': 32, 'SAVE_LOAD_ALL_SLOTS': 33, + 'AUDIO_PLAY': 35, + 'AUDIO_STOP': 36, + 'AUDIO_PAUSE': 37, + 'AUDIO_RESUME': 38, + 'AUDIO_FADE': 39, + 'AUDIO_FADE_WAIT': 40, + 'AUDIO_SET_PAN': 41, + 'AUDIO_SET_LOOP': 42, + 'AUDIO_SET': 43, } PAUSE_FLAG = {'NPC': 1 << 0, 'PLAYER': 1 << 1, 'WORLD': 1 << 2, 'BATTLE': 1 << 3} @@ -122,6 +131,10 @@ BATTLE_STATE = { 'AI_SELECTION': 4, 'MOVES_EXECUTING': 5, 'POST_ROUND': 6, 'ENDED': 7, } SCENE_TYPE = {'NULL': 0, 'INITIAL': 1, 'MAIN_MENU': 2, 'OVERWORLD': 3, 'BATTLE': 4} +AUDIO_CHANNEL = { + 'BGM_0': 0, 'VOICE_0': 1, 'VOICE_1': 2, + 'SFX_0': 3, 'SFX_1': 4, 'SFX_2': 5, 'SFX_3': 6, +} CUTSCENE_TEXT_MAX_CHARS = 256 CUTSCENE_TEXT_MINI_MAX_CHARS = 128 @@ -131,6 +144,7 @@ CUTSCENE_MODAL_MESSAGE_MAX_CHARS = 256 CUTSCENE_MODAL_OPTIONS_MARKERS_MAX = 2 CUTSCENE_MAP_AREA_WAIT_MAX = 4 CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX = 4 +AUDIO_PATH_MAX = 256 class Pool: @@ -331,6 +345,45 @@ def encode_item(item, pool): failure_offset = pool.add_string(item['failureMarker']) buf += struct.pack('