Skip to content

SID Multi-Voice

Step a note pattern once per raster interrupt, handing each of the SID's three voices its note from a frequency table — the skeleton of every C64 music player.

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

Assembly
; =============================================================================
; 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 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–$D418 shape 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.

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.