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

Song Data

Design the data format for songs. Create the first complete musical pattern.

14% of SID Symphony

A rhythm game needs music. This unit defines the song data format and creates the first complete song.

Until now, notes used placeholder data and fixed frequencies. Now each note carries its actual pitch. Hit a note and hear the melody. The game becomes musical.

Run It

Assemble and run:

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

Unit 9 Screenshot

Play through the song and listen. Each hit plays the correct note from the melody. Miss and you hear the harsh noise - a gap in the music. The game and music become one.

Song data — notes now carry pitch information

The Song Format

Each note entry is now three bytes:

; ============================================================================
; SONG DATA FORMAT
; ============================================================================
; Each note entry is 3 bytes:
;   byte 0: beat number (0-63, when note spawns)
;   byte 1: track number (1=Z high, 2=X mid, 3=C low)
;   byte 2: note frequency high byte
;
; End of song marker: $FF
;
; Note frequency table (high byte only):
; C3=$11  D3=$13  E3=$16  F3=$17  G3=$1A  A3=$1D  B3=$21
; C4=$23  D4=$27  E4=$2C  F4=$2F  G4=$35  A4=$3B  B4=$42
; C5=$47  D5=$4F  E5=$58  F5=$5E  G5=$6A  A5=$77  B5=$85
; ============================================================================

song_data:
            ; Bar 1 - Opening phrase
            !byte 0, 1, $47     ; Beat 0, Track 1 (Z), C5
            !byte 2, 2, $2C     ; Beat 2, Track 2 (X), E4
            !byte 4, 3, $11     ; Beat 4, Track 3 (C), C3

            ; Bar 2 - Descending
            !byte 8, 1, $3B     ; A4
            !byte 10, 2, $27    ; D4
            !byte 12, 3, $13    ; D3

            ; ... more bars ...

            !byte $FF           ; End of song marker

The format is:

  • Beat - When the note spawns (0 to song length minus 1)
  • Track - Which track (1=Z, 2=X, 3=C)
  • Frequency - High byte of SID frequency

The $FF byte marks the end of song data.

SID Frequency Reference

The SID uses 16-bit frequency values, but for simplicity we only store the high byte. Here are useful note values:

NoteCDEFGAB
Octave 3$11$13$16$17$1A$1D$21
Octave 4$23$27$2C$2F$35$3B$42
Octave 5$47$4F$58$5E$6A$77$85

Track 1 (Z) plays high notes with sawtooth. Track 2 (X) plays mid notes with pulse. Track 3 (C) plays low notes with triangle. Three voices, three timbres, three octave ranges.

Reading 3-Byte Entries

The spawn routine now reads three bytes per entry:

; ----------------------------------------------------------------------------
; Check Spawn Note - Now reads 3-byte song entries
; ----------------------------------------------------------------------------

check_spawn_note:
            ldy #0

spawn_check_loop:
            lda (song_pos),y
            cmp #$FF
            beq spawn_restart_song

            cmp beat_count
            beq spawn_match
            bcs spawn_done

            jmp spawn_advance

spawn_match:
            iny
            lda (song_pos),y    ; Track number
            sta temp_track
            iny
            lda (song_pos),y    ; Note frequency
            pha
            lda temp_track
            jsr spawn_note_with_freq
            pla                 ; Clean up stack
            dey
            dey

spawn_advance:
            lda song_pos
            clc
            adc #3              ; 3 bytes per entry now
            sta song_pos
            lda song_pos_hi
            adc #0
            sta song_pos_hi
            jmp spawn_check_loop

spawn_done:
            rts

The key change is adc #3 instead of adc #2. Each entry advances by three bytes. The frequency is stored in the note array alongside track and column.

Playing the Correct Note

When a note is hit, we play its stored frequency:

; ----------------------------------------------------------------------------
; Play Voice With Note Frequency
; ----------------------------------------------------------------------------
; When a note is hit, we play its stored frequency, not a fixed pitch.
; This makes each hit sound like part of the melody.

play_voice1_note:
            lda #0
            sta SID_V1_FREQ_LO
            lda hit_note_freq   ; Use the note's actual pitch
            sta SID_V1_FREQ_HI
            lda #VOICE1_WAVE
            ora #$01            ; Gate on
            sta SID_V1_CTRL
            rts

play_voice2_note:
            lda #0
            sta SID_V2_FREQ_LO
            lda hit_note_freq
            sta SID_V2_FREQ_HI
            lda #VOICE2_WAVE
            ora #$01
            sta SID_V2_CTRL
            rts

play_voice3_note:
            lda #0
            sta SID_V3_FREQ_LO
            lda hit_note_freq
            sta SID_V3_FREQ_HI
            lda #VOICE3_WAVE
            ora #$01
            sta SID_V3_CTRL
            rts

The hit_note_freq variable stores the frequency from the hit note. Instead of fixed pitches, each track voice plays whatever note was hit. Hit the right notes and you hear the melody.

Musical Composition

The song data creates a simple 8-bar phrase:

song_data:
            ; Bar 1 - Opening phrase
            !byte 0, 1, $47     ; C5 - high
            !byte 2, 2, $2C     ; E4 - mid
            !byte 4, 3, $11     ; C3 - low

            ; Bar 2 - Descending
            !byte 8, 1, $3B     ; A4
            !byte 10, 2, $27    ; D4
            !byte 12, 3, $13    ; D3
            ; ...

The pattern:

  • Bars 1-2: Establish the tonality (C major)
  • Bars 3-4: Build tension with faster notes
  • Bars 5-6: Introduce variation
  • Bars 7-8: Climax and resolve

When the song loops, players hear the progression repeat. Musical structure makes the game feel purposeful.

Memory Usage

Each song entry is 3 bytes. Our 30-note song uses 90 bytes plus the $FF terminator. A 64-beat song with dense notes might use 100-150 bytes. Plenty of room for longer songs.

The Complete Code

; ============================================================================
; SID SYMPHONY - Unit 9: Song Data
; ============================================================================
; Design the song data format. Create the first complete musical pattern.
; Notes now carry pitch information - each hit plays its actual note.
;
; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
; ============================================================================

; ============================================================================
; CUSTOMISATION SECTION
; ============================================================================

; SID Voice Settings (default for when no note playing)
VOICE1_WAVE = $21               ; Sawtooth
VOICE2_WAVE = $41               ; Pulse
VOICE3_WAVE = $11               ; Triangle

; Default frequencies (used if no note data)
VOICE1_FREQ = $1C               ; High pitch
VOICE2_FREQ = $0E               ; Mid pitch
VOICE3_FREQ = $07               ; Low pitch

VOICE_AD    = $09               ; Attack=0, Decay=9
VOICE_SR    = $00               ; Sustain=0, Release=0
PULSE_WIDTH = $08               ; 50% duty cycle

; Miss sound settings
MISS_FREQ   = $08               ; Low rumble
MISS_WAVE   = $81               ; Noise waveform
MISS_AD     = $00               ; Instant attack, no decay
MISS_SR     = $90               ; High sustain, fast release

; Visual Settings
BORDER_COL  = 0                 ; Black border
BG_COL      = 0                 ; Black background

TRACK1_NOTE_COL = 10            ; Light red
TRACK2_NOTE_COL = 13            ; Light green
TRACK3_NOTE_COL = 14            ; Light blue

TRACK_LINE_COL = 11             ; Dark grey
HIT_ZONE_COL = 7                ; Yellow

HIT_COL     = 1                 ; White - flash on successful hit
PERFECT_COL = 1                 ; White - perfect hit border flash
GOOD_COL    = 7                 ; Yellow - good hit border flash
MISS_COL    = 2                 ; Red - miss border flash

HEALTH_COL  = 5                 ; Green for health bar

; ============================================================================
; SCORING SETTINGS
; ============================================================================

PERFECT_SCORE = 100
GOOD_SCORE    = 50

; ============================================================================
; HEALTH SETTINGS
; ============================================================================

HEALTH_MAX    = 64
HEALTH_START  = 32
HEALTH_PERFECT = 4
HEALTH_GOOD   = 2
HEALTH_MISS   = 8

; ============================================================================
; HIT DETECTION SETTINGS
; ============================================================================

HIT_ZONE_MIN = 2
HIT_ZONE_MAX = 5
HIT_ZONE_CENTRE = 3

; ============================================================================
; SONG SETTINGS
; ============================================================================

SONG_LENGTH   = 64              ; Beats in the song

; ============================================================================
; MEMORY MAP
; ============================================================================

SCREEN      = $0400
COLRAM      = $D800
BORDER      = $D020
BGCOL       = $D021
CHARPTR     = $D018

CHARSET     = $3000

; SID registers
SID         = $D400
SID_V1_FREQ_LO = $D400
SID_V1_FREQ_HI = $D401
SID_V1_PWHI = $D403
SID_V1_CTRL = $D404
SID_V1_AD   = $D405
SID_V1_SR   = $D406

SID_V2_FREQ_LO = $D407
SID_V2_FREQ_HI = $D408
SID_V2_PWHI = $D40A
SID_V2_CTRL = $D40B
SID_V2_AD   = $D40C
SID_V2_SR   = $D40D

SID_V3_FREQ_LO = $D40E
SID_V3_FREQ_HI = $D40F
SID_V3_PWHI = $D411
SID_V3_CTRL = $D412
SID_V3_AD   = $D413
SID_V3_SR   = $D414

SID_VOLUME  = $D418

; CIA keyboard
CIA1_PRA    = $DC00
CIA1_PRB    = $DC01

; Track positions
TRACK1_ROW  = 8
TRACK2_ROW  = 12
TRACK3_ROW  = 16

; Health bar position
HEALTH_ROW  = 23

; Hit zone
HIT_ZONE_COLUMN = 3

; Custom character codes
CHAR_NOTE   = 128
CHAR_TRACK  = 129
CHAR_HITZONE = 130
CHAR_SPACE  = 32
CHAR_BAR_FULL = 131
CHAR_BAR_EMPTY = 132

; Note settings
MAX_NOTES   = 8
NOTE_SPAWN_COL = 37

; Timing
FRAMES_PER_BEAT = 25

; Zero page
ZP_PTR      = $FB
ZP_PTR_HI   = $FC

; Variables
frame_count = $02
beat_count  = $03
song_pos    = $04
song_pos_hi = $05
temp_track  = $06
key_pressed = $07
hit_quality = $08
border_flash = $09
miss_track  = $0A
game_over   = $0B
hit_note_freq = $0C             ; Frequency of note being hit

; ----------------------------------------------------------------------------
; BASIC Stub
; ----------------------------------------------------------------------------

            * = $0801

            !byte $0C, $08
            !byte $0A, $00
            !byte $9E
            !text "2064"
            !byte $00
            !byte $00, $00

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

            * = $0810

start:
            jsr copy_charset
            jsr init_screen
            jsr init_sid
            jsr init_notes
            jsr init_score
            jsr init_health

            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi

            lda #0
            sta frame_count
            sta beat_count
            sta border_flash
            sta game_over

main_loop:
            lda #$FF
wait_raster:
            cmp $D012
            bne wait_raster

            lda game_over
            bne main_loop

            inc frame_count
            lda frame_count
            cmp #FRAMES_PER_BEAT
            bcc no_new_beat

            lda #0
            sta frame_count
            jsr check_spawn_note
            inc beat_count

no_new_beat:
            jsr update_notes
            jsr reset_track_colours
            jsr update_border_flash
            jsr check_keys

            jmp main_loop

; ----------------------------------------------------------------------------
; Copy Character Set from ROM to RAM
; ----------------------------------------------------------------------------

copy_charset:
            sei

            lda $01
            pha
            and #$FB
            sta $01

            ldx #0
copy_loop:
            lda $D000,x
            sta CHARSET,x
            lda $D100,x
            sta CHARSET+$100,x
            lda $D200,x
            sta CHARSET+$200,x
            lda $D300,x
            sta CHARSET+$300,x
            lda $D400,x
            sta CHARSET+$400,x
            lda $D500,x
            sta CHARSET+$500,x
            lda $D600,x
            sta CHARSET+$600,x
            lda $D700,x
            sta CHARSET+$700,x
            inx
            bne copy_loop

            pla
            sta $01

            cli

            jsr define_custom_chars

            lda #$1C
            sta CHARPTR

            rts

; ----------------------------------------------------------------------------
; Define Custom Characters
; ----------------------------------------------------------------------------

define_custom_chars:
            lda #%00000110
            sta CHARSET + (CHAR_NOTE * 8) + 0
            lda #%00011110
            sta CHARSET + (CHAR_NOTE * 8) + 1
            lda #%01111110
            sta CHARSET + (CHAR_NOTE * 8) + 2
            lda #%11111110
            sta CHARSET + (CHAR_NOTE * 8) + 3
            lda #%11111110
            sta CHARSET + (CHAR_NOTE * 8) + 4
            lda #%01111110
            sta CHARSET + (CHAR_NOTE * 8) + 5
            lda #%00011110
            sta CHARSET + (CHAR_NOTE * 8) + 6
            lda #%00000110
            sta CHARSET + (CHAR_NOTE * 8) + 7

            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 0
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 1
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 2
            lda #%11111111
            sta CHARSET + (CHAR_TRACK * 8) + 3
            lda #%11111111
            sta CHARSET + (CHAR_TRACK * 8) + 4
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 5
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 6
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 7

            lda #%01100110
            sta CHARSET + (CHAR_HITZONE * 8) + 0
            sta CHARSET + (CHAR_HITZONE * 8) + 1
            sta CHARSET + (CHAR_HITZONE * 8) + 2
            sta CHARSET + (CHAR_HITZONE * 8) + 3
            sta CHARSET + (CHAR_HITZONE * 8) + 4
            sta CHARSET + (CHAR_HITZONE * 8) + 5
            sta CHARSET + (CHAR_HITZONE * 8) + 6
            sta CHARSET + (CHAR_HITZONE * 8) + 7

            lda #%11111111
            sta CHARSET + (CHAR_BAR_FULL * 8) + 0
            sta CHARSET + (CHAR_BAR_FULL * 8) + 1
            sta CHARSET + (CHAR_BAR_FULL * 8) + 2
            sta CHARSET + (CHAR_BAR_FULL * 8) + 3
            sta CHARSET + (CHAR_BAR_FULL * 8) + 4
            sta CHARSET + (CHAR_BAR_FULL * 8) + 5
            sta CHARSET + (CHAR_BAR_FULL * 8) + 6
            sta CHARSET + (CHAR_BAR_FULL * 8) + 7

            lda #%11111111
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 0
            lda #%10000001
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 1
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 2
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 3
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 4
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 5
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 6
            lda #%11111111
            sta CHARSET + (CHAR_BAR_EMPTY * 8) + 7

            rts

; ----------------------------------------------------------------------------
; Initialize Health
; ----------------------------------------------------------------------------

init_health:
            lda #HEALTH_START
            sta health
            jsr display_health
            rts

; ----------------------------------------------------------------------------
; Initialize Score
; ----------------------------------------------------------------------------

init_score:
            lda #0
            sta score_lo
            sta score_hi
            sta miss_count
            jsr display_score
            jsr display_misses
            rts

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

init_notes:
            ldx #MAX_NOTES-1
            lda #0
init_notes_loop:
            sta note_track,x
            sta note_col,x
            sta note_freq,x     ; Also clear note frequencies
            dex
            bpl init_notes_loop
            rts

; ----------------------------------------------------------------------------
; Check Spawn Note - Now reads 3-byte song entries
; ----------------------------------------------------------------------------

check_spawn_note:
            ldy #0

spawn_check_loop:
            lda (song_pos),y
            cmp #$FF
            beq spawn_restart_song

            cmp beat_count
            beq spawn_match
            bcs spawn_done

            jmp spawn_advance

spawn_match:
            iny
            lda (song_pos),y    ; Track number
            sta temp_track
            iny
            lda (song_pos),y    ; Note frequency
            pha
            lda temp_track
            jsr spawn_note_with_freq
            pla                 ; Clean up stack
            dey
            dey

spawn_advance:
            lda song_pos
            clc
            adc #3              ; 3 bytes per entry now
            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 With Frequency
; Input: A = track, stack has frequency
; ----------------------------------------------------------------------------

spawn_note_with_freq:
            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

spawn_found_slot:
            lda temp_track
            sta note_track,x
            lda #NOTE_SPAWN_COL
            sta note_col,x

            ; Get frequency from stack (caller pushed it)
            tsx
            lda $0103,x         ; Get frequency from stack
            sta note_freq,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_miss

            jsr draw_note
            jmp update_next

update_miss:
            lda note_track,x
            sta miss_track
            lda #0
            sta note_track,x
            jsr handle_miss

update_next:
            inx
            cpx #MAX_NOTES
            bne update_loop
            rts

; ----------------------------------------------------------------------------
; Handle Miss
; ----------------------------------------------------------------------------

handle_miss:
            inc miss_count

            jsr play_miss_sound

            lda #MISS_COL
            sta BORDER
            lda #8
            sta border_flash

            jsr display_misses
            jsr decrease_health

            rts

; ----------------------------------------------------------------------------
; Decrease Health
; ----------------------------------------------------------------------------

decrease_health:
            lda health
            sec
            sbc #HEALTH_MISS
            bcc health_zero
            sta health
            jsr display_health
            jsr check_game_over
            rts

health_zero:
            lda #0
            sta health
            jsr display_health
            jsr check_game_over
            rts

; ----------------------------------------------------------------------------
; Increase Health
; ----------------------------------------------------------------------------

increase_health:
            clc
            adc health
            cmp #HEALTH_MAX
            bcc health_ok
            lda #HEALTH_MAX
health_ok:
            sta health
            jsr display_health
            rts

; ----------------------------------------------------------------------------
; Check Game Over
; ----------------------------------------------------------------------------

check_game_over:
            lda health
            bne not_game_over

            lda #1
            sta game_over
            jsr show_game_over

not_game_over:
            rts

; ----------------------------------------------------------------------------
; Show Game Over
; ----------------------------------------------------------------------------

show_game_over:
            ldx #0
game_over_loop:
            lda game_over_text,x
            beq game_over_done
            sta SCREEN + (12 * 40) + 15,x
            lda #2
            sta COLRAM + (12 * 40) + 15,x
            inx
            jmp game_over_loop
game_over_done:
            lda #2
            sta BORDER
            rts

game_over_text:
            !scr "game over"
            !byte 0

; ----------------------------------------------------------------------------
; Display Health
; ----------------------------------------------------------------------------

display_health:
            lda health
            lsr
            lsr
            lsr
            sta temp_health

            ldx #0
            lda temp_health
            beq draw_empty_bars

draw_full_bars:
            lda #CHAR_BAR_FULL
            sta SCREEN + (HEALTH_ROW * 40) + 16,x
            lda #HEALTH_COL
            sta COLRAM + (HEALTH_ROW * 40) + 16,x
            inx
            cpx temp_health
            bne draw_full_bars

draw_empty_bars:
            cpx #8
            beq health_done
            lda #CHAR_BAR_EMPTY
            sta SCREEN + (HEALTH_ROW * 40) + 16,x
            lda #11
            sta COLRAM + (HEALTH_ROW * 40) + 16,x
            inx
            jmp draw_empty_bars

health_done:
            rts

temp_health: !byte 0

; ----------------------------------------------------------------------------
; Play Miss Sound
; ----------------------------------------------------------------------------

play_miss_sound:
            lda #0
            sta SID_V3_FREQ_LO
            lda #MISS_FREQ
            sta SID_V3_FREQ_HI
            lda #MISS_AD
            sta SID_V3_AD
            lda #MISS_SR
            sta SID_V3_SR
            lda #MISS_WAVE
            ora #$01
            sta SID_V3_CTRL
            rts

; ----------------------------------------------------------------------------
; Draw Note
; ----------------------------------------------------------------------------

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 #CHAR_NOTE
            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 #TRACK1_NOTE_COL
            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 #CHAR_NOTE
            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 #TRACK2_NOTE_COL
            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 #CHAR_NOTE
            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 #TRACK3_NOTE_COL
            sta (ZP_PTR),y
            rts

; ----------------------------------------------------------------------------
; Erase Note
; ----------------------------------------------------------------------------

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 #CHAR_TRACK
            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 #TRACK_LINE_COL
            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 #CHAR_TRACK
            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 #TRACK_LINE_COL
            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 #CHAR_TRACK
            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 #TRACK_LINE_COL
            sta (ZP_PTR),y
            rts

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

init_screen:
            lda #BORDER_COL
            sta BORDER
            lda #BG_COL
            sta BGCOL

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

            ldx #0
            lda #TRACK_LINE_COL
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:
            lda #CHAR_TRACK
            ldx #0
draw_t1:
            sta SCREEN + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne draw_t1

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

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

            rts

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

draw_hit_zones:
            lda #CHAR_HITZONE

            sta SCREEN + ((TRACK1_ROW-2) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK1_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK1_ROW+1) * 40) + HIT_ZONE_COLUMN

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

            sta SCREEN + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COLUMN
            sta SCREEN + ((TRACK3_ROW+2) * 40) + HIT_ZONE_COLUMN

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

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

            sta COLRAM + ((TRACK3_ROW-1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK3_ROW+1) * 40) + HIT_ZONE_COLUMN
            sta COLRAM + ((TRACK3_ROW+2) * 40) + HIT_ZONE_COLUMN

            rts

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

draw_labels:
            ldx #0
draw_score_label:
            lda score_label,x
            beq draw_score_label_done
            sta SCREEN + 1,x
            lda #1
            sta COLRAM + 1,x
            inx
            bne draw_score_label
draw_score_label_done:

            ldx #0
draw_miss_label:
            lda miss_label,x
            beq draw_miss_label_done
            sta SCREEN + 15,x
            lda #2
            sta COLRAM + 15,x
            inx
            bne draw_miss_label
draw_miss_label_done:

            ldx #0
draw_title:
            lda title_text,x
            beq draw_title_done
            sta SCREEN + 27,x
            lda #1
            sta COLRAM + 27,x
            inx
            bne draw_title
draw_title_done:

            ldx #0
draw_health_label:
            lda health_label,x
            beq draw_health_label_done
            sta SCREEN + (HEALTH_ROW * 40) + 8,x
            lda #5
            sta COLRAM + (HEALTH_ROW * 40) + 8,x
            inx
            bne draw_health_label
draw_health_label_done:

            lda #$1A
            sta SCREEN + (TRACK1_ROW * 40)
            lda #TRACK1_NOTE_COL
            sta COLRAM + (TRACK1_ROW * 40)

            lda #$18
            sta SCREEN + (TRACK2_ROW * 40)
            lda #TRACK2_NOTE_COL
            sta COLRAM + (TRACK2_ROW * 40)

            lda #$03
            sta SCREEN + (TRACK3_ROW * 40)
            lda #TRACK3_NOTE_COL
            sta COLRAM + (TRACK3_ROW * 40)

            rts

score_label:
            !scr "score:"
            !byte 0

miss_label:
            !scr "miss:"
            !byte 0

title_text:
            !scr "sid symphony"
            !byte 0

health_label:
            !scr "health:"
            !byte 0

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

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

            lda #$0F
            sta SID_VOLUME

            lda #$00
            sta SID_V1_FREQ_LO
            lda #VOICE1_FREQ
            sta SID_V1_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V1_PWHI
            lda #VOICE_AD
            sta SID_V1_AD
            lda #VOICE_SR
            sta SID_V1_SR

            lda #$00
            sta SID_V2_FREQ_LO
            lda #VOICE2_FREQ
            sta SID_V2_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V2_PWHI
            lda #VOICE_AD
            sta SID_V2_AD
            lda #VOICE_SR
            sta SID_V2_SR

            lda #$00
            sta SID_V3_FREQ_LO
            lda #VOICE3_FREQ
            sta SID_V3_FREQ_HI
            lda #PULSE_WIDTH
            sta SID_V3_PWHI
            lda #VOICE_AD
            sta SID_V3_AD
            lda #VOICE_SR
            sta SID_V3_SR

            rts

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

reset_track_colours:
            ldx #0
            lda #TRACK_LINE_COL
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

            lda #TRACK1_NOTE_COL
            sta COLRAM + (TRACK1_ROW * 40)
            lda #TRACK2_NOTE_COL
            sta COLRAM + (TRACK2_ROW * 40)
            lda #TRACK3_NOTE_COL
            sta COLRAM + (TRACK3_ROW * 40)

            lda #HIT_ZONE_COL
            sta COLRAM + (TRACK1_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK2_ROW * 40) + HIT_ZONE_COLUMN
            sta COLRAM + (TRACK3_ROW * 40) + HIT_ZONE_COLUMN

            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

; ----------------------------------------------------------------------------
; Update Border Flash
; ----------------------------------------------------------------------------

update_border_flash:
            lda border_flash
            beq flash_done
            dec border_flash
            bne flash_done
            lda #BORDER_COL
            sta BORDER
flash_done:
            rts

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

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

            lda #1
            sta key_pressed
            jsr check_hit
            bcc check_x_key
            jsr play_voice1_note
            jsr flash_track1_hit
            jsr award_points

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

            lda #2
            sta key_pressed
            jsr check_hit
            bcc check_c_key
            jsr play_voice2_note
            jsr flash_track2_hit
            jsr award_points

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

            lda #3
            sta key_pressed
            jsr check_hit
            bcc check_keys_done
            jsr play_voice3_note
            jsr flash_track3_hit
            jsr award_points

check_keys_done:
            lda #$FF
            sta CIA1_PRA
            rts

; ----------------------------------------------------------------------------
; Check Hit - Now stores note frequency for playback
; ----------------------------------------------------------------------------

check_hit:
            ldx #0

check_hit_loop:
            lda note_track,x
            beq check_hit_next

            cmp key_pressed
            bne check_hit_next

            lda note_col,x
            cmp #HIT_ZONE_MIN
            bcc check_hit_next
            cmp #HIT_ZONE_MAX+1
            bcs check_hit_next

            ; Store note frequency for sound playback
            lda note_freq,x
            sta hit_note_freq

            cmp #HIT_ZONE_CENTRE
            bcc hit_good
            cmp #HIT_ZONE_CENTRE+2
            bcs hit_good

            lda #2
            sta hit_quality
            jmp hit_found

hit_good:
            lda #1
            sta hit_quality

hit_found:
            jsr erase_note
            lda #0
            sta note_track,x
            sec
            rts

check_hit_next:
            inx
            cpx #MAX_NOTES
            bne check_hit_loop

            lda #0
            sta hit_quality
            clc
            rts

; ----------------------------------------------------------------------------
; Award Points
; ----------------------------------------------------------------------------

award_points:
            lda hit_quality
            cmp #2
            beq award_perfect

            lda score_lo
            clc
            adc #GOOD_SCORE
            sta score_lo
            lda score_hi
            adc #0
            sta score_hi

            lda #GOOD_COL
            sta BORDER
            lda #4
            sta border_flash

            lda #HEALTH_GOOD
            jsr increase_health

            jmp award_done

award_perfect:
            lda score_lo
            clc
            adc #PERFECT_SCORE
            sta score_lo
            lda score_hi
            adc #0
            sta score_hi

            lda #PERFECT_COL
            sta BORDER
            lda #6
            sta border_flash

            lda #HEALTH_PERFECT
            jsr increase_health

award_done:
            jsr display_score
            rts

; ----------------------------------------------------------------------------
; Display Score
; ----------------------------------------------------------------------------

display_score:
            lda score_lo
            sta work_lo
            lda score_hi
            sta work_hi

            ldx #0
div_10000:
            lda work_lo
            sec
            sbc #<10000
            tay
            lda work_hi
            sbc #>10000
            bcc done_10000
            sta work_hi
            sty work_lo
            inx
            jmp div_10000
done_10000:
            txa
            ora #$30
            sta SCREEN + 8

            ldx #0
div_1000:
            lda work_lo
            sec
            sbc #<1000
            tay
            lda work_hi
            sbc #>1000
            bcc done_1000
            sta work_hi
            sty work_lo
            inx
            jmp div_1000
done_1000:
            txa
            ora #$30
            sta SCREEN + 9

            ldx #0
div_100:
            lda work_lo
            sec
            sbc #100
            bcc done_100
            sta work_lo
            inx
            jmp div_100
done_100:
            txa
            ora #$30
            sta SCREEN + 10

            ldx #0
div_10:
            lda work_lo
            sec
            sbc #10
            bcc done_10
            sta work_lo
            inx
            jmp div_10
done_10:
            txa
            ora #$30
            sta SCREEN + 11

            lda work_lo
            ora #$30
            sta SCREEN + 12

            lda #7
            sta COLRAM + 8
            sta COLRAM + 9
            sta COLRAM + 10
            sta COLRAM + 11
            sta COLRAM + 12

            rts

; ----------------------------------------------------------------------------
; Display Misses
; ----------------------------------------------------------------------------

display_misses:
            lda miss_count

            ldx #0
miss_div_10:
            cmp #10
            bcc miss_done_10
            sec
            sbc #10
            inx
            jmp miss_div_10
miss_done_10:
            pha
            txa
            ora #$30
            sta SCREEN + 21
            pla
            ora #$30
            sta SCREEN + 22

            lda #2
            sta COLRAM + 21
            sta COLRAM + 22

            rts

work_lo:    !byte 0
work_hi:    !byte 0

; ----------------------------------------------------------------------------
; Play Voices With Note Frequency
; ----------------------------------------------------------------------------

play_voice1_note:
            lda #0
            sta SID_V1_FREQ_LO
            lda hit_note_freq
            sta SID_V1_FREQ_HI
            lda #VOICE1_WAVE
            ora #$01
            sta SID_V1_CTRL
            rts

play_voice2_note:
            lda #0
            sta SID_V2_FREQ_LO
            lda hit_note_freq
            sta SID_V2_FREQ_HI
            lda #VOICE2_WAVE
            ora #$01
            sta SID_V2_CTRL
            rts

play_voice3_note:
            lda #0
            sta SID_V3_FREQ_LO
            lda hit_note_freq
            sta SID_V3_FREQ_HI
            lda #VOICE3_WAVE
            ora #$01
            sta SID_V3_CTRL
            rts

; ----------------------------------------------------------------------------
; Flash Tracks on Hit
; ----------------------------------------------------------------------------

flash_track1_hit:
            ldx #0
            lda #HIT_COL
flash_t1h_loop:
            sta COLRAM + (TRACK1_ROW * 40),x
            inx
            cpx #38
            bne flash_t1h_loop
            lda #1
            sta COLRAM + (TRACK1_ROW * 40)
            rts

flash_track2_hit:
            ldx #0
            lda #HIT_COL
flash_t2h_loop:
            sta COLRAM + (TRACK2_ROW * 40),x
            inx
            cpx #38
            bne flash_t2h_loop
            lda #1
            sta COLRAM + (TRACK2_ROW * 40)
            rts

flash_track3_hit:
            ldx #0
            lda #HIT_COL
flash_t3h_loop:
            sta COLRAM + (TRACK3_ROW * 40),x
            inx
            cpx #38
            bne flash_t3h_loop
            lda #1
            sta COLRAM + (TRACK3_ROW * 40)
            rts

; ============================================================================
; SONG DATA
; ============================================================================
; Format: beat, track, note_freq_hi
;
; Note frequency table (high byte only for simplicity):
; C3=$11  D3=$13  E3=$16  F3=$17  G3=$1A  A3=$1D  B3=$21
; C4=$23  D4=$27  E4=$2C  F4=$2F  G4=$35  A4=$3B  B4=$42
; C5=$47  D5=$4F  E5=$58  F5=$5E  G5=$6A  A5=$77  B5=$85
;
; Track 1 (Z) = high notes, sawtooth
; Track 2 (X) = mid notes, pulse
; Track 3 (C) = low notes, triangle
; ============================================================================

song_data:
            ; Bar 1 - Opening phrase
            !byte 0, 1, $47     ; C5 - high
            !byte 2, 2, $2C     ; E4 - mid
            !byte 4, 3, $11     ; C3 - low

            ; Bar 2 - Descending
            !byte 8, 1, $3B     ; A4
            !byte 10, 2, $27    ; D4
            !byte 12, 3, $13    ; D3

            ; Bar 3 - Building
            !byte 16, 1, $35    ; G4
            !byte 17, 2, $2C    ; E4
            !byte 18, 1, $3B    ; A4
            !byte 20, 3, $16    ; E3

            ; Bar 4 - Resolution
            !byte 24, 1, $47    ; C5
            !byte 26, 2, $35    ; G4
            !byte 28, 3, $11    ; C3

            ; Bar 5 - New phrase
            !byte 32, 2, $2F    ; F4
            !byte 34, 1, $4F    ; D5
            !byte 36, 3, $17    ; F3

            ; Bar 6 - Variation
            !byte 40, 1, $58    ; E5
            !byte 42, 2, $2C    ; E4
            !byte 44, 3, $11    ; C3
            !byte 46, 2, $27    ; D4

            ; Bar 7 - Climax approach
            !byte 48, 1, $6A    ; G5
            !byte 49, 2, $35    ; G4
            !byte 50, 1, $58    ; E5
            !byte 52, 3, $1A    ; G3
            !byte 54, 2, $2F    ; F4

            ; Bar 8 - Resolution/Loop point
            !byte 56, 1, $47    ; C5
            !byte 58, 2, $2C    ; E4
            !byte 60, 3, $11    ; C3
            !byte 62, 1, $35    ; G4

            !byte $FF           ; End of song

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

note_track:
            !fill MAX_NOTES, 0

note_col:
            !fill MAX_NOTES, 0

note_freq:
            !fill MAX_NOTES, 0  ; Note frequency for each active note

; ----------------------------------------------------------------------------
; Game Variables
; ----------------------------------------------------------------------------

score_lo:   !byte 0
score_hi:   !byte 0
miss_count: !byte 0
health:     !byte 0

Try This: Compose Your Own

Create a different melody:

song_data:
            ; Simple ascending scale
            !byte 0, 3, $11     ; C3
            !byte 4, 3, $13     ; D3
            !byte 8, 3, $16     ; E3
            !byte 12, 3, $17    ; F3
            !byte 16, 2, $1A    ; G3 (switch to mid track)
            !byte 20, 2, $1D    ; A3
            !byte 24, 1, $21    ; B3 (switch to high track)
            !byte 28, 1, $23    ; C4

            !byte $FF

Experiment with different patterns. What makes a satisfying rhythm?

Try This: Denser Patterns

Add more notes for a harder challenge:

            ; Rapid alternation
            !byte 0, 1, $47
            !byte 1, 2, $2C
            !byte 2, 3, $11
            !byte 3, 1, $47
            !byte 4, 2, $2C
            !byte 5, 3, $11
            ; ...

Notes on every beat create intense gameplay.

What You’ve Learnt

  • Data-driven design - Song structure defined in data, not code
  • SID frequencies - Note values for the sound chip
  • Three-byte entries - Expandable format for future features
  • Musical structure - Bars, phrases, and progression

What’s Next

In Unit 10, we’ll add song progress tracking. The game will show how far through the song you are and handle the song ending properly - either victory or game over.

What Changed

Unit 8 → Unit 9
+136-92
11 ; ============================================================================
2-; SID SYMPHONY - Unit 8: Health System
2+; SID SYMPHONY - Unit 9: Song Data
33 ; ============================================================================
4-; A health meter that grows on hits and shrinks on misses. The game can be
5-; lost. Stakes make success meaningful.
4+; Design the song data format. Create the first complete musical pattern.
5+; Notes now carry pitch information - each hit plays its actual note.
66 ;
77 ; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
88 ; ============================================================================
...
1111 ; CUSTOMISATION SECTION
1212 ; ============================================================================
1313
14-; SID Voice Settings
14+; SID Voice Settings (default for when no note playing)
1515 VOICE1_WAVE = $21 ; Sawtooth
1616 VOICE2_WAVE = $41 ; Pulse
1717 VOICE3_WAVE = $11 ; Triangle
1818
19+; Default frequencies (used if no note data)
1920 VOICE1_FREQ = $1C ; High pitch
2021 VOICE2_FREQ = $0E ; Mid pitch
2122 VOICE3_FREQ = $07 ; Low pitch
...
4041
4142 TRACK_LINE_COL = 11 ; Dark grey
4243 HIT_ZONE_COL = 7 ; Yellow
43-
44-FLASH1_COL = 2 ; Red
45-FLASH2_COL = 5 ; Green
46-FLASH3_COL = 6 ; Blue
4744
4845 HIT_COL = 1 ; White - flash on successful hit
4946 PERFECT_COL = 1 ; White - perfect hit border flash
...
6360 ; HEALTH SETTINGS
6461 ; ============================================================================
6562
66-HEALTH_MAX = 64 ; Maximum health
67-HEALTH_START = 32 ; Starting health (half)
68-HEALTH_PERFECT = 4 ; Health gained on perfect hit
69-HEALTH_GOOD = 2 ; Health gained on good hit
70-HEALTH_MISS = 8 ; Health lost on miss
63+HEALTH_MAX = 64
64+HEALTH_START = 32
65+HEALTH_PERFECT = 4
66+HEALTH_GOOD = 2
67+HEALTH_MISS = 8
7168
7269 ; ============================================================================
7370 ; HIT DETECTION SETTINGS
...
7673 HIT_ZONE_MIN = 2
7774 HIT_ZONE_MAX = 5
7875 HIT_ZONE_CENTRE = 3
76+
77+; ============================================================================
78+; SONG SETTINGS
79+; ============================================================================
80+
81+SONG_LENGTH = 64 ; Beats in the song
7982
8083 ; ============================================================================
8184 ; MEMORY MAP
...
158161 hit_quality = $08
159162 border_flash = $09
160163 miss_track = $0A
161-game_over = $0B ; Non-zero when game ended
164+game_over = $0B
165+hit_note_freq = $0C ; Frequency of note being hit
162166
163167 ; ----------------------------------------------------------------------------
164168 ; BASIC Stub
...
204208 cmp $D012
205209 bne wait_raster
206210
207- ; Check if game is over
208211 lda game_over
209- bne main_loop ; Freeze if game over
212+ bne main_loop
210213
211214 inc frame_count
212215 lda frame_count
...
276279 ; ----------------------------------------------------------------------------
277280
278281 define_custom_chars:
279- ; Note character - diamond shape
280282 lda #%00000110
281283 sta CHARSET + (CHAR_NOTE * 8) + 0
282284 lda #%00011110
...
294296 lda #%00000110
295297 sta CHARSET + (CHAR_NOTE * 8) + 7
296298
297- ; Track character - horizontal line
298299 lda #%00000000
299300 sta CHARSET + (CHAR_TRACK * 8) + 0
300301 lda #%00000000
...
312313 lda #%00000000
313314 sta CHARSET + (CHAR_TRACK * 8) + 7
314315
315- ; Hit zone character - vertical bars
316316 lda #%01100110
317317 sta CHARSET + (CHAR_HITZONE * 8) + 0
318318 sta CHARSET + (CHAR_HITZONE * 8) + 1
...
323323 sta CHARSET + (CHAR_HITZONE * 8) + 6
324324 sta CHARSET + (CHAR_HITZONE * 8) + 7
325325
326- ; Health bar full - solid block
327326 lda #%11111111
328327 sta CHARSET + (CHAR_BAR_FULL * 8) + 0
329328 sta CHARSET + (CHAR_BAR_FULL * 8) + 1
...
334333 sta CHARSET + (CHAR_BAR_FULL * 8) + 6
335334 sta CHARSET + (CHAR_BAR_FULL * 8) + 7
336335
337- ; Health bar empty - outline
338336 lda #%11111111
339337 sta CHARSET + (CHAR_BAR_EMPTY * 8) + 0
340338 lda #%10000001
...
382380 init_notes_loop:
383381 sta note_track,x
384382 sta note_col,x
383+ sta note_freq,x ; Also clear note frequencies
385384 dex
386385 bpl init_notes_loop
387386 rts
388387
389388 ; ----------------------------------------------------------------------------
390-; Check Spawn Note
389+; Check Spawn Note - Now reads 3-byte song entries
391390 ; ----------------------------------------------------------------------------
392391
393392 check_spawn_note:
...
406405
407406 spawn_match:
408407 iny
409- lda (song_pos),y
410- jsr spawn_note
408+ lda (song_pos),y ; Track number
409+ sta temp_track
410+ iny
411+ lda (song_pos),y ; Note frequency
412+ pha
413+ lda temp_track
414+ jsr spawn_note_with_freq
415+ pla ; Clean up stack
416+ dey
411417 dey
412418
413419 spawn_advance:
414420 lda song_pos
415421 clc
416- adc #2
422+ adc #3 ; 3 bytes per entry now
417423 sta song_pos
418424 lda song_pos_hi
419425 adc #0
...
433439 rts
434440
435441 ; ----------------------------------------------------------------------------
436-; Spawn Note
442+; Spawn Note With Frequency
443+; Input: A = track, stack has frequency
437444 ; ----------------------------------------------------------------------------
438445
439-spawn_note:
446+spawn_note_with_freq:
440447 sta temp_track
441448
442449 ldx #0
...
453460 sta note_track,x
454461 lda #NOTE_SPAWN_COL
455462 sta note_col,x
463+
464+ ; Get frequency from stack (caller pushed it)
465+ tsx
466+ lda $0103,x ; Get frequency from stack
467+ sta note_freq,x
468+
456469 jsr draw_note
457470 rts
458471
...
505518 sta border_flash
506519
507520 jsr display_misses
508-
509- ; Decrease health
510521 jsr decrease_health
511522
512523 rts
513524
514525 ; ----------------------------------------------------------------------------
515-; Decrease Health - Subtract HEALTH_MISS, clamp at 0
526+; Decrease Health
516527 ; ----------------------------------------------------------------------------
517528
518529 decrease_health:
519530 lda health
520531 sec
521532 sbc #HEALTH_MISS
522- bcc health_zero ; Underflow - set to 0
533+ bcc health_zero
523534 sta health
524535 jsr display_health
525536 jsr check_game_over
...
533544 rts
534545
535546 ; ----------------------------------------------------------------------------
536-; Increase Health - Add amount, clamp at HEALTH_MAX
537-; Input: A = amount to add
547+; Increase Health
538548 ; ----------------------------------------------------------------------------
539549
540550 increase_health:
...
542552 adc health
543553 cmp #HEALTH_MAX
544554 bcc health_ok
545- lda #HEALTH_MAX ; Clamp to max
555+ lda #HEALTH_MAX
546556 health_ok:
547557 sta health
548558 jsr display_health
...
556566 lda health
557567 bne not_game_over
558568
559- ; Game over!
560569 lda #1
561570 sta game_over
562-
563- ; Display game over message
564571 jsr show_game_over
565572
566573 not_game_over:
...
571578 ; ----------------------------------------------------------------------------
572579
573580 show_game_over:
574- ; Display "GAME OVER" in centre of screen
575581 ldx #0
576582 game_over_loop:
577583 lda game_over_text,x
578584 beq game_over_done
579585 sta SCREEN + (12 * 40) + 15,x
580- lda #2 ; Red
586+ lda #2
581587 sta COLRAM + (12 * 40) + 15,x
582588 inx
583589 jmp game_over_loop
584590 game_over_done:
585-
586- ; Flash border red
587591 lda #2
588592 sta BORDER
589-
590593 rts
591594
592595 game_over_text:
...
594597 !byte 0
595598
596599 ; ----------------------------------------------------------------------------
597-; Display Health - 8 character bar
600+; Display Health
598601 ; ----------------------------------------------------------------------------
599602
600603 display_health:
601- ; Health ranges 0-64, displayed as 8 characters
602- ; Each character represents 8 health points
603-
604604 lda health
605- lsr ; Divide by 8
606605 lsr
607606 lsr
608- sta temp_health ; Full bars to draw
607+ lsr
608+ sta temp_health
609609
610- ; Draw full bars
611610 ldx #0
612611 lda temp_health
613612 beq draw_empty_bars
...
621620 cpx temp_health
622621 bne draw_full_bars
623622
624- ; Draw empty bars for remainder
625623 draw_empty_bars:
626624 cpx #8
627625 beq health_done
628626 lda #CHAR_BAR_EMPTY
629627 sta SCREEN + (HEALTH_ROW * 40) + 16,x
630- lda #11 ; Dark grey
628+ lda #11
631629 sta COLRAM + (HEALTH_ROW * 40) + 16,x
632630 inx
633631 jmp draw_empty_bars
...
937935 ; ----------------------------------------------------------------------------
938936
939937 draw_labels:
940- ; Draw "SCORE:" label
941938 ldx #0
942939 draw_score_label:
943940 lda score_label,x
...
949946 bne draw_score_label
950947 draw_score_label_done:
951948
952- ; Draw "MISS:" label
953949 ldx #0
954950 draw_miss_label:
955951 lda miss_label,x
...
961957 bne draw_miss_label
962958 draw_miss_label_done:
963959
964- ; Draw title
965960 ldx #0
966961 draw_title:
967962 lda title_text,x
...
973968 bne draw_title
974969 draw_title_done:
975970
976- ; Draw "HEALTH:" label
977971 ldx #0
978972 draw_health_label:
979973 lda health_label,x
980974 beq draw_health_label_done
981975 sta SCREEN + (HEALTH_ROW * 40) + 8,x
982- lda #5 ; Green
976+ lda #5
983977 sta COLRAM + (HEALTH_ROW * 40) + 8,x
984978 inx
985979 bne draw_health_label
986980 draw_health_label_done:
987981
988- ; Track labels
989- lda #$1A ; Z
982+ lda #$1A
990983 sta SCREEN + (TRACK1_ROW * 40)
991984 lda #TRACK1_NOTE_COL
992985 sta COLRAM + (TRACK1_ROW * 40)
993986
994- lda #$18 ; X
987+ lda #$18
995988 sta SCREEN + (TRACK2_ROW * 40)
996989 lda #TRACK2_NOTE_COL
997990 sta COLRAM + (TRACK2_ROW * 40)
998991
999- lda #$03 ; C
992+ lda #$03
1000993 sta SCREEN + (TRACK3_ROW * 40)
1001994 lda #TRACK3_NOTE_COL
1002995 sta COLRAM + (TRACK3_ROW * 40)
...
11571150 sta key_pressed
11581151 jsr check_hit
11591152 bcc check_x_key
1160- jsr play_voice1
1153+ jsr play_voice1_note
11611154 jsr flash_track1_hit
11621155 jsr award_points
11631156
...
11721165 sta key_pressed
11731166 jsr check_hit
11741167 bcc check_c_key
1175- jsr play_voice2
1168+ jsr play_voice2_note
11761169 jsr flash_track2_hit
11771170 jsr award_points
11781171
...
11871180 sta key_pressed
11881181 jsr check_hit
11891182 bcc check_keys_done
1190- jsr play_voice3
1183+ jsr play_voice3_note
11911184 jsr flash_track3_hit
11921185 jsr award_points
11931186
...
11971190 rts
11981191
11991192 ; ----------------------------------------------------------------------------
1200-; Check Hit
1193+; Check Hit - Now stores note frequency for playback
12011194 ; ----------------------------------------------------------------------------
12021195
12031196 check_hit:
...
12151208 bcc check_hit_next
12161209 cmp #HIT_ZONE_MAX+1
12171210 bcs check_hit_next
1211+
1212+ ; Store note frequency for sound playback
1213+ lda note_freq,x
1214+ sta hit_note_freq
12181215
12191216 cmp #HIT_ZONE_CENTRE
12201217 bcc hit_good
...
12681265 lda #4
12691266 sta border_flash
12701267
1271- ; Increase health for good hit
12721268 lda #HEALTH_GOOD
12731269 jsr increase_health
12741270
...
12881284 lda #6
12891285 sta border_flash
12901286
1291- ; Increase health for perfect hit
12921287 lda #HEALTH_PERFECT
12931288 jsr increase_health
12941289
...
14171412 work_hi: !byte 0
14181413
14191414 ; ----------------------------------------------------------------------------
1420-; Play Voices
1415+; Play Voices With Note Frequency
14211416 ; ----------------------------------------------------------------------------
14221417
1423-play_voice1:
1418+play_voice1_note:
1419+ lda #0
1420+ sta SID_V1_FREQ_LO
1421+ lda hit_note_freq
1422+ sta SID_V1_FREQ_HI
14241423 lda #VOICE1_WAVE
14251424 ora #$01
14261425 sta SID_V1_CTRL
14271426 rts
14281427
1429-play_voice2:
1428+play_voice2_note:
1429+ lda #0
1430+ sta SID_V2_FREQ_LO
1431+ lda hit_note_freq
1432+ sta SID_V2_FREQ_HI
14301433 lda #VOICE2_WAVE
14311434 ora #$01
14321435 sta SID_V2_CTRL
14331436 rts
14341437
1435-play_voice3:
1438+play_voice3_note:
1439+ lda #0
1440+ sta SID_V3_FREQ_LO
1441+ lda hit_note_freq
1442+ sta SID_V3_FREQ_HI
14361443 lda #VOICE3_WAVE
14371444 ora #$01
14381445 sta SID_V3_CTRL
...
14781485 sta COLRAM + (TRACK3_ROW * 40)
14791486 rts
14801487
1481-; ----------------------------------------------------------------------------
1482-; Song Data
1483-; ----------------------------------------------------------------------------
1488+; ============================================================================
1489+; SONG DATA
1490+; ============================================================================
1491+; Format: beat, track, note_freq_hi
1492+;
1493+; Note frequency table (high byte only for simplicity):
1494+; C3=$11 D3=$13 E3=$16 F3=$17 G3=$1A A3=$1D B3=$21
1495+; C4=$23 D4=$27 E4=$2C F4=$2F G4=$35 A4=$3B B4=$42
1496+; C5=$47 D5=$4F E5=$58 F5=$5E G5=$6A A5=$77 B5=$85
1497+;
1498+; Track 1 (Z) = high notes, sawtooth
1499+; Track 2 (X) = mid notes, pulse
1500+; Track 3 (C) = low notes, triangle
1501+; ============================================================================
14841502
14851503 song_data:
1486- !byte 0, 1
1487- !byte 2, 2
1488- !byte 4, 3
1489- !byte 6, 1
1504+ ; Bar 1 - Opening phrase
1505+ !byte 0, 1, $47 ; C5 - high
1506+ !byte 2, 2, $2C ; E4 - mid
1507+ !byte 4, 3, $11 ; C3 - low
14901508
1491- !byte 8, 2
1492- !byte 10, 3
1493- !byte 12, 1
1494- !byte 14, 2
1509+ ; Bar 2 - Descending
1510+ !byte 8, 1, $3B ; A4
1511+ !byte 10, 2, $27 ; D4
1512+ !byte 12, 3, $13 ; D3
14951513
1496- !byte 16, 3
1497- !byte 18, 1
1498- !byte 20, 2
1499- !byte 22, 3
1514+ ; Bar 3 - Building
1515+ !byte 16, 1, $35 ; G4
1516+ !byte 17, 2, $2C ; E4
1517+ !byte 18, 1, $3B ; A4
1518+ !byte 20, 3, $16 ; E3
15001519
1501- !byte 24, 1
1502- !byte 25, 2
1503- !byte 26, 3
1504- !byte 28, 1
1505- !byte 29, 2
1506- !byte 30, 3
1520+ ; Bar 4 - Resolution
1521+ !byte 24, 1, $47 ; C5
1522+ !byte 26, 2, $35 ; G4
1523+ !byte 28, 3, $11 ; C3
15071524
1508- !byte $FF
1525+ ; Bar 5 - New phrase
1526+ !byte 32, 2, $2F ; F4
1527+ !byte 34, 1, $4F ; D5
1528+ !byte 36, 3, $17 ; F3
1529+
1530+ ; Bar 6 - Variation
1531+ !byte 40, 1, $58 ; E5
1532+ !byte 42, 2, $2C ; E4
1533+ !byte 44, 3, $11 ; C3
1534+ !byte 46, 2, $27 ; D4
1535+
1536+ ; Bar 7 - Climax approach
1537+ !byte 48, 1, $6A ; G5
1538+ !byte 49, 2, $35 ; G4
1539+ !byte 50, 1, $58 ; E5
1540+ !byte 52, 3, $1A ; G3
1541+ !byte 54, 2, $2F ; F4
1542+
1543+ ; Bar 8 - Resolution/Loop point
1544+ !byte 56, 1, $47 ; C5
1545+ !byte 58, 2, $2C ; E4
1546+ !byte 60, 3, $11 ; C3
1547+ !byte 62, 1, $35 ; G4
1548+
1549+ !byte $FF ; End of song
15091550
15101551 ; ----------------------------------------------------------------------------
15111552 ; Note Arrays
...
15161557
15171558 note_col:
15181559 !fill MAX_NOTES, 0
1560+
1561+note_freq:
1562+ !fill MAX_NOTES, 0 ; Note frequency for each active note
15191563
15201564 ; ----------------------------------------------------------------------------
15211565 ; Game Variables