Last Week:
Square 2 and Triangle BasicsThis week: We will learn about periods and build a period lookup table that spans 8 octaves.
PeriodsIn
the last two lessons, I've been giving you the values to plug into the
11-bit periods for the Square and Triangle channels. I haven't been
giving you an explanation of what a period is, or where I got those
numbers. So this week we're going to learn about periods.
What is a period?A period refers to the length of a wave, or rather the
time length of the
repeating part of a wave. Take a look at this square wave (x-axis is time):
Notice
how it is repeating. It starts high and remains high for 2 time
units. Then it goes low and remains low for 2 time units. Then it
repeats. When we say period, we are talking about the horizontal time
length of this repeating wave. In this case, the period is 4 time
units. The longer a period is, the lower the note will sound.
Conversely, the shorter a period is, the higher the note will sound.
Look at these 3 Square waves:
Period = 6 time units
Period = 4 time units
Period = 1 time unit
The
top wave has the longest period (6 time units) and it will sound the
lowest. The bottom wave has a short period (1 time unit) and will
sound higher than the other two.
On the NES, we write an 11-bit
period to the APU ports. The smaller the number, the shorter the
period, the higher the note. Larger numbers = longer periods = lower
notes. Look at the following code snippets that write an 11-bit period
to the Square 1 ports:
lda #$C9 sta $4002 lda #$05 sta $4003 ;period $5C9: large number = long period = low note ;---- lda #$09 sta $4002 lda #$00 sta $4003 ;period $009: small number = short period = very high notePeriods -> NotesSo how do we know which 11-bit period values correspond to which notes? The magic forumla is:
P = C/(F*16) - 1
P = Period
C = CPU speed (in Hz)
F = Frequency of the note (also in Hz).
The
value of C differs between NTSC and PAL machines, which is why a game
made for NTSC will sound funny on a PAL NES, and vice-versa.
To
find the period values for notes, we will have to look up note
frequencies and plug them into the formula. Or we can cross our
fingers and hope somebody has already done the work for us and put the
answers in an easy-to-read table. Lucky for us a cool fellow named
Celius has done just that, for both NTSC and PAL. Here are the charts:
http://www.freewebs.com/the_bott/NotesTableNTSC.txt
http://www.freewebs.com/the_bott/NotesTablePAL.txt
Lookup TablesIt is fairly common practice to store period values in a lookup table. A
lookup table
is a table of pre-calculated data stored in ROM. Like an answer
sheet. Lookup tables are used to cut down on complicated,
time-consuming calculations. Let's look at a trivial example. Let's
say you want a subroutine that takes a value in A and returns 3^A. If
you took the brute-force approach, you might write something like this:
multiplier .rs 1; takes a value (0-5) in A and returns 3^Athree_to_the_a: bne .not_zero lda #$01 ;3^0 is 1 rts.not_zero: tay lda #$03.loop: sta multiplier dey beq .done clc adc multiplier adc multiplier jmp .loop.done: rts It works, but it's not very pretty. Here is how we would do it with a lookup table:
;lookup table with pre-calculated answerspowers_of_3: .byte 1, 3, 9, 27, 81, 243 three_to_the_a: tay lda powers_of_3, y rts Easier to code. Easier to read. And it runs faster too.
NESASM3 Tip#1: Local LabelsYou may have noticed in the above example that I put a period in front of some labels:
.done,
.loop,
.not_zero. NESASM3 treats these as local labels. There are two types of labels: global and local. A
global label exists across the whole program and must be unique. A
local label
only exists between two global labels. This means that we can reuse
the names of local labels - they only need to be unique within their
scope. Using local labels saves you the trouble of having to create
unique names for common case labels (like looping). I tend to use
local labels for all labels that occur within subroutines. To make a
label local, stick a period in front of it.
Note Lookup TableLet's
take Celius's tables and turn them into a note lookup table. Period
values are 11 bits so we will need to define our lookup table using
words. Note that .word is the same as .dw. Here is a note_table for
NTSC:
;Note: octaves in music traditionally start from C, not A. ; I've adjusted my octave numbers to reflect this.note_table: .word $07F1, $0780, $0713 ; A1-B1 ($00-$02) .word $06AD, $064D, $05F3, $059D, $054D, $0500, $04B8, $0475, $0435, $03F8, $03BF, $0389 ; C2-B2 ($03-$0E) .word $0356, $0326, $02F9, $02CE, $02A6, $027F, $025C, $023A, $021A, $01FB, $01DF, $01C4 ; C3-B3 ($0F-$1A) .word $01AB, $0193, $017C, $0167, $0151, $013F, $012D, $011C, $010C, $00FD, $00EF, $00E2 ; C4-B4 ($1B-$26) .word $00D2, $00C9, $00BD, $00B3, $00A9, $009F, $0096, $008E, $0086, $007E, $0077, $0070 ; C5-B5 ($27-$32) .word $006A, $0064, $005E, $0059, $0054, $004F, $004B, $0046, $0042, $003F, $003B, $0038 ; C6-B6 ($33-$3E) .word $0034, $0031, $002F, $002C, $0029, $0027, $0025, $0023, $0021, $001F, $001D, $001B ; C7-B7 ($3F-$4A) .word $001A, $0018, $0017, $0015, $0014, $0013, $0012, $0011, $0010, $000F, $000E, $000D ; C8-B8 ($4B-$56) .word $000C, $000C, $000B, $000A, $000A, $0009, $0008 ; C9-F#9 ($57-$5D)Notice
that at the highest octaves, some notes have the same value (C9 and C#9
for example). This is due to rounding. We lose precision the higher
we go, and a lot of the highest notes will sound out of tune as a
result. So in songs we probably wouldn't use octaves 8 and 9. These
high notes could be utilized for sound effects though, so we'll leave
them in.
Once we have a note lookup table, we use the note
we want as an index into the table and pull the period values from it,
like this:
lda #$0C ;the 13th entry in the table (A2) asl a ;multiply by 2 because we are indexing into a table of words tay lda note_table, y ;read the low byte of the period sta $4002 ;write to SQ1_LO lda note_table+1, y ;read the high byte of the period sta $4003 ;write to SQ1_HI To make it easier to know which index to use for each note, we can create a list of
symbols:
;Note: octaves in music traditionally start at C, not A;Octave 1A1 = $00 ;"1" means octave 1.As1 = $01 ;"s" means "sharp"Bb1 = $01 ;"b" means "flat". A# == BbB1 = $02;Octave 2C2 = $03Cs2 = $04Db2 = $04D2 = $05;...A2 = $0CAs2 = $0DBb2 = $0DB2 = $0E;Octave 3C3 = $0F;... etcNow we can use our new symbols instead of the actual index values:
lda #A2 ;A2. #A2 will evaluate to #$0C asl a ;multiply by 2 because we are indexing into a table of words tay lda note_table, y ;read the low byte of the period sta $4002 ;write to SQ1_LO lda note_table+1, y ;read the high byte of the period sta $4003 ;write to SQ1_HI And if later we want to have a series of notes, symbols are much easier to read and alter:
sound_data: .byte C3, E3, G3, B3, C4, E4, G4, B4, C5 ; Cmaj7 (CEGB) sound_data_no_symbols: .byte $0F, $13, $16, $1A, $1B, $1F, $22, $26, $27 ;same as above, but hard to read. Cmaj7 (CEGB) Low Notes On Squares (Sweep Unit)One
last thing needs to be mentioned. It's very important. It has to do
with the Square channels' sweep units. The sweep units can silence
the square channels in certain situations (Periods >= $400, our
lowest notes),
even when disabled. We'll have to take a quick look at the sweep unit ports to solve this problem.
SQ1_SWEEP ($4001), SQ2_SWEEP ($4005)
76543210
||||||||
|||||+++- Shift
||||+---- Negate
|+++----- Sweep Unit Period
+-------- Enable (1: enabled; 0: disabled)I'm not going to go into how it works now, but the unwanted silencing of low notes can be circumvented by
setting the negate flag:
lda #$08 ;set Negate flag on the sweep unit sta $4001 ;or $4005 for Square 2. If you really want to know why, check the Sweep Unit section of blargg's
NES APU Sound Hardware Technical Reference.
What about PAL?For
simplicity, these tutorials are going to use NTSC numbers. Once we
finish our sound engine I'll try to whip up a tutorial about adding PAL
support.
Putting It All TogetherDownload and unzip the
periods.zip
sample files. Make sure periods.asm, periods.chr, note_table.i and
periods.bat are all in the same folder as NESASM3, then double click
periods.bat. That will run NESASM3 and should produce the periods.nes
file. Run that NES file in FCEUXD SP. Use the d-pad to select and play
any note from our note table on the Square 1 channel. Controls are as
follows:
Up - Play selected note
Down - Stop note
Left - Move selection down a note
Right - Move selection up a note
Homework:
Edit periods.asm and add support for the Square 2 and Triangle
channels. Allow the user to select between channels and play different
notes on all three of them.
Homework #2: Read Disch's document
The Frame and NMIs.
Pay special attention to the "Take Full Advantage of NMI" section. We
are going to use this style of NMI handler with our sound engine. In
fact, periods.asm already uses it.
Next Week:
Starting our sound engine.