Reference ยท Formats
SNDDATA.BIN sound banks and PS-ADPCM
79 sound banks in Sony's SDK formats - .hd header, up to three .sq
sequences, .bd PS-ADPCM sample data - concatenated with no directory. The
index is in the executable.
Layout
All little-endian.
bank 79 of them, tiling the file from 0 to 0x27c8800
.hd hdSize bytes
.sq sqSize1 bytes
.sq sqSize2 bytes may be 0
.sq sqSize3 bytes may be 0
padding 0xff to a 2048 boundary
.bd bdSize bytes
padding 0xff to a 2048 boundary
The index
commseTbl u32[3] at 0x00307410 the common sound-effect bank
+0x00 ofs 0
+0x04 hdSize 0x6260
+0x08 bdSize 0x12a560
SQ_LOAD 24 bytes, all s32; a row of -1 is unused
+0x00 ofs byte offset of the bank in SNDDATA.BIN
+0x04 hdSize
+0x08 sqSize1
+0x0c sqSize2
+0x10 sqSize3
+0x14 bdSize
SQ_LOAD tables: typeA, typeC, typeE, typeG, typeI, typeK,
typeM, typeO (10 rows each, 0x00307420 - 0x00307ab0) and piroshi
(0x00307ba0), reached per field type through sqDataField (0x00307bc0);
sqDataDungeon (0x00307c20, 11), sqDataTown (0x00307d40, 10),
sqDataTitle (0x00307e30, 1), sqDataDesktop (0x00307e50, 6),
sqDataToppage (0x00307ee0, 1), sqDataEvent (0x00307f00, 123),
sqDataStream (0x00308b10, 28). 122 rows name the 79 banks.
The IOP (ccSQDataLoadCD, SNDBASE.IRX:0x1e44) reads .hd and the .sqs
from sector lsn + (ofs + 2047) >> 11 and the .bd from
lsn + (ofs + hdSize + sqSize + 2047) >> 11, where lsn is its own
sceCdSearchFile of \DATA\SNDDATA.BIN;1 and sqSize the three summed.
The .bd is streamed to SPU RAM at commseTbl[2] + 0x5020; the common bank's
goes to 0x5010.
.hd chunks
Each chunk: char[4] "SCEI" and a 4-character type, both stored
byte-reversed (IECS sreV = SCEI Vers), then u32 size. Offsets in
Head count from the start of the .hd; offsets in the index tables count
from their chunk. 0xffffffff is an unused slot.
Vers 16 bytes; version 2.0 in all 79
Head +0x0c hdSize, +0x10 bdSize, +0x14 Prog, +0x18 Sset, +0x1c Smpl,
+0x20 Vagi, +0x24 seTimbre (-1 in all 79)
Vagi u32 maxIndex, u32 offset[maxIndex + 1], then per VAG:
u32 bdOffset, u16 rate, u8 loop, u8 0xff
Smpl 42-byte records: u16 vag, velocity range, base note +0x0b, pan +0x0d,
volume +0x10, 0xff +0x11, ADSR1 +0x12, ADSR2 +0x14, then key-follow
and LFO fields
Sset u8 velCurve, u8 velLow, u8 velHigh, u8 n, u16 sample[n]
Prog 36-byte header (u32 split offset, u8 nSplit, u8 splitSize, volume,
pan, ...), then nSplit 20-byte splits: u16 sampleSet, keyLow,
keyCrossfade, keyHigh, ...
Field names past those checked (offsets above) follow Sony's SDK and are not confirmed against code; the synthesizer module is stripped.
.sq
Vers 16 bytes; its size field (+0x08) is where MODMIDI.IRX looks for Sequ
Sequ +0x0c file size, +0x10 Song (0xffffffff in all 150), +0x14 Midi,
+0x18 SE sequence, +0x1c SE song - offsets from the file start
Midi +0x0c max index, +0x10 u32 offsets[] from the chunk: one block in all
block u32 data offset (6 in all 150), u16 ticks per quarter note (480 in all)
then events: [varlen delta] status data...
The events are MIDI with Sony's changes: note off (8n) carries no velocity
byte; bit 7 set in any data byte means the next event has no delta (it is
masked off); running status. Loops are NRPN controllers (CC 99 0 / CC 6 id
marks a start, CC 99 1 / CC 6 id / CC 38 n jumps back, n = 0 forever); FF 51
sets the tempo, FF 2F ends the sequence. How the sequencer plays them is in
the sound engine. 150 .sq files in 78 banks: 147
loop forever (one loop each), 3 end; 13 change tempo.
The parts the game leaves out
Every bank leaves out three parts of Sony's format:
Head.seTimbreis -1 in all 79.sceHSyn_Load(MODHSYN.IRX:0x047c) reads it only from aVersof 2 or later, which all of these are (Vers+0x0e is 2), and takes no SE timbres when it is -1.Sequ's Song, SE sequence and SE song (+0x10, +0x18, +0x1c) are -1 in all 150.sqfiles.- Nothing asks for them either.
SNDBASE.IRXis the only module that importsMODMIDI.IRX, and it calls onlysceMidi_Init,ATick,Load,SelectMidi,MidiPlaySwitchandMidiSetLocation. It plays theMidiblocks and never selects a Song. None of the ten modules the game loads (the disc) plays SE sequences; the sound effects are notes on the synthesizer's ports (the sound engine).
Which file a sequencer plays is decided by the loading code, not by the
file list: sequence 0 starts right after the .hd, sequence 1 after
sqSize1, sequence 2 after sqSize1 + sqSize2, and sequences 1 and 2 exist
only when their own size is non-zero. The 15 rows (11 banks) with sqSize1
0 - rows 2, 6 and 7 of typeE, typeI, typeK and typeM, and sqDataDungeon
rows 3, 7 and 10 - therefore play their second file as both sequence 0 and
sequence 1.
PS-ADPCM
frame 16 bytes, 28 samples
u8 shift | filter << 4
u8 flags bit 0 end, bit 1 repeat, bit 2 loop start
u8[14] nibbles, low nibble first, signed
s = (nibble << 12 >> shift) + ((s1 * F0[filter] + s2 * F1[filter] + 32) >> 6)
clamped to 16 bits
F0 = 0, 60, 115, 98, 122 F1 = 0, 0, -52, -55, -60
The rounding is the SPU2's, which nothing on the disc shows. The decoder
follows PCSX2 (XA_decode_block: the two products summed, then + 32 >> 6). DuckStation's PS1 SPU (Voice::DecodeBlock) shifts each product by
6 on its own, with no rounding. ffmpeg truncates the sum, and over these
banks differs from the rounded sum by up to 112 LSB.
The decoder (tools/adpcm.py, piney_data::sound::adpcm) takes a shift
of 13-15 as 9, as the SPU2 is taken to; no frame on the discs has one
(Counts), so it is not checked.
A VAG runs from its Vagi offset to the next VAG's. Its first frame is 16
zero bytes. A looped VAG ends with a frame whose flags are 0x03, and loops
from the last frame with bit 2, keeping its filter history (s1, s2);
the end frame itself is played. Exactly those have Vagi.loop = 1. A
one-shot's end frame is followed by one unplayed 00 07 77 ... 77 frame.
Counts
1,013 VAGs, 1,402 samples, 1,398 sample sets, 926 programs, 1,398 splits; 314 looped VAGs. Rates: 223 at 11,025 Hz, 140 at 22,050, 341 at 44,100, 1 at 33,074, the rest within a few Hz of those. Over 2,534,127 frames no shift exceeds 12 and no filter exceeds 4.
Notes
typeI[2] (bank 26) has bdSize 0x5cbf0 in its table row - typeI[0]'s
value - where its Head says 0x4ec30, which is what the file layout agrees
with.
The desktop jukebox (Wave[52], INF desktop.prg:0x0042b6f0, WaveData:
title, category, fieldType, bgNum, comment, number) names 51 tracks; a jump
table at 0x0034e130 maps its category to the SQ_LOAD table (1 desktop,
2 town, 3 field, 4 dungeon, 5 event, 6 stream, 7 title), and one at
0x0034e110 to the matching volume table (sqVolTbl*: per row three
SQTBL {int midiPort, int hdPort, u16 vol}). Rows 7, 27 and 47 play the
bank's second sequence, every other row its first; see
the sound engine.
crates/piney-data reads all of this (piney_data::sound): the banks
through the tables piney-gen reads from the executable into the build,
the .hd and .sq chunks and PS-ADPCM, checked against tools/sound.py,
tools/scei.py and tools/adpcm.py bank by bank and sample by sample
(tools/test_sound_rs.py).
Unknown
- The SPU2's own ADPCM rounding (above): the emulators disagree, and it needs a measurement on the console.