Skip navigation
NintendoAge
Welcome, Guest! Please Login or Join
Loading...

Nerdy Nights Sound: Part 6 Tempo, Note Lengths, Buffering and Rests

Oct 23, 2009 at 4:33:19 AM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Last Week: Sound Data, Pointer Tables and Headers

This Week: Tempo, Note Lengths, Buffering and Rests

Timing

Last 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.

Ticker
The 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 frame
stream_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
    
Initializing
Anytime 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 streams
01+     | stream headers (one for each stream)

stream headers:
--------+----------------
byte #  | what it tells us
--------+----------------
00      | which stream (stream number)
01      | status byte
02      | which channel
03      | initial volume (and duty for squares)
04-05   | pointer to data stream
06      | initial tempo

Then 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 Lengths

We 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 Counters
Think 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 6

When 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 Data
Now 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 note

Now we can use these values in our sound data to represent note lengths:

;music data for song 0, square 1 channel
song0_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 constants
thirtysecond = $80
sixteenth = $81
eighth = $82
quarter = $83
half = $84
whole = $85

song0_sq1:
    .byte eighth, C3    ;play a C eighth note
    .byte half, D5      ;play a D half note
    
Pulling from the table
There 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:
    rts

We 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 Lengths

Now 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 = $80
sixteenth = $81
eighth = $82
quarter = $83
half = $84
whole = $85
d_sixteenth = $86
d_eighth = $87
d_quarter = $88
d_half = $89
d_whole = $8A   ;don't forget we are counting in hex

Your 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 Writes

Before, 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 16

We reserved 16 bytes for our temporary ports.  Each one corresponds to an APU port:

soft_apu_ports+0  -> $4000      ;Square 1 ports
soft_apu_ports+1  -> $4001
soft_apu_ports+2  -> $4002
soft_apu_ports+3  -> $4003

soft_apu_ports+4  -> $4004      ;Square 2 ports
soft_apu_ports+5  -> $4005
soft_apu_ports+6  -> $4006
soft_apu_ports+7  -> $4007

soft_apu_ports+8  -> $4008      ;Triangle ports
soft_apu_ports+9  -> $4009 (unused)
soft_apu_ports+10 -> $400A
soft_apu_ports+11 -> $400B

soft_apu_ports+12 -> $400C      ;Noise ports
soft_apu_ports+13 -> $400D (unused)
soft_apu_ports+14 -> $400E
soft_apu_ports+15 -> $400F

Let'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 frame
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    ;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 Sounds
Writing 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 $4003
sound_sq2_old .rs 1  ;the last value written to $4007

Whenever 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_old
se_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
    
Rests

The 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 1
As1 = $01   ;the "s" means "sharp"
Bb1 = $01   ;the "b" means "flat"  A# == Bb, so same value
B1 = $02
;..... other aliases here
F9 = $5c
Fs9 = $5d
Gb9 = $5d
rest = $5e

Now 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 rest
When 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_status
To 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 Byte
76543210
      ||
      |+- 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 Together
Download 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

-------------------------
MetalSlime runs away

My nesdev blog: http://tummaigames.com/blog...


Edited: 11/02/2009 at 05:15 AM by MetalSlime

Oct 23, 2009 at 4:39:44 AM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
This one is long again, but not as long as it looks. Most of the code is stuff we've already written. Wherever I modified an existing subroutine I tried to highlight the new parts in red. I hope that will make it easier to understand the changes.

-------------------------
MetalSlime runs away

My nesdev blog: http://tummaigames.com/blog...

Oct 23, 2009 at 5:12:22 AM
removed-07-06-2016 (214)

< Bowser >
Posts: 5018 - Joined: 06/26/2008
Other
Profile
Man, you're tutorials are so thorough. Fantastic job!

Oct 23, 2009 at 9:06:21 AM
Mario's Right Nut (352)
avatar
(Cunt Punch) < Bowser >
Posts: 6634 - Joined: 11/21/2008
Texas
Profile
Oh god...my brain hurts.
Excellent job though. This is awesome!

-------------------------

This is my shiny thing, and if you try to take it off me, I may have to eat you.

Check out my dev blog.


Oct 23, 2009 at 11:30:01 AM
udisi (88)
avatar
< King Solomon >
Posts: 3270 - Joined: 11/15/2006
United States
Profile
Sweet.

Oct 23, 2009 at 11:14:22 PM
udisi (88)
avatar
< King Solomon >
Posts: 3270 - Joined: 11/15/2006
United States
Profile
well, somehow this broke my splash screen tune, but I do have it working with my sound effects. If I recall though I had to initialize it weird before and had issues with it.

well, looks like after the next week comes out, I will actually be able to put music in. need the looping and envelopes.

now back to trying to fix the stupid splash tune.

Oct 24, 2009 at 12:23:54 AM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Here are some changes I can think of off the top of my head that would need to be made between the old sound format and the new:

1) In your song headers, make sure you add a tempo field for each stream
2) Make sure you add note lengths into your sound data.  For each stream, the very first byte should be a note length.

Can you send me the song data for your song and sfx (in this thread or in a PM)?  Maybe if I see it I can spot the problem.  It's also possible that my code has a bug that I didn't catch.

-------------------------
MetalSlime runs away

My nesdev blog: http://tummaigames.com/blog...

Oct 24, 2009 at 12:54:59 AM
udisi (88)
avatar
< King Solomon >
Posts: 3270 - Joined: 11/15/2006
United States
Profile
I can send it, but I'm pretty sure it has to do with my code. I had problems with it in the last sound engine. I set the current song and jsr sound_load in my gamestate, but I don't sound_init till a certain spot in the animation(which is in a whole other function). The song works, cause I can run it on the title screen instead of a sound effect. If you don't find anything, I'll go look at my build right before this one and see.

It's the only way I could get it to start when I wanted it to before, without a constant tone playing before the sound before.

Here's the engine, it's pretty much just checking for the start of the "homebrew animation" which is the cue to load the song. after that it's just a delay after games pops up before going to the title screen.

EngineSplash:
LDA rowcount
CMP #$01
BCS godelay
LDA #$02
STA current_song
JSR sound_load
godelay:
LDA game2
CMP #$9F
BEQ INCdelay
JMP GameEngineDone
INCdelay:
INC delaycount
LDA delaycount
CMP #$40
BEQ loadgame
JMP GameEngineDone
loadgame:
JSR ClrSprites
JSR sound_disable
LDA #BGTITLE
STA BG_ptr
STA ATT_ptr
JSR loadbackground
JSR loadattribute
JSR LoadTitle
JMP GameEngineDone

here's the actual hackish animation for the splash page. no laughing at my ugly code note the JSR sound_init is right when the first sprites of the "H" are suppose to move into place.

splashanim:

LDA beerpnt
CMP #$04
BNE INCbeer
JMP Homebrew

INCbeer:
INC beercount
LDA beercount
CMP #$20
BEQ beer1
JMP beerover
beer1:
LDA beerpnt
CMP #$00
BNE beer2

LoadPalettesbeer:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadPalettesbeerLoop:
LDA palettebeer, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$20 ; Compare X to hex $20, decimal 32 - copying 32 bytes = 8 palettes
BNE LoadPalettesbeerLoop ; Branch to LoadPalettesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down

LDA beerpnt
CLC
ADC #$01
STA beerpnt
LDA #$00
STA beercount
JMP beerover
beer2:
LDA beerpnt
CMP #$01
BNE beer3

LoadPalettesbeer2:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadPalettesbeer2Loop:
LDA palettebeer2, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$20 ; Compare X to hex $20, decimal 32 - copying 32 bytes = 8 palettes
BNE LoadPalettesbeer2Loop ; Branch to LoadPalettesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down

LDA beerpnt
CLC
ADC #$01
STA beerpnt
LDA #$00
STA beercount
JMP beerover
beer3:
LDA beerpnt
CMP #$02
BNE beer4

LoadPalettesbeer3:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadPalettesbeer3Loop:
LDA palettebeer3, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$20 ; Compare X to hex $20, decimal 32 - copying 32 bytes = 8 palettes
BNE LoadPalettesbeer3Loop ; Branch to LoadPalettesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down

LDA beerpnt
CLC
ADC #$01
STA beerpnt
LDA #$00
STA beercount
JMP beerover
beer4:

LDA beerpnt
CMP #$03
BNE beerover

LoadPalettesbeer4:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadPalettesbeer4Loop:
LDA palettebeer4, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$20 ; Compare X to hex $20, decimal 32 - copying 32 bytes = 8 palettes
BNE LoadPalettesbeer4Loop ; Branch to LoadPalettesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down

LDA #$04
STA beerpnt
LDA #$00
STA beercount

beerover:
RTS

Homebrew:

LDA rowcount
CMP #$0C
BNE INChomebrew
JMP Games

INChomebrew:

INC homebrewcount
LDA homebrewcount
CMP #$08
BEQ loadrow1
JMP hbover
loadrow1:
LDA rowcount
CMP #$00
BNE loadrow2

JSR sound_init
LDA #$5F
STA hone1
LDA #$67
STA hone2

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow2:
LDA rowcount
CMP #$01
BNE loadrow3

LDA #$57
STA htwo1
LDA #$5F
STA htwo2
LDA #$67
STA htwo3

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow3:
LDA rowcount
CMP #$02
BNE loadrow4

LDA #$4F
STA hthree1
LDA #$57
STA hthree2
LDA #$5F
STA hthree3

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow4:
LDA rowcount
CMP #$03
BNE loadrow5

LDA #$4F
STA hfour1
LDA #$57
STA hfour2
LDA #$5F
STA hfour3

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow5:
LDA rowcount
CMP #$04
BNE loadrow6

LDA #$4F
STA hfive1
LDA #$57
STA hfive2

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow6:
LDA rowcount
CMP #$05
BNE loadrow7

LDA #$4F
STA hsix1
LDA #$57
STA hsix2

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow7:
LDA rowcount
CMP #$06
BNE loadrow8

LDA #$4F
STA hseven1
LDA #$57
STA hseven2

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow8:
LDA rowcount
CMP #$07
BNE loadrow9

LDA #$4F
STA height1
LDA #$57
STA height2

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow9:
LDA rowcount
CMP #$08
BNE loadrow10

LDA #$4F
STA hnine1
LDA #$57
STA hnine2
LDA #$5F
STA hnine3

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow10:
LDA rowcount
CMP #$09
BNE loadrow11

LoadPalettes5S:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadPalettes5SLoop:
LDA palette5S, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$20 ; Compare X to hex $20, decimal 32 - copying 32 bytes = 8 palettes
BNE LoadPalettes5SLoop ; Branch to LoadPalettesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down

LDA #$5F
STA hten2

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow11:
LDA rowcount
CMP #$0A
BNE loadrow12

LoadPalettes6S:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ; write the high byte of $3F00 address
LDA #$00
STA $2006 ; write the low byte of $3F00 address
LDX #$00 ; start out at 0
LoadPalettes6SLoop:
LDA palette6S, x ; load data from address (palette + the value in x)
; 1st time through loop it will load palette+0
; 2nd time through loop it will load palette+1
; 3rd time through loop it will load palette+2
; etc
STA $2007 ; write to PPU
INX ; X = X + 1
CPX #$20 ; Compare X to hex $20, decimal 32 - copying 32 bytes = 8 palettes
BNE LoadPalettes6SLoop ; Branch to LoadPalettesLoop if compare was Not Equal to zero
; if compare was equal to 32, keep going down

LDA #$5F
STA heleven2
LDA #$67
STA heleven3

LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
loadrow12:
LDA rowcount
CMP #$0B
BNE hbover

LDA #$5F
STA htweleve1
LDA #$67
STA htweleve2


LDA rowcount
CLC
ADC #$01
STA rowcount
LDA #$00
STA homebrewcount
JMP hbover
hbover:

RTS

Games:

INC gamecount
LDA gamecount
CMP #$20
BNE gamesdone
LDA #$97
STA game1
LDA #$9F
STA game2
gamesdone:

RTS






Edited: 10/24/2009 at 12:56 AM by udisi

Oct 24, 2009 at 6:05:57 AM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
I'll take a closer look at the code when I have a little more time (ie, after the kid goes to bed), but right off the bat the call to sound_init looks fishy. sound_init only needs to be called once, and normally you would call it on startup. Try pulling the jsr sound_init out of the game engine code and sticking it somewhere in your reset code.

Once your sound engine is initialized, you should never have to touch sound_init again. Just call it once on startup and you're done.

-------------------------
MetalSlime runs away

My nesdev blog: http://tummaigames.com/blog...

Oct 24, 2009 at 1:55:20 PM
udisi (88)
avatar
< King Solomon >
Posts: 3270 - Joined: 11/15/2006
United States
Profile
I've tried that. I didn't do that initially cause I didn't want the music to start upon startup. you can load the song and use the initialization function to play when you want it to start.

I actually use the initialization and disable functions throughout. If I have a screen without music, I want to disable, otherwise the song from the previous page will continue to play on the new page.

Oct 25, 2009 at 5:57:57 PM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Originally posted by: udisi

I've tried that. I didn't do that initially cause I didn't want the music to start upon startup. you can load the song and use the initialization function to play when you want it to start.

I actually use the initialization and disable functions throughout. If I have a screen without music, I want to disable, otherwise the song from the previous page will continue to play on the new page.


Music won't play on startup if you don't load a song, so you don't have to worry about that anymore.  The intended usage of the engine is this:

1) call sound_init on startup (in reset)
2) when you want to play a song or sound effect, call sound_load
3) when you want the engine to be quiet (like when changing states/screens), play the silence song (see song0.i included in tempo.zip)

You shouldn't be using sound_disable/sound_init to silence and restart the engine like that.  Everything should be done through sound_load.  You'll save on ROM space this way (fewer JSRs) and you'll have perfect control of when a song/sfx starts and finishes.

-------------------------
MetalSlime runs away

My nesdev blog: http://tummaigames.com/blog...

Oct 25, 2009 at 6:25:20 PM
udisi (88)
avatar
< King Solomon >
Posts: 3270 - Joined: 11/15/2006
United States
Profile
I'll give it a try tonight and see if it helps. I have a lot of stuff happening in the splash screen. palette changes, and sprite movements, maybe there's some register conflicts or something.

Oct 25, 2009 at 9:00:48 PM
udisi (88)
avatar
< King Solomon >
Posts: 3270 - Joined: 11/15/2006
United States
Profile
ok, progress. It had nothing to do with sound_init, sound_load, etc. It had something to do with duty cycle. In the old engine, I made a mistake, when I changed the duty cycle and had enabled the length counter and envelope volume. %10001111, was the value. In the new engine, it has to be %10111111. I had done a bad hex to binary conversion. works fine no matter where I initialize sound, etc. I knew it was probably my issue lol. once again, one damn value screwed things up.

Only thing weird now, is the tune is more fluid. before it seemed more choppy, and I haven't figured out how to get that choppy sound back.


Edited: 10/25/2009 at 09:05 PM by udisi

Oct 27, 2009 at 1:53:13 AM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Typo
Glad you got it fixed.  Little value errors like that are an annoying problem.  I get them all the time too .  One way to avoid this kind of error is to use constants/aliases, which we have been doing for the notes, channels, streams and note lengths.  We could make some constants for the different duty cycles:

duty_0 =        %00110000 ;bottom 4 bits blank, because we will OR with volume
duty_25 =       %01110000
duty_50 =       %10110000
duty_25_neg = %11110000

and then in our song files, instead of declaring the duty_vol byte like this:

.byte %10111111 ;duty 50%, volume F, but I might make a typo!

we could declare it like this:

.byte duty_50 | $0F ;the assembler will translate this into %10111111

At least I think that will work.  I think nesasm reads the "|" as a logical OR.

Sound
The difference in sound (smooth vs. choppy) is probably the result of turning off the saw envelopes and length counter. The saw envelope is the one built-in volume envelope you get with the hardware.  It's a simple volume-decreasing envelope (down to 0), so when you had it enabled it you probably got some hush on the notes.  Enabling the length counter would take control of the note lengths, and the length value that we've been feeding to $4003/$4007 would make those notes quite short.  That's my guess as to why you had a choppy sound before.  

When you turned saw envelopes and the length counter off, you basically told the square to give you a constant volume (which you set to F: %xxxx1111) for full-length notes, so the notes will have the same volume for the whole duration of the note.  Across several notes this gives a fluid sound.

If I'm right about the cause of the sound change (hard to know for sure without actually hearing what you mean by "choppy"), we should be able to fix it when we get to volume envelopes, since that is how we will control note volume and length.  You will be able to create your own volume envelope that gives you short, quieting notes.

Next "week's" lesson is supposed to be opcodes and looping.  I was going to try to fit volume envelopes in too if it didn't get too long, but I was also thinking about splitting it into two lessons.  I wasn't satisfied with the length of the last two lessons (too long).  If I do split them, do you have a preference for which topic I cover first (looping vs. volume envelopes)?  Maybe it will be easier if I cover volume envelopes first.  The tutorials might come faster if I do it that way too.  I'll think about it.

-------------------------
MetalSlime runs away

My nesdev blog: http://tummaigames.com/blog...

Oct 27, 2009 at 11:30:17 AM
udisi (88)
avatar
< King Solomon >
Posts: 3270 - Joined: 11/15/2006
United States
Profile
I would think envelopes would be preference. The music, I'm gonna use is pretty involved, and has envelopes and vibrato, etc. I've been dying to try some roughed in tunes, but the engine hasn't been capable yet.

Looping probably more logical step, but some of the bells and whistles are needed for me personally to try out some of the music that is planned for my game. It uses a lot of duty cycle sweeps, and volume envelopes. If I had those, I should be able to at least put in some partial tunes and see how they play through the engine.

So far though, must say, I'm liking the versatility of the engine, and the simplicity of the song data.

Dec 13, 2009 at 6:41:34 PM
ddribin (0)

(Dave Dribin) < Cherub >
Posts: 11 - Joined: 10/19/2009
United States
Profile
Hey MetalSlime,

Thanks a bunch for these tutorials.  They've been very interesting, and I've been having fun with the sound engine. However, I think I found a bug in sound_load in regards to stream_ticker_total.  In this article, you mention:


Originally posted by: MetalSlime
Then 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.

Then, in the code, it gets initialized to $A0:

    lda #$A0
    sta stream_ticker_total, x

The problem is if a song has a tempo of less than $60, then the ticker won't roll over on the first frame. To hear this, change the tempo of song 2 to $10 and then play it twice in a row. The second time it gets played, the last note is briefly played again.

If I initialize stream_ticker_total to $FF in sound_load, then it seems to fix it. Is this the best fix? What was the reasoning behind using $A0?

Thanks,

-Dave



Dec 14, 2009 at 6:21:58 AM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Originally posted by: ddribin

Hey MetalSlime,

Thanks a bunch for these tutorials.  They've been very interesting, and I've been having fun with the sound engine. However, I think I found a bug in sound_load in regards to stream_ticker_total.  In this article, you mention:


Originally posted by: MetalSlime
Then 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.

Then, in the code, it gets initialized to $A0:

    lda #$A0
    sta stream_ticker_total, x

The problem is if a song has a tempo of less than $60, then the ticker won't roll over on the first frame. To hear this, change the tempo of song 2 to $10 and then play it twice in a row. The second time it gets played, the last note is briefly played again.

If I initialize stream_ticker_total to $FF in sound_load, then it seems to fix it. Is this the best fix? What was the reasoning behind using $A0?

Thanks,

-Dave



Thanks for the comments.  Glad you are finding the tutorials useful! 

You are right, at low tempo values the rollover is late.  $FF sounds like a good fix.  As for what I was thinking using $A0 I'm not sure.  I originally coded in a 16-bit counter for even more tempo precision, but later cut it because the tutorial was getting too long.  With a 16-bit counter, the tempo values tend to be higher so maybe $A0 seemed like a high enough initial value to me.  $FF seems like a better choice right now though.  Nice catch!

I had planned to make the 16-bit counter a homework idea, but looks like I forgot to add it at the end.

Thanks a lot!


-------------------------
MetalSlime runs away

My nesdev blog: http://tummaigames.com/blog...