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;