At a glance

In the game
Every UI, field-script and battle sound effect that is not a voice clip or music
Where
SCUS_942.54 rodata at RAM 0x8006F198 (file offset 0x5F998); no magic
Size / stride
100 rows × 8 bytes, ids 0x00..=0x63; ids ≥ 0x200 come from the current sound bundle instead
Parser
legaia_asset::sfx_table; CLI asset sfx-table <SCUS> [--json]; engine legaia_engine_audio::SfxBank + sfx_ring
Confidence
Confirmed - disc table parses identically to live RAM; bank routing pinned by the initialiser's own writes and byte matches against the extracted VABs (how we know)
Used by
audio stack, move-power cue path, field menu, media browser cue player

Record layout

A cue id indexes the table at an 8-byte stride. The field names are the designer's own, recovered from a runtime debug format string (p / t / l / n / id).

OffsetSizeFieldMeaning
+01p programProgram (instrument) index in the bank
+11t toneTone index within the program; a multi-voice cue uses consecutive tones (+i per voice). Named by index, never resolved by key range
+21l noteMIDI-style note (clusters near 60)
+31n voicesLow 5 bits = voice count (1..=3 in retail); bit 0x20 = sustained mode
+41id categorySelects a 12-byte mixer record, and through it the VAB slot the cue keys (below)
+5..73-Zero; no reader
cue id 0x00..0x63 8-byte descriptor byte +4 = category mixer record 0x80091508 + 12n VAB slot slot = category PROT bank 0868 / 0869 / ...
The category byte is a bank selector: the sound initialiser writes slot = record index into every mixer record, so a cue always names its own bank.

Which bank a cue plays from

Four fixed banks are open at once and each category keys exactly one of them. The other open slots hold variable banks (scene music, side-band, monster sounds) that no descriptor keys - "the SFX bank is the scene's music VAB" is true only of slot 1.

Category / slotDescriptorsBankTypical cuesLoaded by
016System bank, PROT 0868 (resident)UI: confirm 0x20, cursor 0x21, buzz 0x23, cancel 0x37boot
253Class-2 bank, PROT 0869 (0875 when the battle-data selector is 4)Battle / duel: 0x09, 0x4Cbattle loader, Baka Fighter init
630Field bank, PROT 0876Field-script cues 0x2E, 0x2F; player setfield init
111Reward bank, PROT 0889The single cue 0x50battle-reward resolution
10Scene BGM bank (music_01, varies)-BGM loader
30Side-band vab_01 bank (script-selected)-BGM loader
7 / 80The battle's two monster.snd banks-battle loader

Raw TOC indices run two above extraction indices (CDNAME numbering); CDNAME labels read battle_data for 0868 and monster_data for 0869 - a label is a hint, the byte match is the verdict.

Slots share sound RAM in pairs

The field bank (slot 6) and the class-2 battle bank (slot 2) occupy the same physical SPU region, refilled per mode, so they are never resident together. Field modes open slots 0, 1, 3, 6; battle opens 0, 1, 2, 7. The battle mode init closes slot 6 and clears the field-bank latch 0x8007BAFC (so do the minigame warp and field-VM op 0x36 sub 3); the next field init sees the latch clear and reloads PROT 0876. A cue whose slot is closed is skipped, not rerouted.

Slot pairSPU baseRegion sizeBank bodies
0 / 100x101061 440PROT 0868 - 59 136
1 / 50x10010143 360current BGM bank
2 / 60x33010184 320PROT 0869 - 188 128 / PROT 0876 - 174 192
30x6001020 480side-band bank
4 / 70x6501030 720monster.snd A
80x6C81010 240monster.snd B
110x6F01069 616PROT 0889 - 19 344

The sizes are allocation, not enforcement: PROT 0869 overruns the region below slot 3 by 3 808 bytes whenever that slot is closed.

Firing a cue

Two readers consume the table: the per-actor trigger (used by the actor tick) and the per-frame cue ring drainer. The SPU programming under them is PlayStation library code; what is portable is the data and the ring.

The ring: four scheduled one-shots

  • Four slots, each an i16 cue id plus a u32 countdown in vsyncs. A producer writes the id with timer 0 into a round-robin cursor slot; a fifth pending cue replaces one.
  • An ager decrements every timer by the adaptive frame step and floors at zero; a slot at zero with no id becomes -1.
  • The drainer plays a slot only when its timer is exactly zero and its id is still ≥ 0. A cue armed with timer N plays on the frame the countdown first reads zero.
  • The ring value is the descriptor index; overlays that write the ring directly write ids as-is. The move-power cue path stores id - 1 for id < 0x40.

Voice allocation

ModeVoicesRule
One-shot (n & 0x20 == 0)23 - cursor, descendingRolling cursor over a limit of 3 (1 in game modes 3 and 0x17); each voice is stopped before reprogramming
Sustained (n & 0x20)7 .. 7 + count - 1, ascendingThe previous run is released first; the mixer record latches for the held run

The mixer record's enable byte (+0xB) skips a cue before any voice work; while a global override is set every cue is forced onto channel 6.

What a port has to get right

  • Tone by index. The fire path hands the descriptor's tone index straight to the voice programmer. Several retail cues carry a note outside their tone's key window (menu cancel 0x37: tone 5, note 64, window [65,65]), so a key-range lookup renders silence. Engine: VabBank::play_tone beside the sequencer's play_note.
  • UI cues are pitched down on purpose. Program 0 of the UI banks is a one-VAG-per-semitone key map whose center bytes sit 12..26 semitones above the descriptors' notes. Pitch is 0x1000 × 2^((note - center + fine/128)/12) with no sample-rate factor; a port that also multiplies by 22050/44100 drops another octave.
  • The UI ids are global. Confirm / cursor / buzz / cancel are written by the SCUS list kernel that pages every pause-menu window; the Baka Fighter overlay is a co-user, not their source.
  • There is no footstep sound. A probe over every producer, the trigger, the voice programmer and the ring sees nothing fire while walking; the only walk cues (0x2E, 0x2F) come from field-script triggers. A port that wants a footstep has to author one.
  • Stage the right bank. A single-bank port fails quietly: a category-0 id fired through the class-2 bank resolves to a sibling sample - a genuine retail blip, twice as long and a fifth lower.
SPU budget: both pinned banks in one region

The engine stages slot 0 plus the current mode's slot-2 / slot-6 bank together; the BGM region is what remains of the console's 512 KiB of sound RAM.

RegionBytes
PROT 0868 VAG bodies59 136
PROT 0869 VAG bodies188 128
Reserved SFX region (SFX_BANK_SPU_BYTES)249 856 (0x3D000)
BGM region (512 KiB − 0x1000 scratch − the above)270 336
Largest scene BGM VAB staged269 632

One step larger silences music that plays; one step smaller does not fit slot 0 plus PROT 0869. Slot 6 is slot 2's region in retail, and both play hosts refill it per mode the way retail's inits do: PROT 0876 (the field bank) in the field and on the world map, PROT 0869 in battle, a minigame's own bank in its mode. A cue whose slot is closed is silent, as retail's drainer skips it.

Ids ≥ 0x200: the current bundle's record 0
id <  0x200:  desc = table + id*8                       ; this page
id >= 0x200:  desc = bundle + offsets[0] + (id - 0x200)*8 ; current sound bundle, record 0

Both readers resolve the high arm through the sound subsystem's current-bundle pointer, and it has two occupants. In the field the loader repoints it at the scene's prescript bundle on every scene load, so the high bank is the scene's own record 0, sized per scene (jou reserves 96 rows and fills 40; rugi carries 21). In battle it is bse.dat (sound-driver formats) - loaded at battle-scene setup, not at boot: its one caller anywhere on the disc is inside battle init FUN_800513F0. This page previously said "at init".

One column is not static in battle. Before enqueueing a cue the battle cue router FUN_8004FE5C overwrites byte +4 (id, the category) of record[cue_id - 0x200] from a per-actor byte, reaching the bank through its own saved pointer gp+0x678 rather than through the current-bundle slot. So a live cue's VAB slot is chosen by the actor that fired it, and the authored category is a default.

History: the single-bank port

Both viewers and the engine once staged one resident bank and fired every cue through it; peak, duration and "did a voice key on" all passed while the pause menu thumped. Slots 6 and 11 were listed as unpinned before PROT 0876 / 0889 were byte-matched. See do-not-re-walk.

How we know

FunctionAddressWhat it provesDump
Per-actor triggerFUN_800250D4 (0x800250F4..14C)8-byte stride at DAT_8006F198; < 0x200 gate; voice-count key-onfuncs/800250d4.txt
Cue-ring drainerFUN_80016B6CProgram / tone +i / note / channel handed to the voice programmer FUN_80065034; descend-from-23 / ascend-from-7 voice rulesfuncs/80016b6c.txt
Ring ager / producerFUN_8001698C, FUN_80035B50Timer floor at zero; round-robin cursor gp+0x158; ids DAT_8007B6D8[4], timers DAT_8007C338[4]funcs/80035b50.txt
Sound initialiserFUN_8001D424Writes +8 = record index into all 16 mixer records at 0x80091508: category = slotfuncs/8001d424.txt
Bank open gateFUN_80068B98Rejects slot ≥ 0x10 or a closed bank (_DAT_801CE368[id] != 1); repoints the libsnd current-bank globalsfuncs/80068b98.txt
Bank loadersFUN_8001FC00 + FUN_8001E54C; callers FUN_800520F0, FUN_801CF00C, FUN_801D6704, FUN_8004E568, FUN_800243F0Which PROT entry reaches which slot; SPU bases from FUN_800265E8per-function dumps
Byte matchesPROT 0868 / 0876 / 0889Live slot-0 VagAtr page occurs verbatim at 0868 +4; 0876 holds 30 VAGs for 30 category-6 cues; 0889 populates only program 10, which cue 0x50 namessfx_table_real, sfx_table_live
Footstep probeFUN_80018DB0 + producersStanding vs walking for the same vsyncs: no voice programfuncs/80018db0.txt

Parser: SfxTable::from_scus resolves the table from a SCUS_942.54 image; from_table_bytes parses a RAM window. The routing law lives beside it (slot_for_category, prot_index_for_slot, spu_base_for_slot, SLOT_ALIASES). Source of record: docs/formats/sfx-table.md.

See also