2600 Cookbook:
Music Basics
Problem:
You'd like some music in your Atari program.
Solution:
Set AUDC0 or AUDC1 to the appropriate voice,
AUDF0 or AUDF1 to the right pitch, and AUDV0
or AUDV1 to the correct volume. Update as neccesary. :-)
Discussion:
Atari music is a bit odd. It has certain quirks that make it
a bit difficult to transcribe "real" music into it, but it does
a distinctive sound all its own.
You have two voices to play with, and these voices are pretty much
all you have to deal with any music and any special effects in your game.
At any given time, the rightmost 4 bits of each audio control register
(AUDC0 or AUDC1) determine what "kind" of noise tha voice makes. The 16 possible settings have some "pure tone" and some "noise" settings. Often, experimenting is the best way to find the right sound for what you're trying to do. Here is the table from SPG:
D3 | D2 | D1 | D0 | Type of noise or division |
0 | 0 | 0 | 0 | set to 1 |
0 | 0 | 0 | 1 | 4 bit poly |
0 | 0 | 1 | 0 | div 15 -> 4 bit poly |
0 | 0 | 1 | 1 | 5 bit poly -> 4 bit poly |
0 | 1 | 0 | 0 | div 2 : pure tone |
0 | 1 | 0 | 1 | div 2 : pure tone |
0 | 1 | 1 | 0 | div 31 : pure tone |
0 | 1 | 1 | 1 | 5 bit poly -> div 2 |
1 | 0 | 0 | 0 | 9 bit poly (white noise) |
1 | 0 | 0 | 1 | 5 bit poly |
1 | 0 | 1 | 0 | div 31 : pure tone |
1 | 0 | 1 | 1 | set last 4 bits to 1 |
1 | 1 | 0 | 0 | div 6 : pure tone |
1 | 1 | 0 | 1 | div 6 : pure tone |
1 | 1 | 1 | 0 | div 93 : pure tone |
1 | 1 | 1 | 1 | 5 bit poly div 6 |
The rightmost 4 bits of AUDV0 and AUDV1 determine the volume:
0000 is off, 1111 is the loudest.
The frequency registers, AUDF0 and AUDF1, determine the pitch.
The challenge is that the tuning doesn't quite line up
with typical (well, western) music, because you're starting
with a base tone (as set by AUDC0/1) and then dividing that
between 1 and 32. (You set the 5 rightmost bits of AUDF0/1,
and this value is one less than what the base frequency will be
divided by.
UNDER CONSTRUCTION: NEED MORE INFO ON CALCULATING FREQUENCIES
--WAS THERE A PROGRAM FOR THIS??
For NTSC, you get 60 frames a second. It's fairly straight forward
to build an engine that runs every grame and updates the audio registers
as neccesary. This Recipe's Example code is a simplified, not-optimized
engine to do just that.
References:
Example:
;
; simple music playing routine
; by Kirk Israel
; prototype for JoustPong title screen
processor 6502
include vcs.h
include macro.h
org $F000
SEG.U VARS
ORG $80
;each voice uses 4 bytes of RAM
;set aside and label memory
musicRiffNotePointer ds 2
musicRiffNoteCounter ds 1
musicRiffNoteTimeLeft ds 1
musicBeatNotePointer ds 2
musicBeatNoteCounter ds 1
musicBeatNoteTimeLeft ds 1
;music has 16 notes, beat has 12...
;(actually 8 notes, 8 silences, 6 notes, 6 silences...)
MUSICRIFF_NOTECOUNT = #16
MUSICBEAT_NOTECOUNT = #12
SEG CODE
org $F000
;generic start up stuff...
Start
CLEAN_START
LDA #$00 ;start with a black background
STA COLUBK
LDA #14
STA COLUPF
;music riff uses bassy sound
LDA #10
STA AUDC0
;beat uses percussive sound
LDA #8
STA AUDC1
;VSYNC time
MainLoop
VERTICAL_SYNC
LDA #43
STA TIM64T
;musicRiffNoteTimeLeft holds how much
;time the current note is being held for.
;when it hits zero, it's time to start
;playing the next note
DEC musicRiffNoteTimeLeft
BPL DoneWithChangingNote
;if we're here, we're changing notes
;musicRiffNoteCounter is which note we're on.
DEC musicRiffNoteCounter
BPL DoneCheckResetNoteCounter
;If it's zero, we need to reset the counter...
LDA #MUSICRIFF_NOTECOUNT-1
STA musicRiffNoteCounter
DoneCheckResetNoteCounter
;we're changing the pitch being played
;put the note counter into Y and use it as an offset
;into some tables...
LDY musicRiffNoteCounter
;load how long we're holding this note from table, store in musicRiffNoteTimeLeft
LDA MusicLengthData,Y
STA musicRiffNoteTimeLeft
DEC musicRiffNoteTimeLeft ;correct for an 'off by one error'
;load the pitch of this note from table
LDA MusicPitchData,Y
;-1 = we're resting, not playing
BMI ZeroOutSound
;put note pitch into AUDF0
STA AUDF0
;...at a sensible volume
LDA #8
STA AUDV0
JMP DoneSettingPitchAndVolume
ZeroOutSound
;freq. was negative, silence out
LDA #0 ;silence
STA AUDV0
DoneSettingPitchAndVolume
DoneWithChangingNote
;do the same thing for the the beat....
DEC musicBeatNoteTimeLeft
BPL DoneWithChangingBeat
DEC musicBeatNoteCounter
BPL DoneCheckResetBeatCounter
LDA #MUSICBEAT_NOTECOUNT-1
STA musicBeatNoteCounter
DoneCheckResetBeatCounter
LDY musicBeatNoteCounter
LDA BeatLengthData,Y
STA musicBeatNoteTimeLeft
DEC musicBeatNoteTimeLeft
LDA BeatPitchData,Y
BMI ZeroOutBeatSound
STA AUDF1
LDA #8
STA AUDV1
JMP DoneSettingBeatPitchAndVolume
ZeroOutBeatSound
LDA #0
STA AUDV1
DoneSettingBeatPitchAndVolume
DoneWithChangingBeat
;for diagnostic reasons to track down an off by
;one error, I blasted the counter of how many frames
;left for the current sound to the playfield
;then slowed z26 down to a super slow speed:
; z26 -r4
; or somesuch
; then I kept it in just for something to look at :-)
LDA musicRiffNoteTimeLeft
STA PF1
LDA musicBeatNoteTimeLeft
STA PF2
WaitForVblankEnd
LDA INTIM
BNE WaitForVblankEnd
LDY #191
STA WSYNC
STA VBLANK
;main scanline loop...
ScanLoop
STA WSYNC
DEY
BNE ScanLoop
LDA #2
STA WSYNC
STA VBLANK
LDX #30
OverScanWait
STA WSYNC
DEX
BNE OverScanWait
JMP MainLoop
; music data is stored "backwards"
; pitch of -1 = silence
; length is given in frames
MusicPitchData
.byte #-1
.byte #17
.byte #-1
.byte #17
.byte #-1
.byte #16
.byte #-1
.byte #16
.byte #-1
.byte #15
.byte #-1
.byte #15
.byte #-1
.byte #18
.byte #-1
.byte #18
MusicLengthData
.byte #40
.byte #20
.byte #10
.byte #26
.byte #40
.byte #20
.byte #10
.byte #26
.byte #40
.byte #20
.byte #10
.byte #26
.byte #40
.byte #20
.byte #10
.byte #26
BeatPitchData
.byte #-1
.byte #120
.byte #-1
.byte #40
.byte #-1
.byte #120
.byte #-1
.byte #120
.byte #-1
.byte #40
.byte #-1
.byte #120
BeatLengthData
.byte #16
.byte #2
.byte #4
.byte #2
.byte #10
.byte #2
.byte #22
.byte #2
.byte #10
.byte #2
.byte #22
.byte #2
org $FFFC
.word Start
.word Start
Back to 2600 Cookbook