Turn your headphones down, or, maybe even off for this one:
So what was that terrible idea? I figured the only place to go from making tracker mods natively on an Amiga last month would be to code the next song in machine language, get it? Machine….language, anyway….
No one can write in machine language though, so I did the next best thing, and wrote some 6502 assembly targeting the NES.
As @antillese has explained in the context of much better NES songs, the NES has 5 audio channels. 2 pulse channels, a triangle channel, a noise channel, and a sample playback channel. The channels each have 4 bytes of memory mapped to the APU, which actually lives on the NES’s modified 6502 CPU, but it gives us discrete bits of things that we can control and it handles most of the hard stuff for us without having to code anything. Most NES games have songs written in some external tool like a tracker, and then a subroutine in the program rom loads in those songs into the APU on a per frame basis from rom, no direct fiddling with the APU required. But I ask you, where’s the fun in that?
We’re going to write a simple program to load in randomly generated garbage into the APU channels, and then we’re going to turn the channels on and off by increasing the APU flag counter at $4015 and then resetting it in a 16 beat cycle. This will create “music” in so far as it will be pitched notes in a repeated order, as music was defined by the Criminal Justice and Public Order Act 1994. Ready? Let’s jam!
First we’re going to set up the iNES header. We don’t need anything fancy so we’re setting it to the NROM mapper (or lack thereof) that SMB 1 uses.
.segment "HEADER"
.byte "NES"
.byte $1a
.byte 02 ; progrom
.byte 01 ; charrom
.byte 00 ; nrom mapper
.byte 00, 00, 00, 00
.byte 00, 00, 00, 00, 00
Then we’re going to declare some variables in the zeropage, the first 256 bytes of memory that’s fastest to access
.segment "ZEROPAGE"
frame: .res 1
beat: .res 1
seed: .res 2 ; for randomness
Then we’re going to set up our program rom, most of this is boiler plate like setting up memory and waiting for the crt that we’re hypothetically plugged into to cycle twice so we know we’re ready to rock
.segment "STARTUP"
reset:
;boiler
sei
cld
ldx #%10000000
stx $4017
ldx #$00
stx $4010
ldx #$00
txs
ldx #$00
stx $2000
stx $2001
:
bit $2002
bpl :-
txa
clearmem:
sta $0000, x
sta $0100, x
sta $0300, x
sta $0400, x
sta $0500, x
sta $0600, x
sta $0700, x
lda #$ff
sta $0200, x
lda #$00
inx
cpx #$00
bne clearmem
:
bit $2002
bpl :-
lda #$02
sta $4014
nop
cli
lda #%10010000
sta $2000 ; nmi on vblank
lda #%00011110
sta $2001 ; turn on graphics
Now we’re going to initialize our counters and our random seed and get ready for our main loop
; initialize counters
lda #$00
sta frame
sta beat
; initialize the seed to non zero or else it will always be 0
lda #$01
sta seed+0
sta seed+1
; turn pulse on to start
lda #$01
sta $4015
; loop forever and wait for interupts
forever:
jmp forever
Now in our main loop we’re going to fill our APU with garbage and increase the channel on off flags in $4015 to 00010000 and then loop back to 00000001 for, uh “music”. The bits in the byte at $4015 turns on things as follows : nothing nothing nothing sample noise triangle pulse2 pulse1
nmi:
jsr prng ; set the a register to a random value to store in everything
sta $4000
sta $4001
sta $4002
sta $4003
sta $4004
sta $4005
sta $4006
sta $4007
sta $4008
sta $4009
sta $400a
sta $400b
sta $400c
sta $400d
sta $400e
sta $400f
sta $4010
sta $4011
sta $4012
sta $4013
lda frame
cmp #$1d
bne incframe
lda #$ff ; if we're at 30 prepare to roll over
sta frame
; if we're at 30 increment the beat counter
inc $4015 ; change what's playing every beat
lda beat
cmp #$10
bne incbeat
lda #$ff ; if we're at 16 prepare to roll over
sta beat
; if we're at 16 reset the seed
lda #$01
sta seed+0
sta seed+1
sta $4015
incbeat:
inc beat
incframe:
inc frame
rti
prng:
ldy #8
lda seed+0
:
asl
rol seed+1
bcc :+
eor #$39
:
dey
bne :--
sta seed+0
cmp #0
rts
Finally set up where the interrupts are and then where our non existent character rom lives:
.segment "VECTORS"
.word nmi
.word reset
.segment "CHARS"
Voila we have…..something that sounds awful. But hey! It’s audible!
If you want to fiddle around with NES programming, I learned how to do most of this from the truly exhaustive NES dev wiki here: https://www.nesdev.org/wiki/Nesdev_Wiki