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.
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:
- 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). - 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. - 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. - 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”.
Waveform and Gate
The control register at $D404 selects the waveform and controls the gate:
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$D400and$D401shouldn’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.
$D400is the low byte,$D401is the high byte. Swapping them gives a very different pitch. - Sound only plays once? You need the gate OFF→ON cycle to retrigger. Writing
$21when 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$08in$D403for 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.