Skip to content
Game 1 Unit 5 of 16 1 hr learning time

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.

31% of Starfield

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 yellow bullet rising from the ship towards the top of the screen — Unit 4's shot, with no sound.

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:

RegisterAddressHolds
Frequency low$d400low byte of the pitch
Frequency high$d401high byte of the pitch
Control$d404waveform select + gate
Attack / Decay$d405how the sound rises, then falls
Sustain / Release$d406the held level, then the tail
Volume$d418master 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.

Step 1: configure SID voice 1
+16
5656 ; Bullet starts inactive
5757 lda #$00
5858 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
5973
6074 ; ------------------------------------------------
6175 ; Game loop — runs once per frame
...
175189
176190 lda #$01
177191 sta bullet_active
192+
193+ ; (No sound yet — the voice is configured but never gated.)
178194
179195 no_fire:
180196
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.

SID 6581 · recorded from a real shot
Step 1: firing with the voice configured but never gated — silence

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:

PhaseWhat it controlsOur laser
Attacksilence → full volume0 — instant
Decayfull → the sustain level6 — a short, snappy fall
Sustainthe level it holds at0 — none
Releasefade to silence after the gate clears0 — instant

A laser punches in and dies away: instant attack, quick decay, no sustain. Attack and decay share one register, one nibble each:

$D405 — Attack / Decay 7 Atk 3 0 6 Atk 2 0 5 Atk 1 0 4 Atk 0 0 3 Dcy 3 0 2 Dcy 2 1 1 Dcy 1 1 0 Dcy 0 0
Attack = 0 (upper nibble), Decay = 6 (lower nibble): $06. Instant attack, a short, snappy fall.

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.

Step 2: gate the voice when a bullet spawns
+5-1
190190 lda #$01
191191 sta bullet_active
192192
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)
194198
195199 no_fire:
196200
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
$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 (bit 5) + gate on (bit 0) = $21. Writing $20 first clears the gate so the next $21 restarts the envelope.

Now firing makes a sound — but play it:

SID 6581 · recorded from a real shot
Step 2: a sound at last — but a flat, buzzy blip, not a laser

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.

Step 3: sweep the pitch each frame
+21-1
77 ; ------------------------------------------------
88 bullet_active = $02 ; 0 = no bullet, 1 = active
99 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)
1012
1113 ; ------------------------------------------------
1214 ; BASIC stub
...
190192 lda #$01
191193 sta bullet_active
192194
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
194199 lda #$20
195200 sta $d404 ; Sawtooth, gate OFF (reset envelope)
196201 lda #$21
197202 sta $d404 ; Sawtooth, gate ON (start sound)
203+ lda #$0a
204+ sta laser_timer ; sweep down over 10 frames
198205
199206 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:
200220
201221 ; --- Update the bullet ---
202222 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:

SID 6581 · recorded from a real shot
Step 3: the pew — the pitch slides from high to low as the shot fires

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 $d418 non-zero? Zero here and nothing plays, however perfect the rest.
  • Frequency — are $d400/$d401 set? A pitch of zero is silence.
  • Gate — does $d404 toggle $20$21 when 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 $d401 back — 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 (#$40 into laser_freq) and how fast it falls (sbc #$06 each 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/$10 is triangle (softer, rounder); $81/$80 is 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 $d404 starts 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.