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

Nerdy Nights Sound: Part 3 Periods and lookup tables

Aug 28, 2009 at 9:22:33 AM
MetalSlime (0)
avatar
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Last Week: Square 2 and Triangle Basics

This week: We will learn about periods and build a period lookup table that spans 8 octaves.

Periods

In 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 note

Periods -> Notes
So 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 Tables
It 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^A
three_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 answers
powers_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 Labels
You 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 Table
Let'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 1
A1 = $00    ;"1" means octave 1.
As1 = $01   ;"s" means "sharp"
Bb1 = $01   ;"b" means "flat".  A# == Bb
B1 = $02

;Octave 2
C2 = $03
Cs2 = $04
Db2 = $04
D2 = $05
;...
A2 = $0C
As2 = $0D
Bb2 = $0D
B2 = $0E

;Octave 3
C3 = $0F
;... etc

Now 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 Together
Download 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.






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

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


Edited: 10/23/2009 at 04:45 AM by MetalSlime

Dec 10, 2014 at 12:00:33 PM
Mega Mario Man (63)
avatar
(Tim ) < Ridley Wrangler >
Posts: 2743 - Joined: 02/13/2014
Nebraska
Profile
The images in this document are broken links. Can someone repost the pics or fix the links? Thanks!


Also, the link below is broken.
"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."

EDIT: I think this is the new link: http://wiki.nesdev.com/w/index.ph...

-------------------------
Current Project
Isometric Survival Horror

Older Projects
Tailgate Party, Power Pad Demo, Happy Hour

Links
Store, Facebook, Twitter


Edited: 12/10/2014 at 01:42 PM by Mega Mario Man

Apr 11, 2016 at 4:16:44 PM
freeman.pixelz (0)

< Cherub >
Posts: 5 - Joined: 04/08/2016
Profile
Hi,

As the zip file is not downloadable, I tried to retrieve the values of E2, F2, E3, F3, G3, H3.

Can someone tell me if these values are correct?

;Octave 1
A1 = $00 ;"1" means octave 1.
As1 = $01 ;"s" means "sharp"
Bb1 = $01 ;"b" means "flat". A# == Bb
B1 = $02

;Octave 2
C2 = $03
Cs2 = $04
Db2 = $04
D2 = $05

E2 = $06
Es2 = $07
Fb2 = $07
F2 = $08

A2 = $0C
As2 = $0D
Bb2 = $0D
B2 = $0E

;Octave 3
C3 = $0F
Cs3 = $10
Db3 = $10
D3 = $11

E3 = $12
Es3 = $13
Fb3 = $13
F3 = $14

G3 = $15
Gs3 = $16
Hb3 = $16
H3 = $17

A3 = $18
As3 = $19
Bb3 = $19
B3 = $1A

Apr 11, 2016 at 5:15:01 PM
Mog (140)
avatar
(Mr Mog) < King Solomon >
Posts: 4728 - Joined: 05/02/2009
Federated States of Micronesia
Profile
Here is the zip file for part 3

Apr 12, 2016 at 12:00:58 PM
freeman.pixelz (0)

< Cherub >
Posts: 5 - Joined: 04/08/2016
Profile
Thank you Mog!