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:
@@ -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