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 = rest/hold), X = voice base offset
voice_note:
cmp #0
beq .rest
tay ; note index -> Y (freq table lookup)
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 = rest.
; 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 beat
Trade-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