OK, here is the commented source code.
I'm sure you all know that, but let me just remind you that in those times we had severe memory restrictions for both RAM and ROM, so the sound driver and the song data had to be designed to occupy the less bytes possible.
This is the initialization routine, very simple. It uses macros to set the song sound driver pointers to the data of this particular song. You can see how channels 1, 2 and 3 are set to GOMEL, GOCHRD and GOBASS tables.
Code:
GAMEOVER INCHA GOMEL
INCHB GOCHRD
INCHC GOBASS
RTS
This is the table of tracks for channel 1. Since it's a very short tune it only lists one track. In a longer song it would address many more tracks or “chunks” of data. This is a never ending tune, so the final data instructs the driver to repeat the same table again and again.
Code:
GOMEL DW GOMEL1
DW 0,GOMEL
Same for channel 2.
Code:
GOCHRD DW GOCHRD1
DW 0,GOCHRD
Same for channel 3.
Code:
GOBASS DW GOBASS1
DW 0,GOBASS
This is the “real” data of the song, and this concretely is what I used to call the Bass Track, because it would contain mostly the bass notes. With the NES you were almost forced to use the triangle wave channel always for the bass part.
The first command (VIB,0,4,5) tells the driver to set a vibrato with 0 initial delay, 4 depth and a speed of 5 frames. The second command (TRA,-2) sets this track to transpose the notes down 2 semitones.
The next line (PERC 10, GOPERC) initializes a drum track with a note length of 10 for each sound. Since drum sequences are usually cyclic, I used to “invoke” drum tracks and breaks from any of the other tracks (usually the bass track), at any time, instead of having a separated table with all the drum sequences. This reduced the amount of data in the song.
Next, there is a note length command (L40) which tells the driver to set the note length to 40 video frames.
The rest of the data are notes, and tied notes (__), which just leave the last note sounding for the last length set, in this case 40.
The last command (XM) marks the end of the track.
Code:
GOBASS1 DB VIB,0,4,5,TRA,-2
PERC 10,GOPERC
DB L40
DB a1,__
DB __,g1
DB e1,__
DB __,e1
DB a1,__
DB __,g1
DB e1,__
DB __,g1
DB d1,__
DB __,d2
DB a1,__
DB __,a1
DB d1,__
DB __,d2
DB e1,__
DB gs1,B1
DB XM
This is what I used to call the Chord Track, because it would contain most of the chords of the song, in the form of those fast arpeggios that made European chip music so distinctive.
The first command (L10) sets the length note to 10 frames.
The second command (SENV,$D1,3,$64) sets a volume envelope for the sound. The volume envelope routines of my NES and Game Boy drivers were very similar, so I could share code between them, but in the Game Boy I used hardware envelopes (because of hardware restrictions, nasty noises when changing volume), and in the NES I used a simulation by software.
The next command (W1) instructs the channel to set the square waveform to 25% duty. W0 was 50% and W2 12.5% I think.
Next there is an arpeggio command (EF0 to EF15). The arpeggios were defined globally for all the songs and consisted basically on tables of sequential notes used to transpose the current note. One arpeggio would contain a major chord (C,E,G,RET), other a minor chord (C,Ds,G,RET), and others may not contain a chord at all, only transpositions to different octaves. The transposition tables could be cyclic or not, depending on the final command (RET or END). This was another technique to reduce song data, since you didn't need to specify all the notes for a chord, only the root note and the chord type (if different than the previous one).
The next command (DO2 to DO8) instructs the driver to repeat 2 times the block of data found between this and the next LOP command. This was used to reduce the size of the song data too.
Next there is a “procedure” command (P1 to P9). Those commands were used to execute a portion of assembler code which modified whatever was needed in the sound driver. In this case I'm using the procedures to change the “instrument” of the song (sort of hard coded program change).
Code:
GOCHRD1 DB L10,SENV,$D1,3,$64,W1,EF1
DO2
DB P9,a2,P3,c3,e3,a3,c4,a3,e3,c3
LOP
DO2
DB P9,e2,P3,g2,b2,e3,g3,e3,b2,g2
LOP
DO2
DB P9,a2,P3,c3,e3,a3,c4,a3,e3,c3
LOP
DO2
DB P9,e2,P3,g2,b2,e3,g3,e3,b2,g2
LOP
DO2
DB P9,d2,P3,f2,a2,d3,f3,d3,a2,f2
LOP
DO2
DB P9,a2,P3,c3,e3,a3,c4,a3,e3,c3
LOP
DO2
DB P9,d2,P3,f2,a2,d3,f3,d3,a2,f2
LOP
DO2
DB P9,EF5,e2,P3,gs2,b2,e3,gs3,e3,b2,gs2
LOP
DB XM
This is what I used to call the Melody Track, because usually it would contain the main melody of the song. There are some new commands here.
The first new command (REL,5,$02) is what I called the “release effect”. Literally, what this command instructs the driver is to set a decay envelope of $02 just 5 frames before the next note command is reached. This was used to shorten the sound of the notes, like if you released the key before pressing the next one (non-legato). This decay envelope means, in nibbles, no change in initial volume (0) and a decay time of 2, hence the $02 value.
The next new command (nn or NN = not note) is a simple command used to trigger the above mentioned “release effect”.
This particular track has lots of tied note commands, this is because it's a short tune and probably I had enough memory available. Usually I would pack the data so it occupied less bytes by using the note length commands. For example instead of leaving “L10,C3,__,__,__” I would changed it to “L40,C3”. This had to be done by hand at the end of the composition process for all the tracks. Every byte counted!!
Code:
GOMEL1 DB L10,SENV,$39,5,$90,VIB,20,1,4,W1,REL,5,$02
DB e3,__,a3,__,c4,__,__,__
DB __,NN,a3,b3,c4,__,d4,__
DB b3,__,__,g3,e3,__,__,__
DB __,__,__,__,__,f3,e3,d3
DB e3,__,a3,__,c4,__,__,__
DB __,NN,a3,b3,c4,__,d4,__
DB b3,__,__,a3,g3,a3,b3,__
DB __,__,e3,__,__,__,f3,g3
DB a3,__,__,g3,f3,__,e3,f3
DB __,__,e3,__,d3,__,c3,__
DB d3,__,e3,__,e3,__,d3,e3
DB __,__,__,__,__,nn,f3,g3
DB a3,__,__,g3,f3,__,e3,f3
DB __,__,e3,__,c4,__,b3,__
DB e3,__,e3,__,d4,__,c4,__
DB c4,__,__,__,B3,__,__,__
DB XM
Finally, this is the drum track, a pretty simple one. Each number means one different drum sound, except 0 which is no sound. The default note length is set by the channel that initializes the drum track. Setting the length inside the track was also possible using L commands, but this way I could use the same drum track on different songs that used different base note lengths, again to reduce song data.
As you may figure if you listen to the song, 1=bass drum, 2= snare drum, 6 = short hit hat and 5 = slightly different short hit hat.
Code:
GOPERC DB 1,0,6,5,2,0,6,5
DB 1,0,6,5,2,5,1,5
DB END
I would really like to find commented code of other veteran musicians, just to know what commands and techniques they used on their sound drivers and compositions. Please let me know if you know of any place with such stuff
Cheers!