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 <[email protected]>
This commit is contained in:
2026-09-02 17:44:06 -05:00
co-authored by Claude Sonnet 5
parent 9c28d5a3b7
commit 9364768397
31 changed files with 1183 additions and 3 deletions
@@ -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);
+71
View File
@@ -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) {
+82
View File
@@ -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.
*/
+49
View File
@@ -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.
+55 -1
View File
@@ -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
+5 -2
View File
@@ -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
+111
View File
@@ -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 \
} \
}
@@ -17,3 +17,4 @@ add_subdirectory(maparea)
add_subdirectory(ui)
add_subdirectory(battle)
add_subdirectory(save)
add_subdirectory(audio)
@@ -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
)
@@ -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;
}
@@ -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
);
@@ -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);
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
@@ -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;
}
@@ -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
);
+44
View File
@@ -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
}
};
+27
View File
@@ -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;
};
};
+15
View File
@@ -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