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_game_tables::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).

FieldMeaning
p programProgram (instrument) index in the bank
t toneTone index within the program; a multi-voice cue uses consecutive tones (+i per voice). Named by index, never resolved by key range
l noteMIDI-style note (clusters near 60)
n voicesLow 5 bits = voice count (1..=3 in retail); bit 0x20 = sustained mode
id categorySelects a 12-byte mixer record, and through it the VAB slot the cue keys (below)
+5..7Zero across the table; no reader

Row 0x64 onward is unrelated rodata (a dev-path string), so the readers' < 0x200 gate is a bound, not the table size. The drainer still indexes the table for every id below 0x200, so an id past row 0x63 keys whatever the bytes after the table read, and about half of those rows carry a voice count. No shipped producer is known to push such an id; the port keys nothing for one.

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 Terra stands on party seat 1)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.
  • Who writes it in the field: field-VM op 0x36 (sub 0 pushes a cue id, sub 4 sets its delay in vsyncs - scripts pair them) and the motion VM's op 0x09. Both pass the id straight from bytecode, static ids and per-scene ids ≥ 0x200 alike.
  • 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.

The side-band bank a script selects

Category 3 keys the side-band slot, and field-VM op 0x36 sub 1 picks which bank fills it: a request id 2000..=2999 streams the vab_01 block's entry id - 2000 into slot 3, ≥ 3000 streams id - 3000 into slot 6, and 0x1000 parks the request without loading anything. Shipped scripts use only those three forms.

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
  • A one-shot keeps only a voice's last pass. The stop before each reprogram raises the voice's key-off bit and clears its pending key-on, and nothing reaches the chip until the flush. So a voice the wrapping cursor visits twice in one cue keeps only its last pass: that pass's tone, or nothing but the stop when the tone does not resolve. Under the field's limit of 1 a four-voice row lands on voices 23 and 22 twice each, and a jouina row whose tone runs past its program's tones from the third voice on keys nothing at all in retail. The port resolves every pass first and keys afterwards (SfxBank::play_entry).
  • A zero-voice sustained cue. The held-count write sits inside the key-on loop, so a sustained cue with no voices releases the old run but leaves the held count unchanged; SfxBank::play_descriptor does the same for a runtime row.

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.
  • The dialog box cues its picker, not its text. The dialog pager FUN_801D84D0 (field overlay, PROT 0897) reaches the ring only from the option picker's cursor handler: cue 0x21 when an Up / Down press actually moved the cursor (a press clamped at either end is silent), cue 0x20 on the confirm press. The typewriter, page turns and dismissing a box are silent - there is no text blip to port. Engine: legaia_engine_dialog::dialog::{PICKER_MOVE_CUE, PICKER_CONFIRM_CUE}, pushed through World::push_sfx_cue by the three runners that drive a picker.
  • A door has no cue of its own. None of the field overlay's producer calls sits in the warp-tile arm, the walk-on trigger dispatch or the scene-change routine FUN_8001FD44, and the overlay never stores into the ring directly. A door that sounds does so because its own record carries a field-VM op 0x36 ahead of the 0x3F - a script literal.
  • 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

A port that stages slot 0 plus the current mode's slot-2 / slot-6 bank together has this much sound RAM left for music out of the console's 512 KiB:

RegionBytes
PROT 0868 VAG bodies59 136
PROT 0869 VAG bodies188 128
Reserved SFX region249 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, refilled per mode: 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.

One bank does not fit that window: the dance hall's (PROT 1231). Retail overruns into slot 3's closed base; the port has slot 0 below and the top of SPU RAM above, so it lays the overflow in the BGM region's free tail instead (a voice addresses each sample on its own, so a bank need not be contiguous). That half is a borrower: a track that reaches it drops the bank, and the director re-stages it once the free tail moves.

Ids ≥ 0x200: the current bundle's record 0
Cue idDescriptor address
< 0x200table + id*8 - this page
≥ 0x200bundle + offsets[0] + (id - 0x200)*8 - the current sound bundle's 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.

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.

How we know

Provenance by function
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 namesdisc + save-state comparison
Footstep probeFUN_80018DB0 + producersStanding vs walking for the same vsyncs: no voice programfuncs/80018db0.txt

In the port: 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