Skip to content
Game 1 Unit 2 of 64 1 hr learning time

Notes Appear

Notes scroll across the tracks in time with a beat. The rhythm game emerges.

3% of SID Symphony

In Unit 1, you could trigger sounds by pressing keys. Now those keys have a purpose.

This unit adds the core rhythm game mechanic: notes scroll from right to left across the tracks. When a note reaches the hit zone, press the matching key. It’s simple, but it transforms three keyboard triggers into an actual game.

Run It

Assemble and run:

acme -f cbm -o symphony.prg symphony.asm

Unit 2 Screenshot

Notes appear on the right and scroll left toward the yellow hit zone. Press Z, X, or C when a note reaches the zone. The track flashes and the SID plays. A simple test pattern loops endlessly so you can practice your timing.

Notes scroll across the tracks toward the hit zone

How Notes Work

Notes are stored in parallel arrays - one for track number, one for column position:

; Note storage uses parallel arrays
; Each note has a track (1-3) and column position

MAX_NOTES   = 8                 ; Maximum notes on screen at once

note_track:
            !fill MAX_NOTES, 0  ; Track number (0=inactive, 1-3=active)

note_col:
            !fill MAX_NOTES, 0  ; Screen column position (1-37)

; To find an empty slot, scan for note_track[x] == 0
; To process all notes, loop through and skip where note_track[x] == 0

A note with track = 0 is inactive (empty slot). Active notes have track 1, 2, or 3. Each frame, we scan all slots, skip inactive ones, and update active ones.

Beat Timing

The C64 runs at 50 frames per second (PAL). At 120 BPM, that’s 2 beats per second, or 25 frames per beat:

; Beat timing with frame counting
; PAL C64 runs at 50Hz (50 frames per second)
; At 120 BPM, we get 2 beats per second = 25 frames per beat

FRAMES_PER_BEAT = 25            ; 50Hz / 2 beats per second

frame_count = $02               ; Current frame within beat (0-24)
beat_count  = $03               ; Current beat in song (0-255)

main_loop:
            ; Wait for raster sync (once per frame)
            lda #$FF
wait_raster:
            cmp $D012
            bne wait_raster

            ; Increment frame counter
            inc frame_count
            lda frame_count
            cmp #FRAMES_PER_BEAT
            bcc no_new_beat

            ; New beat!
            lda #0
            sta frame_count
            jsr check_spawn_note  ; Spawn notes for this beat
            inc beat_count

no_new_beat:
            ; ... rest of game loop

Every 25 frames, we trigger a new beat. The frame_count variable tracks where we are within the current beat. When it hits 25, we reset it and increment beat_count.

Song Data

The song is stored as pairs of (beat, track):

; Song data format: pairs of (beat, track)
; Beat = which beat to spawn (0, 1, 2, ...)
; Track = which track (1=top/Z, 2=middle/X, 3=bottom/C)
; $FF = end of song (loops back to start)

song_data:
            ; Bar 1 (beats 0-7)
            !byte 0, 1          ; Beat 0, track 1 (Z)
            !byte 2, 2          ; Beat 2, track 2 (X)
            !byte 4, 3          ; Beat 4, track 3 (C)
            !byte 6, 1          ; Beat 6, track 1 (Z)

            ; Bar 2 (beats 8-15)
            !byte 8, 2          ; Beat 8, track 2
            !byte 10, 3         ; Beat 10, track 3
            !byte 12, 1         ; Beat 12, track 1
            !byte 14, 2         ; Beat 14, track 2

            ; Bar 3 (beats 16-23)
            !byte 16, 3
            !byte 18, 1
            !byte 20, 2
            !byte 22, 3

            ; Bar 4 (beats 24-31) - faster pattern
            !byte 24, 1
            !byte 25, 2         ; Consecutive beats!
            !byte 26, 3
            !byte 28, 1
            !byte 29, 2
            !byte 30, 3

            !byte $FF           ; End marker - song loops

Beat 0 means “spawn immediately when the song starts”. Beat 2 means “spawn on the third beat”. The pattern repeats when we hit the $FF marker.

Bar 4 demonstrates faster patterns - notes on consecutive beats (24, 25, 26) require quicker reactions.

Moving Notes

Every frame, active notes move left by one column:

; Move all active notes left by one column each frame
; Notes that scroll past column 1 are deactivated

update_notes:
            ldx #0

update_loop:
            lda note_track,x
            beq update_next     ; Skip inactive notes (track = 0)

            jsr erase_note      ; Remove from old position

            dec note_col,x      ; Move left one column
            lda note_col,x
            cmp #1
            bcc update_deactivate  ; Scrolled off left edge?

            jsr draw_note       ; Draw at new position
            jmp update_next

update_deactivate:
            lda #0
            sta note_track,x    ; Mark as inactive

update_next:
            inx
            cpx #MAX_NOTES
            bne update_loop
            rts

The sequence is:

  1. Erase the note from its current position (restore the track line)
  2. Decrement the column position
  3. If still on screen, draw at the new position
  4. If off the left edge, mark as inactive

This erase-move-draw pattern is the foundation of all character-based animation on the C64.

Try This: Change the Pattern

Edit the song data to create your own rhythm:

song_data:
            ; Your pattern here
            !byte 0, 1          ; Beat 0, track 1
            !byte 1, 2          ; Beat 1, track 2
            !byte 2, 3          ; Beat 2, track 3
            !byte 3, 1          ; Beat 3, track 1
            ; ... continue the pattern
            !byte $FF           ; End marker

Try making notes appear on every beat, or create gaps for breathing room. Notice how different patterns feel to play.

Try This: Change the Tempo

Modify FRAMES_PER_BEAT to change the speed:

FRAMES_PER_BEAT = 50            ; 60 BPM - slow and easy
FRAMES_PER_BEAT = 25            ; 120 BPM - default
FRAMES_PER_BEAT = 17            ; 180 BPM - fast!
FRAMES_PER_BEAT = 12            ; 250 BPM - very challenging

Lower values mean faster beats. At 12 frames per beat, the notes fly across the screen.

The Complete Code

; ============================================================================
; SID SYMPHONY - Unit 2: Notes Appear
; ============================================================================
; Notes scroll from right to left across three tracks. When they reach the
; hit zone, press the matching key to play the sound. A simple test pattern
; loops continuously so you can practice timing.
;
; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
; ============================================================================

; ----------------------------------------------------------------------------
; Memory Addresses
; ----------------------------------------------------------------------------

SCREEN      = $0400             ; Screen memory base
COLRAM      = $D800             ; Colour RAM base
BORDER      = $D020             ; Border colour
BGCOL       = $D021             ; Background colour

; SID registers
SID         = $D400             ; SID base address
SID_V1_FREQ_LO = $D400          ; Voice 1 frequency low
SID_V1_FREQ_HI = $D401          ; Voice 1 frequency high
SID_V1_PWLO = $D402             ; Voice 1 pulse width low
SID_V1_PWHI = $D403             ; Voice 1 pulse width high
SID_V1_CTRL = $D404             ; Voice 1 control register
SID_V1_AD   = $D405             ; Voice 1 attack/decay
SID_V1_SR   = $D406             ; Voice 1 sustain/release

SID_V2_FREQ_LO = $D407          ; Voice 2 frequency low
SID_V2_FREQ_HI = $D408          ; Voice 2 frequency high
SID_V2_PWLO = $D409             ; Voice 2 pulse width low
SID_V2_PWHI = $D40A             ; Voice 2 pulse width high
SID_V2_CTRL = $D40B             ; Voice 2 control register
SID_V2_AD   = $D40C             ; Voice 2 attack/decay
SID_V2_SR   = $D40D             ; Voice 2 sustain/release

SID_V3_FREQ_LO = $D40E          ; Voice 3 frequency low
SID_V3_FREQ_HI = $D40F          ; Voice 3 frequency high
SID_V3_PWLO = $D410             ; Voice 3 pulse width low
SID_V3_PWHI = $D411             ; Voice 3 pulse width high
SID_V3_CTRL = $D412             ; Voice 3 control register
SID_V3_AD   = $D413             ; Voice 3 attack/decay
SID_V3_SR   = $D414             ; Voice 3 sustain/release

SID_FLTLO   = $D415             ; Filter cutoff low
SID_FLTHI   = $D416             ; Filter cutoff high
SID_FLTCTRL = $D417             ; Filter control
SID_VOLUME  = $D418             ; Volume and filter mode

; CIA keyboard
CIA1_PRA    = $DC00             ; CIA1 Port A (keyboard column)
CIA1_PRB    = $DC01             ; CIA1 Port B (keyboard row)

; Colours
BLACK       = 0
WHITE       = 1
RED         = 2
CYAN        = 3
PURPLE      = 4
GREEN       = 5
BLUE        = 6
YELLOW      = 7
ORANGE      = 8
BROWN       = 9
LIGHT_RED   = 10
DARK_GREY   = 11
GREY        = 12
LIGHT_GREEN = 13
LIGHT_BLUE  = 14
LIGHT_GREY  = 15

; Track positions (row on screen)
TRACK1_ROW  = 8                 ; High voice track
TRACK2_ROW  = 12                ; Mid voice track
TRACK3_ROW  = 16                ; Low voice track

; Hit zone column
HIT_ZONE_COL = 3                ; Where notes need to be hit

; Note settings
NOTE_CHAR   = $57               ; Character for note (filled dot/ball)
TRACK_CHAR  = $2D               ; Minus character for track line
MAX_NOTES   = 8                 ; Maximum notes on screen at once
NOTE_SPAWN_COL = 37             ; Where notes appear (right side)

; Timing
FRAMES_PER_BEAT = 25            ; ~120 BPM at 50Hz (PAL)

; Zero page pointers
ZP_PTR      = $FB               ; General purpose pointer
ZP_PTR_HI   = $FC

; ----------------------------------------------------------------------------
; Variables (in low memory)
; ----------------------------------------------------------------------------

frame_count = $02               ; Frame counter (0-255)
beat_count  = $03               ; Current beat in song (0-255)
song_pos    = $04               ; Position in song data (word)
song_pos_hi = $05
temp_track  = $06               ; Temporary storage for track number

; ----------------------------------------------------------------------------
; BASIC Stub - SYS 2064
; ----------------------------------------------------------------------------

            * = $0801

            !byte $0C, $08      ; Pointer to next line
            !byte $0A, $00      ; Line number 10
            !byte $9E           ; SYS token
            !text "2064"        ; Address
            !byte $00           ; End of line
            !byte $00, $00      ; End of program

; ----------------------------------------------------------------------------
; Main Program
; ----------------------------------------------------------------------------

            * = $0810

start:
            jsr init_screen     ; Set up the display
            jsr init_sid        ; Configure SID chip
            jsr init_notes      ; Clear note arrays

            ; Initialize song position
            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi

            ; Initialize counters
            lda #0
            sta frame_count
            sta beat_count

main_loop:
            ; Wait for raster (smooth timing)
            lda #$FF
wait_raster:
            cmp $D012
            bne wait_raster

            ; Update frame counter
            inc frame_count
            lda frame_count
            cmp #FRAMES_PER_BEAT
            bcc no_new_beat

            ; New beat!
            lda #0
            sta frame_count
            jsr check_spawn_note
            inc beat_count

no_new_beat:
            ; Move all notes left
            jsr update_notes

            ; Reset track colours to default
            jsr reset_track_colours

            ; Check keyboard and play sounds
            jsr check_keys

            jmp main_loop

; ----------------------------------------------------------------------------
; Initialize Notes
; ----------------------------------------------------------------------------

init_notes:
            ldx #MAX_NOTES-1
            lda #0
init_notes_loop:
            sta note_track,x
            sta note_col,x
            dex
            bpl init_notes_loop
            rts

; ----------------------------------------------------------------------------
; Check Spawn Note
; ----------------------------------------------------------------------------
; Song data is in beat order. Process all entries matching current beat,
; then return. Entries are (beat, track) pairs ending with $FF.

check_spawn_note:
            ldy #0

spawn_check_loop:
            ; Read beat number for current entry
            lda (song_pos),y
            cmp #$FF
            beq spawn_restart_song

            ; If entry beat > current beat, we're done for this beat
            cmp beat_count
            beq spawn_match         ; Equal - spawn this note
            bcs spawn_done          ; Greater - done for now (entries are ordered)

            ; Entry beat < current beat - shouldn't happen, but skip it
            jmp spawn_advance

spawn_match:
            ; Get track number (next byte)
            iny
            lda (song_pos),y
            jsr spawn_note
            dey                     ; Reset Y for next iteration

spawn_advance:
            ; Move song_pos forward by 2 bytes
            lda song_pos
            clc
            adc #2
            sta song_pos
            lda song_pos_hi
            adc #0
            sta song_pos_hi
            jmp spawn_check_loop

spawn_done:
            rts

spawn_restart_song:
            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi
            lda #0
            sta beat_count
            rts

; ----------------------------------------------------------------------------
; Spawn Note
; ----------------------------------------------------------------------------
; Input: A = track number (1-3)

spawn_note:
            sta temp_track

            ldx #0
spawn_find_slot:
            lda note_track,x
            beq spawn_found_slot
            inx
            cpx #MAX_NOTES
            bne spawn_find_slot
            rts                 ; No empty slot

spawn_found_slot:
            lda temp_track
            sta note_track,x
            lda #NOTE_SPAWN_COL
            sta note_col,x
            jsr draw_note
            rts

; ----------------------------------------------------------------------------
; Update Notes
; ----------------------------------------------------------------------------

update_notes:
            ldx #0

update_loop:
            lda note_track,x
            beq update_next

            jsr erase_note

            dec note_col,x
            lda note_col,x
            cmp #1
            bcc update_deactivate

            jsr draw_note
            jmp update_next

update_deactivate:
            lda #0
            sta note_track,x

update_next:
            inx
            cpx #MAX_NOTES
            bne update_loop
            rts

; ----------------------------------------------------------------------------
; Draw Note
; ----------------------------------------------------------------------------
; Input: X = note index

draw_note:
            lda note_track,x
            cmp #1
            beq draw_note_t1
            cmp #2
            beq draw_note_t2
            cmp #3
            beq draw_note_t3
            rts

draw_note_t1:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #NOTE_CHAR
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #LIGHT_RED
            sta (ZP_PTR),y
            rts

draw_note_t2:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #NOTE_CHAR
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #LIGHT_GREEN
            sta (ZP_PTR),y
            rts

draw_note_t3:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #NOTE_CHAR
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #LIGHT_BLUE
            sta (ZP_PTR),y
            rts

; ----------------------------------------------------------------------------
; Erase Note
; ----------------------------------------------------------------------------
; Input: X = note index

erase_note:
            lda note_track,x
            cmp #1
            beq erase_note_t1
            cmp #2
            beq erase_note_t2
            cmp #3
            beq erase_note_t3
            rts

erase_note_t1:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #TRACK_CHAR
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK1_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK1_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #GREY
            sta (ZP_PTR),y
            rts

erase_note_t2:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #TRACK_CHAR
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK2_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK2_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #GREY
            sta (ZP_PTR),y
            rts

erase_note_t3:
            lda note_col,x
            clc
            adc #<(SCREEN + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(SCREEN + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI

            ldy #0
            lda #TRACK_CHAR
            sta (ZP_PTR),y

            lda note_col,x
            clc
            adc #<(COLRAM + TRACK3_ROW * 40)
            sta ZP_PTR
            lda #>(COLRAM + TRACK3_ROW * 40)
            adc #0
            sta ZP_PTR_HI
            lda #GREY
            sta (ZP_PTR),y
            rts

; ----------------------------------------------------------------------------
; Initialize Screen
; ----------------------------------------------------------------------------

init_screen:
            lda #BLACK
            sta BORDER
            sta BGCOL

            ldx #0
            lda #$20
clr_screen:
            sta SCREEN,x
            sta SCREEN+$100,x
            sta SCREEN+$200,x
            sta SCREEN+$2E8,x
            inx
            bne clr_screen

            ldx #0
            lda #GREY
clr_colour:
            sta COLRAM,x
            sta COLRAM+$100,x
            sta COLRAM+$200,x
            sta COLRAM+$2E8,x
            inx
            bne clr_colour

            jsr draw_tracks
            jsr draw_hit_zones
            jsr draw_labels

            rts

; ----------------------------------------------------------------------------
; Draw Tracks
; ----------------------------------------------------------------------------

draw_tracks:
            ldx #0
            lda #TRACK_CHAR
draw_t1:
            sta SCREEN + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne draw_t1

            ldx #0
draw_t2:
            sta SCREEN + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne draw_t2

            ldx #0
draw_t3:
            sta SCREEN + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne draw_t3

            rts

; ----------------------------------------------------------------------------
; Draw Hit Zones
; ----------------------------------------------------------------------------

draw_hit_zones:
            lda #$7D            ; Pipe character

            sta SCREEN + (TRACK1_ROW * 40) + HIT_ZONE_COL
            sta SCREEN + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COL
            sta SCREEN + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COL

            sta SCREEN + (TRACK2_ROW * 40) + HIT_ZONE_COL
            sta SCREEN + ((TRACK2_ROW-1) * 40) + HIT_ZONE_COL
            sta SCREEN + ((TRACK2_ROW+1) * 40) + HIT_ZONE_COL

            sta SCREEN + (TRACK3_ROW * 40) + HIT_ZONE_COL
            sta SCREEN + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COL
            sta SCREEN + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COL

            lda #YELLOW
            sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COL
            sta COLRAM + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COL
            sta COLRAM + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COL

            sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COL
            sta COLRAM + ((TRACK2_ROW-1) * 40) + HIT_ZONE_COL
            sta COLRAM + ((TRACK2_ROW+1) * 40) + HIT_ZONE_COL

            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COL
            sta COLRAM + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COL
            sta COLRAM + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COL

            rts

; ----------------------------------------------------------------------------
; Draw Labels
; ----------------------------------------------------------------------------

draw_labels:
            ldx #0
draw_title:
            lda title_text,x
            beq draw_title_done
            sta SCREEN + 13,x
            lda #WHITE
            sta COLRAM + 13,x
            inx
            bne draw_title
draw_title_done:

            lda #$1A            ; Z
            sta SCREEN + (TRACK1_ROW * 40)
            lda #LIGHT_RED
            sta COLRAM + (TRACK1_ROW * 40)

            lda #$18            ; X
            sta SCREEN + (TRACK2_ROW * 40)
            lda #LIGHT_GREEN
            sta COLRAM + (TRACK2_ROW * 40)

            lda #$03            ; C
            sta SCREEN + (TRACK3_ROW * 40)
            lda #LIGHT_BLUE
            sta COLRAM + (TRACK3_ROW * 40)

            ldx #0
draw_instr:
            lda instr_text,x
            beq draw_instr_done
            sta SCREEN + (23 * 40) + 3,x
            lda #GREY
            sta COLRAM + (23 * 40) + 3,x
            inx
            bne draw_instr
draw_instr_done:

            rts

title_text:
            !scr "sid symphony"
            !byte 0

instr_text:
            !scr "hit notes when they reach |"
            !byte 0

; ----------------------------------------------------------------------------
; Initialize SID
; ----------------------------------------------------------------------------

init_sid:
            ldx #$18
            lda #0
clear_sid:
            sta SID,x
            dex
            bpl clear_sid

            lda #$0F
            sta SID_VOLUME

            ; Voice 1 - High pitch, sawtooth
            lda #$00
            sta SID_V1_FREQ_LO
            lda #$1C
            sta SID_V1_FREQ_HI
            lda #$09
            sta SID_V1_AD
            lda #$00
            sta SID_V1_SR

            ; Voice 2 - Mid pitch, pulse
            lda #$00
            sta SID_V2_FREQ_LO
            lda #$0E
            sta SID_V2_FREQ_HI
            lda #$08
            sta SID_V2_PWHI
            lda #$09
            sta SID_V2_AD
            lda #$00
            sta SID_V2_SR

            ; Voice 3 - Low pitch, triangle
            lda #$00
            sta SID_V3_FREQ_LO
            lda #$07
            sta SID_V3_FREQ_HI
            lda #$09
            sta SID_V3_AD
            lda #$00
            sta SID_V3_SR

            rts

; ----------------------------------------------------------------------------
; Reset Track Colours
; ----------------------------------------------------------------------------

reset_track_colours:
            ldx #0
            lda #GREY
reset_t1:
            sta COLRAM + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne reset_t1

            ldx #0
reset_t2:
            sta COLRAM + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne reset_t2

            ldx #0
reset_t3:
            sta COLRAM + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne reset_t3

            ; Restore key labels
            lda #LIGHT_RED
            sta COLRAM + (TRACK1_ROW * 40)
            lda #LIGHT_GREEN
            sta COLRAM + (TRACK2_ROW * 40)
            lda #LIGHT_BLUE
            sta COLRAM + (TRACK3_ROW * 40)

            ; Restore hit zone colours
            lda #YELLOW
            sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COL
            sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COL
            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COL

            ; Redraw note colours
            jsr redraw_all_notes

            rts

; ----------------------------------------------------------------------------
; Redraw All Notes
; ----------------------------------------------------------------------------

redraw_all_notes:
            ldx #0
redraw_loop:
            lda note_track,x
            beq redraw_next
            jsr draw_note
redraw_next:
            inx
            cpx #MAX_NOTES
            bne redraw_loop
            rts

; ----------------------------------------------------------------------------
; Check Keys
; ----------------------------------------------------------------------------

check_keys:
            lda #$FD
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_x_key
            jsr play_voice1
            jsr flash_track1

check_x_key:
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$80
            bne check_c_key
            jsr play_voice2
            jsr flash_track2

check_c_key:
            lda #$FB
            sta CIA1_PRA
            lda CIA1_PRB
            and #$10
            bne check_keys_done
            jsr play_voice3
            jsr flash_track3

check_keys_done:
            lda #$FF
            sta CIA1_PRA
            rts

; ----------------------------------------------------------------------------
; Play Voices
; ----------------------------------------------------------------------------

play_voice1:
            lda #$21
            sta SID_V1_CTRL
            rts

play_voice2:
            lda #$41
            sta SID_V2_CTRL
            rts

play_voice3:
            lda #$11
            sta SID_V3_CTRL
            rts

; ----------------------------------------------------------------------------
; Flash Tracks
; ----------------------------------------------------------------------------

flash_track1:
            ldx #0
            lda #RED
flash_t1_loop:
            sta COLRAM + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne flash_t1_loop
            lda #WHITE
            sta COLRAM + (TRACK1_ROW * 40)
            rts

flash_track2:
            ldx #0
            lda #GREEN
flash_t2_loop:
            sta COLRAM + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne flash_t2_loop
            lda #WHITE
            sta COLRAM + (TRACK2_ROW * 40)
            rts

flash_track3:
            ldx #0
            lda #BLUE
flash_t3_loop:
            sta COLRAM + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne flash_t3_loop
            lda #WHITE
            sta COLRAM + (TRACK3_ROW * 40)
            rts

; ----------------------------------------------------------------------------
; Song Data
; ----------------------------------------------------------------------------
; Format: beat, track (1-3)
; $FF marks end of song

song_data:
            ; Simple test pattern - 4 bars at 120 BPM
            ; Bar 1
            !byte 0, 1
            !byte 2, 2
            !byte 4, 3
            !byte 6, 1

            ; Bar 2
            !byte 8, 2
            !byte 10, 3
            !byte 12, 1
            !byte 14, 2

            ; Bar 3
            !byte 16, 3
            !byte 18, 1
            !byte 20, 2
            !byte 22, 3

            ; Bar 4 - faster pattern
            !byte 24, 1
            !byte 25, 2
            !byte 26, 3
            !byte 28, 1
            !byte 29, 2
            !byte 30, 3

            !byte $FF           ; End marker

; ----------------------------------------------------------------------------
; Note Arrays
; ----------------------------------------------------------------------------

note_track:
            !fill MAX_NOTES, 0

note_col:
            !fill MAX_NOTES, 0

What You’ve Learnt

  • Parallel arrays - Multiple arrays with matching indices store related data (track and column for each note)
  • Frame counting - Count frames to trigger events at specific intervals
  • Song data format - Simple (beat, track) pairs define when notes appear
  • Erase-move-draw - The fundamental animation pattern: remove old, update position, draw new
  • Screen coordinates - Calculating addresses with SCREEN + (row * 40) + column

What’s Next

In Unit 3, you’ll make the game your own. Change the SID voices, experiment with waveforms and ADSR, pick different colours. You’ll learn the internals by modifying them.

What Changed

Unit 1 → Unit 2
+506-138
11 ; ============================================================================
2-; SID SYMPHONY - Unit 1: Hello SID
2+; SID SYMPHONY - Unit 2: Notes Appear
33 ; ============================================================================
4-; Your first contact with the SID chip. Three tracks, three keys, three voices.
5-; Press Z/X/C to trigger sounds and see the tracks flash.
4+; Notes scroll from right to left across three tracks. When they reach the
5+; hit zone, press the matching key to play the sound. A simple test pattern
6+; loops continuously so you can practice timing.
67 ;
78 ; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
89 ; ============================================================================
...
7677
7778 ; Hit zone column
7879 HIT_ZONE_COL = 3 ; Where notes need to be hit
80+
81+; Note settings
82+NOTE_CHAR = $57 ; Character for note (filled dot/ball)
83+TRACK_CHAR = $2D ; Minus character for track line
84+MAX_NOTES = 8 ; Maximum notes on screen at once
85+NOTE_SPAWN_COL = 37 ; Where notes appear (right side)
86+
87+; Timing
88+FRAMES_PER_BEAT = 25 ; ~120 BPM at 50Hz (PAL)
89+
90+; Zero page pointers
91+ZP_PTR = $FB ; General purpose pointer
92+ZP_PTR_HI = $FC
93+
94+; ----------------------------------------------------------------------------
95+; Variables (in low memory)
96+; ----------------------------------------------------------------------------
97+
98+frame_count = $02 ; Frame counter (0-255)
99+beat_count = $03 ; Current beat in song (0-255)
100+song_pos = $04 ; Position in song data (word)
101+song_pos_hi = $05
102+temp_track = $06 ; Temporary storage for track number
79103
80104 ; ----------------------------------------------------------------------------
81105 ; BASIC Stub - SYS 2064
...
99123 start:
100124 jsr init_screen ; Set up the display
101125 jsr init_sid ; Configure SID chip
126+ jsr init_notes ; Clear note arrays
127+
128+ ; Initialize song position
129+ lda #<song_data
130+ sta song_pos
131+ lda #>song_data
132+ sta song_pos_hi
133+
134+ ; Initialize counters
135+ lda #0
136+ sta frame_count
137+ sta beat_count
102138
103139 main_loop:
104140 ; Wait for raster (smooth timing)
105141 lda #$FF
106-- cmp $D012
107- bne -
142+wait_raster:
143+ cmp $D012
144+ bne wait_raster
145+
146+ ; Update frame counter
147+ inc frame_count
148+ lda frame_count
149+ cmp #FRAMES_PER_BEAT
150+ bcc no_new_beat
151+
152+ ; New beat!
153+ lda #0
154+ sta frame_count
155+ jsr check_spawn_note
156+ inc beat_count
157+
158+no_new_beat:
159+ ; Move all notes left
160+ jsr update_notes
108161
109162 ; Reset track colours to default
110163 jsr reset_track_colours
...
113166 jsr check_keys
114167
115168 jmp main_loop
169+
170+; ----------------------------------------------------------------------------
171+; Initialize Notes
172+; ----------------------------------------------------------------------------
173+
174+init_notes:
175+ ldx #MAX_NOTES-1
176+ lda #0
177+init_notes_loop:
178+ sta note_track,x
179+ sta note_col,x
180+ dex
181+ bpl init_notes_loop
182+ rts
183+
184+; ----------------------------------------------------------------------------
185+; Check Spawn Note
186+; ----------------------------------------------------------------------------
187+; Song data is in beat order. Process all entries matching current beat,
188+; then return. Entries are (beat, track) pairs ending with $FF.
189+
190+check_spawn_note:
191+ ldy #0
192+
193+spawn_check_loop:
194+ ; Read beat number for current entry
195+ lda (song_pos),y
196+ cmp #$FF
197+ beq spawn_restart_song
198+
199+ ; If entry beat > current beat, we're done for this beat
200+ cmp beat_count
201+ beq spawn_match ; Equal - spawn this note
202+ bcs spawn_done ; Greater - done for now (entries are ordered)
203+
204+ ; Entry beat < current beat - shouldn't happen, but skip it
205+ jmp spawn_advance
206+
207+spawn_match:
208+ ; Get track number (next byte)
209+ iny
210+ lda (song_pos),y
211+ jsr spawn_note
212+ dey ; Reset Y for next iteration
213+
214+spawn_advance:
215+ ; Move song_pos forward by 2 bytes
216+ lda song_pos
217+ clc
218+ adc #2
219+ sta song_pos
220+ lda song_pos_hi
221+ adc #0
222+ sta song_pos_hi
223+ jmp spawn_check_loop
224+
225+spawn_done:
226+ rts
227+
228+spawn_restart_song:
229+ lda #<song_data
230+ sta song_pos
231+ lda #>song_data
232+ sta song_pos_hi
233+ lda #0
234+ sta beat_count
235+ rts
236+
237+; ----------------------------------------------------------------------------
238+; Spawn Note
239+; ----------------------------------------------------------------------------
240+; Input: A = track number (1-3)
241+
242+spawn_note:
243+ sta temp_track
244+
245+ ldx #0
246+spawn_find_slot:
247+ lda note_track,x
248+ beq spawn_found_slot
249+ inx
250+ cpx #MAX_NOTES
251+ bne spawn_find_slot
252+ rts ; No empty slot
253+
254+spawn_found_slot:
255+ lda temp_track
256+ sta note_track,x
257+ lda #NOTE_SPAWN_COL
258+ sta note_col,x
259+ jsr draw_note
260+ rts
261+
262+; ----------------------------------------------------------------------------
263+; Update Notes
264+; ----------------------------------------------------------------------------
265+
266+update_notes:
267+ ldx #0
268+
269+update_loop:
270+ lda note_track,x
271+ beq update_next
272+
273+ jsr erase_note
274+
275+ dec note_col,x
276+ lda note_col,x
277+ cmp #1
278+ bcc update_deactivate
279+
280+ jsr draw_note
281+ jmp update_next
282+
283+update_deactivate:
284+ lda #0
285+ sta note_track,x
286+
287+update_next:
288+ inx
289+ cpx #MAX_NOTES
290+ bne update_loop
291+ rts
292+
293+; ----------------------------------------------------------------------------
294+; Draw Note
295+; ----------------------------------------------------------------------------
296+; Input: X = note index
297+
298+draw_note:
299+ lda note_track,x
300+ cmp #1
301+ beq draw_note_t1
302+ cmp #2
303+ beq draw_note_t2
304+ cmp #3
305+ beq draw_note_t3
306+ rts
307+
308+draw_note_t1:
309+ lda note_col,x
310+ clc
311+ adc #<(SCREEN + TRACK1_ROW * 40)
312+ sta ZP_PTR
313+ lda #>(SCREEN + TRACK1_ROW * 40)
314+ adc #0
315+ sta ZP_PTR_HI
316+
317+ ldy #0
318+ lda #NOTE_CHAR
319+ sta (ZP_PTR),y
320+
321+ lda note_col,x
322+ clc
323+ adc #<(COLRAM + TRACK1_ROW * 40)
324+ sta ZP_PTR
325+ lda #>(COLRAM + TRACK1_ROW * 40)
326+ adc #0
327+ sta ZP_PTR_HI
328+ lda #LIGHT_RED
329+ sta (ZP_PTR),y
330+ rts
331+
332+draw_note_t2:
333+ lda note_col,x
334+ clc
335+ adc #<(SCREEN + TRACK2_ROW * 40)
336+ sta ZP_PTR
337+ lda #>(SCREEN + TRACK2_ROW * 40)
338+ adc #0
339+ sta ZP_PTR_HI
340+
341+ ldy #0
342+ lda #NOTE_CHAR
343+ sta (ZP_PTR),y
344+
345+ lda note_col,x
346+ clc
347+ adc #<(COLRAM + TRACK2_ROW * 40)
348+ sta ZP_PTR
349+ lda #>(COLRAM + TRACK2_ROW * 40)
350+ adc #0
351+ sta ZP_PTR_HI
352+ lda #LIGHT_GREEN
353+ sta (ZP_PTR),y
354+ rts
355+
356+draw_note_t3:
357+ lda note_col,x
358+ clc
359+ adc #<(SCREEN + TRACK3_ROW * 40)
360+ sta ZP_PTR
361+ lda #>(SCREEN + TRACK3_ROW * 40)
362+ adc #0
363+ sta ZP_PTR_HI
364+
365+ ldy #0
366+ lda #NOTE_CHAR
367+ sta (ZP_PTR),y
368+
369+ lda note_col,x
370+ clc
371+ adc #<(COLRAM + TRACK3_ROW * 40)
372+ sta ZP_PTR
373+ lda #>(COLRAM + TRACK3_ROW * 40)
374+ adc #0
375+ sta ZP_PTR_HI
376+ lda #LIGHT_BLUE
377+ sta (ZP_PTR),y
378+ rts
379+
380+; ----------------------------------------------------------------------------
381+; Erase Note
382+; ----------------------------------------------------------------------------
383+; Input: X = note index
384+
385+erase_note:
386+ lda note_track,x
387+ cmp #1
388+ beq erase_note_t1
389+ cmp #2
390+ beq erase_note_t2
391+ cmp #3
392+ beq erase_note_t3
393+ rts
394+
395+erase_note_t1:
396+ lda note_col,x
397+ clc
398+ adc #<(SCREEN + TRACK1_ROW * 40)
399+ sta ZP_PTR
400+ lda #>(SCREEN + TRACK1_ROW * 40)
401+ adc #0
402+ sta ZP_PTR_HI
403+
404+ ldy #0
405+ lda #TRACK_CHAR
406+ sta (ZP_PTR),y
407+
408+ lda note_col,x
409+ clc
410+ adc #<(COLRAM + TRACK1_ROW * 40)
411+ sta ZP_PTR
412+ lda #>(COLRAM + TRACK1_ROW * 40)
413+ adc #0
414+ sta ZP_PTR_HI
415+ lda #GREY
416+ sta (ZP_PTR),y
417+ rts
418+
419+erase_note_t2:
420+ lda note_col,x
421+ clc
422+ adc #<(SCREEN + TRACK2_ROW * 40)
423+ sta ZP_PTR
424+ lda #>(SCREEN + TRACK2_ROW * 40)
425+ adc #0
426+ sta ZP_PTR_HI
427+
428+ ldy #0
429+ lda #TRACK_CHAR
430+ sta (ZP_PTR),y
431+
432+ lda note_col,x
433+ clc
434+ adc #<(COLRAM + TRACK2_ROW * 40)
435+ sta ZP_PTR
436+ lda #>(COLRAM + TRACK2_ROW * 40)
437+ adc #0
438+ sta ZP_PTR_HI
439+ lda #GREY
440+ sta (ZP_PTR),y
441+ rts
442+
443+erase_note_t3:
444+ lda note_col,x
445+ clc
446+ adc #<(SCREEN + TRACK3_ROW * 40)
447+ sta ZP_PTR
448+ lda #>(SCREEN + TRACK3_ROW * 40)
449+ adc #0
450+ sta ZP_PTR_HI
451+
452+ ldy #0
453+ lda #TRACK_CHAR
454+ sta (ZP_PTR),y
455+
456+ lda note_col,x
457+ clc
458+ adc #<(COLRAM + TRACK3_ROW * 40)
459+ sta ZP_PTR
460+ lda #>(COLRAM + TRACK3_ROW * 40)
461+ adc #0
462+ sta ZP_PTR_HI
463+ lda #GREY
464+ sta (ZP_PTR),y
465+ rts
116466
117467 ; ----------------------------------------------------------------------------
118468 ; Initialize Screen
119469 ; ----------------------------------------------------------------------------
120-; Clears screen and draws the three tracks with hit zones
121470
122471 init_screen:
123- ; Set border and background
124472 lda #BLACK
125473 sta BORDER
126474 sta BGCOL
127475
128- ; Clear screen with spaces
129476 ldx #0
130- lda #$20 ; Space character
131-- sta SCREEN,x
477+ lda #$20
478+clr_screen:
479+ sta SCREEN,x
132480 sta SCREEN+$100,x
133481 sta SCREEN+$200,x
134482 sta SCREEN+$2E8,x
135483 inx
136- bne -
484+ bne clr_screen
137485
138- ; Set all colours to grey
139486 ldx #0
140487 lda #GREY
141-- sta COLRAM,x
488+clr_colour:
489+ sta COLRAM,x
142490 sta COLRAM+$100,x
143491 sta COLRAM+$200,x
144492 sta COLRAM+$2E8,x
145493 inx
146- bne -
494+ bne clr_colour
147495
148- ; Draw track lines
149496 jsr draw_tracks
150-
151- ; Draw hit zones
152497 jsr draw_hit_zones
153-
154- ; Draw labels
155498 jsr draw_labels
156499
157500 rts
...
159502 ; ----------------------------------------------------------------------------
160503 ; Draw Tracks
161504 ; ----------------------------------------------------------------------------
162-; Draws horizontal lines for each track using minus characters
163505
164506 draw_tracks:
165- ; Track 1 (row 8)
166507 ldx #0
167- lda #$2D ; Minus character for track line
168-- sta SCREEN + (TRACK1_ROW * 40),x
508+ lda #TRACK_CHAR
509+draw_t1:
510+ sta SCREEN + (TRACK1_ROW * 40),x
169511 inx
170512 cpx #38
171- bne -
513+ bne draw_t1
172514
173- ; Track 2 (row 12)
174515 ldx #0
175-- sta SCREEN + (TRACK2_ROW * 40),x
516+draw_t2:
517+ sta SCREEN + (TRACK2_ROW * 40),x
176518 inx
177519 cpx #38
178- bne -
520+ bne draw_t2
179521
180- ; Track 3 (row 16)
181522 ldx #0
182-- sta SCREEN + (TRACK3_ROW * 40),x
523+draw_t3:
524+ sta SCREEN + (TRACK3_ROW * 40),x
183525 inx
184526 cpx #38
185- bne -
527+ bne draw_t3
186528
187529 rts
188530
189531 ; ----------------------------------------------------------------------------
190532 ; Draw Hit Zones
191533 ; ----------------------------------------------------------------------------
192-; Draws vertical bars at the hit zone position
193534
194535 draw_hit_zones:
195- ; Draw vertical line at hit zone column
196- ; Using pipe character for hit zone marker
197536 lda #$7D ; Pipe character
198537
199- ; Hit zone spans from track 1 to track 3
200538 sta SCREEN + (TRACK1_ROW * 40) + HIT_ZONE_COL
201539 sta SCREEN + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COL
202540 sta SCREEN + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COL
...
209547 sta SCREEN + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COL
210548 sta SCREEN + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COL
211549
212- ; Colour the hit zones yellow
213550 lda #YELLOW
214551 sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COL
215552 sta COLRAM + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COL
...
228565 ; ----------------------------------------------------------------------------
229566 ; Draw Labels
230567 ; ----------------------------------------------------------------------------
231-; Draws track labels and instructions
232568
233569 draw_labels:
234- ; Title "SID SYMPHONY" at top
235570 ldx #0
236-- lda title_text,x
237- beq +
571+draw_title:
572+ lda title_text,x
573+ beq draw_title_done
238574 sta SCREEN + 13,x
239575 lda #WHITE
240576 sta COLRAM + 13,x
241577 inx
242- bne -
243-+
244- ; Track labels
245- ; "Z" for track 1
578+ bne draw_title
579+draw_title_done:
580+
246581 lda #$1A ; Z
247582 sta SCREEN + (TRACK1_ROW * 40)
248583 lda #LIGHT_RED
249584 sta COLRAM + (TRACK1_ROW * 40)
250585
251- ; "X" for track 2
252586 lda #$18 ; X
253587 sta SCREEN + (TRACK2_ROW * 40)
254588 lda #LIGHT_GREEN
255589 sta COLRAM + (TRACK2_ROW * 40)
256590
257- ; "C" for track 3
258591 lda #$03 ; C
259592 sta SCREEN + (TRACK3_ROW * 40)
260593 lda #LIGHT_BLUE
261594 sta COLRAM + (TRACK3_ROW * 40)
262595
263- ; Instructions at bottom
264596 ldx #0
265-- lda instr_text,x
266- beq +
267- sta SCREEN + (23 * 40) + 5,x
597+draw_instr:
598+ lda instr_text,x
599+ beq draw_instr_done
600+ sta SCREEN + (23 * 40) + 3,x
268601 lda #GREY
269- sta COLRAM + (23 * 40) + 5,x
602+ sta COLRAM + (23 * 40) + 3,x
270603 inx
271- bne -
272-+
604+ bne draw_instr
605+draw_instr_done:
606+
273607 rts
274608
275609 title_text:
...
277611 !byte 0
278612
279613 instr_text:
280- !scr "press z, x, c to play"
614+ !scr "hit notes when they reach |"
281615 !byte 0
282616
283617 ; ----------------------------------------------------------------------------
284618 ; Initialize SID
285619 ; ----------------------------------------------------------------------------
286-; Sets up SID with three distinct voices ready to play
287620
288621 init_sid:
289- ; Clear all SID registers first
290622 ldx #$18
291623 lda #0
292-- sta SID,x
624+clear_sid:
625+ sta SID,x
293626 dex
294- bpl -
627+ bpl clear_sid
295628
296- ; Set volume to maximum
297629 lda #$0F
298630 sta SID_VOLUME
299631
300- ; Voice 1 - High pitch, sawtooth wave
632+ ; Voice 1 - High pitch, sawtooth
301633 lda #$00
302634 sta SID_V1_FREQ_LO
303- lda #$1C ; High frequency (~523 Hz, C5)
635+ lda #$1C
304636 sta SID_V1_FREQ_HI
305- lda #$09 ; Attack=0, Decay=9
637+ lda #$09
306638 sta SID_V1_AD
307- lda #$00 ; Sustain=0, Release=0
639+ lda #$00
308640 sta SID_V1_SR
309641
310- ; Voice 2 - Mid pitch, pulse wave
642+ ; Voice 2 - Mid pitch, pulse
311643 lda #$00
312644 sta SID_V2_FREQ_LO
313- lda #$0E ; Mid frequency (~262 Hz, C4)
645+ lda #$0E
314646 sta SID_V2_FREQ_HI
315- lda #$08 ; 50% pulse width
647+ lda #$08
316648 sta SID_V2_PWHI
317- lda #$09 ; Attack=0, Decay=9
649+ lda #$09
318650 sta SID_V2_AD
319- lda #$00 ; Sustain=0, Release=0
651+ lda #$00
320652 sta SID_V2_SR
321653
322- ; Voice 3 - Low pitch, triangle wave
654+ ; Voice 3 - Low pitch, triangle
323655 lda #$00
324656 sta SID_V3_FREQ_LO
325- lda #$07 ; Low frequency (~131 Hz, C3)
657+ lda #$07
326658 sta SID_V3_FREQ_HI
327- lda #$09 ; Attack=0, Decay=9
659+ lda #$09
328660 sta SID_V3_AD
329- lda #$00 ; Sustain=0, Release=0
661+ lda #$00
330662 sta SID_V3_SR
331663
332664 rts
...
334666 ; ----------------------------------------------------------------------------
335667 ; Reset Track Colours
336668 ; ----------------------------------------------------------------------------
337-; Returns tracks to their default colours
338669
339670 reset_track_colours:
340- ; Track 1 - default grey
341671 ldx #0
342672 lda #GREY
343-- sta COLRAM + (TRACK1_ROW * 40),x
673+reset_t1:
674+ sta COLRAM + (TRACK1_ROW * 40),x
344675 inx
345676 cpx #38
346- bne -
677+ bne reset_t1
347678
348- ; Track 2 - default grey
349679 ldx #0
350-- sta COLRAM + (TRACK2_ROW * 40),x
680+reset_t2:
681+ sta COLRAM + (TRACK2_ROW * 40),x
351682 inx
352683 cpx #38
353- bne -
684+ bne reset_t2
354685
355- ; Track 3 - default grey
356686 ldx #0
357-- sta COLRAM + (TRACK3_ROW * 40),x
687+reset_t3:
688+ sta COLRAM + (TRACK3_ROW * 40),x
358689 inx
359690 cpx #38
360- bne -
691+ bne reset_t3
361692
362693 ; Restore key labels
363694 lda #LIGHT_RED
...
372703 sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COL
373704 sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COL
374705 sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COL
706+
707+ ; Redraw note colours
708+ jsr redraw_all_notes
709+
710+ rts
711+
712+; ----------------------------------------------------------------------------
713+; Redraw All Notes
714+; ----------------------------------------------------------------------------
375715
716+redraw_all_notes:
717+ ldx #0
718+redraw_loop:
719+ lda note_track,x
720+ beq redraw_next
721+ jsr draw_note
722+redraw_next:
723+ inx
724+ cpx #MAX_NOTES
725+ bne redraw_loop
376726 rts
377727
378728 ; ----------------------------------------------------------------------------
379729 ; Check Keys
380730 ; ----------------------------------------------------------------------------
381-; Reads keyboard and triggers sounds for Z, X, C keys
382731
383732 check_keys:
384- ; Check Z key (row 1, column 2)
385- ; Z is at keyboard matrix position: row=$FD (bit 1 low), col=$10 (bit 4)
386- lda #$FD ; Select row 1 (bit 1 = 0)
733+ lda #$FD
387734 sta CIA1_PRA
388735 lda CIA1_PRB
389- and #$10 ; Check column 4 (Z key)
390- bne + ; Branch if not pressed
736+ and #$10
737+ bne check_x_key
391738 jsr play_voice1
392739 jsr flash_track1
393-+
394- ; Check X key (row 2, column 7)
395- ; X is at keyboard matrix position: row=$FB (bit 2 low), col=$80 (bit 7)
396- lda #$FB ; Select row 2 (bit 2 = 0)
740+
741+check_x_key:
742+ lda #$FB
397743 sta CIA1_PRA
398744 lda CIA1_PRB
399- and #$80 ; Check column 7 (X key)
400- bne + ; Branch if not pressed
745+ and #$80
746+ bne check_c_key
401747 jsr play_voice2
402748 jsr flash_track2
403-+
404- ; Check C key (row 2, column 4)
405- ; C is at keyboard matrix position: row=$FB (bit 2 low), col=$10 (bit 4)
406- lda #$FB ; Select row 2 (bit 2 = 0)
749+
750+check_c_key:
751+ lda #$FB
407752 sta CIA1_PRA
408753 lda CIA1_PRB
409- and #$10 ; Check column 4 (C key)
410- bne + ; Branch if not pressed
754+ and #$10
755+ bne check_keys_done
411756 jsr play_voice3
412757 jsr flash_track3
413-+
414- ; Reset keyboard scanning
758+
759+check_keys_done:
415760 lda #$FF
416761 sta CIA1_PRA
417-
418762 rts
419763
420764 ; ----------------------------------------------------------------------------
421-; Play Voice 1
765+; Play Voices
422766 ; ----------------------------------------------------------------------------
423-; Triggers voice 1 (high, sawtooth)
424767
425768 play_voice1:
426- lda #$21 ; Gate on + sawtooth waveform
769+ lda #$21
427770 sta SID_V1_CTRL
428771 rts
429-
430-; ----------------------------------------------------------------------------
431-; Play Voice 2
432-; ----------------------------------------------------------------------------
433-; Triggers voice 2 (mid, pulse)
434772
435773 play_voice2:
436- lda #$41 ; Gate on + pulse waveform
774+ lda #$41
437775 sta SID_V2_CTRL
438776 rts
439-
440-; ----------------------------------------------------------------------------
441-; Play Voice 3
442-; ----------------------------------------------------------------------------
443-; Triggers voice 3 (low, triangle)
444777
445778 play_voice3:
446- lda #$11 ; Gate on + triangle waveform
779+ lda #$11
447780 sta SID_V3_CTRL
448781 rts
449782
450783 ; ----------------------------------------------------------------------------
451-; Flash Track 1
784+; Flash Tracks
452785 ; ----------------------------------------------------------------------------
453-; Highlights track 1 in red
454786
455787 flash_track1:
456788 ldx #0
457789 lda #RED
458-- sta COLRAM + (TRACK1_ROW * 40),x
790+flash_t1_loop:
791+ sta COLRAM + (TRACK1_ROW * 40),x
459792 inx
460793 cpx #38
461- bne -
462- ; Keep label visible
794+ bne flash_t1_loop
463795 lda #WHITE
464796 sta COLRAM + (TRACK1_ROW * 40)
465797 rts
466-
467-; ----------------------------------------------------------------------------
468-; Flash Track 2
469-; ----------------------------------------------------------------------------
470-; Highlights track 2 in green
471798
472799 flash_track2:
473800 ldx #0
474801 lda #GREEN
475-- sta COLRAM + (TRACK2_ROW * 40),x
802+flash_t2_loop:
803+ sta COLRAM + (TRACK2_ROW * 40),x
476804 inx
477805 cpx #38
478- bne -
479- ; Keep label visible
806+ bne flash_t2_loop
480807 lda #WHITE
481808 sta COLRAM + (TRACK2_ROW * 40)
482809 rts
483-
484-; ----------------------------------------------------------------------------
485-; Flash Track 3
486-; ----------------------------------------------------------------------------
487-; Highlights track 3 in blue
488810
489811 flash_track3:
490812 ldx #0
491813 lda #BLUE
492-- sta COLRAM + (TRACK3_ROW * 40),x
814+flash_t3_loop:
815+ sta COLRAM + (TRACK3_ROW * 40),x
493816 inx
494817 cpx #38
495- bne -
496- ; Keep label visible
818+ bne flash_t3_loop
497819 lda #WHITE
498820 sta COLRAM + (TRACK3_ROW * 40)
499821 rts
822+
823+; ----------------------------------------------------------------------------
824+; Song Data
825+; ----------------------------------------------------------------------------
826+; Format: beat, track (1-3)
827+; $FF marks end of song
828+
829+song_data:
830+ ; Simple test pattern - 4 bars at 120 BPM
831+ ; Bar 1
832+ !byte 0, 1
833+ !byte 2, 2
834+ !byte 4, 3
835+ !byte 6, 1
836+
837+ ; Bar 2
838+ !byte 8, 2
839+ !byte 10, 3
840+ !byte 12, 1
841+ !byte 14, 2
842+
843+ ; Bar 3
844+ !byte 16, 3
845+ !byte 18, 1
846+ !byte 20, 2
847+ !byte 22, 3
848+
849+ ; Bar 4 - faster pattern
850+ !byte 24, 1
851+ !byte 25, 2
852+ !byte 26, 3
853+ !byte 28, 1
854+ !byte 29, 2
855+ !byte 30, 3
856+
857+ !byte $FF ; End marker
858+
859+; ----------------------------------------------------------------------------
860+; Note Arrays
861+; ----------------------------------------------------------------------------
862+
863+note_track:
864+ !fill MAX_NOTES, 0
865+
866+note_col:
867+ !fill MAX_NOTES, 0
500868