The current URL is datacrystal.tcrf.net.
EarthBound/Audio: Difference between revisions
m (→Example code: sigh.) |
(→Example code: There, that should do it. I hope.) |
||
Line 26: | Line 26: | ||
A picture can be worth a thousand words, and sometimes code is kinda like a picture... so the following is what the game basically does when it loads a song. (This code is based solely on the data structures, not the game's actual code.) Relevant parts of song pack 01 are assumed to already be pre-loaded where necessary. | A picture can be worth a thousand words, and sometimes code is kinda like a picture... so the following is what the game basically does when it loads a song. (This code is based solely on the data structures, not the game's actual code.) Relevant parts of song pack 01 are assumed to already be pre-loaded where necessary. | ||
(NOTE: In reality, SPC RAM is an entirely separate address space that has to be accessed in a particular way, not an array | (NOTE: In reality, SPC RAM is an entirely separate address space that has to be accessed in a particular way, not an array. The use of array notation is to make it clear what is going on.) | ||
// Note - these offsets reflect the position of the code in ROM | // Note - these offsets reflect the position of the code in ROM | ||
Line 33: | Line 33: | ||
void ** const SPC_PACKS = 0x04fb47; | void ** const SPC_PACKS = 0x04fb47; | ||
byte * const SONG_PACKS = 0x04f908; | byte * const SONG_PACKS = 0x04f908; | ||
// Makes it easy for a pointer to be interpreted either way | |||
typedef struct { | |||
word length; | |||
word spc_addr; | |||
} SpcChunkHeader; | |||
void load_song(int song_num) | void load_song(int song_num) | ||
Line 43: | Line 49: | ||
if(pack != 0xff) { | if(pack != 0xff) { | ||
// Load the pack | // Load the pack | ||
byte *data = SPC_PACKS[pack]; | |||
while(1) { | while(1) { | ||
// Load each sub-chunk | // Load each sub-chunk | ||
SpcChunkHeader header; | |||
if(length == 0) | memcpy(&header, data, sizeof(header)); | ||
if(header.length == 0) | |||
break; | break; | ||
word spc_addr = *data+ | data += sizeof(header); | ||
word spc_addr = header.spc_addr; | |||
byte *end = data + header.length; | |||
while(spc_addr < end) | |||
SPC_RAM[spc_addr++] = *data++; | |||
} | } | ||
} | } |
Revision as of 07:22, 25 February 2008
SPC RAM
As with all SNES games, audio data must be loaded into the SPC700's RAM before it can be played. This RAM contains all the song data, the instrument data, the sample data, and even the music playback code itself. SPC files are nothing more than a straightforward dump of SPC RAM with a 256-byte header attached (and a footer with state information that doesn't concern us here), so you can easily convert a RAM offset to an SPC file offset by adding 0x100 for the header.
Important locations here are 6C00, which is where sample pointers are loaded, 7000, where sample data is loaded, and 6E00, where instruments are loaded. The starting offset of a song is much more arbitrary, ranging from 2FDD-6400 depending on the song; the address of a particular song in SPC RAM can be found via the Song SPC Pointer Table. Finally, 0500-468A (loaded in from pack 1, but never swapped out) contains the music-playing code as well as a few short songs.
SPC Packs
The game loads chunks, or "packs", of data from the ROM into SPC RAM. Each song has up to three packs associated with it. The table showing which song is related to which pack is located at 4F90A. The table has 191 (BF) entries. The first entry in the table is song 01, not song 00 (i.e., the table is indexed as if it began at 4F908). Song 00 does not point to an actual song, and its space in the table is probably used by the preceding data chunk. Similarly, the last song is actually song C0, although it is entry BF in the table. Each entry simply has three bytes indicating the pack number; FF is a dummy value to mean a pack should not be loaded.
The packs themselves are located at 4FB47. There are 169 (9A) entries; the first entry is pack 00. Each pack is further divided into one or more sub-chunks with the a four-byte header. The first word of the header is the length of the data (not including this header information), and the second word is the address in SPC RAM to load the chunk into. After the chunk of data is either another chunk with the same type of header (which may be followed by another chunk, and so on), or 0000, which marks the end of the pack. Each chunk contains one of: a song, some instruments, some samples, or a sample pointer table. Note that chunks from different packs may overlap; I presume the packs are loaded in the order that they occur in the table at 4F90A.
Note that the 0500-468A and 6F80-6F97 chunks of pack 01 are loaded at all times. The former contains a couple of short songs that are frequently needed, and is the only sub-chunk to contain more than one song.
Patterns
The first pattern of the song is the first data to be found in the song data. So if, for example, the Song SPC Pointer Table has 0x6000 for your particular song, the first pattern is at 0x6000 in SPC RAM. The value 0x00FF marks a jump, with the following two bytes containing the address to jump to. The game only uses this to loop back to a previous pattern. The value 0x0000 indicates the end of the song (i.e., stop playing), which will never be reached in a song with a loop.
The values pointed to are themselves pointers to the actual pattern data. In C notation, the pattern data for a pattern is at **pattern_tbl[i], not just *pattern_tbl[i]. This extra level indirection seems pointless, but nonetheless, it's there.
The pattern data itself is a table of eight pointers. (Yes, even more pointers!) Each pointer corresponds to a channel. If the value is 0x0000, the channel is silent for that pattern, which is often done on channel 8 to allow sound effects to be played on it.
Song SPC Pointer Table
This table is located at 262B8C in the ROM and contains 191 (0xBF) entries. Not all songs are loaded into the same offset in SPC RAM, so this table is used to tell where in SPC RAM the song is located. Each entry is a two-byte pointer in little-endian (LSB first) format.
The first entry in the table is actually song 01, not song 00 (i.e., the table is accessed as if it began at 262B8A instead of 262B8C).
Example code
A picture can be worth a thousand words, and sometimes code is kinda like a picture... so the following is what the game basically does when it loads a song. (This code is based solely on the data structures, not the game's actual code.) Relevant parts of song pack 01 are assumed to already be pre-loaded where necessary.
(NOTE: In reality, SPC RAM is an entirely separate address space that has to be accessed in a particular way, not an array. The use of array notation is to make it clear what is going on.)
// Note - these offsets reflect the position of the code in ROM // (including the header) for clarity. For the real addresses used // in the SNES's memory space, subtract 0x200 and add 0xc00000. void ** const SPC_PACKS = 0x04fb47; byte * const SONG_PACKS = 0x04f908;
// Makes it easy for a pointer to be interpreted either way typedef struct { word length; word spc_addr; } SpcChunkHeader; void load_song(int song_num) { byte *song_packs = &SONG_PACKS[song_num*3]; // Load the song's SPC packs for(int i = 0; i < 3; i++) { int pack = song_packs[i]; if(pack != 0xff) { // Load the pack byte *data = SPC_PACKS[pack]; while(1) { // Load each sub-chunk SpcChunkHeader header; memcpy(&header, data, sizeof(header)); if(header.length == 0) break; data += sizeof(header); word spc_addr = header.spc_addr; byte *end = data + header.length; while(spc_addr < end) SPC_RAM[spc_addr++] = *data++; } } } }