Basic sound program for the NES (Nintendo Entertainment System) using VASM compiler
This is a pretty basic program for making sound on the NES (Nintendo Entertainment System). It is a simple, static screen, which contains both background tiles and "foreground tiles" AKA sprites, and plays some music ("Happy Birthday").
If you're looking to compile the code, you'll need to use the VASM compiler. You can get it here: http://www.compilers.de/vasm.html . It's a great compiler that can be used for a variety of systems, and it was made by Dr. Volker Barthelmann. Check it out!To compile simply use:
vasm6502_oldstyle.exe DIR/basicsound.asm -chklabels -nocase -Fbin -o "DIR2/basicsound.nes"So this "game" (and I use the term generously) is only a mild step up from our basic graphics tutorial: instead of a static screen, we've got a static screen with sound. I guess that's an improvement, right? Ever wanted to give a friend an NES cartridge that was a birthday card? This tutorial is pretty much exactly that! Of course, to do anything useful with it, we must be able to learn from the code, maybe even make some modifications. Wouldn't that be grand? Well, let me give you the tour...Before we go further, I would recommend you check out my "Hello World" (https://github.com/thomaslantern/hello-world) and "Basic Graphics" (https://github.com/thomaslantern/nes-basic-graphics) tutorials for the NES, as they cover a lot of the initial steps needed to go over what we're doing here. Once you've done that, you're ready to start learning about NES sound and how we can make some cool noise!
For the sake of this tutorial, we will be skipping some of the more in-depth programming (I'm looking at you, DMC samples), but we'll briefly go over how the addressing for sound works on the NES.
This particular program uses only first sound channel of the NES, of which there are five:
- First Pulse Wave
- Second Pulse Wave
- Triangle Pulse Wave
- Noise
- DMC Sound Sample
So let's take a look at the first channel. To use it, we have to load values into the addresses from $4000 to $4003:
- $4000: From left to right we have: Duty cycle (2 bits), envelope length (2 bits), and volume (4 bits).
- Duty cycle is the "shape" of the sound/sine wave, and volume is fairly straightforward. By writing %11111111 (binary number 11111111) we get our instrument at full volume, with duty cycle 75%.
- I don't honestly remember what the envelope length does exactly (I believe it's a timer of sorts that eventually expires and turns off the sound, so an alternate way of timing your notes rather than using the screen refresh to time them, as I did). I was told to set them to 11, so I did. As you can see, there's a lot to learn with every element of ASM6502, but keep at it! When in doubt, just try some values and see if they work - that kind of thinking has served me well so far with ASM6502, and it hasn't kept me from learning new things either!
P = C / (F * 16) + 1
P = 1790000 / (440 * 16) + 1
= 1790000 / 7040 + 1
~= 255
P = 1790000 / (220 * 16) + 1
= 1790000 / 3520 + 1
~= 509, which is hex $1FD,
so you would load #$FD into $4002, and load #$01 into $4003. Hopefully that makes sense!
Of course, we can't actually have music or sound without turning the sound on. For that we need $4015. To do this we can write $01 to $4015 using LDA #$01 followed by STA $4015. This turns on the one instrument we are using for this tutorial.
It's great to have the sound on, but if there are no notes to be played, how will you know the difference? We need some notes, and for that our best friend will be an LUT (look up table). Look up tables or LUTs allow us to store our notes in a sequential way, that makes it easy as a programmer to read the order of the notes, and how they're going to be played in the song. In short, having LUTs allows us to loop in our code, which makes it much easier to read, modify, and understand. And who doesn't love easier to read code?
Here's a look up table for all the notes you might need for the NES:
; Table of different notes and their values
notes:
dw $07F1, $0780, $0713 ; A1 to B1 ($00-$02)
dw $06AD, $064D, $05F3, $059D, $054D, $0500 ; C2 to F2 ($03-$08)
dw $04B8, $0475, $0435, $03F8, $03BF, $0389 ; F#2 to B2 ($09-$0E)
dw $0356, $0326, $02F9, $02CE, $02A6, $027F ; C3 to F3 ($0F-$15)
dw $025C, $023A, $021A, $01FB, $01DF, $01C4 ; F#3 to B3 ($16-$1A)
dw $01AB, $0193, $017C, $0167, $0151, $013F ; C4 to F4 ($1B-$20)
dw $012D, $011C, $010C, $00FD, $00EF, $00E2 ; F#4 to B4 ($20-$26)
dw $00D2, $00C9, $00BD, $00B3, $00A9, $009F ; C5 to F5 ($27-$2C)
dw $0096, $008E, $0086, $007E, $0077, $0070 ; F#5 to B5 ($2D-$32)
dw $006A, $0064, $005E, $0059, $0054, $004F ; C6 to F6 ($33-$38)
dw $004B, $0046, $0042, $003F, $003B, $0038 ; F#6 to B6 ($39-$3E)
dw $0034, $0031, $002F, $002C, $0029, $0027 ; C7 to F7 ($3F-$45)
dw $0025, $0023, $0021, $001F, $001D, $001B ; F#7 to B7 ($46-$4A)
dw $001A, $0018, $0017, $0015, $0014, $0013 ; C8 to F8 ($4B-$50)
dw $0012, $0011, $0010, $000F, $000E, $000D ; F#8 to B8 ($51-$56)
dw $000C, $000C, $000B, $000A, $000A, $0009, $0008 ; C9 to F#9 ($57-$5D)
[Related sidenote: if you don't understand the naming and numbering of the notes, please see this music theory page.] To use this table, we must be a bit careful. Because each of these notes is a word (2 bytes), the way we access them requires us to double the number of the note required to actually access the right data from the table. We also want to put this value into our x register so we can index to the correct note. For example, if we want the second note, we need to store 2 in x:
ldx #2 ; Store 2 in x register
lda notes,x ; Load the bottom value of the first note
sta $4002 ; Store the bottom half of the note
lda notes+1,x ; Load the upper value of the first note
sta $4003 ; Store the upper half of the note
Note that we have to write notes+1 to access the higher end of that second note, even though $4003 is where we store the higher half of the note value. I'm not exactly sure why this is (and there's probably a good reason!), but for now just take it on faith (and hopefully at some point this remark will be replaced by a coherent explanation!)
Of course, writing all of our code this way wouldn't be practical, and we would very quickly run out of space on our NES cartridge simply playing a few notes. If we're going to play a series of notes (a song), we're going to need a loop of some kind. So rather than loading a particular number into our x register, we need to construct another table of values to load into it:
; Basic idea for song_notes table
song_notes:
db 1, 3, 4, 5, 6, 2, 2, 1
db 3, 5, 2, 3, 5, 7, 2, 3
db 4, 4, 2, 4, 3, 2, 4, 1
db 4, 4, 2, 4, 4, 4, 1, 1
This solves the problem of efficiency, but not readability. To make our code even more clear, we should create some labels for our notes. Near the beginning of the program (after our header), we'll want something like the following (please see the code for this tutorial for the full example):
; Labels for note names
A1 equ $00
As1 equ $01 ; 's' for "sharp" (i.e. A#)
B1 equ $02
; etc...
Now, with our labels in place, we can write a look-up table that is a little easier to understand and read:
; Basic idea for song_notes table
song_notes:
db C3, C3, G3, G3, A3, A3, G3
db F3, F3, E3, E3, D3, D3, C3
; etc...
Doing this also allows us to write code that is: a) easier to read, and b) more efficient (looping):
; (This is a code snippet straight from basicsound.asm)
; Load up a new note
ldx ntnum
lda birthday_length,x
sta sqlen ; Length of new note (this ticks down every clock cycle)
ldx ctnt ; This loads the current note
txa ; We transfer to accumulator so we can easily double it
asl ; Double value since using words with our notes
tax ; Put back in x-register
lda notes,x
sta $4002
lda notes+1,x
sta $4003
In this snippet of code, we are doing a couple of things:
- ldx ntnum: loading the current note number (which incrememts every time we play a new note)
- lda birthday_length,x: loading the appropriate note length using the birthday_length LUT and ntnum After that we load the current note which is stored in ctnt. Then (as the notes above suggest) we load into the accumulator (txa), double it (asl), and transfer it back to the x-register (tax). Remember, the doubling allows us to work properly with words instead of bytes (our notes are word-sized because they take up two bytes).
This should make more sense. If it helps, look at the code in the context of the entire program. Remember to read slowly, chip away at it. Don't ever worry if you don't completely understand the code; the goal is, at a minimum, to just try to understand a little more of the code each time. You can do it!
This readme, coupled with the commenting in the code, should be of help when deciphering the meaning of everything, so you can make modifications and create your own musical masterpieces. Be sure to check out my other tutorials on NES/ASM6502 programming, ideally in the following order:
- NES Hello World: https://github.com/thomaslantern/nes-hello-world/
- NES Basic Graphics: https://github.com/thomaslantern/nes-basic-graphics/
- NES Basic Sound (this one!)
- NES Basic Controls: https://github.com/thomaslantern/nes-basic-controls
- NES First Game! (Birthday Blast): https://github.com/thomaslantern/nes-birthday-blast
As always, feel free to go here and top up your knowledge on the things I haven't covered: https://www.nesdev.org/wiki/APU_basics)
Happy Coding!