Audio data (sound list, banks, BFW.SND, MUSIC.SND)¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4) and the NTSC-U disc. The
disc checks (2026-10-06) were made with coney-tools audio and throwaway scripts outside the repository and report
counts only. How the game plays this data is on Sound.
Purpose¶
Where every sound, piece of music and line of speech lives on the disc, how a sound's name leads to its bytes, and
how the bytes decode. In one paragraph: a sound is found by the CRC-32 of its name in the sound list (chunk
0x29 of warriors.glr); its record gives a size, an offset, a sample-rate index and a sound class (chunk
0x48) whose flags say whether it streams from IOP/BFW.SND or plays from a bank (<name>.msb with its index
<name>.msd in the WAD) loaded into sound RAM. Music is separate: the music list (chunk 0x31) points into
IOP/MUSIC.SND. Everything is headerless PS2 SPU2 ADPCM; stereo data is two mono streams interleaved in blocks.
Cutscene movies (PSS/*.BIK) carry their own Bink audio and are not covered here.
Original structure¶
The game drives Sony's SCEE MultiStream library (its SOUND_* error strings, 0x0054b708-0x0054d4b8; the
IOP half is MODULES/IOP.IRX, File I/O). Public knowledge of that library and of
the SPU2 is cited as such; everything else is from this executable.
| Address | Name (ours) | Role | Evidence |
|---|---|---|---|
0x0010f320 / 0x0010f900 |
SoundList_Load |
pops chunk 0x29, makes one 4-byte entry per record (a pointer to it) |
confirmed (code) |
0x001119d0 |
SoundList_Find |
binary search of the entries by the record's +0x08 |
confirmed (code) |
0x0010f360 / 0x0010f9d8 |
MusicList_Load |
pops chunk 0x31; per 104-byte record a 16-byte track {record, crc32(name), 1, 2000} |
confirmed (code) |
0x0010f3a0 |
SoundClasses_Load |
pops chunk 0x48; the class table pointer 0x00598670 |
confirmed (code) |
0x0010f3d8 / 0x0010f988 |
StereoTable_Load |
pops chunk 0x49; count 0x00598678, records 0x00598674 |
confirmed (code) |
0x0010efe0-0x0010f2f0 |
sound record accessors | the fields below, one function each | confirmed (code) |
0x0014c620 |
AudioDevice_LoadBank(name, partial) |
reads <name>.msb into sound RAM and <name>.msd into IOP memory |
confirmed (code) |
0x0014caf8 |
AudioDevice_Start(task) |
starts a sound: a stream from BFW.SND or a bank sample by hash |
confirmed (code) |
0x0014d528 |
AudioDevice_OpenMusic |
device slot +0x20: opens MUSIC.SND on a stream pair (IOP command 0x1f) and sets the track's range (0x29) |
confirmed (code) |
0x0014d590 |
AudioDevice_PrepareMusic |
slot +0x128: sizes the buffers, plays the parent stream muted, links the child, sets the MIB end offset, preloads (0x2f) |
confirmed (code) |
0x0014d738 |
AudioDevice_StartMusic |
slot +0x130: starts the music (command 0x2e); details on Sound |
confirmed (code) |
0x00150078 |
RateToPitch(rate) |
rate × 4096 / 48000, the SPU2 pitch word |
confirmed (code) |
0x0014d2d8 |
AudioDevice_Duration(size, rate) |
⌊⌊size × 3.5⌋ / (2 × rate)⌋ × 1000 ms: whole seconds only |
confirmed (code) |
Data¶
PS2 ADPCM (all sound data)¶
Public SPU2 knowledge: 16-byte frames of 28 samples. Byte 0 holds the shift (low nibble) and the predictor (high
nibble, filters (0,0) (60,0) (115,-52) (98,-55) (122,-60) / 64); byte 1 holds the flags (bit 0 end, bit 1 repeat,
bit 2 loop start); 14 bytes of signed 4-bit samples follow, low nibble first. A sample is
(nibble << 12 >> shift) + (s1 × f0 + s2 × f1 + 32) >> 6, clamped to 16 bits. No file has a VAG header.
Corroboration: of 855,701 frames in 300 random streamed sounds of BFW.SND, none has a predictor above 4 or a
shift above 12; the decoded sounds end in silence (the last 5 % at a median 1 % of the sound's RMS); music and
stereo sounds decode to channels that correlate at 0.87-1.0, against 0.0-0.25 when split at a wrong interleave.
The sound list (chunk 0x29)¶
u32 count (25,395), then count records of 16 bytes, sorted by hash (the lookup is a binary search):
| Offset | Type | Meaning | Evidence |
|---|---|---|---|
+0x00 |
u32 | size in bytes of ADPCM (a stereo sound's is a placeholder, 64; see stereo) | confirmed (code) at 0x0010f258, 0x0014caf8 |
+0x04 |
u32 | offset: in BFW.SND for a streamed sound, in its bank for a bank sound |
confirmed (code) at 0x0010f268; bank offsets from the data |
+0x08 |
u32 | CRC-32 of the name as written (vags/character/voices/5/attack_01; hash) |
confirmed (code) at 0x0010f080 |
+0x0c |
u8 | pitch variation in percent: each play picks a factor in 1 ± v / 100 |
confirmed (code) at 0x0014caf8 |
+0x0d |
u8 | volume in percent (0-250; 100 as recorded) | confirmed (code) at 0x0010f188 |
+0x0e |
u8 | sample-rate index into the table at 0x0050a9e0 |
confirmed (code) at 0x0010f238 |
+0x0f |
u8 | sound class, an index into chunk 0x48 |
confirmed (code) at 0x0010f2a0 |
The rate table (0x0050a9e0, 73 u16): index i is 750 × i Hz for the steps of 750, with 8000, 11025,
16000, 22050, 44100 and 48000 slotted in (index 11 = 8000, 17 = 11025, 25 = 16000, 34 = 22050, 36 = 22500,
65 = 44100, 71 = 48000; index 72 is 9999, unused). Records use 27 indexes; 36 (22,500 Hz) has 17,975 records and 34
(22,050 Hz) 4,331.
Sound classes (chunk 0x48)¶
246 records of 5 bytes with no count (1,232 bytes with padding), indexed by the record's +0x0f:
| Byte | Meaning | Evidence |
|---|---|---|
| 0 | near distance, m: full volume within it | confirmed (code) at 0x0010f1b0, 0x0011a170 |
| 1 | far distance ÷ 5: silent beyond 5 × b m |
confirmed (code) at 0x0010f1e8, 0x0011a170 |
| 2 | flags, below | confirmed (code) |
| 3 | priority, lower is more important (2-21) | confirmed (code) at 0x001120c8, 0x00112700 |
| 4 | channels, 1 or 2 (only class 224 is 2) | confirmed (code) at 0x0010f2a0 |
| Flag | Meaning | Evidence |
|---|---|---|
0x01 |
loops until stopped | confirmed (code) at 0x0010f2c8 (a stream's "once" flag is its inverse) |
0x02, 0x08 |
positional (3D): either bit | confirmed (code) at 0x0010f018 |
0x04 |
streamed from BFW.SND; clear: played from a bank in sound RAM |
confirmed (code) at 0x0010f2f0; the data split exactly so (below) |
0x10 |
directional: louder in front of the source (character voices) | confirmed (code) at 0x0010efe0, 0x0011a170 |
0x20 |
streams on the reserved channels 10-12 instead of 5-9 (the small_loops ambience) |
confirmed (code) at 0x0010f050, 0x00112560 |
0x40 |
no reader found (set on four classes, voices among them) | inferred |
Interface sounds have near = far = 0 and no positional bit; speech (vags/speeches/...) is 0x06 with priority 4;
character voice lines are 0x1c or 0x4c.
The stereo table (chunk 0x49)¶
u32 count (248), 12 bytes of zeros, then count records {u32 hash, u32 interleave, u32 lastBlock, u32 blocks}
(0x0010f098, 0x0010f138, 0x0010f0e8; confirmed (code)). Every one of the 248 is a class-224 (stereo) sound of
the list. In BFW.SND a stereo sound at the record's offset is blocks blocks of interleave bytes (always
0x8000) of the left channel then interleave of the right; the game asks for 2 × interleave × blocks bytes and
ends the stream lastBlock bytes into the last block (SOUND_SetMIBEndOffset, 0x00150660). These are 32,250 Hz
music-like beds.
Banks: .msb and .msd in the WAD¶
A bank is <name>.msb (ADPCM samples back to back) and <name>.msd ({u32 hash, u32 offset} pairs into the
.msb, sorted by offset, ended by a zero pair). Every one of the 2,675 hashes in the 21 banks is in the sound list,
and the list's record gives the sample's size (the .msd spans match it exactly), rate and class; its offset is the
bank offset. A bank sample is an ordinary one-shot sample: flags 0, ending with a frame flagged 1 and a silent frame
flagged 7. The 21 banks: sound (319 sounds, the default in levels), menu (14, the front end), pause (4),
armies (261), gallery, level3, level81, spookorama, birdie, diego, lizzie, luther (188-311 each),
one with no recovered name (205), and the load-screen banks load_00 ... load_06 and armload (2 each: the
left and right halves of a load-screen sound). AudioDevice_LoadBank builds ./ee_files/%s.msb and %s.msd
(0x0054b6a0, 0x0054b6b8), finds them in the WAD index, streams the .msb into sound RAM (stream 0) and the
.msd to the IOP, then registers the index (0x00151dc0). Confirmed (code); the load_NN names were found by
hashing the format %s_%2.2d (0x00547000) the load screen uses.
The same samples also exist in BFW.SND (a bank sample's bytes were found there at other offsets), but the game
plays bank sounds only from the bank: the start call passes the hash, not an offset (0x0014e370).
IOP/BFW.SND (1,402,861,568 bytes)¶
Raw ADPCM, every streamed sound of the list at its record's offset: 22,472 mono and 248 stereo. All 22,720 start on
2,048-byte boundaries (sectors) and carry no end flags: the game stops a stream at its size. The file is opened by
the IOP as cdrom0:\IOP\BFW.SND;1 (0x0054b650) on streams 1-12 (0x0014bbd8). Confirmed (code) and corroborated by the
decode checks above.
The music list (chunk 0x31) and IOP/MUSIC.SND¶
u32 count (345), then 104-byte records:
| Offset | Type | Meaning | Evidence |
|---|---|---|---|
+0x00 |
f32 | volume, 1.0 on the disc; SndCfgMusicInfo overwrites it |
confirmed (code) at 0x0010d870, 0x0010e558 |
+0x04 |
u32 | sample rate: 30,000 (293 tracks), 44,100 (38) or 32,250 (14) | confirmed (code) at 0x0014d590 |
+0x08, +0x0c |
u32 | 64 and 32 in every record; no reader found | inferred |
+0x10 |
u32 | offset in MUSIC.SND (2,048-aligned) |
confirmed (code) at 0x0014d528 |
+0x14 |
u32 | channels, 2 in every record | confirmed (code) |
+0x18 |
u32 | interleave: bytes per channel per block (0x6d80-0x8000) |
confirmed (code) at 0x0014d590 |
+0x1c |
u32 | blocks | confirmed (code) at 0x0014d528 |
+0x20 |
u32 | bytes of each channel in the last block (SOUND_SetMIBEndOffset) |
confirmed (code) at 0x0014d590 |
+0x24 |
u32 | CRC-32 of the name | confirmed (code) at 0x0010ef18 (the hash is recomputed from the name) |
+0x28 |
char[64] | the name, music/<track> |
confirmed (code) |
MUSIC.SND (960,063,488 bytes) holds the tracks back to back, each 2 × interleave × blocks bytes, left block then
right block. The game opens it as cdrom0:\IOP\MUSIC.SND;1 (0x0054b6e0) on stream 1 (with 2 as the right
channel) or stream 3 (with 4), at the record's rate. The track names are listed in
Sound and music (301 of the 345 are configured).
Coney's implementation¶
coney-tools audio (repo:python/src/coney_tools/audio.py) reads the
tables, lists the banks, streams and music with counts and hashes, and decodes one sound or track to a WAV file in
your scratch folder for listening (coney-tools). The engine reads the same data in
src/audio/ (sound_data.cpp, sound_bank.cpp, adpcm.cpp, sound_stream.cpp; Sound).
Open questions¶
- The bank
0xf83ec65e(.msd, WAD entry 9,804): its name (205 sounds, mostly breakables and pick-ups). - Music records'
+0x08(64) and+0x0c(32): MultiStream header fields? - Class flag
0x40. - The sound list holds records with equal hashes (the first pair at records 115 and 116): which one the game's binary search finds.