Laser Sound
Configure a SID voice and gate it on each shot, so firing makes a sharp laser 'pew' — your first taste of the C64's sound hardware.
Press fire — pew. The bullet launches and a short laser sound cuts in. The ship already shoots; this unit gives the shot a voice. The new hardware is the SID — the C64's sound chip — and we'll meet it the way every voice gets made: set the voice up, then gate it on.
Where we start
Unit 4 left a ship that fires a bullet. It works, but it's silent.

The SID chip
The SID (Sound Interface Device) is a three-voice synthesiser built into the C64. It lives at $d400–$d418, the same memory-mapped way the VIC-II graphics chip lives at $d000. Each voice has registers for pitch, waveform, and an envelope; there's one shared volume for the whole chip. We'll use voice 1:
| Register | Address | Holds |
|---|---|---|
| Frequency low | $d400 | low byte of the pitch |
| Frequency high | $d401 | high byte of the pitch |
| Control | $d404 | waveform select + gate |
| Attack / Decay | $d405 | how the sound rises, then falls |
| Sustain / Release | $d406 | the held level, then the tail |
| Volume | $d418 | master volume (all three voices) |
Milestone 1 — set the voice up
Before the game loop, we configure voice 1: full volume, a mid-high pitch, and an envelope shaped like a laser.
| 56 | 56 | ; Bullet starts inactive | |
| 57 | 57 | lda #$00 | |
| 58 | 58 | sta bullet_active | |
| 59 | + | | |
| 60 | + | ; SID setup — voice 1 laser sound | |
| 61 | + | lda #$0f | |
| 62 | + | sta $d418 ; Volume to maximum | |
| 63 | + | | |
| 64 | + | lda #$00 | |
| 65 | + | sta $d400 ; Frequency low byte | |
| 66 | + | lda #$10 | |
| 67 | + | sta $d401 ; Frequency high byte ($1000 = mid-high pitch) | |
| 68 | + | | |
| 69 | + | lda #$06 | |
| 70 | + | sta $d405 ; Attack=0, Decay=6 (a short, snappy fall) | |
| 71 | + | lda #$00 | |
| 72 | + | sta $d406 ; Sustain=0, Release=0 | |
| 59 | 73 | | |
| 60 | 74 | ; ------------------------------------------------ | |
| 61 | 75 | ; Game loop — runs once per frame | |
| ... | |||
| 175 | 189 | | |
| 176 | 190 | lda #$01 | |
| 177 | 191 | sta bullet_active | |
| 192 | + | | |
| 193 | + | ; (No sound yet — the voice is configured but never gated.) | |
| 178 | 194 | | |
| 179 | 195 | no_fire: | |
| 180 | 196 | |
The complete step 1 program
; Starfield - Unit 5: Laser Sound
; Cumulative steps: step-00 (silent ship + bullet) -> step-01 (+ SID configured) -> step-02 (+ gate: a flat blip) -> step-03 (+ pitch sweep: a 'pew')
; Assemble: acme -f cbm -o <step>.prg <step>.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)
lda #%00000001
sta $d015 ; Enable sprite 0
lda #$00
sta $d010 ; sprite high-X bits clear (ship starts under X=256)
; Sprite 1 setup (bullet)
lda #129
sta $07f9 ; Data pointer (block 129 = $2040)
lda #$07
sta $d028 ; Colour (yellow)
; 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 #$06
sta $d405 ; Attack=0, Decay=6 (a short, snappy fall)
lda #$00
sta $d406 ; Sustain=0, Release=0
; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
; Wait for the raster beam to reach line 255
; This syncs our code to the display (~50Hz PAL)
- lda $d012
cmp #$ff
bne -
; --- Read joystick and move ship ---
; UP (bit 0) — clamp to Y >= 50
lda $dc00 ; Read joystick port 2
and #%00000001 ; Isolate bit 0
bne not_up ; Bit is 1 = NOT pressed (active low)
lda $d001
cmp #52 ; 50 + room for a 2-pixel move
bcc not_up ; already at the top — don't move
dec $d001 ; Move ship up (decrease Y)
dec $d001 ; 2 pixels per frame
not_up:
; DOWN (bit 1) — clamp to Y <= 234
lda $dc00
and #%00000010
bne not_down
lda $d001
cmp #233 ; 234 - room for a 2-pixel move
bcs not_down ; already at the bottom — don't move
inc $d001 ; Move ship down (increase Y)
inc $d001
not_down:
; LEFT (bit 2) — 9-bit X, clamp to X >= 24
lda $dc00
and #%00000100
bne not_left
lda $d010
and #$01
bne left_ok ; high bit set: X >= 256, always safe to go left
lda $d000
cmp #26 ; 24 + room for a 2-pixel move
bcc not_left ; already at the left edge — don't move
left_ok:
; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
lda $d000
bne +
lda $d010
eor #$01 ; the eor bit-flip from the Primer, on sprite 0's high X bit
sta $d010
+ dec $d000
lda $d000
bne +
lda $d010
eor #$01
sta $d010
+ dec $d000
not_left:
; RIGHT (bit 3) — 9-bit X, clamp to X <= 320
lda $dc00
and #%00001000
bne not_right
lda $d010
and #$01
beq right_ok ; high bit clear: X < 256, always safe to go right
lda $d000
cmp #63 ; (320 - 256) - room for a 2-pixel move
bcs not_right ; already at the right edge — don't move
right_ok:
; after each step, flip the 9th bit when X wraps $ff -> $00
inc $d000
bne +
lda $d010
eor #$01
sta $d010
+ inc $d000
bne +
lda $d010
eor #$01
sta $d010
+
not_right:
; --- Fire button (bit 4) ---
lda $dc00
and #%00010000
bne no_fire ; Bit is 1 = NOT pressed
; Only spawn if no bullet is already flying
lda bullet_active
bne no_fire
; Spawn the bullet at the ship's position
lda $d000 ; Ship X (low byte) -> bullet X
sta $d002
lda $d001 ; Ship Y -> bullet Y
sta bullet_y
; Copy the ship's 9th X bit (bit 0) to the bullet's (bit 1),
; so a shot fired from the right half spawns under the ship
lda $d010
and #%11111101 ; clear the bullet's 9th bit first
sta $d010
lda $d010
and #$01 ; the ship's 9th bit
asl ; shift it into the bullet's position (bit 1)
ora $d010
sta $d010
; Enable sprite 1 (keep sprite 0 enabled)
lda $d015
ora #%00000010
sta $d015
lda #$01
sta bullet_active
; (No sound yet — the voice is configured but never gated.)
no_fire:
; --- Update the bullet ---
lda bullet_active
beq no_bullet
; Move it up 4 pixels a frame
lda bullet_y
sec
sbc #$04
sta bullet_y
sta $d003 ; sprite 1 Y
; Gone off the top? (Y < 30) -> remove it
cmp #$1e
bcs no_bullet
lda #$00
sta bullet_active
lda $d015
and #%11111101 ; disable sprite 1, keep sprite 0
sta $d015
lda $d010
and #%11111101 ; clear the bullet's 9th bit
sta $d010
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
Now fire. The bullet launches exactly as before — and you hear nothing.
That silence is the whole point of this step: a configured voice makes no sound on its own. Volume, pitch, and envelope are all set, but nothing has told the voice to start. (Play the clip above and you hear nothing — the recording is genuinely silent.) The thing that starts it is the gate, and we add it in Step 2.
The ADSR envelope
Every sound has a shape over time. The SID builds that shape from four values — attack, decay, sustain, release — packed into two registers:
| Phase | What it controls | Our laser |
|---|---|---|
| Attack | silence → full volume | 0 — instant |
| Decay | full → the sustain level | 6 — a short, snappy fall |
| Sustain | the level it holds at | 0 — none |
| Release | fade to silence after the gate clears | 0 — instant |
A laser punches in and dies away: instant attack, quick decay, no sustain. Attack and decay share one register, one nibble each:
Milestone 2 — gate it on each shot
The control register $d404 selects the waveform and holds the gate — bit 0. Setting the gate to 1 starts the envelope's attack; clearing it begins the release. We pick the sawtooth waveform (bit 5), bright and buzzy, the archetypal laser tone.
There's a catch: writing $21 when the gate is already set does nothing — the envelope won't restart. To retrigger on every shot you clear the gate, then set it: off, then on.
| 190 | 190 | lda #$01 | |
| 191 | 191 | sta bullet_active | |
| 192 | 192 | | |
| 193 | - | ; (No sound yet — the voice is configured but never gated.) | |
| 193 | + | ; Trigger laser sound (gate off then on to retrigger) | |
| 194 | + | lda #$20 | |
| 195 | + | sta $d404 ; Sawtooth, gate OFF (reset envelope) | |
| 196 | + | lda #$21 | |
| 197 | + | sta $d404 ; Sawtooth, gate ON (start sound) | |
| 194 | 198 | | |
| 195 | 199 | no_fire: | |
| 196 | 200 | |
The complete program
; Starfield - Unit 5: Laser Sound
; Cumulative steps: step-00 (silent ship + bullet) -> step-01 (+ SID configured) -> step-02 (+ gate: a flat blip) -> step-03 (+ pitch sweep: a 'pew')
; Assemble: acme -f cbm -o <step>.prg <step>.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)
lda #%00000001
sta $d015 ; Enable sprite 0
lda #$00
sta $d010 ; sprite high-X bits clear (ship starts under X=256)
; Sprite 1 setup (bullet)
lda #129
sta $07f9 ; Data pointer (block 129 = $2040)
lda #$07
sta $d028 ; Colour (yellow)
; 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 #$06
sta $d405 ; Attack=0, Decay=6 (a short, snappy fall)
lda #$00
sta $d406 ; Sustain=0, Release=0
; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
; Wait for the raster beam to reach line 255
; This syncs our code to the display (~50Hz PAL)
- lda $d012
cmp #$ff
bne -
; --- Read joystick and move ship ---
; UP (bit 0) — clamp to Y >= 50
lda $dc00 ; Read joystick port 2
and #%00000001 ; Isolate bit 0
bne not_up ; Bit is 1 = NOT pressed (active low)
lda $d001
cmp #52 ; 50 + room for a 2-pixel move
bcc not_up ; already at the top — don't move
dec $d001 ; Move ship up (decrease Y)
dec $d001 ; 2 pixels per frame
not_up:
; DOWN (bit 1) — clamp to Y <= 234
lda $dc00
and #%00000010
bne not_down
lda $d001
cmp #233 ; 234 - room for a 2-pixel move
bcs not_down ; already at the bottom — don't move
inc $d001 ; Move ship down (increase Y)
inc $d001
not_down:
; LEFT (bit 2) — 9-bit X, clamp to X >= 24
lda $dc00
and #%00000100
bne not_left
lda $d010
and #$01
bne left_ok ; high bit set: X >= 256, always safe to go left
lda $d000
cmp #26 ; 24 + room for a 2-pixel move
bcc not_left ; already at the left edge — don't move
left_ok:
; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
lda $d000
bne +
lda $d010
eor #$01 ; the eor bit-flip from the Primer, on sprite 0's high X bit
sta $d010
+ dec $d000
lda $d000
bne +
lda $d010
eor #$01
sta $d010
+ dec $d000
not_left:
; RIGHT (bit 3) — 9-bit X, clamp to X <= 320
lda $dc00
and #%00001000
bne not_right
lda $d010
and #$01
beq right_ok ; high bit clear: X < 256, always safe to go right
lda $d000
cmp #63 ; (320 - 256) - room for a 2-pixel move
bcs not_right ; already at the right edge — don't move
right_ok:
; after each step, flip the 9th bit when X wraps $ff -> $00
inc $d000
bne +
lda $d010
eor #$01
sta $d010
+ inc $d000
bne +
lda $d010
eor #$01
sta $d010
+
not_right:
; --- Fire button (bit 4) ---
lda $dc00
and #%00010000
bne no_fire ; Bit is 1 = NOT pressed
; Only spawn if no bullet is already flying
lda bullet_active
bne no_fire
; Spawn the bullet at the ship's position
lda $d000 ; Ship X (low byte) -> bullet X
sta $d002
lda $d001 ; Ship Y -> bullet Y
sta bullet_y
; Copy the ship's 9th X bit (bit 0) to the bullet's (bit 1),
; so a shot fired from the right half spawns under the ship
lda $d010
and #%11111101 ; clear the bullet's 9th bit first
sta $d010
lda $d010
and #$01 ; the ship's 9th bit
asl ; shift it into the bullet's position (bit 1)
ora $d010
sta $d010
; Enable sprite 1 (keep sprite 0 enabled)
lda $d015
ora #%00000010
sta $d015
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 the bullet ---
lda bullet_active
beq no_bullet
; Move it up 4 pixels a frame
lda bullet_y
sec
sbc #$04
sta bullet_y
sta $d003 ; sprite 1 Y
; Gone off the top? (Y < 30) -> remove it
cmp #$1e
bcs no_bullet
lda #$00
sta bullet_active
lda $d015
and #%11111101 ; disable sprite 1, keep sprite 0
sta $d015
lda $d010
and #%11111101 ; clear the bullet's 9th bit
sta $d010
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
Now firing makes a sound — but play it:
It's a sound, but your ear doesn't buy it as a laser. The pitch just sits there. The arcade laser we all carry in our heads slides — it starts high and drops as the shot leaves — and that downward slide is most of what makes a "pew" a pew. We add it in the next step.
The off-then-on gate cycle, meanwhile, is the pattern for every repeated SID sound — without it, the second shot in a burst would find the gate already set and stay silent.
Milestone 3 — sweep the pitch into a "pew"
A laser isn't a fixed note; it's a fast downward slide. The SID won't sweep on its own, so we do it in software: start the pitch high, then drop it a little every frame while the sound rings.
One catch makes this a real lesson. The SID's frequency registers are write-only — you can't read $d401 back to find the current pitch. So we keep our own copy in a zero-page byte, lower that each frame, and write it to the chip.
| 7 | 7 | ; ------------------------------------------------ | |
| 8 | 8 | bullet_active = $02 ; 0 = no bullet, 1 = active | |
| 9 | 9 | bullet_y = $03 ; Bullet Y position | |
| 10 | + | laser_timer = $04 ; Frames of laser pitch-sweep remaining (0 = idle) | |
| 11 | + | laser_freq = $05 ; Our copy of the sweep pitch (SID freq regs are write-only) | |
| 10 | 12 | | |
| 11 | 13 | ; ------------------------------------------------ | |
| 12 | 14 | ; BASIC stub | |
| ... | |||
| 190 | 192 | lda #$01 | |
| 191 | 193 | sta bullet_active | |
| 192 | 194 | | |
| 193 | - | ; Trigger laser sound (gate off then on to retrigger) | |
| 195 | + | ; Trigger laser sound: start the pitch high, gate off then on | |
| 196 | + | lda #$40 | |
| 197 | + | sta laser_freq ; start high | |
| 198 | + | sta $d401 ; SID frequency high byte | |
| 194 | 199 | lda #$20 | |
| 195 | 200 | sta $d404 ; Sawtooth, gate OFF (reset envelope) | |
| 196 | 201 | lda #$21 | |
| 197 | 202 | sta $d404 ; Sawtooth, gate ON (start sound) | |
| 203 | + | lda #$0a | |
| 204 | + | sta laser_timer ; sweep down over 10 frames | |
| 198 | 205 | | |
| 199 | 206 | no_fire: | |
| 207 | + | | |
| 208 | + | ; --- Laser pitch sweep: the 'pew' --- | |
| 209 | + | ; Drop the pitch a little each frame while the sweep is running. | |
| 210 | + | ; We keep our own copy because SID frequency registers are write-only. | |
| 211 | + | lda laser_timer | |
| 212 | + | beq no_sweep | |
| 213 | + | lda laser_freq | |
| 214 | + | sec | |
| 215 | + | sbc #$06 | |
| 216 | + | sta laser_freq | |
| 217 | + | sta $d401 ; write the new pitch to the SID | |
| 218 | + | dec laser_timer | |
| 219 | + | no_sweep: | |
| 200 | 220 | | |
| 201 | 221 | ; --- Update the bullet --- | |
| 202 | 222 | lda bullet_active |
The complete program
; Starfield - Unit 5: Laser Sound
; Cumulative steps: step-00 (silent ship + bullet) -> step-01 (+ SID configured) -> step-02 (+ gate: a flat blip) -> step-03 (+ pitch sweep: a 'pew')
; Assemble: acme -f cbm -o <step>.prg <step>.asm
; ------------------------------------------------
; Zero-page variables
; ------------------------------------------------
bullet_active = $02 ; 0 = no bullet, 1 = active
bullet_y = $03 ; Bullet Y position
laser_timer = $04 ; Frames of laser pitch-sweep remaining (0 = idle)
laser_freq = $05 ; Our copy of the sweep pitch (SID freq regs are write-only)
; ------------------------------------------------
; 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)
lda #%00000001
sta $d015 ; Enable sprite 0
lda #$00
sta $d010 ; sprite high-X bits clear (ship starts under X=256)
; Sprite 1 setup (bullet)
lda #129
sta $07f9 ; Data pointer (block 129 = $2040)
lda #$07
sta $d028 ; Colour (yellow)
; 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 #$06
sta $d405 ; Attack=0, Decay=6 (a short, snappy fall)
lda #$00
sta $d406 ; Sustain=0, Release=0
; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
; Wait for the raster beam to reach line 255
; This syncs our code to the display (~50Hz PAL)
- lda $d012
cmp #$ff
bne -
; --- Read joystick and move ship ---
; UP (bit 0) — clamp to Y >= 50
lda $dc00 ; Read joystick port 2
and #%00000001 ; Isolate bit 0
bne not_up ; Bit is 1 = NOT pressed (active low)
lda $d001
cmp #52 ; 50 + room for a 2-pixel move
bcc not_up ; already at the top — don't move
dec $d001 ; Move ship up (decrease Y)
dec $d001 ; 2 pixels per frame
not_up:
; DOWN (bit 1) — clamp to Y <= 234
lda $dc00
and #%00000010
bne not_down
lda $d001
cmp #233 ; 234 - room for a 2-pixel move
bcs not_down ; already at the bottom — don't move
inc $d001 ; Move ship down (increase Y)
inc $d001
not_down:
; LEFT (bit 2) — 9-bit X, clamp to X >= 24
lda $dc00
and #%00000100
bne not_left
lda $d010
and #$01
bne left_ok ; high bit set: X >= 256, always safe to go left
lda $d000
cmp #26 ; 24 + room for a 2-pixel move
bcc not_left ; already at the left edge — don't move
left_ok:
; before each step, flip the 9th bit when X is about to wrap $00 -> $ff
lda $d000
bne +
lda $d010
eor #$01 ; the eor bit-flip from the Primer, on sprite 0's high X bit
sta $d010
+ dec $d000
lda $d000
bne +
lda $d010
eor #$01
sta $d010
+ dec $d000
not_left:
; RIGHT (bit 3) — 9-bit X, clamp to X <= 320
lda $dc00
and #%00001000
bne not_right
lda $d010
and #$01
beq right_ok ; high bit clear: X < 256, always safe to go right
lda $d000
cmp #63 ; (320 - 256) - room for a 2-pixel move
bcs not_right ; already at the right edge — don't move
right_ok:
; after each step, flip the 9th bit when X wraps $ff -> $00
inc $d000
bne +
lda $d010
eor #$01
sta $d010
+ inc $d000
bne +
lda $d010
eor #$01
sta $d010
+
not_right:
; --- Fire button (bit 4) ---
lda $dc00
and #%00010000
bne no_fire ; Bit is 1 = NOT pressed
; Only spawn if no bullet is already flying
lda bullet_active
bne no_fire
; Spawn the bullet at the ship's position
lda $d000 ; Ship X (low byte) -> bullet X
sta $d002
lda $d001 ; Ship Y -> bullet Y
sta bullet_y
; Copy the ship's 9th X bit (bit 0) to the bullet's (bit 1),
; so a shot fired from the right half spawns under the ship
lda $d010
and #%11111101 ; clear the bullet's 9th bit first
sta $d010
lda $d010
and #$01 ; the ship's 9th bit
asl ; shift it into the bullet's position (bit 1)
ora $d010
sta $d010
; Enable sprite 1 (keep sprite 0 enabled)
lda $d015
ora #%00000010
sta $d015
lda #$01
sta bullet_active
; Trigger laser sound: start the pitch high, gate off then on
lda #$40
sta laser_freq ; start high
sta $d401 ; SID frequency high byte
lda #$20
sta $d404 ; Sawtooth, gate OFF (reset envelope)
lda #$21
sta $d404 ; Sawtooth, gate ON (start sound)
lda #$0a
sta laser_timer ; sweep down over 10 frames
no_fire:
; --- Laser pitch sweep: the 'pew' ---
; Drop the pitch a little each frame while the sweep is running.
; We keep our own copy because SID frequency registers are write-only.
lda laser_timer
beq no_sweep
lda laser_freq
sec
sbc #$06
sta laser_freq
sta $d401 ; write the new pitch to the SID
dec laser_timer
no_sweep:
; --- Update the bullet ---
lda bullet_active
beq no_bullet
; Move it up 4 pixels a frame
lda bullet_y
sec
sbc #$04
sta bullet_y
sta $d003 ; sprite 1 Y
; Gone off the top? (Y < 30) -> remove it
cmp #$1e
bcs no_bullet
lda #$00
sta bullet_active
lda $d015
and #%11111101 ; disable sprite 1, keep sprite 0
sta $d015
lda $d010
and #%11111101 ; clear the bullet's 9th bit
sta $d010
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
Now it pews:
That's the whole difference between a sound and a laser: the same gated sawtooth, its pitch dropped from a bright high tone to a low one in under a fifth of a second, fading to silence as it falls. Unit 4 let you see the bullet leave; now you hear it go. And the tuning — how high it starts, how fast it falls — is exactly the kind of single number that turns "it works" into "it feels good." Start the sweep higher for a sharper zap; fall slower for a longer, lazier pew.
When it's wrong, see why
Silent SID code is the most common frustration on this machine, and guessing rarely finds it. Walk the chain a sound needs, in order — check each value in your code, or live if your emulator has a monitor:
- Volume — is
$d418non-zero? Zero here and nothing plays, however perfect the rest. - Frequency — are
$d400/$d401set? A pitch of zero is silence. - Gate — does
$d404toggle$20→$21when you fire? If it stays at$21, you've hit the retrigger trap: the envelope never restarts. - Sweep — no "pew", just a blip? The SID's frequency registers are write-only, so you can't read
$d401back — check your zero-page copy (laser_freq) instead: it should jump high on the shot and count down. If it never changes, the sweep block isn't running.
Step 1 was a controlled version of exactly this — every register set except the gate. Finding the one missing link in the chain beats re-reading the whole program hoping to spot it.
Before and after
Three steps, three results you can hear. Step 1: silence — a voice fully configured but never started. Step 2: a blip — one off-then-on write to the gate sets the envelope running, but the pitch sits still. Step 3: a pew — sweeping that pitch downwards each frame turns the blip into a laser. Same voice throughout; the difference is when it starts and how its pitch moves.
Try this
- Tune the pew. Two numbers shape it: the starting pitch (
#$40intolaser_freq) and how fast it falls (sbc #$06each frame). Start higher (#$60) for a sharper zap; subtract less (#$03) for a slower, lazier slide. Find the one you like. - Change the waveform. Swap bit 5 in
$d404:$11/$10is triangle (softer, rounder);$81/$80is noise (a static burst — good for explosions later).
What you've learnt
- The SID chip (
$d400–$d418) — three voices, memory-mapped like the VIC-II. - Voice registers — frequency, control, and ADSR for voice 1, plus the shared volume at
$d418. - The ADSR envelope — attack, decay, sustain, release shape a sound over time; ours is instant-attack, quick-decay, no-sustain.
- The gate bit — bit 0 of
$d404starts the envelope; a configured voice stays silent until it's set. - The retrigger pattern — gate off then on, so each shot restarts the sound.
- Software pitch sweep — the SID holds a steady note; sliding the frequency each frame is what turns a blip into a "pew". Game feel lives in that slide.
- Write-only registers — you can't read the SID's frequency back, so keep your own copy in RAM and write it to the chip.
What's next
The ship can shoot, and now it sounds like it. Next an enemy appears — a sprite that moves on its own, giving you something to shoot at.