Last Week:
Periods, Lookup TablesThis Week: Sound Engine Basics. We will setup the framework to get our sound engine running.
Sound EngineNow
that we know how to get notes to play we can start thinking about our
sound engine. What do we want it to be able to do? How will the main
program interact with it?
It's good practice to separate the
different pieces of your program. The sound engine shouldn't be
messing with main program code and vice-versa. If you mix them, your
code becomes harder to read, the danger of variable conflicts increases
and you open yourself up to hard-to-find bugs. If you keep the
different pieces of your program separate, you get the opposite: your
code reads well, you avoid variable conflicts, and bugs are easier to
trace. Separation also improves your ability to reuse code. If your
sound engine only accesses its own internal routines and variables, it
makes it that much easier to pull it out from one game and plug it into
another.
There has to be some communication between the main
program and the sound engine of course. The main program needs to be
able to tell the sound engine to do things like: "Play song 2" or "shut
up". But we don't want the main program sticking its nose in the sound
engine's business. We only want it to issue commands. The sound
engine will handle the rest on its own.
To set this up, we will
create a small set of subroutines that the main program can use to
invoke the sound engine and give it commands. I'll call these
subroutines "
entrances". We want as few entrances into the
sound engine as possible. The sound engine itself will have several
internal subroutines it can work with, but the main program will only
use the entrances.
EntrancesSo what will our entrance
subroutines be? We need to think about what the main program would
need to tell the sound engine to do. Here is a list of entrances we
might want for our sound engine:
-Initialize sound engine (sound_init)
-Load new song/sfx (sound_load)
-Play a frame of music/sfx (sound_play_frame)
-Disable sound engine (sound_disable)
The
names in paranthesis are what I'm going to call the subroutines in
code. I prefixed them with sound_ for readability. You can tell at a
glance that they are sound routines. Here is a rundown of what our
commands will do:
sound_init will enable channels and silence them. It will also initialize sound engine variables.
sound_load
will take a song/sfx number as input. It will use that song number to
index into a table of pointers to song headers. It will read the
appropriate header and set up sound engine variables. If that didn't
make sense, don't worry. We'll be covering this stuff next week.
sound_play_frame
will advance the sound engine by one frame. It will run the note
timers, read from the data streams (if necessary), update sound
variables and make writes to the APU ports. This stuff will also be
covered in future weeks.
sound_disable will disable channels via $4015 and set a disable flag variable.
We
already know enough to knock out two of those, sound_init and
sound_disable. Let's write them now. We'll write skeleton code for
the other entrance subroutines as well. A few things to mention before
we do that though:
RAMA sound engine requires a lot
of RAM. A large sound engine might even take up a full page of RAM.
For this tutorial, we'll stick all our sound engine variables on the
$300 page of RAM. There is nothing magic about this number. I chose
$300 for convenience. $000 is your zero-page RAM. $100 is your
stack. If you completed the original Nerdy Nights series, $200 will be
your Sprite OAM. So $300 is next in line.
ROMThe
sound engine itself won't require a lot of ROM space for code, but if
you have a lot of music your song data might take up a lot of space.
For this reason, I'm going to change our header to give us two 16k
PRG-ROM banks, like this:
.inesprg 2 ;2x 16kb PRG code Now
we have twice as much ROM space, just in case we need it. BTW, this is
the maximum amount of ROM we can have without using a mapper.
Noise ChannelI
purposely haven't covered the Noise channel yet. We will want to
silence it in our init code though, so I will go ahead and teach that
much. Noise channel volume is controlled via port $400C. It works the
same as $4000/$4004 does for the Square channels, except there is no
Duty Cycle control:
NOISE_ENV ($400C)76543210 |||||| ||++++- Volume |+----- Saw Envelope Disable (0: use internal counter for volume; 1: use Volume for volume) +------ Length Counter Disable (0: use Length Counter; 1: disable Length Counter) Like the Squares, we will silence the Noise channel by setting both disable flags, and setting the Volume to 0.
Skeleton Sound EngineLet's
write the entrance subroutines to our sound engine. Most of this code
should be very familiar to you if you completed the first three
tutorials in this series.
.rsset $0300 ;sound engine variables will be on the $0300 page of RAM sound_disable_flag .rs 1 ;a flag variable that keeps track of whether the sound engine is disabled or not. ;if set, sound_play_frame will return without doing anything. .bank 0 .org $8000 ;we have two 16k PRG banks now. We will stick our sound engine in the first one, which starts at $8000.sound_init: lda #$0F sta $4015 ;enable Square 1, Square 2, Triangle and Noise channels lda #$30 sta $4000 ;set Square 1 volume to 0 sta $4004 ;set Square 2 volume to 0 sta $400C ;set Noise volume to 0 lda #$80 sta $4008 ;silence Triangle lda #$00 sta sound_disable_flag ;clear disable flag ;later, if we have other variables we want to initialize, we will do that here. rts sound_disable: lda #$00 sta $4015 ;disable all channels lda #$01 sta sound_disable_flag ;set disable flag rtssound_load: ;nothing here yet rts sound_play_frame: lda sound_disable_flag bne .done ;if disable flag is set, don't advance a frame ;nothing here yet.done: rts Driving the Sound EngineWe
have the framework setup for our sound engine to run. The main program
now has subroutines it can call to issue commands to the sound engine.
Most of them don't do anything yet, but we can still integrate them
into the main program. First we will want to make a call to sound_init
somewhere in our reset code:
RESET: sei cld ldx #$FF txs inx ;... clear memory, etc jsr sound_init ;... more reset stuff Next
we need something to drive our sound engine. Music is time-based. In
any piece of music, assuming a constant tempo, each quarter note needs
to last exactly as long as every other quarter note. A whole note has
to be exactly as long as four quarter notes. If our sound engine is
going to play music, it needs to be time-based as well. We have a
subroutine, sound_play_frame, that will advance our sound engine a
frame at a time. Now we need to ensure it gets called repeatedly at a
regular time interval.
One way to do this is to stick it in the
NMI. Recall that when enabled, the NMI will trigger at the start of
every vblank. Vblank is the only safe time to write to the PPU, so the
NMI is typically full of drawing code. We don't want to waste our
precious vblank time running sound code, but what about after we are
finished drawing? If we stick our call to sound_play_frame at the end
of NMI, after the drawing code, we are set. sound_play_frame gets
called once per frame, and we avoid stepping on the PPU's toes. And
since sound_play_frame doesn't write to the PPU registers, it doesn't
matter if our sound code spills out of vblank.
Let's setup the NMI to drive our sound engine:
NMI: pha ;save registers txa pha tya pha ;do sprite DMA ;update palettes if needed ;draw stuff on the screen ;set scroll jsr sound_play_frame ;run our sound engine after all drawing code is done. ;this ensures our sound engine gets run once per frame. lda #$00 sta sleeping ;did you do your homework and read Disch's document last week? ;http://nesdevhandbook.googlepages... pla ;restore registers tay pla tax pla rti
.includeTo further separate our sound engine from the main
program, we can keep all our sound engine code in a separate file.
NESASM3 gives us a directive .include that we can use to copy a source
file into our main program. We actually used this directive last week
to include the note_table.i file, which contained our period lookup
table.
Using .include to copy a source file into our code is
very similar to how we use .incbin to import a .chr file. Assuming our
sound engine code is saved in a file called sound_engine.asm, we will
add the following code to our main program:
.include "sound_engine.asm"We
will continue to include note_table.i, but since it is part of our
sound engine we will stick the .include directive in the
sound_engine.asm file.
It's not bad practice to use includes a
lot. You can pull your joypad routines out and stick them in their own
file. You can have separate files for your gamestate code, for your
PPU routines and for just about anything else you can think of.
Breaking up your code like this will make it easier to find things as
your program gets larger and more complicated. It also makes it easier
to plug your old routines into new programs.
Putting It All TogetherDownload
and unzip the
skeleton.zip sample files. Make sure skeleton.asm,
sound_engine.asm, skeleton.chr, note_table.i, sound_data.i and
skeleton.bat are all in the same folder as NESASM3, then double click
skeleton.bat. That will run NESASM3 and should produce the skeleton.nes
file. Run that NES file in FCEUXD SP.
I've hardcoded sound_load
and sound_play_frame to play a little melody on the Square 1 channel.
It uses a simple frame counter to control note speed. The data for the
music is found in the sound_data.i file. Use the controller to
interact with the sound engine. Controls are as follows:
A: Play sound from the beginning (sound_load)
B: Initialize the sound engine (sound_init)
Start: Disable sound engine (sound_disable)
Try
editing sound_engine.asm to change the data stream that
sound_play_frame reads from. The different data streams available are
located in sound_data.i. Try adding your own data stream to
sound_data.i too. Use the note symbols we made last week and terminate
your data stream with $FF.
Homework: Write two new sound engine entrance subroutines for the main program to use:
1. sound_pause: pauses playback of the sound, but retains the current position in the data stream.
2. sound_unpause: if the sound is currently paused, resumes play from the saved position.
Then modify handle_joypad to allow the user to pause/unpause the music.
Homework #2: If the ideas presented in Disch's
The Frames and NMIs document are still fuzzy in your head, read it again.
Extra Credit:
See if you can understand how my drawing buffer works. Use Disch's
document to help you. I won't cover drawing buffers in these sound
tutorials, for obvious reasons, but it is definitely worth your time to
learn how to use them.
Next week:
Sound Data, Pointer Tables, Headers