Skip to content
Game 1 Unit 4 of 128 1 hr learning time

Laser Sound

Make the SID chip produce a laser sound when the fire button is pressed — your first taste of the C64's legendary sound hardware.

3% of Starfield

Press fire — pew! The bullet launches and a short laser sound plays. Same game as Unit 3, but now the SID chip gives your shots a voice. Two small additions to the code, one giant leap for game feel.

The SID Chip

The SID (Sound Interface Device) is the C64’s sound chip — a three-voice synthesiser built right into the hardware. It lives at addresses $D400 to $D418, just like the VIC-II graphics chip lives at $D000.

Each voice has its own set of registers for frequency, waveform, and envelope. Voice 1 starts at $D400, voice 2 at $D407, voice 3 at $D40E. There’s also a shared volume register at $D418.

We’ll only use voice 1 in this unit. The SID can do far more — filters, ring modulation, synchronisation — but that’s for later. Right now, we just want a satisfying pew.

Setting Up the Sound

Before the game loop starts, we configure voice 1 with the right pitch and envelope:

        ; SID setup — voice 1 laser sound
        lda #$0f
        sta $d418           ; Volume to maximum

        lda #$00
        sta $d400           ; Frequency low byte
        lda #$10
        sta $d401           ; Frequency high byte ($1000 = mid-high pitch)

        lda #$09
        sta $d405           ; Attack=0, Decay=9
        lda #$00
        sta $d406           ; Sustain=0, Release=0

Four things happen here:

  1. Volume ($D418) — set to $0F (maximum). This is the master volume for all three voices. Bits 0–3 control volume; bits 4–7 control filters (we’ll ignore those for now).
  2. Frequency ($D400/$D401) — a 16-bit value split across two registers. Low byte in $D400, high byte in $D401. We set $1000, which gives a mid-high pitch suitable for a laser.
  3. Attack/Decay ($D405) — the upper nibble is Attack (0 = instant), the lower nibble is Decay (9 = quick fade). The sound hits full volume immediately and fades out over about half a second.
  4. Sustain/Release ($D406) — both zero. After the decay phase, the sound drops to silence and stays there.

The ADSR Envelope

Every sound has a shape over time. The SID controls this with four parameters packed into two registers:

  • Attack — how quickly the sound rises from silence to full volume (0 = instant, 15 = slowest)
  • Decay — how quickly it falls from full volume to the sustain level (0 = instant, 15 = slowest)
  • Sustain — the volume level the sound holds at after the decay phase (0 = silent, 15 = full)
  • Release — how quickly it fades to silence after you turn it off (0 = instant, 15 = slowest)

For our laser, we want: instant attack, quick decay, no sustain. The sound punches in and dies away — a sharp “pew”.

$D405 — Attack / Decay 7 Atk 3 0 6 Atk 2 0 5 Atk 1 0 4 Atk 0 0 3 Dcy 3 1 2 Dcy 2 0 1 Dcy 1 0 0 Dcy 0 1
Attack = 0 (upper nibble), Decay = 9 (lower nibble). Value: $09. Instant attack, quick decay.

Waveform and Gate

The control register at $D404 selects the waveform and controls the gate:

$D404 — Voice 1 Control 7 Noise 0 6 Pulse 0 5 Saw 1 4 Tri 0 3 Test 0 2 Ring 0 1 Sync 0 0 Gate 1
Sawtooth waveform (bit 5) + gate on (bit 0) = $21. The gate starts the ADSR envelope.

The waveform bits (4–7) select which sound shape the voice produces. Only set one at a time:

  • Bit 4 = Triangle — soft, flute-like
  • Bit 5 = Sawtooth — bright, buzzy (our laser)
  • Bit 6 = Pulse — hollow, square-wave-ish (needs pulse width set)
  • Bit 7 = Noise — random, good for explosions

The gate bit (bit 0) is the trigger. Setting it to 1 starts the ADSR envelope — the sound begins its attack phase. Clearing it to 0 begins the release phase.

Triggering the Sound

Each time a bullet spawns, we need to retrigger the sound. There’s a catch: if the gate is already on, writing $21 again does nothing. The ADSR won’t restart. You have to turn the gate off then on again:

        ; Trigger laser sound (gate off then on to retrigger)
        lda #$20
        sta $d404           ; Sawtooth, gate OFF (reset envelope)
        lda #$21
        sta $d404           ; Sawtooth, gate ON (start sound)

The first write ($20) clears the gate bit while keeping sawtooth selected. The second write ($21) sets it again. This OFF→ON cycle resets the envelope, so each shot gets a fresh “pew” regardless of where the previous sound was in its decay.

This pattern — gate off, gate on — is something you’ll use every time you want to retrigger a SID voice. It’s one of those small details that makes the difference between sound that works and sound that doesn’t.

The Complete Code

; Starfield - Unit 4: Laser Sound
; Assemble with: acme -f cbm -o starfield.prg starfield.asm

; ------------------------------------------------
; Zero-page variables
; ------------------------------------------------
bullet_active = $02     ; 0 = no bullet, 1 = active
bullet_y      = $03     ; Bullet Y position

; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Initialisation
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen
        ldx #$00
-       lda #$20
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

        ; Sprite 0 setup (ship)
        lda #128
        sta $07f8           ; Data pointer (block 128 = $2000)
        lda #172
        sta $d000           ; X position
        lda #220
        sta $d001           ; Y position
        lda #$01
        sta $d027           ; Colour (white)

        ; Sprite 1 setup (bullet)
        lda #129
        sta $07f9           ; Data pointer (block 129 = $2040)
        lda #$07
        sta $d028           ; Colour (yellow)

        ; Enable sprite 0 only (bullet starts disabled)
        lda #%00000001
        sta $d015

        ; Bullet starts inactive
        lda #$00
        sta bullet_active

        ; SID setup — voice 1 laser sound
        lda #$0f
        sta $d418           ; Volume to maximum

        lda #$00
        sta $d400           ; Frequency low byte
        lda #$10
        sta $d401           ; Frequency high byte ($1000 = mid-high pitch)

        lda #$09
        sta $d405           ; Attack=0, Decay=9
        lda #$00
        sta $d406           ; Sustain=0, Release=0

!ifdef SCREENSHOT_MODE {
        ; Place a static bullet for screenshot capture
        ; Set sprite registers directly but leave bullet_active = 0
        ; so the game loop won't move or deactivate it
        lda $d000
        sta $d002           ; Bullet X = ship X
        lda #140
        sta $d003           ; Bullet Y mid-screen
        lda #%00000011
        sta $d015           ; Enable both sprites
}

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
-       lda $d012
        cmp #$ff
        bne -

        ; --- Read joystick and move ship ---

        ; UP (bit 0)
        lda $dc00
        and #%00000001
        bne not_up
        dec $d001
        dec $d001
not_up:

        ; DOWN (bit 1)
        lda $dc00
        and #%00000010
        bne not_down
        inc $d001
        inc $d001
not_down:

        ; LEFT (bit 2)
        lda $dc00
        and #%00000100
        bne not_left
        dec $d000
        dec $d000
not_left:

        ; RIGHT (bit 3)
        lda $dc00
        and #%00001000
        bne not_right
        inc $d000
        inc $d000
not_right:

        ; --- Fire button (bit 4) ---
        lda $dc00
        and #%00010000
        bne no_fire         ; Bit is 1 = NOT pressed

        ; Fire is pressed — only spawn if no bullet active
        lda bullet_active
        bne no_fire         ; Already active, skip

        ; Spawn bullet at ship position
        lda $d000           ; Ship X → bullet X
        sta $d002
        lda $d001           ; Ship Y → bullet Y
        sta bullet_y

        ; Enable sprite 1 (keep sprite 0 enabled)
        lda $d015
        ora #%00000010
        sta $d015

        ; Mark bullet active
        lda #$01
        sta bullet_active

        ; Trigger laser sound (gate off then on to retrigger)
        lda #$20
        sta $d404           ; Sawtooth, gate OFF (reset envelope)
        lda #$21
        sta $d404           ; Sawtooth, gate ON (start sound)

no_fire:

        ; --- Update bullet ---
        lda bullet_active
        beq no_bullet       ; Not active, skip

        ; Move bullet up (4 pixels per frame)
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003           ; Update sprite 1 Y position

        ; Check if bullet has left the screen (Y < 30)
        cmp #$1e
        bcs no_bullet       ; Y >= 30, still on screen

        ; Deactivate bullet
        lda #$00
        sta bullet_active

        ; Disable sprite 1 (keep sprite 0 enabled)
        lda $d015
        and #%11111101
        sta $d015

no_bullet:
        jmp game_loop

; ------------------------------------------------
; Sprite data at $2000 (block 128) — ship
; ------------------------------------------------
*= $2000
        !byte $00,$18,$00   ;        ##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$7e,$00   ;      ######
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $03,$ff,$c0   ;   ############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$e7,$e0   ;  ###..####..###
        !byte $03,$c3,$c0   ;   ##....##....##
        !byte $01,$ff,$80   ;    ##########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$24,$00   ;       #..#
        !byte $00,$00,$00   ;

; ------------------------------------------------
; Sprite data at $2040 (block 129) — bullet
; ------------------------------------------------
*= $2040
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

If It Doesn’t Work

  • No sound? Check that volume is set ($D418 = $0F), the gate bit is being set ($D404 = $21), and frequency isn’t zero (both $D400 and $D401 shouldn’t be $00).
  • Sound doesn’t stop? The ADSR sustain should be 0. Check that $D406 = $00. If sustain is non-zero, the sound will hold at that level until the gate is cleared.
  • Wrong pitch? Frequency is a 16-bit value split across two registers. $D400 is the low byte, $D401 is the high byte. Swapping them gives a very different pitch.
  • Sound only plays once? You need the gate OFF→ON cycle to retrigger. Writing $21 when the gate is already on won’t restart the envelope.

Try This: Change the Pitch

The frequency value $1000 gives a mid-high laser pitch. Try different values:

  • $0800 — Lower pitched. More of a “bwom” than a “pew”.
  • $2000 — Higher pitched. A sharp, piercing zap.
  • $3000 — Very high. Getting into retro sci-fi territory.

Remember: the frequency is 16-bit. Change $D401 (high byte) for big jumps, $D400 (low byte) for fine tuning.

Try This: Different Waveform

Swap the waveform bit in the control register:

  • $11/$10 — Triangle. Softer, rounder — more “boop” than “pew”.
  • $81/$80 — Noise. Random frequencies — sounds like a small explosion or static burst.
  • $41/$40 — Pulse. Hollow, square-wave tone. You’ll also need to set the pulse width registers ($D402/$D403) — try $08 in $D403 for a 50% duty cycle.

What You’ve Learnt

  • SID chip ($D400$D418) — The C64’s three-voice synthesiser, mapped to memory just like the VIC-II.
  • Voice registers — Each voice has frequency, control, and ADSR registers. Voice 1 starts at $D400.
  • ADSR envelope — Four parameters that shape how a sound starts, sustains, and fades. Packed into two bytes.
  • Waveform selection — Bits 4–7 of the control register select triangle, sawtooth, pulse, or noise.
  • Gate bit — Bit 0 of the control register. Setting it starts the envelope; clearing it begins release.
  • Retrigger pattern — Gate OFF then ON to restart the ADSR. Essential for repeated sounds.

What’s Next

The ship fights alone. In Unit 5, an enemy sprite appears — moving on its own, giving you something to shoot at.