NES Advantage

Making NES Development Less Hard

Adding Music with FamiStudio

No NES game is complete without music. Luckily, there's FamiStudio! This music tracker is custom-built for NES developers, and it does the job nicely. But I found the documentation a bit incomplete, especially when it comes to integrating music into your game, so I wrote this tutorial.

Exporting from FamiStudio

So you've got some cool chiptunes written in FamiStudio and you're ready to add them to your game as background music. Nice! The first step is to export the tracks in the right format. Luckily, FamiStudio makes this easy.

To open the Export Songs window, hit the button in the top left that looks like a document with a big right-facing arrow on it.

On the left side of the Export Songs window is a list of every format FamiStudio can export to. Choose FamiStudio Music Code. Exporting in this format will allow us to play tracks in our game using FamiStudio's NES music engine.

You'll now be presented with various options for your export. Select CA65 from the "Format" dropdown menu. (Obviously you would choose NESASM or ASM6 if you're using one of those compilers.) Leave "Separate Files" and "Generate song list include" unchecked.

Finally, you're given a list of all the songs in your project. Check all the ones you want to include in your game and uncheck the rest. If you have songs and sound effects in the same project, make sure you don't export any of the SFX tracks.

Hit the big checkmark icon to run the export. Give your file a nice, clear name like "mygame_music" and make sure to include an assembly language file extension (.s or .asm). This file will get included in your compiled ROM, so save it somewhere with the file structure of your game in development.

The Other File You Need

Now you have an assembly file containing all your music data. But how do we play it in our game? For that, we need the FamiStudio sound engine. Head over to the FamiStudio website, scroll down to the Downloads section, and download the NES Sound Engine (the one with a cool 6502 icon).

This gets you a .zip file that expands into a folder with lots of interesting stuff in it. For example, there's a demo ROM that lets you try out the FamiStudio engine's functionality by playing various music tracks and sound effects. But really, we only need one file: "famistudio_ca65.s". (Or, again, the file matching your assembler.) Grab this and move it to your development files, next to the music data file you exported from FamiStudio.

Setting Your Settings

To use the sound engine, there are a few settings we need to check before we integrate the files into our game.

First, open the music data file you exported ("mygame_music.s" or whatever). At the very top of this file, you'll see something like this (or at least you will as of version 4.20).

.if FAMISTUDIO_CFG_C_BINDINGS .export _music_data_trapfinder=music_data_trapfinder .endif

Go ahead and comment out these lines, or just delete them. They're meant to support code written in C, but in my experience, the FAMISTUDIO_CFG_C_BINDINGS variable can't be found, so the build breaks. Since we're not using C, we can just remove this bit.

Now, open "famistudio_ca65.s". This is a nicely written bit of code with tons of comments to guide you, but we need to set a few settings for it to run properly.

First, scroll down to around line 76, the section titled "SEGMENT CONFIGURATION". This is where we tell the sound engine where to find our zero page, RAM, and code segments. You'll see three .define statements, one for each of those three segments. For each, make sure the value following the variable name is the name of that segment in your code.

For example, my setup looks like this.

.define FAMISTUDIO_CA65_ZP_SEGMENT ZEROPAGE .define FAMISTUDIO_CA65_RAM_SEGMENT BSS .define FAMISTUDIO_CA65_CODE_SEGMENT CODE

These should match what's defined in the SEGMENT portion of your .cfg file, as well as the .segment statements throughout your code. For example, when I'm using the RAM segment, I call .segment "BSS". Your zero page might be called ZEROPAGE or ZP, your RAM segment is likely BSS or RAM, and so on. Don't use quotation marks in this config.

Immediately below this section in the sound engine is "AUDIO EXPANSION CONFIGURATION". We can ignore this for now; it's only relevant if your game uses a mapper with an audio expansion, like MMC5. Note that the sound engine only supports one extension—you can't turn on both MMC5 and VRC6, for example.

Next comes "GLOBAL ENGINE CONFIGURATION", with another set of options. Make sure the line for your TV type, PAL or NTSC, is live. The other type must be commented out. The next two options must be on if you plan to use sound effects as well as music. I'm less familiar with the rest, but you can safely leave them commented out.

Finally we reach "SUPPORTED FEATURES CONFIGURATION". I'm also not familiar with the meanings of all these options, but they have to do with various composition options within FamiStudio. Basically, if you used a certain option when creating any of your tracks, it must be enabled here, otherwise you'll get playback bugs. If you don't need an option, leave it commented out; this will save RAM in your compiled game.

If you're having playback issues or catching other bugs after adding music, this is a good place to look, especially if an emulator debugger like MESEN's shows you randomly hitting BRK opcodes or the IRQ interrupt. You can easily debug by turning these settings on and off. For example, to get music to play in my first game, I needed to activate FAMISTUDIO_USE_VOLUME_TRACK.

Integrating the Sound Engine

At last, we're ready to integrate the engine into our code! First, let's talk about structure. I currently run all the audio functionality through a simple file called "audio.asm", which contains subroutines to initialize the sound engine, play different music tracks and sound effects, and so on. This file is added via my makefile, as is the sound engine file itself—but not the music data we exported.

... ca65 src/audio/famistudio_ca65.s ca65 src/audio/audio.asm ...

Over in "audio.asm", we bring in everything we'll need from the sound engine, as well as the music data file. All the subroutines being imported here are exported by "famistudio_ca65.s"—they are, essentially, the public API that the sound engine exposes. (If you're also using sound effects, there will be more to import. See here.)

... .import famistudio_init .import famistudio_music_play .import famistudio_music_stop .import famistudio_music_pause .include "trapfinder_music.s" ...

Now all the pieces are in place, so let's make some music!

Initializing the Sound Engine

The first call we have to make to the sound engine is to famistudio_init. This subroutine sets up the music engine by telling it where to find the music data and whether to run in NTSC or PAL. We pass this information using the X, Y, and A registers.

.export audio_init .proc audio_init ; load music data address from "mygame_music.s" LDX #.lobyte(music_data_mygame) LDY #.hibyte(music_data_mygame) ; non-zero for NTSC LDA #1 ; initialize music engine JSR famistudio_init RTS .endproc

This is what my audio intialization subroutine looks like if we leave out the sound effects portion. It's fairly straightforward.

First, we load the low and high bytes of a data address into X and Y. This address points to the music data in the "mygame_music.s" file, and if we look in that file, we'll see an export near the top called music_data_mygame. Yours will be different—"mygame" here is based on the project name you set in FamiStudio—so get the correct export label from your data file. Our subroutine has access to this label because we .included the music data file in the previous step.

All that's left is to set the TV type in the A register. Use 0 for PAL. For NTSC, any non-zero value will do. We call the famistudio_init subroutine, and that's it!

So when do we run this initialization routine? I call it from my top-level main routine, the one that runs the core game loop. At this point the reset interrupt has already run, but nothing else. Obviously we only need to call it once, so place it before the game loop begins.

Loading a Song

With the engine initialized, we can ask it to play us a tune. In the subroutine that runs when my title screen loads (creatively named load_title_screen) I make a call to another routine in "audio.asm", audio_title_screen. This tells the sound engine to play a specific track, which I've chosen as the title screen music. Let's see how it works.

.export audio_title_screen .proc audio_title_screen JSR famistudio_music_stop LDA #0 JSR famistudio_music_play RTS .endproc

Again, pretty straightforward. First, it tells the engine to stop any music that might currently be playing. (I discovered through experimentation that doing this before starting a new track is a necessity for a reliable song-switching experience.) Then we load the track number—zero in this case—into register A and call famistudio_music_play. And... that's it!

How do you know what the track numbers are? Look back at your music data file. The FamiStudio export kindly added comments above each chunk in the music_data_mygame export, giving the name and number of every track. Thanks, export!

Playing Sound

There's only one more step to get music running in our game, and it's a quick one. How does the sound engine know to step forward through a track, playing each note at the right time? For this, we'll rely on the NES's built-in clock: the NMI interrupt. (And all the hard work of FamiStudio's developers, of course.)

Over in your NMI interrupt code, import the famistudio_update subroutine. Then, at the end of the interrupt handler itself, call it.

JSR famistudio_update

That's it! This will trigger the sound engine during every NMI, moving music (and sound effect) tracks forward by one sixtieth-of-a-second frame. (Or fiftieth, if you're running on NTSC.)

Copyright ©2024 Nathaniel Webb/Nat20 Games