Last Week:
Finite Looping, Key Changes, Chord Progressions
This Week: Simple Drums
DrumsThis week we are going to take a look at drums. I saved them until now because they use the
Noise
channel, which operates much differently from the other 3 channels.
With the Square and Triangle channels we manually set the waveform
period to choose what note to play. Our note lookup table was just a
table of periods to plug into the channel ports. The Noise channel on
the other hand produces random noise. We don't use a note table at all.
Noise ChannelThe noise channel doesn't produce notes, it produces noise. We communicate with the noise channel through 3 ports:
$400C,
$400E,
$400F. Note that port $400D is
unused. Let's take a closer look at the Noise ports.
Volume - $400C$400C let's you control the volume of the Noise channel. It works just like the Square channels, except there is no Duty Cycle:
NOI_ENV ($400C)76543210 |||||| ||++++- Volume |+----- Saw Envelope Disable (0: use internal counter for volume; 1: use Volume for volume) +------ Length Counter Disable (0: use Length Counter; 1: disable Length Counter) For
our purposes, we will set Saw Envelope Disable and Length Counter
Disable and forget about them. This will allow us to have full control
of "note" length and volume via Volume Envelopes. We did the same
thing for the Square channels.
Random Generator - $400E$400E let's us control the settings for the random generator. It looks like this:
NOI_RAND ($400E)76543210| ||||| ++++- Sound Type+-------- ModeMode sets the mode. There are two modes,
Mode-0 and
Mode-1.
Mode-0 sounds dull and breathy, like static sssh. Mode-1 sounds more
sharp, robotic and buzzy. There's really no good way to describe in
words, so the best way to know the difference in sound is to listen to
both modes. (see below)
Each mode has 16 possible sounds, selected by
Sound Type.
This means that the Noise channel really only gives us 32 possible
sounds total! More complex sound effects on the Noise channel are
created by playing combinations of these 32 noises one after another.
To hear what each of the 32 sounds sound like, listen to
Song 8 in this week's sample program. It plays the 16 Mode-0 sounds followed by the 16 Mode-1 sounds.
Note:
In this tutorial I will assign each of the 32 Noise channel sounds a
number 00-1F. The left number (0 or 1) refers to the
Mode. The right number (0-F) refers to the
Sound Type. For example, sound "04" means "Mode 0, Sound Type 4". Sound "1E" means "Mode 1, Sound Type E".
Length Counter - $400F$400F is the Noise channel's length counter. We disabled the length counter in $400C, so we can ignore this port completely!
Simple Noise Drums
The simplest way to make a drum sound on the Noise
channel is to play a single Sound Type under a volume envelope that
decays to 0 (silence). Many games use this kind of drum exclusively.
The Guardian Legend for example only uses two drum sounds throughout
the whole game: 04 and 06. Battle Kid makes heavy use this simple drum
style too. Lots of games do.
So how will we represent simple
drums in the sound data? Recall that our sound engine distinguishes
between Notes, Note Lengths and Opcodes using ranges:
.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 ;range A0-FF.note_length: ;do note length stuff ;range 80-9F.note: ;do note stuff ;range 00-7F Although
we won't use our note table for the Noise channel, we will treat drums
like notes as far as ranges are concerned. So we need our drum data
values to be in the range of $00-$7F. That's easy. We'll assign the
Mode-0 sounds to $00-$0F and Mode-1 sounds to $10-$1F. Some drum data
might look like this then:
example_drum_data: .byte eighth, $04 .byte sixteenth, $1E, $1E, $1F .byte d_eighth, $04 .byte sixteenth, $06, $06, $08, $08 .byte eighth, $17, $07 .byte loop .word example_drum_dataSince
we are not using the note table for Noise, we will need to alter our
Note code to check the channel and branch to different code if we are
processing the Noise channel (new stuff in red):
.note: ;do Note stuff sta sound_temp2 ;save the note value lda stream_channel, x ;what channel are we using? cmp #NOISE ;is it the Noise channel? bne .not_noise jsr se_do_noise ;if so, JSR to a subroutine to handle noise data jmp .reset_ve ;and skip the note table when we return.not_noise: ;else grab a period from the note_table lda sound_temp2 ;restore note value 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 ;check if it's a rest and modify the status flag appropriately jsr se_check_rest.reset_ve: lda #$00 sta stream_ve_index, x .update_pointer: iny tya clc adc stream_ptr_LO, x sta stream_ptr_LO, x bcc .end inc stream_ptr_HI, x.end: rts This
code checks to see if the channel is the Noise channel. If so it JSRs
to a special Noise subroutine. Upon return, it jumps over the
note_table code and updates the volume envelope and stream pointer like
normal.
So what does our special Noise subroutine,
se_do_noise, look like? The job of
se_do_noise
will be to take the note value and convert it into something we can
write to $400E (NOI_RAND). Recall that $400E expects the Mode number
in bit7 and the Sound Type in bits 0-3:
NOI_RAND ($400E)76543210| ||||| ++++- Sound Type+-------- ModeMode-0
sounds don't need to be converted at all. We represent them with the
values $00-$0F, which correspond exactly to the values we need to write
to $400E.
Mode-1 sounds need to be tweaked a bit. We
represent Mode-1 sounds with the values $10-$1F, or in binary
%00010000-%00011111. Notice we identify the Mode number using bit4.
Port $400E expects the Mode number in bit7 though, not bit4, so we will
need to set bit7 ourselves:
se_do_noise: lda sound_temp2 ;restore the note value and #%00010000 ;isolate bit4 beq .mode0 ;if it's clear, Mode-0, so no conversion.mode1: lda sound_temp2 ;else Mode-1, restore the note value ora #%10000000 ;set bit 7 to set Mode-1 sta sound_temp2.mode0: lda sound_temp2 sta stream_note_LO, x ;temporary port that gets copied to $400E rts Now everything is set. Note values of $00-$0F will get written to
stream_note_LO directly. Note values of $10-$1F will get converted to $90-$9F first and then get written to
stream_note_LO. Note that we do not bother clearing bit4 for Mode-1 sounds. Bit4 has no effect on $400E so we can just leave it as it is.
Drum DecayThe only thing left to add is a volume envelope for the simple drums to use. It should be short and decay to zero (silence):
volume_envelopes:
.word se_ve_1
.word se_ve_2
.word se_ve_3
.word se_ve_tgl_1
.word se_ve_tgl_2
.word se_battlekid_loud
.word se_battlekid_loud_long
.word se_battlekid_soft
.word se_battlekid_soft_long
.word se_drum_decay se_drum_decay: .byte $0E, $09, $08, $06, $04, $03, $02, $01, $00 ;7 frames per drum. Experiment to get the length and attack you want. .byte $FF ve_drum_decay = $09You can of course make multiple volume envelopes for the drums to choose from.
ConclusionNow we can add drum data to our songs. Here is the drum data for The Guardian Legend boss song we've been using:
;in the song header, after the header info for Squares and Triangle: .byte MUSIC_NOI ;which stream .byte $01 ;status byte: enabled .byte NOISE ;which channel .byte $30 ;initial volume_duty value (disable length counter and saw envelope) .byte ve_drum_decay ;volume envelope .word song1_noise ;pointer to the sound data stream .byte $53 ;tempo song1_noise: .byte eighth, $04 ;this song only uses drum 04 (Mode-0, Sound Type 4) for a snare .byte sixteenth, $04, $04, $04 .byte d_eighth, $04 .byte sixteenth, $04, $04, $04, $04 .byte eighth, $04, $04 .byte loop .word song1_noise Try adding some drums to your own songs!
Putting It All TogetherDownload and unzip the
drums.zip sample files. Make sure the following files are in the same folder as NESASM3:
drums.asm
sound_engine.asm
sound_opcodes.asm
drums.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
song8.i
drums.bat
Double click drums.bat. That will run NESASM3 and should produce the drums.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-Song7 are the same as last week, but a few of them (1, 4 and 6) have drums now.
Song8
is a new "song" that plays each of the Noise channel's 32 sounds, in
order from 00-1F. First it plays them with a sustained volume envelope
so that you can hear how they sound drawn out. Next they are played
using the 7-frame ve_drum_decay volume envelope we made so you can hear
how they sound as simple drum sounds.
Next Week: More Complex Drums, Noise SFX