Last Week:
Sound Data, Pointer Tables and HeadersThis Week: Tempo, Note Lengths, Buffering and Rests
TimingLast
week we put together a huge chunk of the sound engine. We finally got
it to play something that resembled a song. But we had big limitations
when it came to timing and note lengths. We were using a single frame
counter to keep time across all 6 streams of the sound engine. This is
a problem because it imposes the music's speed on our sound effects.
If you were to use such a system in a real game, your sound effects
would speed up or slow down whenever you change to a faster or slower
song.
We also made the mistake of advancing the sound engine
when our frame counter hit its mark, but skipping it when it didn't.
What happens if a game event triggers a sound effect one or two frames
after the counter hits its mark? The sound effect won't start until
the next time our counter reaches its mark - we have to wait for it!
There will be a delay. Not good.
Worst of all, our frame
counter also doesn't allow for variable note lengths. Unless every
song you write is going to consist of only 32nd notes, this is a
problem. It becomes apparent then that we need a more complex timing
system.
Tempo
We'll correct the first two problems by ripping out the universal
counter and giving each stream it's own private counter. We'll also
change our method of counting. Our old method of counting frames and
taking action when we reach a certain number is very limited. For
example, let's say that we have a song and our frame counter is taking
action every 4 frames. Maybe the song sounds a tad faster than we
want, so we slow it down by changing the speed to update once every 5
frames. But now the song sounds too slow. The speed we really want is
somewhere in between 4 and 5, but we can't get there with our frame
counting method. Instead we'll use a ticker.
TickerThe ticker method involves taking a number (a
tempo)
and adding it to a total, frame by frame. Eventually, that total will
wraparound from FF->00 and when it does the carry flag will be set
(a
tick). This carry flag tick will be the signal we look for to advance our stream.
For
example, let's say our tempo value is $40 and our total starts at $00.
After one frame we will add our tempo to the total. $00 + $40 = $40.
Now our total is $40. Another frame goes by (2). We add our tempo to
the total again. $40 + $40 = $80. Our total is $80. Another frame
goes by (3). $80 + $40 = $C0. Another frame goes by (4). $C0 + $40 =
$00. Carry flag is set. TICK! A tick tells us that it is time to
advance this stream. When we finish updating, we start adding again
until we get another tick.
As you can see, a tempo value of $40
will advance our stream once every 4 frames. If you do some math (256
/ 5), you will discover that a tempo of $33 will advance the stream
roughly every 5 frames. If $40 is too fast for your song and $33 is
too slow, you still have the values $34-$39 to experiment with. Much
more versatile! To see why this works, let's see what happens with a
tempo value of say $36:
$00 + $36 + $36 + $36 + $36 + $36 = $0E (Tick in 5 frames)$0E + $36 + $36 + $36 + $36 + $36 = $1C (Tick in 5 frames)$1C + $36 + $36 + $36 + $36 = $02 (Tick in 4 frames)$02 + $36 + $36 + $36 + $36 + $26 = $10 (Tick in 5 frames)A
tempo of $36 produces a tick every 5 frames most of the time, but
sometimes it only takes 4 frames. You might think that this disparity
would make our song sound uneven, but really a single frame only lasts
about 1/60 of a second. Our ears won't notice. It will sound just
right to us.
Here is some code that demonstrates how to implement a ticker:
stream_tempo .rs 6 ;the value to add to our ticker total each framestream_ticker_total .rs 6 ;our running ticker total.sound_play_frame: lda sound_disable_flag bne .done ;if disable flag is set, don't advance a frame ldx #$00.loop: lda stream_status, x and #$01 beq .endloop ;if stream disabled, skip this stream ;add the tempo to the ticker total. If there is a FF-> 0 transition, there is a tick lda stream_ticker_total, x clc adc stream_tempo, x sta stream_ticker_total, x bcc .endloop ;carry clear = no tick. if no tick, we are done with this stream jsr se_fetch_byte ;else there is a tick, so do stuff ;do more stuff.endloop: inx cpx #$06 bne .loop.done: rts InitializingAnytime we add a new feature to our sound engine we will want to ask ourselves the following questions:
1) Is this a feature that needs to be initialized for each song/sfx?
2) If so, are the values we use to initialize the feature variable (ie, not necessarily the same for every song/sfx)?
If the answer to question #1 is yes, we will have to update sound_load to initialize the feature.
If
the answer to question #2 is also yes, we will have to add a field to
the song header format. The values to plug into the initialization are
different for each song, so the songs' headers will need to provide
those values for us.
In the case of our new timing scheme, we
have two variables that need to be initialized: sound_ticker_total and
sound_tempo. Of the two, only sound_tempo will be variable. Different
songs will have different tempos, but they won't need to have different
starting sound_ticker_totals. So we will have to add one new field to
our song header format for tempo:
main header:--------+----------------byte # | what it tells us--------+----------------00 | number of streams01+ | stream headers (one for each stream)stream headers:--------+----------------byte # | what it tells us--------+----------------00 | which stream (stream number)01 | status byte02 | which channel03 | initial volume (and duty for squares)04-05 | pointer to data stream06 | initial tempoThen
we will need to edit sound_load to read this new byte for each stream
and store it in RAM. We'll also want to initialize stream_ticker_total
to some fixed starting value, preferably a high one so that the first
tick will happen without a delay. Finally, we will have to update all
of our songs to include tempos in their headers.
Note LengthsWe
still have the problem of note lengths. Songs are made up of notes of
variable length: quarter notes, eighth notes, sixteenth notes, etc.
Our sound engine needs to be able to differentiate between different
note lengths. But how? We will use note length counters.
Note Length CountersThink
of the fastest note you'd ever need to play, say a 32nd note. Since
that will be our fastest note, we'll give it the smallest count
possible: $01. The next fastest note is a 16th note. In music, a 16th
note equals two 32nd notes. In other words, a 16th note lasts twice as
long as a 32nd note. So we will give it a count value that is twice
the count value of our 32nd note: $02. The next fastest note is an 8th
note. An 8th note equals two 16th notes. It is twice as long as a
16th note. So its count value will be twice that of the 16th note:
$04. Going all the way up to a whole note, we can produce a lookup
table like this:
note_length_table: .byte $01 ;32nd note .byte $02 ;16th note .byte $04 ;8th note .byte $08 ;quarter note .byte $10 ;half note .byte $20 ;whole note We'll add more entries later for things like dotted quarter notes, but for now this is sufficient to get us started.
To play different note lengths, we will give each stream a note length counter:
stream_note_length_counter .rs 6When
a note is played, for example an 8th note, its count value will be
pulled from the note_length_table and stored in the stream's note
length counter. Then every time a tick occurs we will decrement the
counter. When the note length counter reaches 0, it will signal to us
that our note has finished playing and it is time for the next note.
To say it another way, a note's count value is simply how many ticks it
lasts. An eighth note is 4 ticks long. A quarter note is 8 ticks
long. A half note is 16 ticks long ($10).
Note Lengths in DataNow
we need to add note lengths to our sound data. Recall that we
specified that byte values in the range of $80-$9F were note lengths:
se_fetch_byte: lda stream_ptr_LO, x sta sound_ptr lda stream_ptr_HI, x sta sound_ptr+1 ldy #$00 lda [sound_ptr], y bpl .note ;if < #$80, it's a Note cmp #$A0 bcc .note_length ;else if < #$A0, it's a Note Length.opcode: ;else it's an opcode ;do Opcode stuff.note_length: ;do note length stuff.note: ;do note stuff So
the first byte value that we can use for note lengths is $80. We are
going to be reading from a lookup table (note_length_table above), so
we should assign the bytes in the same order as the lookup table.
-----+--------------byte | note length-----+--------------$80 | 32nd note$81 | 16th note$82 | 8th note$83 | quarter note$84 | half note$85 | whole noteNow we can use these values in our sound data to represent note lengths:
;music data for song 0, square 1 channelsong0_sq1: .byte $82, C3 ;play a C eighth note .byte $84, D5 ;play a D half note Of
course, memorizing which byte value corresponds to which note length is
a pain. Let's create some aliases to make it easier on us when we are
creating our sound data:
;note length constantsthirtysecond = $80sixteenth = $81eighth = $82quarter = $83half = $84whole = $85song0_sq1: .byte eighth, C3 ;play a C eighth note .byte half, D5 ;play a D half note Pulling from the tableThere
is a small problem here. Lookup tables index from 0. This wasn't a
problem for note values (C5, D3, G6) because our note range started
from 0 ($00-$7f). But our note length data has a range of $80-$9F.
Somehow we will need to translate the note length byte that comes from
the data stream into a number we can use to index into our table. In
other words, we need to figure out a way to turn $80 into $00, $81 into
$01, $82 into $02, etc. Anything come to mind?
If you thought
"just subtract $80 from the note length value", give yourself a
cookie. If you thought "just chop off the 7-bit", give yourself two
cookies. Both solutions work, but the second solution is a little bit
faster and only takes one instruction to perform:
se_fetch_byte: lda stream_ptr_LO, x sta sound_ptr lda stream_ptr_HI, x sta sound_ptr+1 ldy #$00.fetch: lda [sound_ptr], y bpl .note ;if < #$80, it's a Note cmp #$A0 bcc .note_length ;else if < #$A0, it's a Note Length.opcode: ;else it's an opcode ;do Opcode stuff.note_length: ;do note length stuff and #%01111111 ;chop off bit7 sty sound_temp1 ;save Y because we are about to destroy it tay lda note_length_table, y ;get the note length count value sta stream_note_length_counter, x ;stick it in our note length counter ldy sound_temp1 ;restore Y iny ;set index to next byte in the stream jmp .fetch ;fetch another byte.note: ;do note stuff Notice
that we jump back up to .fetch after we set the note length counter.
This is so that we can read the note that will surely follow the note
length in the data stream. If we simply stop after setting the note
length, we'll know how long to play, but we won't know which note to
play!
Here's an updated sound_play_frame routine that implements
both the ticker and the note length counters. Notice how the note
length counter is only decremented when we have a tick, and we only
advance the stream when the note length counter reaches zero:
sound_play_frame: lda sound_disable_flag bne .done ;if disable flag is set, don't advance a frame ldx #$00.loop: lda stream_status, x and #$01 beq .endloop ;if stream disabled, skip this stream ;add the tempo to the ticker total. If there is a FF-> 0 transition, there is a tick lda stream_ticker_total, x clc adc stream_tempo, x sta stream_ticker_total, x bcc .endloop ;carry clear = no tick. if no tick, we are done with this stream dec stream_note_length_counter, x ;else there is a tick. decrement the note length counter bne .endloop ;if counter is non-zero, our note isn't finished playing yet jsr se_fetch_byte ;else our note is finished. Time to read from the data stream ;do more stuff. set volume, note, sweep, etc.endloop: inx cpx #$06 bne .loop.done: rtsWe
have one last change to make. When we load a new song we will want it
to start playing immediately, so we should initialize the
stream_note_length_counter in the sound_load routine to do just that.
Our sound_play_frame routine decrements the counter and takes action if
the result is zero. Therefore, to ensure that our song starts
immediately, we should initialize our stream_note_length_counter to $01:
;somewhere inside the loop of sound_load lda #$01 sta stream_note_length_counter, x And
now our engine supports note lengths. But there is still room for
improvement. What if we want to play a series of 8th notes? Not an
uncommon thing to have in music. Here is how our data would have to
look now:
sound_data: .byte eighth, C5, eighth, E5, eighth, G5, eighth, C6, eighth, E6, eighth, G6, eighth, C7 ;Cmajor That's
a lot of "eighth" bytes. Wouldn't it be better to just state "eighth"
once, and assume that all notes following it are eighth notes? Like
this:
sound_data: .byte eighth, C5, E5, G5, C6, E6, G6, C7 ;Cmajor That
saved us 6 bytes of ROM space. And if you consider that a game may
have 20+ songs, each with 4 streams of data, each with potentially
several strings of equal-length notes, this kind of change might save
us hundreds, maybe even thousands of bytes! Let's do it.
To
pull this off, we will have to store the current note length count
value in RAM. Then when our note length counter runs to 0, we will
refill it with our RAM count value.
stream_note_length .rs 6 ;note length count value;-------se_fetch_byte: lda stream_ptr_LO, x sta sound_ptr lda stream_ptr_HI, x sta sound_ptr+1 ldy #$00.fetch: lda [sound_ptr], y bpl .note ;if < #$80, it's a Note cmp #$A0 bcc .note_length ;else if < #$A0, it's a Note Length.opcode: ;else it's an opcode ;do Opcode stuff.note_length: ;do note length stuff and #%01111111 ;chop off bit7 sty sound_temp1 ;save Y because we are about to destroy it tay lda note_length_table, y ;get the note length count value sta stream_note_length, x ;save the note length in RAM so we can use it to refill the counter sta stream_note_length_counter, x ;stick it in our note length counter ldy sound_temp1 ;restore Y iny ;set index to next byte in the stream jmp .fetch ;fetch another byte.note: ;do note stuff ;--------sound_play_frame: lda sound_disable_flag bne .done ;if disable flag is set, don't advance a frame ldx #$00.loop: lda stream_status, x and #$01 beq .endloop ;if stream disabled, skip this stream ;add the tempo to the ticker total. If there is a FF-> 0 transition, there is a tick lda stream_ticker_total, x clc adc stream_tempo, x sta stream_ticker_total, x bcc .endloop ;carry clear = no tick. if no tick, we are done with this stream dec stream_note_length_counter, x ;else there is a tick. decrement the note length counter bne .endloop ;if counter is non-zero, our note isn't finished playing yet lda stream_note_length, x ;else our note is finished. reload the note length counter sta stream_note_length_counter, x jsr se_fetch_byte ;Time to read from the data stream ;do more stuff.endloop: inx cpx #$06 bne .loop.done: rts Adding
those 4 lines of code just saved us hundreds of bytes of ROM space. A
nice tradeoff for 6 bytes of RAM. Now our data will be made up of
"strings", where we have a note length followed by a series of notes:
sound_data: .byte eighth, C5, E5, G5, C6, E6, G6, quarter, C6 ;six 8th notes and a quarter note Easy to read and easy to write.
Other Note LengthsNow that everything is setup, we can add more
note lengths to our note_length_table. Dotted notes are very common in
music. Dotted notes are equal in length to the note plus the next
fastest note. For example, a dotted quarter note = a quarter note + an
8th note. A dotted 8th note = an 8th note + a 16th note. Let's add
some dotted notes to our table:
note_length_table:
.byte $01 ;32nd note
.byte $02 ;16th note
.byte $04 ;8th note
.byte $08 ;quarter note
.byte $10 ;half note
.byte $20 ;whole note
;---dotted notes
.byte $03 ;dotted 16th note
.byte $06 ;dotted 8th note
.byte $0C ;dotted quarter note
.byte $18 ;dotted half note
.byte $30 ;dotted whole note?
The
actual order of our note_length_table doesn't matter. We just have to
make sure our aliases are in the same order as the table:
;note length constants (aliases)thirtysecond = $80sixteenth = $81eighth = $82quarter = $83half = $84whole = $85d_sixteenth = $86d_eighth = $87d_quarter = $88d_half = $89d_whole = $8A ;don't forget we are counting in hexYour
music will determine what other entries you'll need to add to your note
length table. If one of your songs has a really really long note, like
3 whole notes tied together, add it to the table ($60) and make an
alias for it (whole_x3). If your song contains a note that is seven
8th notes long (a half note plus a dotted quarter note tied together),
add it to the table ($1C) and make an alias for it (seven_eighths).
Buffering APU WritesBefore, we've been writing to the
APU one stream at a time. If two different streams shared a channel,
they would both write to the same APU ports. If three streams were to
share a channel, which is possible if there are two different sound
effects loaded into SFX_1 and SFX_2, all three would write to the same
APU ports in the same frame. This is bad practice. It can also cause
some unwanted noise on the square channels.
A better method is
to buffer our writes. Instead of writing to the APU ports directly,
each stream will instead write its data to temporary ports in RAM.
We'll keep our loop order, so sfx streams will still overwrite the
music streams. Then when all the streams are done, we will copy the
contents of our temporary RAM ports directly to the APU ports all at
once. This ensures that the APU ports only get written to once per
frame max. To do this, we first need to reserve some RAM space for our
temporary port variables:
soft_apu_ports .rs 16We reserved 16 bytes for our temporary ports. Each one corresponds to an APU port:
soft_apu_ports+0 -> $4000 ;Square 1 portssoft_apu_ports+1 -> $4001soft_apu_ports+2 -> $4002soft_apu_ports+3 -> $4003soft_apu_ports+4 -> $4004 ;Square 2 portssoft_apu_ports+5 -> $4005soft_apu_ports+6 -> $4006soft_apu_ports+7 -> $4007soft_apu_ports+8 -> $4008 ;Triangle portssoft_apu_ports+9 -> $4009 (unused)soft_apu_ports+10 -> $400Asoft_apu_ports+11 -> $400Bsoft_apu_ports+12 -> $400C ;Noise portssoft_apu_ports+13 -> $400D (unused)soft_apu_ports+14 -> $400Esoft_apu_ports+15 -> $400FLet's
implement this by working backwards. First we will edit
sound_play_frame and pull our call to se_set_apu out of the loop. We
do this because we only want to write to the APU once, after all the
streams are done looping:
;--------------------------; sound_play_frame advances the sound engine by one framesound_play_frame: lda sound_disable_flag bne .done ;if disable flag is set, don't advance a frame ldx #$00.loop: lda stream_status, x and #$01 ;check whether the stream is active beq .endloop ;if the channel isn't active, skip it ;add the tempo to the ticker total. If there is a FF-> 0 transition, there is a tick lda stream_ticker_total, x clc adc stream_tempo, x sta stream_ticker_total, x bcc .endloop ;carry clear = no tick. if no tick, we are done with this stream dec stream_note_length_counter, x ;else there is a tick. decrement the note length counter bne .endloop ;if counter is non-zero, our note isn't finished playing yet lda stream_note_length, x ;else our note is finished. reload the note length counter sta stream_note_length_counter, x jsr se_fetch_byte ;snip .endloop: inx cpx #$06 bne .loop jsr se_set_apu.done: rts Next we will modify se_set_apu to copy the temporary APU ports to the real APU ports:
se_set_apu: ldy #$0F.loop: cpy #$09 beq .skip ;$4009 is unused cpy #$0D beq .skip ;$400D is unused lda soft_apu_ports, y sta $4000, y.skip: dey bpl .loop ;stop the loop when Y is goes from $00 -> $FF rts Now
we have to write the subroutine that will populate the temporary APU
ports with a stream's data. This part will get more complicated as we
add more features to our sound engine, but for now it's quite simple:
se_set_temp_ports: lda stream_channel, x asl a asl a tay lda stream_vol_duty, x sta soft_apu_ports, y ;vol lda #$08 sta soft_apu_ports+1, y ;sweep lda stream_note_LO, x sta soft_apu_ports+2, y ;period LO lda stream_note_HI, x sta soft_apu_ports+3, y ;period HI rts We
will make the call to se_set_temp_ports after our call to
se_fetch_byte, where the old se_set_apu call was before we snipped it
out of the loop. Notice that we don't bother to check the channel
before writing the sweep. se_set_apu takes care of this part for us.
There's no harm in writing these values to RAM, so we'll avoid
branching here to simplify the code.
Crackling SoundsWriting
to the 4th port of the Square channels ($4003/$4007) has the side
effect of resetting the sequencer. If we write here too often, we will
get a nasty crackling sound out of our Squares. This is not good.
The
way our engine is setup now, we call se_set_apu once per frame.
se_set_apu writes to $4003/$4007, so these ports will get written to
once per frame. This is too often. We need to find a way to write
here less often. We will do this by cutting out redundant writes. If
the value we want to write this frame is the same as the value written
last frame, skip the write.
First we will need to keep track of what was last written to the ports. This will require some new variables:
sound_sq1_old .rs 1 ;the last value written to $4003sound_sq2_old .rs 1 ;the last value written to $4007Whenever
we write to one of these ports, we will also write the value to the
corresponding sound_port4_old variable. Saving this value will allow
us to compare against it next frame. To implement this, we will have
to unroll our loop in se_set_apu:
se_set_apu:.square1: lda soft_apu_ports+0 sta $4000 lda soft_apu_ports+1 sta $4001 lda soft_apu_ports+2 sta $4002 lda soft_apu_ports+3 sta $4003 sta sound_sq1_old ;save the value we just wrote to $4003.square2: lda soft_apu_ports+4 sta $4004 lda soft_apu_ports+5 sta $4005 lda soft_apu_ports+6 sta $4006 lda soft_apu_ports+7 sta $4007 sta sound_sq2_old ;save the value we just wrote to $4007.triangle: lda soft_apu_ports+8 sta $4008 lda soft_apu_ports+10 sta $400A lda soft_apu_ports+11 sta $400B.noise: lda soft_apu_ports+12 sta $400C lda soft_apu_ports+14 sta $400E lda soft_apu_ports+15 sta $400F rts Now
we have a variable that will keep track of the last value written to a
channel's 4th port. The next step is to add a check before we write:
se_set_apu:.square1: lda soft_apu_ports+0 sta $4000 lda soft_apu_ports+1 sta $4001 lda soft_apu_ports+2 sta $4002 lda soft_apu_ports+3 cmp sound_sq1_old ;compare to last write beq .square2 ;don't write this frame if they were equal sta $4003 sta sound_sq1_old ;save the value we just wrote to $4003.square2: lda soft_apu_ports+4 sta $4004 lda soft_apu_ports+5 sta $4005 lda soft_apu_ports+6 sta $4006 lda soft_apu_ports+7 cmp sound_sq2_old beq .triangle sta $4007 sta sound_sq2_old ;save the value we just wrote to $4007.triangle: lda soft_apu_ports+8 sta $4008 lda soft_apu_ports+10 ;there is no $4009, so we skip it sta $400A lda soft_apu_ports+11 sta $400B.noise: lda soft_apu_ports+12 sta $400C lda soft_apu_ports+14 ;there is no $400D, so we skip it sta $400E lda soft_apu_ports+15 sta $400F rts Finally
we have to consider initialization. The only case we really have to
worry about is the first time a song is played in the game. Consider
what happens if we initialize the sound_sq1_old and sound_sq2_old
variables to $00. We are essentially saying that on startup (RESET)
the last byte written to $4003/$4007 was a $00, which isn't true of
course. On startup, no write has ever been made to these ports. If we
initialize to $00, and if the first note of the first song played has a
$00 for the high 3 bits of its period, it will get skipped. That is
not what we want. Instead, we should initialize these variables to
some value that will never be written to $4003/$4007, like $FF. This
ensures that the first note(s) played in the game won't be skipped.
sound_init: lda #$0F sta $4015 ;enable Square 1, Square 2, Triangle and Noise channels lda #$00 sta sound_disable_flag ;clear disable flag ;later, if we have other variables we want to initialize, we will do that here. lda #$FF sta sound_sq1_old sta sound_sq2_oldse_silence: lda #$30 sta soft_apu_ports ;set Square 1 volume to 0 sta soft_apu_ports+4 ;set Square 2 volume to 0 sta soft_apu_ports+12 ;set Noise volume to 0 lda #$80 sta soft_apu_ports+8 ;silence Triangle rts RestsThe final topic we will cover this lesson is rests. A
rest
is a period of silence in between notes. Like notes, rests can be of
variable length: quarter rest, half rest, whole rest, etc. In other
words a rest is a silent note.
So how will we implement it? We
will handle rests by considering a rest to be special case note. We
will give the rest a dummy period in our note table. Then, when we
fetch a byte from the data stream and determine the byte to be a note,
we will add an extra check to see if that note is a rest. If it is, we
will make sure that it shuts up the stream.
First let's add the
rest to our note table. We will give it a dummy period. It doesn't
really matter what value we use. I'm going to give it a period of
$0000. We will also want to add the rest to our list of note aliases:
note_table: .word $07F1, $0780, etc... ;....more note table values here .word $0000 ;rest. Last entry ;Note: octaves in music traditionally start at C, not A A1 = $00 ;the "1" means Octave 1As1 = $01 ;the "s" means "sharp"Bb1 = $01 ;the "b" means "flat" A# == Bb, so same valueB1 = $02;..... other aliases hereF9 = $5cFs9 = $5dGb9 = $5drest = $5eNow
we can use the symbol "rest" in our music data. "rest" will evaluate
to the value $5E, which falls within our note range ($00-$7F). When
our sound engine encounters a $5E in the data stream, it will pull the
period ($0000) from the note table and store it in RAM. A period of
$0000 is actually low enough to silence the square channels, but the
triangle channel is still audible at this period so we have more work
to do.
Checking for a restWhen we encounter a rest,
we will want to tell the sound engine to shut this stream up until the
next note. The rest functions differently from all the other notes, so
we will need to make a special check for it in our code. We will make
a subroutine se_check_rest to do this for us:
se_fetch_byte: lda stream_ptr_LO, x sta sound_ptr lda stream_ptr_HI, x sta sound_ptr+1 ldy #$00.fetch: lda [sound_ptr], y bpl .note ;if < #$80, it's a Note cmp #$A0 bcc .note_length ;else if < #$A0, it's a Note Length.opcode: ;else it's an opcode ;do Opcode stuff.note_length: ;do Note Length stuff.note: ;do Note stuff sty sound_temp1 ;save our index into the data stream asl a tay lda note_table, y sta stream_note_LO, x lda note_table+1, y sta stream_note_HI, x ldy sound_temp1 ;restore data stream index ;check if it's a rest jsr se_check_rest .update_pointer: iny tya clc adc stream_ptr_LO, x sta stream_ptr_LO, x bcc .end inc stream_ptr_HI, x.end: rts se_check_rest
will check to see if the note value is equal to $5E or not. If it is,
we will need to tell the sound engine to silence the stream. If the
note isn't equal to $5E, we can go on our merry way.
How will we
silence our stream then? This is actually a little complicated.
Recall that the stream's volume (stream_vol_duty) is set in the song's
header. se_set_temp_ports copies the value of stream_vol_duty to
soft_apu_ports. If we have se_check_rest modify the stream_vol_duty
variable directly (set it to 0 volume), the old volume value
disappears. We won't know what to restore it to when we are done with
our rest. Oh no!
What we will want to do instead is leave
stream_vol_duty alone. We will copy it into soft_apu_ports every frame
as usual. Then, after the copy we will check to see if we are
currently resting. If we are, we will make another write
soft_apu_ports with a value that will set the volume to 0. Make sense?
stream_statusTo
do this we will need to keep track of our resting status in a
variable. If our sound engine encounters a $5E in the data stream,
we'll turn our resting status on. If it's not, we'll turn our resting
status off. There are only two possibilities: on or off. Rather than
declare a whole new block of variables and waste six bytes of RAM,
let's assign one of the bits in our stream_status variable to be our
rest indicator:
Stream Status Byte76543210 || |+- Enabled (0: stream disabled; 1: enabled) +-- Rest (0: not resting; 1: resting) Our new subroutine se_check_rest will be in charge of setting or clearing this bit of the status byte:
se_check_rest: lda [sound_ptr], y ;read the note byte again cmp #rest ;is it a rest? (==$5E) bne .not_rest lda stream_status, x ora #%00000010 ;if so, set the rest bit in the status byte bne .store ;this will always branch. bne is cheaper than a jmp..not_rest: lda stream_status, x and #%11111101 ;clear the rest bit in the status byte.store: sta stream_status, x rts Then we modify se_set_temp_ports to check the rest bit and silence the stream if it is set:
se_set_temp_ports: lda stream_channel, x asl a asl a tay lda stream_vol_duty, x sta soft_apu_ports, y ;vol lda #$08 sta soft_apu_ports+1, y ;sweep lda stream_note_LO, x sta soft_apu_ports+2, y ;period LO lda stream_note_HI, x sta soft_apu_ports+3, y ;period HI ;check the rest flag. if set, overwrite volume with silence value lda stream_status, x and #%00000010 beq .done ;if clear, no rest, so quit lda stream_channel, x cmp #TRIANGLE ;if triangle, silence with #$80 beq .tri lda #$30 ;else, silence with #$30 bne .store ;this will always branch. bne is cheaper than a jmp..tri: lda #$80.store: sta soft_apu_ports, y.done: rts That's it. Now our engine supports rests! They work just like notes, so their lengths are controlled with note lengths:
song_data: ;this data has two quarter rests in it. .byte half, C2, quarter, rest, eighth, D4, C4, quarter, B3, rest Putting It All TogetherDownload and unzip the
tempo.zip sample files. Make sure the following files are in the same folder as NESASM3:
tempo.asm
sound_engine.asm
tempo.chr
note_table.i
note_length_table.i
song0.i
song1.i
song2.i
song3.i
song4.i
song5.i
tempo.bat
Double click tempo.bat. That will run NESASM3 and should produce the tempo.nes file. Run that NES file in FCEUXD SP.
Use the controller to select songs and play them. Controls are as follows:
Up: Play
Down: Stop
Right: Next Song/SFX
Left: Previous Song/SFX
Song0
is a silence song. Not selectable. tempo.asm "plays" song0 to stop
the music when you press down. See song0.i to find out how it works.
Song1 is last week's evil sounding series of minor thirds, but much faster now thanks to tempo settings.
Song2 is the same short sound effect from last week.
Song3
is a simple descending chord progression. We saved some bytes in the
triangle data using note lengths (compare to last week's file)
Song4 is a new song that showcases variable note lengths and rests.
Song5
is a short sound effect. It plays 10 notes extremely fast. Play it
over songs and see how it steals the SQ2 channel from the music.
Try
creating your own songs and sound effects and add them into the mix.
To add a new song you will need to take the following steps:
1)
create a song header and song data (use the included songs as
reference). Don't forget to add tempos for each stream in your
header. Data streams are terminated with $FF.
2) add your header to the song_headers pointer table at the bottom of sound_engine.asm
3) update the constant NUM_SONGS to reflect the new song number total (also at the bottom of sound_engine.asm)
Although
not necessary, I recommend keeping your song data in a separate file
like I've done with song0.i, song1.i, song2.i and song3.i. This makes
it easier to find the data if you need to edit your song later. If you
do this, don't forget to .include your file.
Next Week:
Volume Envelopes