Skip navigation
Welcome, Guest! Please Login or Join

Loading...

Nerdy Nights Sound: Part 9 Finite Loops, Key Changes, Chord Progressions

Nov 29, 2009 at 10:38:36 PM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Last Week: Opcodes and Looping

This Week: More opcodes: Finite Loops, Key Changes, Chord Progressions

Opcodes
Last week we learned how to use opcodes.  Opcodes allow a song's streams to call a subroutine mid-play.  This is a very powerful tool.  We learned some of the most common opcodes: infinite loop (really a jump), change volume envelopes and change duty cycles.  Today we are going to expand on opcodes and learn some cool opcode tricks that can save us a lot (!) of bytes and time.

Finite Looping

Last week we added the infinite loop opcode, which was really just an unconditional jump back to an earlier part of the song.  Today we're going to add a finite loop opcode.  A finite loop opcode tells the sound engine to repeat a particular section of a song X times, where X is some number defined by you.  In the Battle Kid theme song I added last week there is a passage that looks like this:

    .byte sixteenth
    .byte A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4
    .byte A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4
    .byte A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4
    .byte A3, C4, E4, A4, A3, E4, E3, E2
    
This is really just the same 4 notes repeated over and over again.  Wouldn't it be cooler if we could do something like this instead:

    .byte sixteenth
    .byte A3, C4, E4, A4
    .byte loop_13_times_please
    .byte A3, E4, E3, E2
    
That saves a lot of bytes.  We go from 56 bytes all the way down to around 10.  The Battle Kid song actually plays this same phrase on both square channels, so really we go from 100+ bytes down to 20 or so.  That's a big deal!  If we consider how common repetitions of 4 or 8 occur in music, we can easily see that having a finite loop opcode could potential save us hundreds if not thousands of bytes in our sound data.

Finite Looping?
So what is a finite loop really?  We saw that with an infinite loop it was really more like an unconditional jump.  When the sound engine hits the infinite loop opcode, it jumps back, always, no matter what, no questions asked.   A finite loop on the other hand is a conditional jump.  It checks a counter.  If the counter isn't 0 it jumps.  If it is 0, it doesn't jump.  

Loop Counter

First things first we need a loop counter.  Each stream will have the ability to loop, so each stream will need its own loop counter:

stream_loop1 .rs 6  ;loop counter variable (one for each stream)

We will want to initialize this to 0 in our sound_load code:

    lda #$00
    sta stream_loop1, x

Next we will need a way to set this counter to some value.  Some games bundle this up together in the finite loop opcode, but I prefer to make it its own opcode:

;-----------------------------------------------------------------------
;this is our JUMP TABLE!
sound_opcodes:
    .word se_op_endsound            ;$A0
    .word se_op_infinite_loop       ;$A1
    .word se_op_change_ve           ;$A2
    .word se_op_duty                ;$A3
    .word se_op_set_loop1_counter   ;$A4
    ;etc, one entry per subroutine
 
;these are aliases to use in the sound data.
endsound = $A0
loop = $A1
volume_envelope = $A2
duty = $A3
set_loop1_counter = $A4
 
se_op_set_loop1_counter:
    lda [sound_ptr], y      ;read the argument (# times to loop)
    sta stream_loop1, x     ;store it in the loop counter variable
    rts

Now we have an easy way to set the loop counter any time we want, like this:

    ;somewhere in sound data:
    .byte set_loop1_counter, $04    ;repeat 4 times

Looping With The Counter

Our finite loop opcode will work like the infinite loop opcode, with two changes:

1) it will decrement the loop counter
2) it will check the result and only jump on a non-zero result

Let's write it:

;-----------------------------------------------------------------------
;this is our JUMP TABLE!
sound_opcodes:
    .word se_op_endsound            ;$A0
    .word se_op_infinite_loop       ;$A1
    .word se_op_change_ve           ;$A2
    .word se_op_duty                ;$A3
    .word se_op_set_loop1_counter   ;$A4
    .word se_op_loop1               ;$A5
    ;etc, one entry per subroutine
 
;these are aliases to use in the sound data.
endsound = $A0
loop = $A1
volume_envelope = $A2
duty = $A3
set_loop1_counter = $A4
loop1 = $A5
 
se_op_loop1:
    dec stream_loop1, x     ;decrement the counter
    lda stream_loop1, x     ;and check it
    beq .last_iteration     ;if zero, we are done looping
.loop_back:
    lda [sound_ptr], y      ;read ptr LO from the data stream
    sta stream_ptr_LO, x    ;update our data stream position
    iny
    lda [sound_ptr], y      ;read ptr HI from the data stream
    sta stream_ptr_HI, x    ;update our data stream position
 
    sta sound_ptr+1         ;update the pointer to reflect the new position.
    lda stream_ptr_LO, x
    sta sound_ptr
    ldy #$FF                ;after opcodes return, we do an iny.  Since we reset  
                            ;the stream buffer position, we will want y to start out at 0 again.
    rts
.last_iteration:
    iny                     ;skip the first byte of the address argument
                            ; the second byte will be skipped automatically upon return
                            ; (see se_fetch_byte.  There is an "iny" after "jsr se_opcode_launcher")
    rts
    
Now we can loop.  To use the Battle Kid example above, we go from this (56 bytes):

    .byte A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4
    .byte A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4
    .byte A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4, A3, C4, E4, A4
    .byte A3, C4, E4, A4, A3, E4, E3, E2
    
to this (13 bytes):
    
    .byte set_loop1_counter, 13 ;repeat 13 times.
.intro_loop:                    ;make sure our loop point is AFTER we set the counter!
    .byte A3, C4, E4, A4        ;the phrase to repeat.
    .byte loop1                 ;finite loop opcode
    .word .intro_loop           ;address to jump back to
 
    .byte A3, E4, E3, E2        ;the last 4 notes
    
Pretty nice savings.  Chances are we will be using this opcode set a lot.

Bonus
We can save a few more bytes here.  You may have noticed that the code in the .loop_back section of our finite loop opcode is identical to the infinite loop code:

se_op_loop1:
 
    ;---snip---
 
.loop_back:
    lda [sound_ptr], y      ;read ptr LO from the data stream
    sta stream_ptr_LO, x    ;update our data stream position
    iny
    lda [sound_ptr], y      ;read ptr HI from the data stream
    sta stream_ptr_HI, x    ;update our data stream position
 
    sta sound_ptr+1         ;update the pointer to reflect the new position.
    lda stream_ptr_LO, x
    sta sound_ptr
    ldy #$FF                ;after opcodes return, we do an iny.  Since we reset  
                            ;the stream buffer position, we will want y to start out at 0 again.
    rts

    ;---snip---
    
Compare with:

se_op_infinite_loop:
    lda [sound_ptr], y      ;read ptr LO from the data stream
    sta stream_ptr_LO, x    ;update our data stream position
    iny
    lda [sound_ptr], y      ;read ptr HI from the data stream
    sta stream_ptr_HI, x    ;update our data stream position
 
    sta sound_ptr+1         ;update the pointer to reflect the new position.
    lda stream_ptr_LO, x
    sta sound_ptr
    ldy #$FF                ;after opcodes return, we do an iny.  Since we reset  
                            ;the stream buffer position, we will want y to start out at 0 again.
    rts
    
Why have identical code in two places?  Let's cut out the whole .loop_back section and replace it with a "jmp se_op_infinite_loop":

se_op_loop1:
    dec stream_loop1, x     ;decrement the counter
    lda stream_loop1, x     ;check the counter
    beq .last_iteration     ;if zero, we are done looping
    jmp se_op_infinite_loop ;if not zero, loop back
.last_iteration:
    iny                     ;skip the first byte of the address argument
                            ; the second byte will be skipped automatically upon return
                            ; (see se_fetch_byte after "jsr se_opcode_launcher")
    rts
    
Multiple Finite Loops
You may have been wondering why I named the finite loop opcode "loop1".  Why stick a 1 on the end there?  This is because sometimes one finite loop opcode isn't enough.  Consider the following song structure.  Assume each letter represents a long series of notes:

    A A A B C
    A A A B C
    A A A B C
    A A A B C
    
With one finite loop opcode you could reduce it to this:

    (A A A B C)x4
    
But if you had two finite loop opcodes available, you could nest them to reduce it even further:

    (Ax3 B C)x4
    
If the music you write has a lot of patterns like this, it may be worth your while to have two or more finite loop opcodes available to you so that you can nest them.  To add another finite loop opcode you need to:

1) declare another loop counter variable block in RAM (stream_loop2 .rs 6)
2) initialize the new loop counter to 0 in the sound_load routine.
3) add a new opcode for setting the new loop counter (se_op_set_loop2_counter)
4) add a new opcode to check the new counter and loop (se_op_loop2)
5) make sure to add the new opcodes to the jump table and give them an alias (set_loop2_counter, loop2).


Each finite loop opcode you add requires 6 bytes of RAM (a limited resource!), so please consider carefully if it is worth the tradeoff.  It all depends on your music data.


Changing Keys

Another useful feature to have is the ability to change keys.  Imagine you write a song and you have it all done.  Then at the last minute you decide you want it to be in another key, say a step (2 notes) lower.  Rather than rewrite the whole song by hand (it takes forever), wouldn't it be nice if there was an opcode that you could set to automatically subtract two from every note?  What if you have a song pattern that gets played in more than one key (a rhythm track for a Blues song, for example)?  We could save lots of bytes if we can figure out a way to write the pattern once, and then loop it while changing keys each iteration.  Let's do it.

Note Offset
We will implement keys by having a note offset variable:

stream_note_offset .rs 6    ;note offset

The note offset is a value that gets added to the note value before pulling the period out of the note_table.  We will initialize stream_note_offset to 0 so that the default behavior is to add 0 to the note (resulting in no change).  However, if we set stream_note_offset to some value via an opcode, it will change the notes.  Here is an updated se_fetch_byte that demonstrates how this works:

se_fetch_byte:
    ;...snip...
.note:
    ;do Note stuff
    sty sound_temp1     ;save our index into the data stream
    clc
    adc stream_note_offset, x   ;add note offset
    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

    ;...snip...
    
Imagine what would happen if we have stream_note_offset set to 2.  Say we read a C4 note from the data stream:
    
1. A C4 note is equivalent to hex value #$1b (see aliases in note_table.i)
2. we add stream_note_offset to this value.  #$1b + #$02 = #$1d.
3. hex value #$1d is equivalent to a D4 note (see note_table.i)
4. wow, we raised the note up a step!

Using the same value for stream_note_offset, if we had a string of notes like this:

C4, E4, G4, B4, C5, E5, G5, E5, B5, C6 ;Cmaj7

it would get translated to:

D4, Fs4, A4, C#5, D5, Fs5, A5, C#6, D6 ;Dmaj7

Using stream_note_offset we can easily transpose entire sections of music into other keys.  As mentioned above, we will initialize a stream's stream_note_offset to zero:

sound_load:
    ;---snip---
    lda #$00
    sta stream_note_offset, x
    ;---snip---
    
Set Note Offset
Now let's make an opcode that will set stream_note_offset to a specific value:

;-----------------------------------------------------------------------
;this is our JUMP TABLE!
sound_opcodes:
    .word se_op_endsound            ;$A0
    .word se_op_infinite_loop       ;$A1
    .word se_op_change_ve           ;$A2
    .word se_op_duty                ;$A3
    .word se_op_set_loop1_counter   ;$A4
    .word se_op_loop1               ;$A5
    .word se_op_set_note_offset     ;$A6
 
;these are aliases to use in the sound data.
endsound = $A0
loop = $A1
volume_envelope = $A2
duty = $A3
set_loop1_counter = $A4
loop1 = $A5
set_note_offset = $A6

se_op_set_note_offset:
    lda [sound_ptr], y          ;read the argument
    sta stream_note_offset, x      ;set the note offset.
    rts

Now we can set the note offset anytime we want in the data stream:

;oops, after writing the song, I realized I wanted it to be in D instead.  No problem.
sound_data:
    .byte set_note_offset, 2
    .byte C2, C3, C4, C5, ;etc.. more notes in the key of C.
    
Adjust Note Offset
Setting the note offset to a specific value has very limited application.  It's like a one-time keychange.  More often we will want to set the note offset to some relative value.  For example, instead of setting stream_note_offset to 2, we might want to set stream_note_offset to "the current offset + 2".  If we had an opcode that let us adjust stream_note_offset by a relative value, we could use it together with loops.  First let's write the opcode:

;-----------------------------------------------------------------------
;this is our JUMP TABLE!
sound_opcodes:
    .word se_op_endsound            ;$A0
    .word se_op_infinite_loop       ;$A1
    .word se_op_change_ve           ;$A2
    .word se_op_duty                ;$A3
    .word se_op_set_loop1_counter   ;$A4
    .word se_op_loop1               ;$A5
    .word se_op_set_note_offset     ;$A6
    .word se_op_adjust_note_offset  ;$A7
 
;these are aliases to use in the sound data.
endsound = $A0
loop = $A1
volume_envelope = $A2
duty = $A3
set_loop1_counter = $A4
loop1 = $A5
set_note_offset = $A6
adjust_note_offset = $A7

se_op_adjust_note_offset:
    lda [sound_ptr], y          ;read the argument (what value to add)
    clc
    adc stream_note_offset, x   ;add it to the current offset
    sta stream_note_offset, x   ;and save.
    rts
    
Let's look at this opcode in use.  Say we have a long arpeggiated line like this:

C2, E2, G2, B2, C3, E3, G3, B3, C4, E4, G4, B4, C5, E5, G5, B5, C6, E6, G6, B6, C7 ;Cmaj7 (21 bytes)

This passage just repeats the same 4 notes (C E G B) over 5 octaves.  

    .byte set_loop1_counter, 5      ;loop 5 times
.loop
    .byte C2, E2, G2, B2            ;these are the 4 notes to loop
    .byte adjust_note_offset, 12    ;each iteration add 12 to the offset (ie, go up an octave)
    .byte loop1                     
    .word .loop
 
    .byte C2                        ;will be a C7.  Cmaj7 (12 bytes)
    
The first time through the loop it will play C2, E2, G2, B2.  The second time through the loop it will play C3, E3, G3, B3.  The third time through will be C4, E4, G4, B4, etc.  Using our opcodes, we reduce the size of our data from 21 bytes to 12 bytes.  That's almost 50% savings.

Battle Kid
To take a better example, let's look at the bassline to the Battle Kid theme song.  Last week, it looked like this:

song6_tri:
    .byte eighth
    .byte A3, A3, A4, A4, A3, A3, A4, A4
    .byte G3, G3, G4, G4, G3, G3, G4, G4            ;down a step (-2)
    .byte F3, F3, F4, F4, F3, F3, F4, F4            ;down a step (-2)
    .byte Eb3, Eb3, Eb4, Eb4, Eb3, Eb3, Eb4, Eb4    ;down a step (-2)
    .byte loop
    .word song6_tri
    ;36 bytes
    
We have a pattern here: X3, X3, X4, X4, X3, X3, X4, X4, where X = some note.  It just so happens that each new X is just the previous X minus 2. Using our new opcode, we can rewrite the bassline like this:

song6_tri:
    .byte eighth
    .byte set_loop1_counter, 4              ;repeat 4 times
.loop:
    .byte A3, A3, A4, A4, A3, A3, A4, A4    ;series of notes to repeat
    .byte adjust_note_offset, -2            ;go down a step
    .byte loop1
    .word .loop
 
    .byte set_note_offset, 0                ;after 4 repeats, reset note offset to 0.
    .byte loop                              ;infinite loop
    .word song6_tri                         
    ;21 bytes
    
We drop from 36 bytes to 21 bytes of ROM space.  About 40% savings!
    
Loopy Sound Effects
We can produce some cool sound effects if we combine loops and key changes at high tempos.  Look at this one (tempo is $FF):

song7_square2:
    .byte set_loop1_counter, $08    ;repeat 8 times
.loop:
    .byte thirtysecond, D7, D6, G6  ;play two D notes at different octaves and a G.  Pretty random
    .byte adjust_note_offset, -4    ;go down 2 steps
    .byte loop1
    .word .loop
    .byte endsound
    
This sound effect plays a simple 3-note pattern in descending keys super fast.  The sound data is only 12 bytes, but it produces a pretty complex sound effect.  Listen to song7 in this week's sample files to hear it.  By experimenting with loops like this we can come up with some sounds that would be difficult to compose by hand.
    
Complex Chord Progressions
We made some good savings percentage-wise on the bassline to Battle Kid.  But we were lucky.  The chord progression went down in consistent steps: -2, -2, -2.  It was possible to loop this because we adjust the note_offset by the same value (-2) each time.  But what if we had a pattern that was repeated in a more complicated way?  We do.  Let's look at the rhythm pattern for our Guardian Legend boss song:

song1_square1:
    .byte eighth
    .byte A2, A2, A2, A3, A2, A3, A2, A3
    .byte F3, F3, F3, F4, F3, F4, F3, F4            ;+8 (A2 + 8 = F3)
    .byte A2, A2, A2, A3, A2, A3, A2, A3            ;-8
    .byte F3, F3, F3, F4, F3, F4, F3, F4            ;+8
    .byte E3, E3, E3, E4, E3, E4, E3, E4            ;-1
    .byte E3, E3, E3, E4, E3, E4, E3, E4            ;+0
    .byte Ds3, Ds3, Ds3, Ds4, Ds3, Ds4, Ds3, Ds4    ;-1
    .byte D3, D3, D3, D4, D3, D4, D3, D4            ;-1
    .byte C3, C3, C3, C4, C3, C4, C3, C4            ;-2
    .byte B2, B2, B2, B3, B2, B3, B2, B3            ;-1
    .byte As2, As2, As2, As3, As2, As3, As2, As3    ;-1
    .byte A2, A2, A2, A3, A2, A3, A2, A3            ;-1
    .byte Gs2, Gs2, Gs2, Gs3, Gs2, Gs3, Gs2, Gs3    ;-1
    .byte G2, G2, G2, G3, G2, G3, G2, G3            ;-1
    .byte loop                                      ;+2 (loop back to A2)
    .word song1_square1
    
Here we have another pattern: Xi, Xi, Xi, Xi+1, Xi, Xi+1, Xi, Xi+1, where X = some note and i = some octave.  Cool.  A pattern means we have an opportunity to save bytes by looping.  But wait.  Unlike Battle Kid, this pattern jumps around in an inconsistent way.  What should we do?

Super TGL Transposition Trick
I learned this trick from The Guardian Legend, so I call it the TGL Transposition Trick.  What we do is we loop the pattern, and then use the loop counter as an index into a lookup table.  The lookup table contains note offset values.  Because the loop counter decrements, our lookup table will be sequentially backwards.

Wait, what?  Let's looks at our example:

song1_square1:
    .byte eighth
    .byte set_loop1_counter, 14             ;repeat 14 times
.loop:
    .byte A2, A2, A2, A3, A2, A3, A2, A3
    ;pull a value from lookup_table and
    ; add it to stream_note_offset
    .byte loop1                             ;finite loop (14 times)
    .word .loop
 
    .byte loop                              ;infinite loop
    .word song1_square1
 
.lookup_table:
    .byte 2, -1, -1, -1, -1, -1, -2
    .byte -1, -1, 0, -1, 8, -8, 8       ;14 entries long, reverse order
    
I'm going to break it down in a second here, but first let me tell you that the part highlighted in red above will be covered by a single opcode, transpose.  The transpose opcode takes a 2-byte argument, so altogether that commented section will be replaced with 3 bytes of data.  So if we count up all of the bytes in our rhythm sound data we get 34 bytes.  The original was 116 bytes.  By using the TGL Transposition Trick, we save 82 bytes.  That's 70%!

song1_square1:
    .byte eighth
    .byte set_loop1_counter, 14             ;repeat 14 times
.loop:
    .byte A2, A2, A2, A3, A2, A3, A2, A3
    .byte transpose                         ;the transpose opcode take a 2-byte argument
    .word .lookup_table                     ;which is the address of the lookup table
 
    .byte loop1                             ;finite loop (14 times)
    .word .loop
 
    .byte loop                              ;infinite loop
    .word song1_square1
 
.lookup_table:
    .byte 2, -1, -1, -1, -1, -1, -2
    .byte -1, -1, 0, -1, 8, -8, 8       ;14 entries long, reverse order

;*** altogether 34 bytes ***

The transpose opcode will set up a pointer variable to point to the lookup table.  Then it will take the loop counter, subtract 1, and use the result as an index into the table.  We subtract 1 because the tables index from zero.  If we loop 14 times, our table will have 14 entries numbered 0-13.  Once the transpose opcode has its index, it will pull a value from the table.  This value will be added to stream_note_offset.

Before we write the opcode, let's trace through the data to see how it works.  We'll start at the very first byte of song1_square1:

1) set note length to eighth notes
2) set the loop counter to 14

(.loop iteration 1)
3) play a series of notes: A2, A2, A2, A3, A2, A3, A2, A3
4) transpose opcode.  Setup a pointer to lookup_table.  Use our loop counter, minus one, as an index.  The loop counter is 14 now, so we will pull out .lookup_table+13, which is an 8.  Add 8 to the current stream_note_offset: 0 + 8 = 8.
5) decrement the loop counter (14->13) and loop back to the .loop label

(iteration 2)
6) our new string of notes with the +8: F3, F3, F3, F4, F3, F4, F3, F4.
7) transpose opcode.  Loop counter is 13.  Grab .lookup_table+12, which is -8.  Add -8 to stream_note_offset: 8 + -8 = 0.
8) decrement loop counter (13->12) and loop back to .loop label

(iteration 3)
9) our new string of notes with the +0: A2, A2, A2, A3, A2, A3, A2, A3
10) transpose opcode.  Loop counter is 12.  Grab .lookup_table+11, which is 8.  Add 8 to stream_note_offset: 0 + 8 = 8.
11) decrement loop counter (12->11) and loop back to .loop label

(iteration 4)
12) our new string of notes with the +8: F3, F3, F3, F4, F3, F4, F3, F4.
13) transpose opcode.  Loop counter is 11.  Grab .lookup_table+10, which is -1.  Add -1 to stream_note_offset: 8 + -1 = 7.
14) decrement loop counter (11->10) and loop back to .loop label

(iteration 4)
15) our new string of notes with the +7: E3, E3, E3, E4, E3, E4, E3, E4.
16) transpose opcode.  Loop counter is 10.  Grab .lookup_table+9, which is 0.  Add 0 to stream_note_offset: 7 + 0 = 7.
17) decrement loop counter (10->9) and loop back to .loop label

etc.  On the last iteration our loop counter is 1.  We grab .lookup_table+0 and add it to stream_note_offset.  Then we decrement the loop counter (1->0).  Our loop counter is now 0, so our loop breaks.  Pretty cool, no?  Let's write it.

;-----------------------------------------------------------------------
;this is our JUMP TABLE!
sound_opcodes:
    .word se_op_endsound            ;$A0
    .word se_op_infinite_loop       ;$A1
    .word se_op_change_ve           ;$A2
    .word se_op_duty                ;$A3
    .word se_op_set_loop1_counter   ;$A4
    .word se_op_loop1               ;$A5
    .word se_op_set_note_offset     ;$A6
    .word se_op_adjust_note_offset  ;$A7
    .word se_op_transpose           ;$A8
 
;these are aliases to use in the sound data.
endsound = $A0
loop = $A1
volume_envelope = $A2
duty = $A3
set_loop1_counter = $A4
loop1 = $A5
set_note_offset = $A6
adjust_note_offset = $A7
transpose = $A8

se_op_transpose:
    lda [sound_ptr], y          ;read low byte of the pointer to our lookup table
    sta sound_ptr2              ;store it in a new pointer variable
    iny
    lda [sound_ptr], y          ;read high byte of pointer to table
    sta sound_ptr2+1
 
    sty sound_temp              ;save y because we are about to destroy it
    lda stream_loop1, x         ;get loop counter, put it in Y
    tay                         ;   this will be our index into the lookup table
    dey                         ;subtract 1 because indexes start from 0.
 
    lda [sound_ptr2], y         ;read a value from the table.
    clc
    adc stream_note_offset, x   ;add it to the note offset
    sta stream_note_offset, x
 
    ldy sound_temp              ;restore Y
    rts
    
There is a new pointer variable here, sound_ptr2.  Actually, what I really did was rename jmp_ptr to sound_ptr2.  The new name let's me know it's for sound engine use only.  Since we finish with jmp_ptr as soon as we jump, there are no pointer conflicts here.

Conclusion
This is just an example of how clever use of opcodes and looping can save you lots of bytes.  Keep in mind that this transpose opcode is only useful if you write music that has repeating patterns in the rhythm section.  If you don't, then save yourself some bytes and cut the opcode from your sound engine.

Putting It All Together
Download and unzip the opcodes2.zip sample files.  Make sure the following files are in the same folder as NESASM3:

    opcodes2.asm
    sound_engine.asm
    sound_opcodes.asm
    opcodes2.chr
    note_table.i
    note_length_table.i
    vol_envelopes.i
    song0.i
    song1.i
    song2.i
    song3.i
    song4.i
    song5.i
    song6.i
    song7.i
    opcodes2.bat

Double click opcodes2.bat. That will run NESASM3 and should produce the opcodes2.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.
Song1-Song6 are the same as last week, but they take up less ROM-space now
Song7 is a new sound effect created by looping a key change at high tempo.

As usual, try adding your own songs and sound effects in using the new opcodes.  Experiment.

Next Week: Noise, Simple Drums

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

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


Edited: 01/18/2010 at 02:33 AM by MetalSlime

Nov 29, 2009 at 11:02:33 PM
udisi (88)
avatar
< King Solomon >
Posts: 3261 - Joined: 11/15/2006
United States
Profile
sweeeeet. this is getting good. Tom had some stuff working in the last engine, but this should really simplify things.

Nov 30, 2009 at 8:43:25 AM
Mario's Right Nut (350)
avatar
(Cunt Punch) < Bowser >
Posts: 6574 - Joined: 11/21/2008
Texas
Profile
This looks great! I can't wait to start making music. Thanks!

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

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.


May 2, 2010 at 10:36:49 PM
SUBSCRIBER
GradualGames (39)
avatar
(Derek Andrews) < Meka Chicken >
Posts: 922 - Joined: 10/09/2009
Pennsylvania
Profile
Thanks for such a thorough sound tutorial! Using some of the core concepts here and important facts (such as what happens when writing to the 4th register of square channels), I've written my own sound engine for use in my game.

-------------------------
Creators of: Nomolos: Storming the CATsle, and The Legends of Owlia.

May 17, 2010 at 4:19:07 PM
Mario's Right Nut (350)
avatar
(Cunt Punch) < Bowser >
Posts: 6574 - Joined: 11/21/2008
Texas
Profile

Holy Christ...this one is pretty intense. My brain hurts.

I do have a question though.  I have tried to use negative numbers before and the assembler rejected it. Not to sound too dumb, but how are you getting it to just let you "add negative numbers"?


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

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.



Edited: 05/17/2010 at 04:23 PM by Mario's Right Nut

May 18, 2010 at 11:39:08 PM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Originally posted by: Mario's Right Nut

Holy Christ...this one is pretty intense. My brain hurts.

I do have a question though.  I have tried to use negative numbers before and the assembler rejected it. Not to sound too dumb, but how are you getting it to just let you "add negative numbers"?



-1 gets interpreted by the assembler as $FF.  -2 is $FE, -3 is $FD, etc.  I use the negative values because it's easier to read and understand what they're used for, but in the final ROM they will appear as FF, FE, etc.

If you try out some 8bit addition, you can see that they work the same:

1 + FF = 0  (carry set)
2 + FF = 1
3 + FF = 2
etc..

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

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

May 19, 2010 at 8:45:13 AM
Mario's Right Nut (350)
avatar
(Cunt Punch) < Bowser >
Posts: 6574 - Joined: 11/21/2008
Texas
Profile
You know, since I made that post I had been thinking about it. I came up with the solution of "adding $FF or $FE" to subtract. But I like that the assembler will interpret the numbers for you. That makes life a lot easier. I will have to make another stab at it. I must have been doing something retarted like $-01 or something before.

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

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.