Skip to content

Sound

The sound chip is an ARMSID, a drop-in replacement for the MOS 6581 that the Commodore 64 made famous. Three voices, each with its own oscillator, waveform and envelope, plus filters they all share.

There are two ways to use it: five Kernal calls that handle the common case, or the 29 registers underneath when you want more.

The easy way

InitSIDSet the chip up from cold — the Kernal has already done this
SidSetVolumeMaster volume in A, 0 to 15
SidPlayNoteVoice in A (0–2), frequency low byte in X, high in Y
SidSilenceGate every voice off
BeepThe one the machine makes at power-on

SidPlayNote starts a note and returns immediately. It does not wait, which is the point: the note goes on sounding while your program does something else. SysDelay is how you hold it, and SidSilence is how you stop it.

What you get is a triangle wave with one fixed envelope — no attack, a quick decay, and a short release. It is a clean, slightly flutey note, and it is the right thing for game noises and for anything where you would rather write the tune than the synthesizer. When you want a different sound, the registers are further down this page.

Frequency

The chip counts in steps of about one-sixteenth of a hertz, so the number it wants is not the number you want. The conversion is:

register value = hertz × 16.75

which is exactly what SOUND does in BASIC, and which the assembler can do for you at build time so the machine never divides anything:

asm
.define NOTE(hz) (hz * 67 / 4)

  lda #0                        ; voice 0
  ldx #<NOTE(440)               ; concert A
  ldy #>NOTE(440)
  jsr SidPlayNote

A short table of the useful ones:

NoteHzNoteHz
C262G392
D294A440
E330B494
F349C′523

Double the frequency to go up an octave, halve it to go down.

A tune

asm
; A six-note fanfare.
;
; The sound chip does not think in hertz. Its frequency registers count in
; steps of about one-sixteenth of a hertz, so a note is the frequency you want
; multiplied by 67 and divided by 4 — worked out here while the program is
; being assembled, so the machine never does the arithmetic at all.

.setcpu "65C02"

.include "6502.inc"

.segment "CODE"

BasicStartup:
  .byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00

.define NOTE(hz) (hz * 67 / 4)  ; hertz as the sound chip counts it

NOTES = 6

Index := $40

Start:
  lda #<Title
  ldy #>Title
  jsr PrintStr

  lda #12                       ; volume, 0 to 15
  jsr SidSetVolume

  stz Index
NextNote:
  ldx Index
  lda Letter,x                  ; say which note is playing
  jsr Chrout
  lda #' '
  jsr Chrout

  ldx Index
  ldy FreqHigh,x
  lda FreqLow,x
  tax                           ; X = low byte, Y = high byte
  lda #0                        ; voice 0, of three
  jsr SidPlayNote

  ldx Index
  lda Length,x                  ; hold it, in hundredths of a second
  ldx #0
  jsr SysDelay
  jsr SidSilence

  inc Index
  lda Index
  cmp #NOTES
  bne NextNote

  jsr PrintCRLF
  rts

; C  E  G  C'  G  C'
FreqLow:  .lobytes NOTE(262), NOTE(330), NOTE(392), NOTE(523), NOTE(392), NOTE(523)
FreqHigh: .hibytes NOTE(262), NOTE(330), NOTE(392), NOTE(523), NOTE(392), NOTE(523)
Length:   .byte 15, 15, 15, 30, 15, 45
Letter:   .byte "CEGCGC"

Title:    .byte "A LITTLE FANFARE", CHAR_CR, CHAR_LF, $00
RUN
A LITTLE FANFARE
C E G C G C

OK

Three parallel tables — low bytes, high bytes, lengths — indexed by one counter, is the shape almost every tune player has. Adding a fourth table of voice numbers is how you get chords.

Why the frequencies are split into two tables

SidPlayNote wants the low byte in X and the high byte in Y, and the index has to live somewhere while both are being loaded. Splitting the table means one ldx Index covers both fetches; keeping it as words would mean doubling the index every time through.

The registers underneath

$9800 upwards, and worth knowing when the envelope matters.

Per voiceWhat it does
SID_Vn_FREQ_LO / _HIThe frequency, as above
SID_Vn_PW_LO / _HIPulse width, for the pulse waveform only
SID_Vn_CTRLWaveform and gate — see below
SID_Vn_ADAttack in the high nibble, decay in the low
SID_Vn_SRSustain in the high nibble, release in the low

The control register is where the character comes from:

Bit
7Noise
6Pulse
5Sawtooth
4Triangle
3Test — resets the oscillator
2Ring modulation with the voice below
1Sync with the voice below
0Gate — the note starts when this goes high and releases when it goes low

So a plucked sawtooth on voice 1 is:

asm
  lda #$00                      ; attack 0, decay 0 — instant
  sta SID_V1_AD
  lda #$F8                      ; sustain 15, release 8 — rings out
  sta SID_V1_SR
  lda #<NOTE(330)
  sta SID_V1_FREQ_LO
  lda #>NOTE(330)
  sta SID_V1_FREQ_HI
  lda #%00100001                ; sawtooth, gate on
  sta SID_V1_CTRL

and letting go is lda #%00100000 — the same byte with the gate bit cleared. The release phase then plays out on its own, which is why SidSilence clears gates rather than frequencies: zeroing an oscillator mid-note freezes the waveform at whatever level it had reached and you hear a thump.

SID_MODE_VOL at $9818 holds the master volume in its low nibble; the high nibble selects the filters, which is a rabbit hole with a good map already drawn.

What ARMSID does and does not reproduce

It is a microcontroller with a DAC, and it emulates both the 6581 and the later 8580. Waveforms, envelopes and ring modulation come out right. The filters are the part where any 6581 is unpredictable — the real chips varied chip-to-chip — so a filter sweep that sounded a particular way on one Commodore 64 will not sound identical here, or on another Commodore 64.

Nothing complains if there is no sound chip

The sound routines check HW_PRESENT and quietly do nothing when there is no card fitted. A game that beeps on a hit keeps playing on a machine with no sound. That is deliberate — but it does mean silence is not evidence of a bug in your note table.

Next: the keyboard and the sticks.

Written for BIOS v1.5. Released under the MIT License.