This spec is far from completion. And it may contains error. Use it at your own risk.
23 June 2007 SBNK update
20 June 2007 SSEQ Events
6 June 2007 SBNK + general update
23 May 2007 First published
For enquiries please contact me at "kiwi.ds AT gmail.com"
Crystal - the author of CrystalTile2.exe
loveemu - the author of sseq2mid.exe, swave2wave.exe & strm2wave.exe
Nintendon - the author of ndssndext.exe
DJ Bouche - the author of sdattool.exe
VGMTrans - the author of VGMTrans.exe
Tables of Contents |
0. Introduction |
"The DS SDK has all the tools in it to convert MIDI files to the DS format, and has text file templates to define the soundbanks." CptPiard from VGMix
The SDAT file is used by Nitro Composer to pack various types of sound files in a single file for use in NDS rom. Not all rom has a SDAT file. But it seems that SDAT file is very popular among the NDS game developers.
Inside the SDAT you will find: SSEQ (Sequence), SSAR (Sequence Archive), SBNK (Sound Bank), SWAR (Wave Archive), STRM (Stream).
SSAR is a collection of SSEQ, while SWAR is a collection of SWAV.
char 1 byte // signed char BYTE 1 byte // unsigned char short 2 byte // signed short WORD 2 byte // unsigned short int 4 byte // signed int UINT 4 byte // unsigned int long 4 byte // signed long DWORD 4 byte // unsigned long
typedef struct tagNdsStdFile { char type[4]; // i.e. 'SDAT' or 'SBNK' ... UINT magic; // 0x0100feff or 0x0100fffe UINT nFileSize; // Size of this file (include this structure) WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks } NDSSTDF;
1. SDAT File Format |
-------------------------------- | Header | -------------------------------- | Symbol Block | -------------------------------- | Info Block | -------------------------------- | File Allocation Table (FAT) | -------------------------------- | File Block | --------------------------------
typedef struct tagSDATHeader { struct tagNdsStdFile { char type[4]; // 'SDAT' = 0x54414453 UINT magic; // 0x0100feff UINT nFileSize; // Size of this SDAT file (include this structure) WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks, usually 4, but some have 3 only } file; UINT nSymbOffset; // offset of Symbol Block = 0x40 UINT nSymbSize; // size of Symbol Block UINT nInfoOffset; // offset of Info Block UINT nInfoSize; // size of Info Block UINT nFatOffset; // offset of FAT UINT nFatSize; // size of FAT UINT nFileOffset; // offset of File Block UINT nFileSize; // size of File Block BYTE reserved[16]; // unused, 0s } SDATHEADER;
typedef struct tagSDATSymbol { char type[4]; // 'SYMB' = 0x424D5953 UINT nSize; // size of this Symbol Block UINT nRecOffset[8]; // offset of a Record (note below) UINT reserved[6]; // unused, zeros } SDATSYMB;
Record No. | Record Name | Description |
0 | SEQ | Sequence |
1 | SEQARC | Sequence Archive |
2 | BANK | Bank |
3 | WAVEARC | Wave Archive |
4 | PLAYER* | Player |
5 | GROUP | Group |
6 | PLAYER2* | Player2 |
7 | STRM | Stream |
* Records 4 and 5 do not appear in SMAP file |
typedef struct tagSDATSymbolRec { UINT nCount; // No of entries in this record UINT nEntryOffset[1]; // array of offsets of each entry } SDATSYMBREC;For Record 1 (SEQARC), it is a group which contains sub-records. The sub-record is of the same structure as SDATSYMBREC (above). Record 1 has the following structure:
typedef struct tagSDATSymbolRec2 { UINT nCount; // No of entries in this record struct { UINT nEntryOffset; // offset of this Group's symbol UINT nSubRecOffset; // offset of the sub-record } Group[1]; // array of offsets of each entry } SDATSYMBREC2;Below is an example to access these records:
SDATSYMB *symb; SDATSYMBREC *symb_rec; int i, j; char *szSymbol; ... // access record 0 'SSEQ' symb_rec = (SDATSYMBREC *) ( (BYTE *)symb + symb->RecOffset[0] ); for (i = 0; i < symb_rec->nCount; i++) { // print out the symbol szSymbol = (char *) ( (char *)symb + symb_rec->nEntryOffset[i] ); printf("%s\n", szSymbol); } ... SDATSYMBREC2 symb_rec2; // access record 1 'SSAR' symb_rec2 = (SDATSYMBREC *)( (BYTE *)symb + symb->RecOffset[1] ); for (i = 0; i < symb_rec2->nCount; i++) { szSymbol = (char *) ( (char *)symb + symb_rec2->Group[i].nEntryOffset ); printf("%s\n", szSymbol); SDATSYMBREC *symb_subrec = (SDATSYMBREC *) ( (BYTE *)symb + symb_rec2->Group[i].nSubRecOffset ); for (j = 0; j < symb_subrec->nCount; j++) { // print out sub record's symbols szSymbol = (char *) ( (char *)symb + symb_subrec->nEntryOffset[i] ); printf("%s\n", szSymbol); } }
typedef struct tagSDATInfo { char type[4]; // 'INFO' = 0x4F464E49 UINT nSize; // size of this Info Block = Header.nInfoSize UINT nRecOffset[8]; // offset of a Record UINT reserved[6]; // unused, zeros } SDATINFO;
typedef struct tagSDATInfoRec { UINT nCount; // No of entries in this record UINT nEntryOffset[1]; // array of offsets of each entry } SDATINFOREC;
typedef struct tagSDATInfoSseq { WORD fileID; WORD unknown; WORD bnk; BYTE vol; BYTE cpr; BYTE ppr; BYTE ply; BYTE unk1; BYTE unk2; } SDATINFOSSEQ;Remarks: Once I thought fileID should be 32bits in size. But I saw in the SDAT file of San DS which contains 0x00ff in the high words ...
typedef struct tagSDATInfoSsar { WORD fileID; WORD unknown; } SDATINFOSSAR;Remarks: no info is available for SEQARC files. The info of each archived SEQ is stored in that SEQARC file.
typdef struct tagSDATInfoBank { WORD fileID; WORD unknown; WORD wa[4]; // 0xffff if not in use }Remarks: Each bank can links to up to 4 WAVEARC files. The wa[4] stores the WAVEARC entry number.
typedef struct tagSDATInfoSwar { WORD fileID; WORD unknown; } SDATINFOSwar;Remarks: This is not a new structure. It is the same as SDATINFOSSAR above for Record 1.
typedef struct tagSDATInfoPlayer { BYTE unknown[8]; // nothing is known yet... } SDATINFOPlayer;Remarks: None
typedef struct tagSDATInfoPlayer { UINT nCount; // number of sub-records struct { // array of Group UINT type; UINT nEntry; } Group[1]; } SDATINFOPlayer;Remarks: SDATINFOPlayer::Group::type can be one of the following values. nEntry is the entry number in the relevant Record (0-3).
Value | Type |
0x0700 | SEQ |
0x0803 | SEQARC |
0x0601 | BANK |
0x0402 | WAVEARC |
typedef struct SDATInfoPlayer2 { BYTE nCount; BYTE v[16]; // 0xff is not in use BYTE reserved[7]; // padding, 0s } SDATINFOPLAYER2;Remarks: The use is unknown. The first byte states how many of the v[16] is used (non 0xff).
typedef struct SDATInfoStrm { WORD fileID; WORD unknown; BYTE vol; BYTE pri; BYTE ply; BYTE reserved[5]; } SDATINFOSTRM;Remarks: 'vol' means volume, 'ply' means play?, 'pri' means priority (I guess)
typedef struct tagSDATFAT { char type[4]; // 'FAT ' = 0x20544146 UINT nSize; // size of the FAT UINT nCount; // Number of FAT records SDATFATREC Rec[1]; // Arrays of FAT records } SDATFAT;
typedef struct tagSDATFATREC { UINT nOffset; // offset of the sound file UINT nSize; // size of the Sound file BYTE reserved[8]; // always 0s } SDATFATREC;
typedef struct tagSDATFILE { char type[4]; // 'FILE' = 0x454C4946 UINT nSize; // size of this block UINT nCount; // number of sound files UINT reserved; // always 0 } SDATFILE;
2. SSEQ File Format |
typedef struct tagSseq { struct tagNdsStdFile { char type[4]; // 'SSEQ' UINT magic; // 0x0100feff UINT nFileSize; // Size of this SSEQ file WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks = 1 } file; struct { char type[4]; // 'DATA' = 0x41544144 UINT nSize; // Size of this structure = nFileSize - 16 UINT nDataOffset; // Offset of the sequence data = 0x1c BYTE data[1]; // Arrays of sequence data } data; } SSEQ;
NB. For the details of the SSEQ file, please refer to loveemu's sseq2mid
The design of SSEQ is more programming-oriented while MIDI is hardware-oriented. In MIDI, to produce a sound, a Note-On event is sent to the midi-instrument and then after a certain time, a Note-Off is sent to stop the sound (though it is also acceptable to send a Note-On message with 0 velocity). In SSEQ, a sound is produced by one event only which carries with data such as note, velocity and duration. So the SSEQ-sequencer knows exactly what and how to play and when to stop.
A SSEQ can have at maximum 16 tracks, notes in the range of 0..127. Each quartet note has a fixed tick length of 48.
Status Byte | Parameter | Description |
0xFE | 2 bytes It indicates which tracks are used. Bit 0 for track 0, ... Bit 15 for track 15. If the bit is set, the corresponding track is used. | Indication begin of multitrack. A series of event 0x93 follows. |
0x93 | 4 bytes 1st byte is track number [0..15] The other 3 bytes are the relative adress of track data. Add nDataOffset (usually 0x1C) to find out the absolute address. | SSEQ is similar to MIDI in that track data are stored one after one track. Unlike mod music. |
0x00 .. 0x7f | Velocity: 1 byte [0..127] Duration: Variable Length | NOTE-ON. Duration is expressed in tick. 48 for quartet note. Usually it is NOT a multiple of 3. |
0x80 | Duration: Variable Length | REST. It tells the SSEQ-sequencer to wait for a certain tick. Usually it is a multiple of 3. |
0x81 | Bank & Program Number: Variable Length | bits[0..7] is the program number, bits[8..14] is the bank number. Bank change is seldomly found, so usually bank 0 is used. |
0x94 | Destination Address: 3 bytes (Add nDataOffset (usually 0x1C) to find out the absolute address.) | JUMP. A jump must be backward. So that the song will loop forever. |
0x95 | Call Address: 3 bytes (Add nDataOffset (usually 0x1C) to find out the absolute address.) | CALL. It's like a function call. The SSEQ-sequncer jumps to the address and starts playing at there, until it sees a RETURN event. |
0xFD | NONE | RETURN. The SSEQ will return to the caller's address + 4 (a Call event is 4 bytes in size). |
0xA0 .. 0xBf | See loveemu's sseq2mid for more details. | Some arithmetic / compare operations. Not playback related. |
0xC0 | Pan Value: 1 byte [0..127], middle is 64 | PAN |
0xC1 | Volume Value: 1 byte [0..127] | VOLUME |
0xC2 | Master Volume Value: 1 byte [0..127] | MASTER VOLUME |
0xC3 | Value: 1 byte [0..64] (Add 64 to make it a MIDI value) | TRANSPOSE (Channel Coarse Tuning) |
0xC4 | Value: 1 byte | PITCH BEND |
0xC5 | Value: 1 byte | PITCH BEND RANGE |
0xC6 | Value: 1 byte | TRACK PRIORITY |
0xC7 | Value: 1 byte [0: Poly, 1: Mono] | MONO/POLY |
0xC8 | Value: 1 byte [0: Off, 1: On] | TIE (unknown) |
0xC9 | Value: 1 byte | PORTAMENTO CONTROL |
0xCA | Value: 1 byte [0: Off, 1: On] | MODULATION DEPTH |
0xCB | Value: 1 byte | MODULATION SPEED |
0xCC | Value: 1 byte [0: Pitch, 1: Volume, 2: Pan] | MODULATION TYPE |
0xCD | Value: 1 byte | MODULATION RANGE |
0xCE | Value: 1 byte | PORTAMENTO ON/OFF |
0xCF | Time: 1 byte | PORTAMENTO TIME |
0xD0 | Value: 1 byte | ATTACK RATE |
0xD1 | Value: 1 byte | DECAY RATE |
0xD2 | Value: 1 byte | SUSTAIN RATE |
0xD3 | Value: 1 byte | RELEASE RATE |
0xD4 | Count: 1 byte (how many times to be looped) | LOOP START MARKER |
0xFC | NONE | LOOP END MARKER |
0xD5 | Value: 1 byte | EXPRESSION |
0xD6 | Value: 1 byte | PRINT VARIABLE (unknown) |
0xE0 | Value: 2 byte | MODULATION DELAY |
0xE1 | BPM: 2 byte | TEMPO |
0xE3 | Value: 2 byte | SWEEP PITCH |
0xFF | NONE | EOT: End Of Track |
3. SSAR File Format |
typedef struct tagSsarRec { int nOffset; // relative offset of the archived SEQ file, absolute offset = nOffset + SSAR::nDataOffset WORD bnk; // bank BYTE vol; // volume BYTE cpr; // channel pressure BYTE ppr; // polyphonic pressure BYTE ply; // play BYTE reserved[2]; } SSARREC; typedef struct tagSsar { struct tagNdsStdFile { char type[4]; // 'SSAR' UINT magic; // 0x0100feff UINT nFileSize; // Size of this SSAR file WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks = 1 } file; struct { char type[4]; // 'DATA' = 0x41544144 UINT nSize; // Size of this structure UINT nDataOffset; // Offset of data UINT nCount; // nCount * 12 + 32 = nDataOffset SSARREC Rec[1]; // nCount of SSARREC } data; } SSAR;
NB. Archived SSEQ files are not stored in sequence (order). So Rec[0].nOffset may point to 0x100 but Rec[1].nOffset points to 0x40.
NB. Archived SSEQ files cannot be readily extracted from SSAR file because data in one SSEQ may 'call' data in other SSEQ.
4. SBNK File Format |
SBNK stands for "Sound Bank". A bank is linked to up to 4 SWAR files which contain the samples. It define the instruments by which a SSEQ sequence can use. You may imagine SSEQ + SBNK + SWAR are similar to module music created by trackers.
typedef struct tagSbnkInstrument { BYTE fRecord; // can be either 0, 1..3, 16 or 17 WORD nOffset; // absolute offset of the data in file BYTE reserved; } SBNKINS; typedef struct tagSbnk { struct tagNdsStdFile { char type[4]; // 'SBNK' UINT magic; // 0x0100feff UINT nFileSize; // Size of this SBNK file WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks = 1 } file; struct { char type[4]; // 'DATA' = 0x41544144 UINT nSize; // Size of this structure BYTE reserved[32]; // reserved, 0s UINT nCount; // number of instrument SBNKINS Ins[1]; } data; } SBNK;
So, after SBNK::data, there come SBNK::data::nCount of SBNKINS. After the last SBNKINS, there will be SBNK::data::nCount of instrument records. In each instrument records, we can find one or more wave/note definitions.
If SBNKINS::fRecord = 0, it is empty. SBNKINS::nOffset will also = 0.
If SBNKINS::fRecord < 16, the record is a note/wave definition. I have seen values 1, 2 and 3. But it seems the value does not affect the wave/note definition that follows. Instrument record size is 16 bytes.
swav number 2 bytes // the swav used swar number 2 bytes // the swar used. NB. cross-reference to "1.3.2 Info Block - Entry, Record 2 BANK" note number 1 byte // 0..127 Attack Time 1 byte // 0..127 Decay Time 1 byte // 0..127 Sustain Level 1 byte // 0..127 Release Time 1 byte // 0..127 Pan 1 byte // 0..127, 64 = middle
If SBNKINS::fRecord = 16, the record is a range of note/wave definitions. The number of definitions = 'upper note' - 'lower note' + 1. The Instrument Record size is 2 + no. of definitions * 12 bytes.
lower note 1 byte // 0..127 upper note 1 byte // 0..127 unknown 2 bytes // usually == 01 00 swav number 2 bytes // the swav used swar number 2 bytes // the swar used. note number 1 byte Attack Time 1 byte Decay Time 1 byte Sustain Level 1 byte Release Time 1 byte Pan 1 byte ... ... ... unknown 2 bytes // usually == 01 00 swav number 2 bytes // the swav used swar number 2 bytes // the swar used. note number 1 byte Attack Time 1 byte Decay Time 1 byte Sustain Level 1 byte Release Time 1 byte Pan 1 byte
For example, lower note = 30, upper note = 40, there will be 40 - 30 + 1 = 11 wave/note definitions.
The first wave/note definition applies to note 30.
The second wave/note definition applies to note 31.
The third wave/note definition applies to note 32.
...
The eleventh wave/note definition applies to note 40.
If SBNKINS::fRecord = 17, the record is a regional wave/note definition.
The first 8 bytes defines the regions. They divide the full note range [0..127] into several regions (max. is 8) An example is: 25 35 45 55 65 127 0 0 (So there are 6 regions: 0..25, 26..35, 36..45, 46..55, 56..65, 66..127) Another example: 50 59 66 83 127 0 0 0 (5 regions: 0..50, 51..59, 60..66, 67..84, 85..127) Depending on the number of regions defined, the corresponding number of wave/note definitions follow: unknown 2 bytes // usually == 01 00 swav number 2 bytes // the swav used swar number 2 bytes // the swar used. note number 1 byte Attack Time 1 byte Decay Time 1 byte Sustain Level 1 byte Release Time 1 byte Pan 1 byte ... ... In the first example, for region 0..25, the first wave/note definition applies. For region 26..35, the 2nc wave/note definition applies. For region 36..45, the 3rd wave/note definition applies. ... For region 66..127, the 6th wave/note definition applies.
REMARKS: Unknown bytes before wave/defnition definition = 5, not 1 in stage_04_bank.sbnk, stage_04.sdat, Rom No.1156
The articulation data affects the playback of the SSEQ file. They are 'Attack Time', 'Decay Time', 'Sustain Level', 'Release Time' and 'Pan'.
I have studied the output values in DLS bank file produced by VGMTrans. See this file for more details.
5. SWAV File Format |
SWAV doesn't appear in SDAT. They may be found in the ROM elsewhere. They can also be readily extracted from a SWAR file (see below).
// info about the sample typedef struct tagSwavInfo { BYTE nWaveType; // 0 = PCM8, 1 = PCM16, 2 = (IMA-)ADPCM BYTE bLoop; // Loop flag = TRUE|FALSE WORD nSampleRate; // Sampling Rate WORD nTime; // (ARM7_CLOCK / nSampleRate) [ARM7_CLOCK: 33.513982MHz / 2 = 1.6756991e7] WORD nLoopOffset; // Loop Offset (expressed in words (32-bits)) UINT nNonLoopLen; // Non Loop Length (expressed in words (32-bits)) } SWAVINFO; // Swav file format typedef struct tagSwav { struct tagNdsStdFile { char type[4]; // 'SWAV' UINT magic; // 0x0100feff UINT nFileSize; // Size of this SWAV file WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks = 1 } file; struct { char type[4]; // 'DATA' UINT nSize; // Size of this structure SWAVINFO info; // info about the sample BYTE data[1]; // array of binary data } data; } SWAV;
6. SWAR File Format |
typedef struct tagSwar { struct tagNdsStdFile { char type[4]; // 'SWAR' UINT magic; // 0x0100feff UINT nFileSize; // Size of this SWAR file WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks = 1 } file; struct { char type[4]; // 'DATA' UINT nSize; // Size of this structure BYTE reserved[0x20]; UINT nSample; // Number of Samples } data; UINT nOffset[1]; // array of offsets of samples } SWAR;
NB. After the array of offsets, the binary samples follow. Each sample has a SWAVINFO structure before the sample data. Therefore, it is easy to make a SWAV from the samples in SWAR.
7. STRM File Format |
typedef struct tagSTRMHeader { char type[4]; // 'STRM' BYTE magic[4]; // 0xFF 0xFE 0x00 0x01 UINT nFileSize; // Size of this file WORD nSize; // Size of this structure = 0x10 WORD nChunk; // Number of Chunks, always 2 } STRMHEADER;
typedef struct tagSTRMHead { struct tagNdsStdFile { char type[4]; // 'STRM' UINT magic; // 0x0100feff UINT nFileSize; // Size of this STRM file WORD nSize; // Size of this structure = 16 WORD nBlock; // Number of Blocks = 2 } file; struct { char type[4]; // 'HEAD' UINT nSize; // Size of this structure BYTE nWaveType; // 0 = PCM8, 1 = PCM16, 2 = (IMA-)ADPCM BYTE bLoop; // Loop flag = TRUE|FALSE BYTE nChannel; // Channels BYTE unknown; // always 0 WORD nSampleRate; // Sampling Rate (perhaps resampled from the original) WORD nTime; // (1.0 / rate * ARM7_CLOCK / 32) [ARM7_CLOCK: 33.513982MHz / 2 = 1.6756991e7] UINT nLoopOffset; // Loop Offset (samples) UINT nSample; // Number of Samples UINT nDataOffset; // Data Offset (always 68h) UINT nBlock; // Number of Blocks UINT nBlockLen; // Block Length (Per Channel) UINT nBlockSample; // Samples Per Block (Per Channel) UINT nLastBlockLen; // Last Block Length (Per Channel) UINT nLastBlockSample; // Samples Per Last Block (Per Channel) BYTE reserved[32]; // always 0 } head; struct { char type[4]; // 'DATA' UINT nSize; // Size of this structure BYTE data[1]; // Arrays of wave data } data; } SDATSTRM;