SNES programming tutorial. Example 10.
Here’s an interesting video on the SPC700 and SNES audio.
Today, we are going to talk about SNES music. The APU (Sony SPC700) is a different chip entirely, and has its own 64k of RAM. At the beginning of our program, we need to load the APU with our SPC file. It is an audio program that runs automatically.
The APU is connected to an 8 channel DSP (digital sound processor). The song will direct the DSP to play different sound samples at different rates to make tones. If you are familiar with MIDI, it is similar. The samples can be looped or not. The samples are compressed into a native compression called BRR (bit rate reduction).
BRR samples are very large, and you will probably be only able to fit 10-15 samples. Each will have to be edited (perhaps with Audacity) to less than a second each, and at a reduced sample rate. We are going to work with SNESGSS (written by Shiru). I got the files from here, and from the source code for one of Shiru’s SNES projects.
However, don’t use the .exe here. Apparently, there is a bug in the SNES hardware, where if the APU reads a value at the exact moment it is changed, it sometimes reads the wrong value. This can cause the game to crash. See the discussion here…
Calima wrote a bug patch, which I have directly patched into the .exe (now called snesgssP.exe (p for patch). You can get it here…
and also grab the music.asm file. You will need it.
Anyway, the patch works, but it forced me to overwrite something, and I chose to disable the streaming function. I tested this on a real SNES dozens of times with no problems.
SNESGSS prefers to have 16-bit MONO WAV samples at sample rate 32000 or 16000. I have tried 8000, but there is no improvement.
There seems to be a bug in Audacity, when you resample to another rate (Tracks/Resample), it seems to work, but then saves it to the original rate. But if you resample it, cut it, and then open a new window and start a new project at the target rate, and then paste it and then save it, it works. I don’t know what’s up with that.
Recording at the desired rate has no problems. 16000 seems to be a nice sweet spot on audio quality and file size.
SNESGSS also suggests tuning the samples to B +21 cents. I did not. I left all my samples at C. They are not in tune with the samples provided with SNESGSS, which I did not use. I think those are tuned to B +21.
Hit the WAV button near the middle of the screen to load your samples. Setting the envelopes similar to this seems good (15, 1, 7, 16) You can press the 2x or 4x buttons if you run out of room for files, to downsample by half. You can set a Loop, and adjust the numbers in “from” and “to”… which I find incredibly difficult to use.
I wouldn’t mess with the volume or EQ settings. That is something you should have done in Audacity while editing. Just keep in mind that the SNES tends to weaken the upper range and make bright sounds feel dull. You might have to do a treble boost for the lead instruments.
This tracker will convert our samples to BRR, but not until your final export. Unfortunately, you can’t import BRR samples to it from other sources. You could use other SNESGSS instruments (the ones that Shiru provided, for example).
Here you can check the size of all the files. Obliviously, you can’t have a bigger SPC file than 64k, the size of the APU RAM.
I should note, that we only load 1 song in the APU RAM at a time. Staring a new song will load a new song (over the previous song), so that only one song is loaded at any time. That should give you a little flexibility on overall size.
Here is the main editor. You type Z-M keys for lower octave, Q-P keys for upper octave. You can change the octave by pressing the octave button. So, this is a standard tracker, it goes downward as the song plays.
You can toggle channels on and off by clicking on the word “channel 1”, etc. You can divide things into sections. Press the spacebar to mark the end of a section. Then you can repeat the previous section with an R00 command.
The order of things is Note, Instrument, Volume 0-99, and Special effects. The SP column is for song speed (smaller is faster). You can scroll up and down with PgUp and PgDn keys, and also Home and End goes to the next section.
CTRL+End marks the end of the song, and CTRL+Home marks the loop back point.
You can import Famitracker and MIDI files (notes only), but I haven’t tried.
On this page, you can mark a song as a “Sound effect”.
Once the songs are done, you File/Export. And that will produce several files.
spc700.bin is our main SPC file. It holds the program and the samples and the sound effects data.
music_1.bin (one file per song) is the song data.
sounds.asm and sounds.h we don’t need. Don’t include them. This was for a different assembler / C compiler. You might want to look at it to find the value of each sound effect.
.define SFX_DING 0
…tells us that the DING sound effect is called with the value zero.
If you look in
you will see a file called Split.py. This is a python script to split the SPC file into 2 pieces if needed. Let me explain that a bit. We are mapping our SNES game as LoROM, which means that our banks are only 32k. The SPC file could be 64k in size. It needs to be split up to be included without editing the linker file.
Also the music loader function will fail to load correctly if it is >32k. Because of that, I have been copying the SPC file to the 7f0000-7fffff WRAM first, and then calling the INIT function (which copies the SPC file to the APU RAM).
The example code, however, is smaller than 32k, so this step is unnecessary.
Let’s go over the music.asm file, which you should have grabbed from one of my example folders. I had to modify the original code to work with ca65.
spc_init – should be called at the start of the game, with interrupts off (NMI, IRQ, controllers). With AXY16 you load A with the address of the of SPC file (spc700.bin) and X with the bank of the SPC file, and JSL to spc_init.
This is all well and good if the SPC file is < 32k, but if it’s over 32k, and we are mapped as LoROM (ROM banks of 32k), I have had to first copy all the SPC files to $7f0000-7fffff WRAM and then load A with 0000 and X with $7f and then JSL to spc_init.
spc_init expects the SPC file to be contiguous.
SPC file > 32k also means I need to either modify the linker script, or split the SPC file into 2 chunks, and include them into 2 different banks. I decided on the later. You could do this in a hex editor. I wrote a little python script to do the same (Split.py). If you had python3 installed, you would call a command line promt and type “Split.py spc700.bin” and it would split it into 2 files (smaller than 32k).
By the way. Running this function takes a long time. It could take 2 seconds or more.
spc_load_data is an internal function, for loading data to the APU RAM.
spc_play_song loads a song (data) to the APU RAM and then starts playing it. This also should be done with interrupts off. Note that this system only loads one song at a time to the APU RAM. If you have a song in and then load another song, it overwrites the first song.
With AXY16 load A with the address of the song data (ike music_1.bin) and load X with the bank of the song, then JSL to spc_play_song. Once it’s done, it will begin playing the song automatically.
spc_command_asm is an internal function. It’s what sends signals to the APU.
spc_stereo is to set mono (default) or stereo audio. Load A (8 or 16) with 0 for mono, 1 for stereo. Audio channels can be panned left or right.
spc_global_volume is to set the max volume, 0-127. It can also be used to fade in or fade out. One of the variables is called speed, and it is the step value, to go from previous volume to the new volume. 255 is the default speed, which is instant change (any value >= 127 would be instant). Speed of 7 seems nice for a fade, and will take 2 seconds to transition. Don’t give it a speed of zero, the volume won’t change.
AXY8 or AXY16, load A with the speed of volume change (1-255), and load X with the new volume (0-127), then jJSL spc_global_volume.
The SNES has a master volume variable, which affects all channels. That’s what this sets, and doesn’t affect individual channel volumes.
spc_channel_volume sets the max volume for an individual audio channel. AXY8 or AXY16, load A with the channels and load X with the volume (0-127) and the JSL to spc_channel_volume. I’m not sure what circumstances I would use this. Maybe to silence or dim a lead instrument, for a change in dramatic tone.
Note, the channel here is a bitfield, with each bit representing a channel.
0000 0001 = channel 1
0000 0010 = channel 2
0000 0100 = channel 3
0000 1000 = channel 4
0001 0000 = channel 5
0010 0000 = channel 6
0100 0000 = channel 7
1000 0000 = channel 8
For example, LDA #$42 (0100 0010) would effect channels 2 and 7.
music_stop stops the song. JSL here.
music_pause will pause and unpause the song (and not effect the sound effects that are playing). Load A (8 or 16) with 1 for pause and 0 for unpause, then JSL here.
sound_stop_all stops all sounds, song and sound effects. JSL here.
sfx_play_center plays a sound effect, pan center. With AXY8 or AXY16, load A with the # of the sound effect, load X with the max volume of the sound effect (0-127), and load Y with the channel (0-7), the sound effect should play. Channel needs to be higher than the max channel for the song playing. Therefore, you must reserve some empty channels in the song, if you want sound effects to play with it.
sfx_play_left, is the same, but pan left.
sfx_play_right, is the same, but pan right.
sfx_play is an internal function that the 3 above functions call.
Streaming has been removed.
This is the audio loading code from the example. It was for an SPC file smaller than 32k.
AXY16 ;copy music to 7f0000 BLOCK_MOVE (music_code_end-music_code), music_code, $7f0000 ;copy the music code and samples to the Audio RAM lda #0000 ldx #$7f ;address 7f0000 jsl spc_init AXY16 lda #$0001 jsl spc_stereo
…and at the bottom we have
.segment "RODATA6" music_code: .incbin "MUSIC/spc700.bin" music_code_end:
In another test project, I had an SPC file bigger than 32k. I split the SPC file in half and put then in ROM banks 6 and 7.
.segment "RODATA6" music_code: .incbin "MUSIC/spc700_1.bin" .segment "RODATA7" music_code2: .incbin "MUSIC/spc700_2.bin" music_code2_end:
And the copying to the APU RAM was the same, except that I had 2 move instructions to copy to $7f0000.
;copy music to 7f0000 BLOCK_MOVE $8000, music_code, $7f0000 BLOCK_MOVE (music_code2_end-music_code2), music_code2, $7f8000 ;copy the music code and samples to the Audio RAM lda #0000 ldx #$7f ;address 7f0000 jsl spc_init
…so that spc_init could load the entire SPC file as one contiguous chunk.
Then I load the song, and start it playing (before I turn on NMI interrupts).
AXY16 lda #.loword(song1) ldx #^song1 jsl spc_play_song
By the way “.loword()” gets a 16 bit value from a 24 bit label. ^ gets the bank of a label.
Now I just need to set up a trigger for the sound effect. We already have that yellow block triggering the screen to go dark, so I just snuck in a little more code there. I didn’t want it re-starting the same sound effect over and over and over each frame, so I added a variable to remember the LAST FRAME, if we were over the yellow block, and skip a trigger in that case.
cmp bright_var2 ;compare to last frame beq Past_Yellow ;skip if last frame is true AXY8 lda #0 ;= ding ldx #127 ;= volume ldy #6 ; = channel jsl sfx_play_center
Our song plays from channels 1-4 (ie. 0-3), and our sound effect uses 2 channels, so we could have set this to 4,5, or 6. This function is zero based index, ie. values 0-7. So 6 means it will play on channels 7 and 8. Sorry for flip flopping between zero based and one based numbers. Hope this isn’t too confusing.
However, if we loaded X with 0,1,2, or 3. It would not play. If we loaded X with 7, only the first channel of the sound effect would play.
Here’s a picture of the demo again. It looks the same as the previous example.
Here’s a Youtube video, if you want to hear it.
There are other programs for getting music onto a Super Nintendo.
You could use SNESMOD with OpenMPT. I still need to research this more before I can recommend it. I have heard that a version of SNESMOD by AugustusBlackheart and KungFuFurby is good. Sorry I can’t be more informative here.
Another program, BRRTools, can convert audio files to BRR. I haven’t used it, but the SNESGSS tool uses the same code. It says you can turn BRR samples into WAV and WAV into BRR. This could be a way to use existing BRR samples in our SNESGSS projects (by using this tool to convert them into WAV files first).