SMW SPC Engine Guide v0.01
Table of contents
1) What is the SPC700?
1b) What is the S-DSP?
1c) Why this setup?
1d) What is the SMW SPC engine?
2) Basic SPC
2a) Storing to the audio ports
2b) Calling addmusic
2c) Calling Sampletool
3) Intermediate SPC
3a) SPC upload routine
3b) The full routine
3c) NSPC Audio RAM
3d) Rough visual representation of audio ram
4) Advanced SPC
4a) Programming custom commands
4b) SPC700 assembler
4c) Solving mysteries & Using the debugger
- This guide -
This is a tutorial made to provide some information on the SPC700 and S-DSP sound chip used by the SNES.
- For who is this guide intended? -
This guide is aimed at users with intermediate to advanced ASM knowledge, but little to no knowledge of how the SPC, DSP, or smw music data works. If you don't know what a pointer is or why they exist, or don't have a firm understanding of how the 65816 processor status flags work, you need more experience before digging through the SPC engine routines. You should get some experience fooling around with blocks, levelasm, sprites, etc, first. If you've already written hacks for the SPC, then that's about the extent of this guide, and you won't find much here you didn't already know.
You will also need to love the sound of the SNES or this will all seem very pointless and small.
- For what will this guide be completely useless? -
Basic music porting. Nothing here will help with music porting, ripping SPCS, converting midis, or "getting addmusic to work". We're concerned with the actual data upload code to the SPC and the inner workings of the sound engine used by SMW.
1) What is the SPC700?
Instead of copying the definition out of some document, I'll give my personal description of it. The SPC700 is a lot like the SNES CPU. It's a processor. It does the same type of stuff the 65816 does. It moves things around in memory, copies data, performs mathematical operations, does comparisons, and branches. The SPC700 doesn't even create any sound output. It is simply put, a processor.
The SPC700 differs from the SNES CPU in that it comes with 3 hardware timers, which it controls via its memory mapped registers at $00fa~$00ff. The SPC700 is also the only means by which anything can communicate with the S-DSP.
1b) What is the S-DSP?
The S-DSP is the sound generating work horse. Here's the definition of DSP:
Compared to a processor like the SPC700, the S-DSP is completely different. It doesn't perform operations like a processor. It's not concerned with moving stuff around in memory, comparing values, or branching. It has to output sound in real time, so it works in tandem with the SPC700, which organizes and sends data to it. The DSP could be likened to a musical instrument, and the SPC to the musician who plays it.A digital signal processor (DSP) is a specialized microprocessor with an optimized architecture for the fast operational needs of digital signal processing.
Where exactly the SPC700 ends and the S-DSP begins is a bit of a fuzzy subject, but it doesn't matter either.
1c) Why this setup?
The SPC700 is a pain in the butt. Why does it have to get in the way of giving the 65816 direct access to the sound output of the S-DSP?
One possible reason would be speed. Music should ideally be timed. Imagine if your game code had to stop every few dozen cycles to process an interrupt to check a hardware timer, and possibly having to deal with processing a bunch of music data. The strain and subsequent game slowdown would be immense. This is the perfect job for a coprocessor like the SPC700.
1d) What is the SMW SPC engine?
When you first turn on your SNES, there isn't actually anything in the SPC, other than the boot ROM. The boot ROM is just a basic program in the SPC that you can communicate with and upload your own code through. It basically just sits there and waits for you to send it things, and places the stuff wherever you say. There is no default music handling code in the SPC. Each game developer had to write their own music engine (or reuse an old engine from another game).
N-SPC, or whatever you want to call it, is the main program that many Nintendo games use to play music and sound data. This music handling program is sent to the SPC, generally when the game boots up, using the boot ROM upload routine. This program handles everything we're concerned with.
The only way to get meaningful results out of music hacking is to make changes to the spc engine. The barriers you face in this task are logistical.
1. The SPC700 doesn't use the same opcodes or instruction set you're used to with the 65816, although they are somewhat similar. You still need to learn a new programming language.
2. Since the engine itself is a program that gets uploaded to the SPC, you will have to learn the art of wrestling with the SPC upload routine to get your custom code into audio ram
3. There isn't much information available on the smw sound engine.
4. SPC hacking is about data. You not only have to create the routines that handle the data, but you have to have compatible data to send it. In other words, you still have to actually make the music or sprites or levels that make use of whatever features you were to add to it.
If you've used any of those MORE.bins floating around out there, those are essentially edits to the spc engine. They add to or change the capabilities of the code that gets uploaded to the SPC, allowing for the music data to be interpreted in new ways.
2) Basic SPC
That was all background information that won't help you do anything tangible. Let's explore what can be done by communicating with the SPC700, starting with the most basic.
2a) Storing to the audio ports
Code: Select all
LDA #$03
STA $2142$2142 is an SNES hardware register and one of the SPCs audio ports. The other audio ports are $2140, $2141, and $2143. These are the only means by which you can actually communicate with the SPC at all. Unfortunately, the cpu/you don't have direct access to the SPC audio ram. Everything you do goes through these four ports.
There is nothing special about $2142 that makes it a "music selection" register or anything. It's just the way the smw SPC engine was designed by the original sound programmer. In other games, the ports at $2140 will be used, but they may be used in completely different ways. Storing $03 to $2142 in another game may cause it to go into upload mode, or make the music fade out, or play sound effects, or just crash because you did something wrong. The point I'm trying to drive home here is that these ports are just gateways used for communicating with the code you uploaded into the SPC. When the SMW sound engine in the SPC checks to see what is in its input port at $2142 and finds $03 there, it uses the number 3 to find the data for the song data we know as the underwater music, and basically, begins playing the song.
If you were to use the excellent APU debugger in bsnes and watch the input ports, you would see all this happening piece by piece.
2b) Calling addmusic
There isn't a whole lot you can do by just throwing bytes at the SPC data ports. Let's try to actually do something useful and solve a problem.
An often experienced dilemma is the one of having vanilla and "custom" music in the same level. If you've ever tried to play both in the same level, you would know that the sound just crashes. This is because there are no vanilla songs loaded into audio ram when you have custom music, and vice versa. Vanilla SMW music is extremely well made and compact. Most of the soundtrack can fit in ARAM at one time. Most custom music is big and bulky, and only a couple of songs can fit into ARAM at one time (many games only have 1 song in ARAM at a time). By telling the SPC to play a song that isn't there, it usually ends up taking data from some invalid location and crashing itself.
Since the song you want to play doesn't exist, you're going to have to make it exist by sending it to ARAM. This is kind of a complicated process, but luckily, your custom music hack can handle it for you.
Of course, your custom music hack (I used Romi's when writing this, but it's starting to show its age. AddmusicM is the future) has been uploading music whenever you entered a level or the overworld. Why can't you just tell it to upload music whenever you want? Well, you can. If you know exactly where the hack was placed in the rom, you can simply JSL to it with your song number in A (sometimes you also need to put your song number in $1DFB). Some of the addmusics expect certain conditions, such as level mode ($0100) to be $11, and brightness to be at 0. You'll have to satisfy these conditions, or addmusic will just return without loading anything.
If you don't know or don't want to keep looking up the location of addmusic in your rom everytime it decides to move, you can do what I do, which is use its hijack point as a pointer to it.
(Romi's) addmusic hijacks at $00:9740 in the smw rom. The code at that location looks something like this.
Code: Select all
JSL $xxyyzzNote that music uploads only occur normally during level loading and such, so NMI is always off. For doing this in the middle of a level, NMI will be ON and will freeze the game as soon as it fires. In other words, turn NMI off until you're done uploading.
This code is functional. Put your song number in A.
Code: Select all
TAX ; hold song # in X
STA $0DDA ;\ this stuff is required by romi's addmusic
STA $1DFB ;|
LDA #$11 ;|
STA $0100 ;/
STZ $4200 ; disable NMI
LDA $13BF ;\ stuff required by romi's addmusic
PHA ;|
STZ $13BF ;|
LDA $0DAE ;|
PHA ;|
LDA $0DAF ;|
PHA ;/
PHK ; We push our return address onto the stack
PER RETURNADDR-1
TDC ;\
PHA ;| set bank 0
PLB ;/
TXA ; get song # back in A
JML [$9741] ; We use the addmusic hijack as
; a pointer to jump to the hack
RETURNADDR:
; it loads the song and returns here to the
; return address we pushed
PLA ; now just pull all this stuff back
STA $0DAF
PLA
STA $0DAE
LDA #$14
STA $0100
PLA
STA $13BF
LDA #$81
STA $4200 ; reenable NMI2c) Calling Sampletool
It's possible to do the same thing we just did with addmusic, with sample tool. Sample tool operates on level numbers, so you feed it the level number that has the sample bank you want, and it loads it. Sampletool expects the level number to be at RAM $0E, so put it there.
Sampletool's hijack doesn't reside in bank 0, so it's a bit trickier to get to. If you can find the sample tool hack in your rom, great. It never moves around, so you can just JSL to it. If you want to use the pointer method, you'll use $05D8E7, which is where sampletool hijacks from.
Code: Select all
PHK
PLB
REP #$30
LDA #$001F ;\ Let's say we want to load the sample bank of Level 1F
STA $0E ;/ dump $001F at $0E
ASL ; sampletool expects Y to contain the level number * 2
TAY ;
SEP #$30
STZ $4200 ; disable NMI
PHP
PHK ; push the return address
PER SAMPLERETURN-1
SEP #$10
LDA $05D8E7 ;\ read the bytes directly from the JSL in the ROM
STA $00 ;| and dump them at $0000
LDA $05D8E8 ;|
STA $01 ;|
LDA $05D8E9 ;|
STA $02 ;/
TYA ; get level number back in A
REP #$30
JML [$0000] ; use the bytes dumped at $0000 as a pointer
SAMPLERETURN:
; it loads the sample bank set for level 1F and returns here
PLP
SEP #$103) Intermediate SPC
3a) SPC upload routine
If you want to upload your own data to the SPC, you'll need to learn the upload routine. It's a tedious, 1 byte at a time upload to the SPC, full of annoying handshakes. I've also got to warn that this won't be of any use for normal hacking. You could get by fine just by calling addmusic and sampletool and letting them handle the data uploads.
If you've ever looked at the routine in all.log, it probably looked like the dog's dinner. It's sandwiched between the rest of the boot sequence and is in fact fully optimized spaghetti code. There are a couple of different entry points to the routine. One loads the pointers for the level music, one the pointers for the overworld, and one containing the ending music. At boot, it uses a different entry point which first uploads the NSPC engine itself. There's also "upload misc data", which is the most interesting one for us since we'll use it to upload whatever arbitrary stuff we want to.
So let's look at the standard misc spc upload piece by piece.
Code: Select all
StrtSPCMscUpld:
LDA #$FF ; Get SPC ready for upload
STA $2141
JSR UploadDataToSPC
RTS
Code: Select all
UploadDataToSPC:
SEI ; Disable interrupts
STZ $4200 ; Disable NMI *NOT part of original code*
JSR SPC700UploadLoop
LDA #$80
STA $4200 ; Reenable NMI *NOT part of original code*
CLI ; Reenable interrupts
RTS
Since the game only ever loaded banks during level load while NMI was off, NMI isn't natively shut off. We add that part ourselves so we can load stuff in the middle of a level when NMI is turned ON.
Code: Select all
SPC700UploadLoop:
PHP
REP #$30 ; 16-bit A & X/Y
LDY #$0000 ; set count to 0
LDA #$BBAA ; 0xBBAA is what $2140-41 will contain when
WaitForSPCEcho1: ; the SPC is ready
CMP $2140 ; Keep looping until it is ready
BNE WaitForSPCEcho1 ;
SEP #$20 ; 8-bit A
LDA #$CC ; 0xCC is what we send to the SPC to tell it we're ready
BRA GetLengthADDR
Code: Select all
GetLengthADDR:
PHA
REP #$20 ; 16-bit A
LDA [$00],y ; Get data block length.
INY
INY ; Y+2, point to next word in table
TAX ; LENGTH goes into X
LDA [$00],y ; Get target address.
INY ; Y+2, point to next word in table
INY
STA $2142 ; Send address to APU port 2&3
SEP #$20
CPX #$0001 ; Roll the carry flag from the operation CPX #$0001
LDA #$00 ; into bit 0 of A. in other words, if X is nonzero,
ROL ; load A with 0x01. otherwise A=0x00
STA $2141 ; Send it to APU port 1
ADC #$7F ; If A = 1, overflow flag will be set here
PLA ;
STA $2140 ; Store either 0xCC or our count to APU port 0 signaling that we are ready to transfer
WaitForSPCEcho0:
CMP $2140 ; Wait for the SPC to echo the value we've sent
BNE WaitForSPCEcho0
BVS GetDataBytes ; If the overflow flag was set earlier, branch, there
; is more data to send.
STZ $2140 ; Otherwise, finish up SPC transfer
STZ $2141
STZ $2142
STZ $2143
PLP
Return:
RTS
There's a bit of tricky optimization going on with the overflow flag in there. All I can advise is read the comments and try to see how this saves the routine from using a free byte of RAM somewhere.
Anyway, since there is data to send and the overflow flag is set, we go to GetDataBytes
Code: Select all
GetDataBytes:
LDA [00],y ; Put our first data byte in the high byte of the
INY ; accumulator, and 0x00 (count) in the low byte
XBA
LDA #$00
BRA StoreByte
The routine will start fetching one byte of music data in A and XBAing it into the high byte of A. The low byte is set to $00 because it's going to be used as a counter from now on. The code will constantly switch these back and forth between counting in A and sending a byte of data from A to the SPC.
Code: Select all
StoreByte:
REP #$20 ; Accum (16 bit)
STA $2140
SEP #$20 ; Accum (8 bit)
DEX ; decrement LENGTH
BNE GetDataBytes2
Code: Select all
GetDataBytes2:
XBA ; move count to high byte
LDA [$00],y ; get new data byte in low byte
INY
XBA ; move count back into low byte
WaitForSPCEcho2:
CMP $2140 ; wait for SPC to echo the count
BNE WaitForSPCEcho2
INC A ; increment the count
StoreByte:
` ...
It loops back around to StoreByte until length = 0. Then comes actually the final bit of the routine.
Code: Select all
WaitForSPCEcho3:
CMP $2140 ; wait for SPC to echo count as usual
BNE WaitForSPCEcho3
Add3:
ADC #$03 ; prevent our index count from being 0
BEQ Add3
GetLengthADDR:
...
At this point, the code rolls back into GetLengthADDR. Let's take one last look at that part.
Code: Select all
GetLengthADDR:
PHA
REP #$20 ; 16-bit A
LDA MY_DATA,y ; Get data block length.
INY
INY ; Y+2, point to next word in table
TAX ; LENGTH goes into X
LDA MY_DATA,y ; Get target address.
INY ; Y+2, point to next word in table
INY
STA $2142 ; Send address to APU port 2&3
SEP #$20
CPX #$0001 ; Roll the carry flag from the operation CPX #$0001
LDA #$00 ; into bit 0 of A. in other words, if X is nonzero,
ROL ; load A with 0x01. otherwise A=0x00
STA $2141 ; Send it to APU port 1
ADC #$7F ; If A = 1, overflow flag will be set here
PLA ;
STA $2140 ; Store either 0xCC or our count to APU port 0 signaling that we are ready to transfer
WaitForSPCEcho0:
CMP $2140 ; Wait for the SPC to echo the value we've sent
BNE WaitForSPCEcho0
BVS GetDataBytes ; If the overflow flag was set earlier, branch, there
; is more data to send.
STZ $2140 ; Otherwise, finish up SPC transfer
STZ $2141
STZ $2142
STZ $2143
PLP
Return:
RTS
The audio ports are cleaned up with STZ and the processor flags are finally pulled back, and the routine returns to the wrappers.
Code: Select all
UploadDataToSPC:
SEI ; Disable interrupts
STZ $4200 ; Disable NMI *NOT part of original code*
JSR SPC700UploadLoop
LDA #$80
STA $4200 ; Reenable NMI *NOT part of original code*
CLI ; Reenable interrupts
RTS
Code: Select all
StrtSPCMscUpld:
LDA #$FF ; Get SPC ready for upload
STA $2141
JSR UploadDataToSPC
RTS
3b) The full routine
It's not as long as it first seemed.
Code: Select all
; before calling this routine,
; at $000000, put a 24-bit pointer to
; the data you want to upload
StrtSPCMscUpld:
LDA #$FF ; Get SPC ready for upload
STA $2141
JSR UploadDataToSPC
RTS
UploadDataToSPC:
SEI ; Disable interrupts
STZ $4200 ; Disable NMI *NOT part of original code*
JSR SPC700UploadLoop
LDA #$80
STA $4200 ; Reenable NMI *NOT part of original code*
CLI ; Reenable interrupts
RTS
SPC700UploadLoop:
PHP
REP #$30 ; 16-bit A & X/Y
LDY #$0000
LDA #$BBAA ; 0xBBAA is what $2140-41 will contain when
WaitForSPCEcho1: ; the SPC is ready
CMP $2140 ; Keep looping until it is ready
BNE WaitForSPCEcho1 ;
SEP #$20 ; 8-bit A
LDA #$CC ; 0xCC is what we send to the SPC to tell it we're ready
BRA GetLengthADDR
GetDataBytes:
LDA [$00],y ; Put our first data byte in the high byte of the
INY ; accumulator, and 0x00 in the low byte
XBA
LDA #$00
BRA StoreByte
GetDataBytes2:
XBA
LDA [$00],y
INY
XBA
WaitForSPCEcho2:
CMP $2140
BNE WaitForSPCEcho2
INC A
StoreByte:
REP #$20 ; Accum (16 bit)
STA $2140
SEP #$20 ; Accum (8 bit)
DEX ; decrement LENGTH
BNE GetDataBytes2
WaitForSPCEcho3:
CMP $2140
BNE WaitForSPCEcho3
Add3:
ADC #$03 ; prevent our index count from being 0
BEQ Add3
GetLengthADDR:
PHA
REP #$20 ; 16-bit A
LDA [$00],y ; Get data block length.
INY
INY ; Y+2, point to next word in table
TAX ; LENGTH goes into X
LDA [$00],y ; Get target address.
INY ; Y+2, point to next word in table
INY
STA $2142 ; Send address to APU port 2&3
SEP #$20
CPX #$0001 ; Roll the carry flag from the operation CPX #$0001
LDA #$00 ; into bit 0 of A. in other words, if X is nonzero,
ROL ; load A with 0x01. otherwise A=0x00
STA $2141 ; Send it to APU port 1
ADC #$7F ; If A = 1, overflow flag will be set here
PLA ;
STA $2140 ; Store either 0xCC to APU port 0, signaling that we are ready to transfer, or our nonzero index count
WaitForSPCEcho0:
CMP $2140 ; Wait for the SPC to echo the value we've sent
BNE WaitForSPCEcho0
BVS GetDataBytes ; If the overflow flag was set earlier, branch, there
; is more data to send.
STZ $2140 ; Otherwise, finish up SPC transfer
STZ $2141
STZ $2142
STZ $2143
PLP
Return:
RTS
This is the exact code sitting in every SMW ROM in the world. Unfortunately it returns with RTS and is in bank 0, so you can't call it whenever you want unless you get a bit fancy. What you could do is change the first block to contain an RTL
Code: Select all
StrtSPCMscUpld:
LDA #$FF ; Get SPC ready for upload
STA $2141
JSR UploadDataToSPC
RTL
- CONTINUED IN NEXT POST -

