Code status: illustrative code. Adapt the listing to your program and check it on your target machine. This page links no assembly or execution record for the listing itself.
Overview
One voice plays one note; setting a voice up gives it a waveform and an envelope. Music is the step up: all three voices at once, each handed a fresh note on a regular beat. On the C64 that beat comes from the raster interrupt — the same 50 Hz tick that drives the display drives the tune. Count those interrupts into rows, and on each row look up a frequency per voice from a note table and gate it. Voice 1 takes the lead, voice 2 the harmony, voice 3 the bass. That loop — a tick, a pattern, a frequency table — is every C64 music driver in miniature.
Code
; =============================================================================
; SID MULTI-VOICE - C64
; A raster-IRQ tick steps a pattern; each of the 3 voices gets its note
; IRQ wiring is exactly as in Raster Splits — music_tick is what it calls
; =============================================================================
SID_FREQ_LO = $D400 ; per voice: base + 0
SID_FREQ_HI = $D401 ; base + 1
SID_CTRL = $D404 ; base + 4 (waveform + gate)
SID_AD = $D405 ; base + 5
SID_SR = $D406 ; base + 6
SID_VOLUME = $D418
VOICE1 = 0 ; the three voices are 7 bytes apart
VOICE2 = 7
VOICE3 = 14
TEMPO = 6 ; raster frames per row
ROWS = 8
music_init:
lda #$0F
sta SID_VOLUME ; master volume up
ldx #VOICE1 ; same plucked envelope on every voice
jsr set_env
ldx #VOICE2
jsr set_env
ldx #VOICE3
jsr set_env
lda #TEMPO
sta mtimer
lda #0
sta mrow
rts
set_env: ; X = voice base offset
lda #$09
sta SID_AD,x ; attack 0, decay 9
lda #$00
sta SID_SR,x ; sustain 0, release 0 -> a pluck
rts
; --- Call once per raster IRQ; a new row lands every TEMPO frames ---
music_tick:
dec mtimer
beq .row
rts ; between rows — voices hold
.row:
lda #TEMPO
sta mtimer
ldy mrow ; reload the row index before each read
lda pat_v1,y
ldx #VOICE1
jsr voice_note ; lead
ldy mrow
lda pat_v2,y
ldx #VOICE2
jsr voice_note ; harmony
ldy mrow
lda pat_v3,y
ldx #VOICE3
jsr voice_note ; bass
inc mrow
lda mrow
cmp #ROWS
bne .done
lda #0
sta mrow ; loop the pattern
.done:
rts
; A = note index (0 = hold), X = voice base offset
voice_note:
cmp #0
beq .rest
tay ; note index -> Y (freq table lookup)
lda #$20 ; lower gate while retaining sawtooth
sta SID_CTRL,x
lda freq_lo,y
sta SID_FREQ_LO,x
lda freq_hi,y
sta SID_FREQ_HI,x
lda #$21 ; sawtooth + gate on -> strike the note
sta SID_CTRL,x
.rest:
rts
mtimer: !byte 0
mrow: !byte 0
; SID frequency values, PAL. Index 0 = hold.
; C-3 C-4 E-4 G-4
freq_lo: !byte 0, $B3, $67, $EE, $13
freq_hi: !byte 0, $08, $11, $15, $1A
pat_v1: !byte 2, 3, 4, 3, 2, 3, 4, 3 ; lead: a C-E-G arpeggio
pat_v2: !byte 4, 4, 2, 2, 4, 4, 2, 2 ; harmony
pat_v3: !byte 1, 0, 1, 0, 1, 0, 1, 0 ; bass on the beatTrade-offs
| Aspect | Cost |
|---|---|
| CPU | A few bytes most frames; a little more on a row change |
| Memory | The frequency table, the patterns, two bytes of state |
| Limitation | Three voices — and an effect must borrow one from the song |
When to use: Any in-game music. Every SID tune, from title screens to demos, is this shape with more on top.
When to avoid: A single reactive bleep; that’s a note trigger, not a sequencer.
The raster IRQ is the clock
The player keeps no timer of its own — it rides the raster interrupt. Wire an IRQ to fire once a frame, call music_tick from it, and you have a rock-steady 50 Hz (PAL) heartbeat for free. mtimer counts those frames down; only when it reaches zero does a new row land, so TEMPO sets the speed — 6 frames a row is the classic tracker default. It’s why C64 music and C64 raster effects are the same craft: both are things you do on the interrupt, and a demo’s music and its display are driven by the very same tick.
Three voices, three jobs
The SID has exactly three voices, evenly spaced 7 bytes apart — $D400, $D407, $D40E — which is why the code indexes every register with a base offset in X. The classic division of labour is lead, harmony and bass, and because all three share the SID’s single filter, a driver often reserves voice 3 for bass or drums so it can cut it in and out (voice 3 can even be silenced from the display without a gate, via $D418 bit 7). Wanting a fourth voice means faking it — rapidly switching one voice between two notes, the arpeggio “chord” that gives SID music its unmistakable shimmer.
The hard restart
Re-striking a note is the SID’s famous trap. Gating a voice on (CTRL bit 0 going 0→1) starts the attack — but if the voice is already gated on from the last note, writing the same waveform does nothing and the envelope never restarts. Worse, the SID’s envelope generator has a well-known bug where a poorly-timed gate leaves the volume stuck. The cure every driver uses is the hard restart: a frame or two before each note, gate the voice off and load a fast release, so the envelope is reliably at zero and ready to attack cleanly. The skeleton above re-gates in place for clarity; a shipping player sets its notes up one tick early precisely to hard-restart them.
From skeleton to player
A real SID driver adds four things to this frame:
- Instruments — a waveform, pulse-width and ADSR per note, rewritten each row, so a voice can pluck, sweep or drone rather than blare one flat saw.
- The filter —
$D415–$D418shape the tone across voices; sweeping the cutoff each frame is half of what makes SID music sing. - Effects — vibrato, portamento, pulse-width modulation and arpeggio, all run on the in-between ticks.
- An order list — a sequence of pattern numbers, so a tune reuses a handful of short patterns instead of storing every bar.
Add those and a note-trigger effects routine that borrows a voice, and you have a player like the ones behind the C64’s greatest scores — the shape a tool such as GoatTracker exports.
Related
Patterns: SID Voice Setup, SID Note Trigger, Raster Splits
Vault: The SID Chip | Commodore 64
music_init assumes the program owns the SID, its I/O mapping is visible and other voices/filter state have been initialised. Each nonzero note lowers then raises its gate; zero leaves the previous voice alone. It is not a rest. The routine changes A/X/Y and flags, so an IRQ dispatcher saves them and owns acknowledgement/return. Real envelope retrigger timing still depends on SID behaviour; gate-off does not reset its envelope to zero.