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:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('<HH', success_offset, failure_offset)
|
||||
|
||||
elif item_type == 'AUDIO_PLAY':
|
||||
write_string_field(buf, item['file'], AUDIO_PATH_MAX)
|
||||
buf += struct.pack('<B', AUDIO_CHANNEL[item['channel'].upper()])
|
||||
buf += struct.pack('<f', float(item.get('volume', 1.0)))
|
||||
buf += struct.pack('<f', float(item.get('pan', 0.0)))
|
||||
buf += struct.pack('<B', 1 if item.get('looping', False) else 0)
|
||||
buf += struct.pack('<B', int(item.get('loopCount', 0)))
|
||||
buf += struct.pack('<f', float(item.get('loopStart', -1.0)))
|
||||
buf += struct.pack('<f', float(item.get('loopTo', 0.0)))
|
||||
|
||||
elif item_type in ('AUDIO_STOP', 'AUDIO_PAUSE', 'AUDIO_RESUME', 'AUDIO_FADE_WAIT'):
|
||||
buf += struct.pack('<B', AUDIO_CHANNEL[item['channel'].upper()])
|
||||
|
||||
elif item_type == 'AUDIO_FADE':
|
||||
buf += struct.pack('<B', AUDIO_CHANNEL[item['channel'].upper()])
|
||||
buf += struct.pack('<f', float(item['from']))
|
||||
buf += struct.pack('<f', float(item['to']))
|
||||
buf += struct.pack('<f', float(item['duration']))
|
||||
buf += struct.pack('<B', EASING[item.get('easing', 'LINEAR').upper()])
|
||||
|
||||
elif item_type == 'AUDIO_SET_PAN':
|
||||
buf += struct.pack('<B', AUDIO_CHANNEL[item['channel'].upper()])
|
||||
buf += struct.pack('<f', float(item['pan']))
|
||||
|
||||
elif item_type == 'AUDIO_SET_LOOP':
|
||||
buf += struct.pack('<B', AUDIO_CHANNEL[item['channel'].upper()])
|
||||
buf += struct.pack('<B', 1 if item.get('looping', True) else 0)
|
||||
buf += struct.pack('<B', int(item.get('loopCount', 0)))
|
||||
buf += struct.pack('<f', float(item.get('loopStart', -1.0)))
|
||||
buf += struct.pack('<f', float(item.get('loopTo', 0.0)))
|
||||
|
||||
elif item_type == 'AUDIO_SET':
|
||||
buf += struct.pack('<B', AUDIO_CHANNEL[item['channel'].upper()])
|
||||
buf += struct.pack('<f', float(item['pan']))
|
||||
buf += struct.pack('<B', 1 if item.get('looping', True) else 0)
|
||||
buf += struct.pack('<B', int(item.get('loopCount', 0)))
|
||||
buf += struct.pack('<f', float(item.get('loopStart', -1.0)))
|
||||
buf += struct.pack('<f', float(item.get('loopTo', 0.0)))
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unhandled cutscene item type: {item_type}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user