THEORY (as I understand it)
Let c be the CPU clock speed. If samples are written to $4011 every a cycles, then the sampling rate is c/a. A tone that has frequency f will require (c/a)/f = c/(af) writes to $4011 for one period. For a square wave, half of these writes should be the high value and half should be the low value. Let w be the number of these writes for the high or low value. Then w = c/(2 a f). You can rearrange this to get that f = c/(2 a w) is the frequency of a wave generated by a given combination of values of a and w.
IMPLEMENTATION
My target is the NTSC NES. ASM6 is the assembler, and I use its ALIGN directive where needed in order to avoid branches and loads from crossing page boundaries. The goal is to be able to play the notes of the C major scale ranging from C1 to B6, where A4 is the A above middle C. I decided to use Pythagorean tuning based on A4 = 440 Hz. Using the formula for f from above, I wrote a Python script to find the combination of a and w that would yield the closest frequency to the desired one for each note in each octave. This is my code for PCM playback:
Code: Select all
;Variables:
;Y = value written
;delay_target (zp)
;num_writes_lo (zp)
;num_writes_hi (zp)
write_y_4011:
sty $4011 ; 4 cycles
sec ; 2 cycles
lda num_writes_lo ; 3 cycles
sbc #1 ; 2 cycles
sta num_writes_lo ; 3 cycles
lda num_writes_hi ; 3 cycles
sbc #0 ; 2 cycles
sta num_writes_hi ; 3 cycles
bcc + ; 3 cycles if taken, 2 cycles not
; delay_target = TARGET - 57 cycles
; 3 + 27 + 3 + 4 + 2 + 3 + 2 + 3 + 3 + 2 + 3 + 2 = 57
lda delay_target ; 3 cycles
jsr delay_a_27_clocks
jmp write_y_4011 ; 3 cycles
+ rts ; 6 cycles
; 2 + 3 + 2 + 3 + 3 + 2 + 3 + 3 + 6 = 27 cycles elapsed after final write
;Variables:
;X = note index
;duration_lo (zp)
;duration_hi (zp)
;writes_lo (zp)
;writes_hi (zp)
;cycle_target (zp)
play_note:
lda note_dur_lo,x ; 4 cycles
sta duration_lo ; 3 cycles
lda note_dur_hi,x ; 4 cycles
sta duration_hi ; 3 cycles
lda note_wr_lo,x ; 4 cycles
sta writes_lo ; 3 cycles
lda note_wr_hi,x ; 4 cycles
sta writes_hi ; 3 cycles
lda note_cycles,x ; 4 cycles
sta cycle_target ; 3 cycles
sec ; 2 cycles
sbc #57 ; 2 cycles
sta delay_target ; 3 cycles
- ldy #$73 ; 2 cycles
lda writes_lo ; 3 cycles
sta num_writes_lo ; 3 cycles
lda writes_hi ; 3 cycles
sta num_writes_hi ; 3 cycles
jsr write_y_4011 ; 6 cycles jsr + 4 sty, 27 cycles after final write
ldy #$0C ; 2 cycles
lda writes_lo ; 3 cycles
sta num_writes_lo ; 3 cycles
lda writes_hi ; 3 cycles
sta num_writes_hi ; 3 cycles
; TODO (done): delay TARGET - 85 cycles
; 27 + 2 + 12 + 3 + 2 + 2 + 27 + 10 = 85
lda cycle_target ; 3 cycles
sec ; 2 cycles
sbc #85 ; 2 cycles
jsr delay_a_27_clocks
jsr write_y_4011 ; 6 cycles jsr + 4 sty, 27 cycles after final write
sec ; 2 cycles
lda duration_lo ; 3 cycles
sbc #1 ; 2 cycles
sta duration_lo ; 3 cycles
lda duration_hi ; 3 cycles
sbc #0 ; 2 cycles
sta duration_hi ; 3 cycles
bcc + ; 3 cycles if taken, 2 cycles not
; TODO (done): delay TARGET - 108 cycles
; 27 + 2 + 3 + 2 + 3 + 3 + 2 + 3 + 2 + 3 + 2 + 2 + 27 + 3 + 2 + 12 + 10 = 108
lda cycle_target ; 3 cycles
sec ; 2 cycles
sbc #108 ; 2 cycles
jsr delay_a_27_clocks
jmp - ; 3 cycles
+ rts ; 6 cycles
Some of the high tones sound flat (lower pitch than expected). The most problematic ones to my ear are G5, C6, and F6. This is also evident when playing notes an octave apart (e.g., E6 sounds flat when played after E5). Something also sounds off about the tones in the lowest octave, but I find it difficult to express just what it is. When using a note tuning app on my phone to check the tuning, all of the notes are slightly flat starting from the lowest octave, and the deviation from expected worsens with each succeeding octave. An interesting thing is that the deviation reported by the app (in cents) is higher than expected based on calculating the cents between the frequencies. In case this were a side effect of using Pythagorean tuning, I wrote another script to determine the a and w values for 12-tone equal temperament tuning, but the results were no better.
I have attached the ROMs that I made for these tests. Any input on the situation is welcome.