Table base + record layout

FieldValue
Base addressDAT_8006F198 (file offset 0x5F998 in SCUS_942.54)
Index formDAT_8006F198 + sound_id*8
Stride0x8 bytes
Entry count100 descriptors (sound ids 0x00..=0x63)

The runtime readers gate on sound_id < 0x200, but that is an upper bound, not the size: only ids 0x00..=0x63 are real descriptors (every one populated - voice count 1..=3, trailing bytes zero). Id 0x64 onward is unrelated rodata, starting with the \PSX.EXE dev-path string, so the table's true extent is 100 entries.

OffsetNameField
+0pprogram / VAG index - selects the loaded bank's program-attr entry
+1ttone / ADSR-region base; a multi-voice cue uses consecutive regions (+i per voice)
+2lnote-level voice attribute (MIDI-ish, clusters near 60)
+3nlow 5 bits = voice count; bit 0x20 = sustained / continuous mode
+4idcategory / mixer-channel index (column in the channel-volume tables DAT_80091510 / DAT_80091513)
+5..7-no observed runtime reader (zero across the whole table)

The field names are the designer's own, recovered from the runtime debug format string "setbl p:%d t:%d l:%d n:%d id:%d".

Consumers

Two functions read the table, both indexing &DAT_8006F198 + id*8:

FunctionRole
FUN_800250D4(sound_id, voice)The per-actor SFX trigger (from the actor tick FUN_80021DF4). Uses only the voice count (n & 0x1F), SpuKeyOn-ing (FUN_800653C8) that many consecutive voices.
FUN_80016B6CThe per-frame SFX cue-ring drainer. Walks the 4-entry ring DAT_8007B6D8 (the same ring FUN_8004FCC8 and the move-power +0x0d sound cues write into), then programs voice_count voices via FUN_80065034 - the libsnd SpuSetVoiceAttr analogue that takes program (+0), note/region (+1 +i), attr (+2), and the channel volume picked by category (+4).

The SPU programming itself (FUN_80065034SpuSetVoiceAttr) is libsnd and out of clean-room scope - the engine has its own SPU. What is portable is the static data.

Program bank - the active scene's music VAB

The descriptors' program / tone fields index a VAB, and that VAB is not a dedicated SFX master - it is the per-scene scene-VAB-stream bank the BGM sequencer has open. FUN_80065034 reads the libsnd "current bank" globals: _DAT_801ce33c (VAB-header base), _DAT_801ce334 (ProgAtr at +0x20), _DAT_801ce340 (VagAtr at +0x820) - so a sound effect plays through the low programs of the same bank the music does.

Pinned from the save-state catalogue:

  • The bank varies per scene - across catalogued captures the open bank is 13 distinct VABs (used-program counts ranging 1..=16).
  • For a music_01-scene state the live bank is byte-identical to the disc music_01 VAB (PROT 1004 at offset +4): the VabHdr and every program's ProgAtr attribute bytes (+0..7) match exactly; only the PsyQ reserved per-program pointer field (ProgAtr +8..15) is runtime-patched to the RAM VagAtr address.

Because scene banks differ in size, a cue resolves only where its program / tone exists - SFX availability is scene-dependent, not a guaranteed reservation. The engine therefore needs no separate SFX bank load: SfxBank::from_descriptors(...) plays through the scene's already-loaded BGM VabBank via SfxBank::play_one_shot(spu, vab).

Provenance

Decoded directly from the disc, and cross-checked byte-for-byte against live save-state RAM: the table window at 0x8006F198 read out of a catalogued mednafen state's main RAM parses to the identical 100 descriptors as the disc SCUS_942.54, confirming the table is static rodata and the parser offset is right. The two cue ids the engine's default SFX bank already references resolve to 0x1A = program 3 / note 67 and 0x4C = program 3 / tone 8 (voice count 2).

Parser

legaia_asset::sfx_table::SfxTable::from_scus resolves the table from a SCUS_942.54 image (PSX-EXE t_addr → file-offset map, identical to the item-name table resolver); from_table_bytes parses a raw window straight out of save-state RAM. SfxDescriptor exposes the decoded fields plus voice_count() / sustained() / is_active(). The disc-gated sfx_table_real test pins the layout against the real executable, and sfx_table_live validates the parse against live RAM and feeds the descriptors into legaia_engine_audio::SfxBank::from_descriptors - the data path the engine uses to fire cues through the SPU.

See also