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

Song Playback

Play the song from data. Progress bar shows how far through. The rhythm feels right.

16% of SID Symphony

Players need to know where they are in the song. This unit adds progress tracking.

A progress bar shows how far through the song the player has reached. Each beat advances the bar. Players can see the end approaching. It creates anticipation - “just a bit more” or “almost there.”

Run It

Assemble and run:

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

Unit 10 Screenshot

Watch the cyan progress bar at the bottom. It fills as the song plays. The health bar above it shows your performance. Two measures of progress - through the song and through survival.

Song progress bar fills as the song plays

Tracking Song Position

A new variable tracks the current beat within the song:

; ----------------------------------------------------------------------------
; Song Variables
; ----------------------------------------------------------------------------

SONG_LENGTH   = 64              ; Total beats in the song
PROGRESS_WIDTH = 16             ; Width of progress bar in characters

song_beat     = $0D             ; Current beat in song (0 to SONG_LENGTH-1)

; ----------------------------------------------------------------------------
; Advance Song - Increment beat and update progress
; ----------------------------------------------------------------------------

advance_song:
            inc beat_count
            inc song_beat

            ; Check if song has ended
            lda song_beat
            cmp #SONG_LENGTH
            bcc song_continues

            ; Song loops for now (Unit 11 will add proper end)
            lda #0
            sta song_beat
            sta beat_count
            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi

song_continues:
            jsr display_progress
            rts

The song_beat variable counts from 0 to 63 (SONG_LENGTH minus 1). Each beat increments it. When it reaches SONG_LENGTH, the song loops back to the start.

The Progress Bar

The progress bar converts song position to visual representation:

; ----------------------------------------------------------------------------
; Display Progress - Show song progress bar
; ----------------------------------------------------------------------------

display_progress:
            ; Calculate progress: (song_beat * PROGRESS_WIDTH) / SONG_LENGTH
            ; For 64-beat song with 16-char bar: song_beat / 4

            lda song_beat
            lsr                 ; Divide by 4
            lsr
            sta temp_progress

            ; Draw filled portion
            ldx #0
            lda temp_progress
            beq draw_empty_progress

draw_full_progress:
            lda #CHAR_BAR_FULL
            sta SCREEN + (PROGRESS_ROW * 40) + 12,x
            lda #PROGRESS_COL
            sta COLRAM + (PROGRESS_ROW * 40) + 12,x
            inx
            cpx temp_progress
            bne draw_full_progress

draw_empty_progress:
            cpx #PROGRESS_WIDTH
            beq progress_done
            lda #CHAR_BAR_EMPTY
            sta SCREEN + (PROGRESS_ROW * 40) + 12,x
            lda #11             ; Dark grey
            sta COLRAM + (PROGRESS_ROW * 40) + 12,x
            inx
            jmp draw_empty_progress

progress_done:
            rts

temp_progress: !byte 0

The calculation uses bit shifting for division. With a 64-beat song and 16-character bar, each character represents 4 beats. Two LSR instructions divide by 4. The result tells us how many filled blocks to draw.

Why Progress Matters

Progress creates emotional engagement:

  • Early song - Full bar ahead feels daunting
  • Midway - Momentum builds, player feels committed
  • Near end - Tension peaks, “almost there” excitement
  • Complete - Satisfaction of finishing

Without progress indication, players don’t know if they’re at beat 10 or beat 50. The bar transforms an abstract challenge into a visible journey.

Layout Considerations

The screen now shows two bars:

Row 23: HEALTH: [########........]
Row 24: SONG:   [####............]

Health shows performance quality. Progress shows song position. Two complementary pieces of information. The player knows both “how am I doing?” and “how far to go?”

The Main Loop

The main loop now calls advance_song each beat:

no_new_beat:
            ; ...
            jmp main_loop

; Called once per beat
advance_song:
            inc beat_count
            inc song_beat
            ; Check for song end
            ; Update progress display

The beat counter drives everything - note spawning, song position, progress display. One timing system controls all game events.

The Complete Code

; ============================================================================
; SID SYMPHONY - Unit 10: Song Playback
; ============================================================================
; Song plays with proper timing. Progress bar shows how far through the song.
; Notes spawn at exactly the right beat. The rhythm feels right.
;
; Controls: Z = Track 1 (high), X = Track 2 (mid), C = Track 3 (low)
; ============================================================================

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

; SID Voice Settings
VOICE1_WAVE = $21               ; Sawtooth
VOICE2_WAVE = $41               ; Pulse
VOICE3_WAVE = $11               ; Triangle

VOICE1_FREQ = $1C
VOICE2_FREQ = $0E
VOICE3_FREQ = $07

VOICE_AD    = $09
VOICE_SR    = $00
PULSE_WIDTH = $08

; Miss sound settings
MISS_FREQ   = $08
MISS_WAVE   = $81
MISS_AD     = $00
MISS_SR     = $90

; Visual Settings
BORDER_COL  = 0
BG_COL      = 0

TRACK1_NOTE_COL = 10
TRACK2_NOTE_COL = 13
TRACK3_NOTE_COL = 14

TRACK_LINE_COL = 11
HIT_ZONE_COL = 7

HIT_COL     = 1
PERFECT_COL = 1
GOOD_COL    = 7
MISS_COL    = 2

HEALTH_COL  = 5
PROGRESS_COL = 3                ; Cyan for progress 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              ; Total beats in the song
PROGRESS_WIDTH = 16             ; Width of progress bar in characters

; ============================================================================
; 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

; HUD positions
HEALTH_ROW  = 23
PROGRESS_ROW = 24

; 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
song_beat   = $0D               ; Current beat in song (0 to SONG_LENGTH-1)

; ----------------------------------------------------------------------------
; 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
            jsr init_song

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 advance_song
            jsr check_spawn_note

no_new_beat:
            jsr update_notes
            jsr reset_track_colours
            jsr update_border_flash
            jsr check_keys

            jmp main_loop

; ----------------------------------------------------------------------------
; Initialize Song
; ----------------------------------------------------------------------------

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

            lda #0
            sta frame_count
            sta beat_count
            sta song_beat
            sta border_flash
            sta game_over

            jsr display_progress
            rts

; ----------------------------------------------------------------------------
; Advance Song - Increment beat and update progress
; ----------------------------------------------------------------------------

advance_song:
            inc beat_count
            inc song_beat

            ; Check if song has ended
            lda song_beat
            cmp #SONG_LENGTH
            bcc song_continues

            ; Song loops for now (Unit 11 will add proper end)
            lda #0
            sta song_beat
            sta beat_count
            lda #<song_data
            sta song_pos
            lda #>song_data
            sta song_pos_hi

song_continues:
            jsr display_progress
            rts

; ----------------------------------------------------------------------------
; Display Progress - Show song progress bar
; ----------------------------------------------------------------------------

display_progress:
            ; Calculate progress: (song_beat * PROGRESS_WIDTH) / SONG_LENGTH
            ; For 64-beat song with 16-char bar: song_beat / 4

            lda song_beat
            lsr                 ; Divide by 4
            lsr
            sta temp_progress

            ; Draw filled portion
            ldx #0
            lda temp_progress
            beq draw_empty_progress

draw_full_progress:
            lda #CHAR_BAR_FULL
            sta SCREEN + (PROGRESS_ROW * 40) + 12,x
            lda #PROGRESS_COL
            sta COLRAM + (PROGRESS_ROW * 40) + 12,x
            inx
            cpx temp_progress
            bne draw_full_progress

draw_empty_progress:
            cpx #PROGRESS_WIDTH
            beq progress_done
            lda #CHAR_BAR_EMPTY
            sta SCREEN + (PROGRESS_ROW * 40) + 12,x
            lda #11             ; Dark grey
            sta COLRAM + (PROGRESS_ROW * 40) + 12,x
            inx
            jmp draw_empty_progress

progress_done:
            rts

temp_progress: !byte 0

; ----------------------------------------------------------------------------
; 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
            sta CHARSET + (CHAR_TRACK * 8) + 1
            sta CHARSET + (CHAR_TRACK * 8) + 2
            lda #%11111111
            sta CHARSET + (CHAR_TRACK * 8) + 3
            sta CHARSET + (CHAR_TRACK * 8) + 4
            lda #%00000000
            sta CHARSET + (CHAR_TRACK * 8) + 5
            sta CHARSET + (CHAR_TRACK * 8) + 6
            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
            dex
            bpl init_notes_loop
            rts

; ----------------------------------------------------------------------------
; Check Spawn Note
; ----------------------------------------------------------------------------

check_spawn_note:
            ldy #0

spawn_check_loop:
            lda (song_pos),y
            cmp #$FF
            beq spawn_done      ; Don't auto-restart here

            cmp beat_count
            beq spawn_match
            bcs spawn_done

            jmp spawn_advance

spawn_match:
            iny
            lda (song_pos),y
            sta temp_track
            iny
            lda (song_pos),y
            pha
            lda temp_track
            jsr spawn_note_with_freq
            pla
            dey
            dey

spawn_advance:
            lda song_pos
            clc
            adc #3
            sta song_pos
            lda song_pos_hi
            adc #0
            sta song_pos_hi
            jmp spawn_check_loop

spawn_done:
            rts

; ----------------------------------------------------------------------------
; Spawn Note With 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

            tsx
            lda $0103,x
            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) + 12,x
            lda #HEALTH_COL
            sta COLRAM + (HEALTH_ROW * 40) + 12,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) + 12,x
            lda #11
            sta COLRAM + (HEALTH_ROW * 40) + 12,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

            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 #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) + 4,x
            lda #5
            sta COLRAM + (HEALTH_ROW * 40) + 4,x
            inx
            bne draw_health_label
draw_health_label_done:

            ldx #0
draw_progress_label:
            lda progress_label,x
            beq draw_progress_label_done
            sta SCREEN + (PROGRESS_ROW * 40) + 4,x
            lda #3
            sta COLRAM + (PROGRESS_ROW * 40) + 4,x
            inx
            bne draw_progress_label
draw_progress_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

progress_label:
            !scr "song:"
            !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
; ----------------------------------------------------------------------------

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

            lda note_freq,x
            sta hit_note_freq

            lda note_col,x
            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
; ============================================================================

song_data:
            ; Bar 1
            !byte 0, 1, $47
            !byte 2, 2, $2C
            !byte 4, 3, $11

            ; Bar 2
            !byte 8, 1, $3B
            !byte 10, 2, $27
            !byte 12, 3, $13

            ; Bar 3
            !byte 16, 1, $35
            !byte 17, 2, $2C
            !byte 18, 1, $3B
            !byte 20, 3, $16

            ; Bar 4
            !byte 24, 1, $47
            !byte 26, 2, $35
            !byte 28, 3, $11

            ; Bar 5
            !byte 32, 2, $2F
            !byte 34, 1, $4F
            !byte 36, 3, $17

            ; Bar 6
            !byte 40, 1, $58
            !byte 42, 2, $2C
            !byte 44, 3, $11
            !byte 46, 2, $27

            ; Bar 7
            !byte 48, 1, $6A
            !byte 49, 2, $35
            !byte 50, 1, $58
            !byte 52, 3, $1A
            !byte 54, 2, $2F

            ; Bar 8
            !byte 56, 1, $47
            !byte 58, 2, $2C
            !byte 60, 3, $11
            !byte 62, 1, $35

            !byte $FF

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

note_track:
            !fill MAX_NOTES, 0

note_col:
            !fill MAX_NOTES, 0

note_freq:
            !fill MAX_NOTES, 0

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

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

Try This: Different Bar Colours

Change the progress bar colour based on position:

display_progress:
            lda song_beat
            cmp #48             ; Last quarter
            bcs progress_yellow
            lda #3              ; Cyan
            jmp set_progress_col
progress_yellow:
            lda #7              ; Yellow when near end
set_progress_col:
            sta progress_colour

Yellow in the final stretch heightens tension.

Try This: Percentage Display

Add a numeric percentage:

            ; After updating bar
            lda song_beat
            ; Multiply by 100, divide by 64
            ; (or approximate: song_beat * 3 / 2)
            ; Display two digits

Numbers complement the visual bar.

What You’ve Learnt

  • Progress tracking - Counting beats through the song
  • Visual progress bars - Converting numbers to filled blocks
  • Player psychology - How progress indication affects engagement
  • Screen layout - Organising multiple status displays

What’s Next

In Unit 11, we’ll handle the song end properly. When the song finishes, the game shows results - final score, accuracy percentage, and whether you succeeded or failed. The loop becomes a complete experience.

What Changed

Unit 9 → Unit 10
+193-126
11 ; ============================================================================
2-; SID SYMPHONY - Unit 9: Song Data
2+; SID SYMPHONY - Unit 10: Song Playback
33 ; ============================================================================
4-; Design the song data format. Create the first complete musical pattern.
5-; Notes now carry pitch information - each hit plays its actual note.
4+; Song plays with proper timing. Progress bar shows how far through the song.
5+; Notes spawn at exactly the right beat. The rhythm feels right.
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 (default for when no note playing)
14+; SID Voice Settings
1515 VOICE1_WAVE = $21 ; Sawtooth
1616 VOICE2_WAVE = $41 ; Pulse
1717 VOICE3_WAVE = $11 ; Triangle
1818
19-; Default frequencies (used if no note data)
20-VOICE1_FREQ = $1C ; High pitch
21-VOICE2_FREQ = $0E ; Mid pitch
22-VOICE3_FREQ = $07 ; Low pitch
19+VOICE1_FREQ = $1C
20+VOICE2_FREQ = $0E
21+VOICE3_FREQ = $07
2322
24-VOICE_AD = $09 ; Attack=0, Decay=9
25-VOICE_SR = $00 ; Sustain=0, Release=0
26-PULSE_WIDTH = $08 ; 50% duty cycle
23+VOICE_AD = $09
24+VOICE_SR = $00
25+PULSE_WIDTH = $08
2726
2827 ; Miss sound settings
29-MISS_FREQ = $08 ; Low rumble
30-MISS_WAVE = $81 ; Noise waveform
31-MISS_AD = $00 ; Instant attack, no decay
32-MISS_SR = $90 ; High sustain, fast release
28+MISS_FREQ = $08
29+MISS_WAVE = $81
30+MISS_AD = $00
31+MISS_SR = $90
3332
3433 ; Visual Settings
35-BORDER_COL = 0 ; Black border
36-BG_COL = 0 ; Black background
34+BORDER_COL = 0
35+BG_COL = 0
3736
38-TRACK1_NOTE_COL = 10 ; Light red
39-TRACK2_NOTE_COL = 13 ; Light green
40-TRACK3_NOTE_COL = 14 ; Light blue
37+TRACK1_NOTE_COL = 10
38+TRACK2_NOTE_COL = 13
39+TRACK3_NOTE_COL = 14
4140
42-TRACK_LINE_COL = 11 ; Dark grey
43-HIT_ZONE_COL = 7 ; Yellow
41+TRACK_LINE_COL = 11
42+HIT_ZONE_COL = 7
4443
45-HIT_COL = 1 ; White - flash on successful hit
46-PERFECT_COL = 1 ; White - perfect hit border flash
47-GOOD_COL = 7 ; Yellow - good hit border flash
48-MISS_COL = 2 ; Red - miss border flash
44+HIT_COL = 1
45+PERFECT_COL = 1
46+GOOD_COL = 7
47+MISS_COL = 2
4948
50-HEALTH_COL = 5 ; Green for health bar
49+HEALTH_COL = 5
50+PROGRESS_COL = 3 ; Cyan for progress bar
5151
5252 ; ============================================================================
5353 ; SCORING SETTINGS
...
7878 ; SONG SETTINGS
7979 ; ============================================================================
8080
81-SONG_LENGTH = 64 ; Beats in the song
81+SONG_LENGTH = 64 ; Total beats in the song
82+PROGRESS_WIDTH = 16 ; Width of progress bar in characters
8283
8384 ; ============================================================================
8485 ; MEMORY MAP
...
126127 TRACK2_ROW = 12
127128 TRACK3_ROW = 16
128129
129-; Health bar position
130+; HUD positions
130131 HEALTH_ROW = 23
132+PROGRESS_ROW = 24
131133
132134 ; Hit zone
133135 HIT_ZONE_COLUMN = 3
...
162164 border_flash = $09
163165 miss_track = $0A
164166 game_over = $0B
165-hit_note_freq = $0C ; Frequency of note being hit
167+hit_note_freq = $0C
168+song_beat = $0D ; Current beat in song (0 to SONG_LENGTH-1)
166169
167170 ; ----------------------------------------------------------------------------
168171 ; BASIC Stub
...
190193 jsr init_notes
191194 jsr init_score
192195 jsr init_health
193-
194- lda #<song_data
195- sta song_pos
196- lda #>song_data
197- sta song_pos_hi
198-
199- lda #0
200- sta frame_count
201- sta beat_count
202- sta border_flash
203- sta game_over
196+ jsr init_song
204197
205198 main_loop:
206199 lda #$FF
...
218211
219212 lda #0
220213 sta frame_count
214+ jsr advance_song
221215 jsr check_spawn_note
222- inc beat_count
223216
224217 no_new_beat:
225218 jsr update_notes
...
228221 jsr check_keys
229222
230223 jmp main_loop
224+
225+; ----------------------------------------------------------------------------
226+; Initialize Song
227+; ----------------------------------------------------------------------------
228+
229+init_song:
230+ lda #<song_data
231+ sta song_pos
232+ lda #>song_data
233+ sta song_pos_hi
234+
235+ lda #0
236+ sta frame_count
237+ sta beat_count
238+ sta song_beat
239+ sta border_flash
240+ sta game_over
241+
242+ jsr display_progress
243+ rts
244+
245+; ----------------------------------------------------------------------------
246+; Advance Song - Increment beat and update progress
247+; ----------------------------------------------------------------------------
248+
249+advance_song:
250+ inc beat_count
251+ inc song_beat
252+
253+ ; Check if song has ended
254+ lda song_beat
255+ cmp #SONG_LENGTH
256+ bcc song_continues
257+
258+ ; Song loops for now (Unit 11 will add proper end)
259+ lda #0
260+ sta song_beat
261+ sta beat_count
262+ lda #<song_data
263+ sta song_pos
264+ lda #>song_data
265+ sta song_pos_hi
266+
267+song_continues:
268+ jsr display_progress
269+ rts
270+
271+; ----------------------------------------------------------------------------
272+; Display Progress - Show song progress bar
273+; ----------------------------------------------------------------------------
274+
275+display_progress:
276+ ; Calculate progress: (song_beat * PROGRESS_WIDTH) / SONG_LENGTH
277+ ; For 64-beat song with 16-char bar: song_beat / 4
278+
279+ lda song_beat
280+ lsr ; Divide by 4
281+ lsr
282+ sta temp_progress
283+
284+ ; Draw filled portion
285+ ldx #0
286+ lda temp_progress
287+ beq draw_empty_progress
288+
289+draw_full_progress:
290+ lda #CHAR_BAR_FULL
291+ sta SCREEN + (PROGRESS_ROW * 40) + 12,x
292+ lda #PROGRESS_COL
293+ sta COLRAM + (PROGRESS_ROW * 40) + 12,x
294+ inx
295+ cpx temp_progress
296+ bne draw_full_progress
297+
298+draw_empty_progress:
299+ cpx #PROGRESS_WIDTH
300+ beq progress_done
301+ lda #CHAR_BAR_EMPTY
302+ sta SCREEN + (PROGRESS_ROW * 40) + 12,x
303+ lda #11 ; Dark grey
304+ sta COLRAM + (PROGRESS_ROW * 40) + 12,x
305+ inx
306+ jmp draw_empty_progress
307+
308+progress_done:
309+ rts
310+
311+temp_progress: !byte 0
231312
232313 ; ----------------------------------------------------------------------------
233314 ; Copy Character Set from ROM to RAM
...
298379
299380 lda #%00000000
300381 sta CHARSET + (CHAR_TRACK * 8) + 0
301- lda #%00000000
302382 sta CHARSET + (CHAR_TRACK * 8) + 1
303- lda #%00000000
304383 sta CHARSET + (CHAR_TRACK * 8) + 2
305384 lda #%11111111
306385 sta CHARSET + (CHAR_TRACK * 8) + 3
307- lda #%11111111
308386 sta CHARSET + (CHAR_TRACK * 8) + 4
309387 lda #%00000000
310388 sta CHARSET + (CHAR_TRACK * 8) + 5
311- lda #%00000000
312389 sta CHARSET + (CHAR_TRACK * 8) + 6
313- lda #%00000000
314390 sta CHARSET + (CHAR_TRACK * 8) + 7
315391
316392 lda #%01100110
...
380456 init_notes_loop:
381457 sta note_track,x
382458 sta note_col,x
383- sta note_freq,x ; Also clear note frequencies
459+ sta note_freq,x
384460 dex
385461 bpl init_notes_loop
386462 rts
387463
388464 ; ----------------------------------------------------------------------------
389-; Check Spawn Note - Now reads 3-byte song entries
465+; Check Spawn Note
390466 ; ----------------------------------------------------------------------------
391467
392468 check_spawn_note:
...
395471 spawn_check_loop:
396472 lda (song_pos),y
397473 cmp #$FF
398- beq spawn_restart_song
474+ beq spawn_done ; Don't auto-restart here
399475
400476 cmp beat_count
401477 beq spawn_match
...
405481
406482 spawn_match:
407483 iny
408- lda (song_pos),y ; Track number
484+ lda (song_pos),y
409485 sta temp_track
410486 iny
411- lda (song_pos),y ; Note frequency
487+ lda (song_pos),y
412488 pha
413489 lda temp_track
414490 jsr spawn_note_with_freq
415- pla ; Clean up stack
491+ pla
416492 dey
417493 dey
418494
419495 spawn_advance:
420496 lda song_pos
421497 clc
422- adc #3 ; 3 bytes per entry now
498+ adc #3
423499 sta song_pos
424500 lda song_pos_hi
425501 adc #0
...
427503 jmp spawn_check_loop
428504
429505 spawn_done:
430- rts
431-
432-spawn_restart_song:
433- lda #<song_data
434- sta song_pos
435- lda #>song_data
436- sta song_pos_hi
437- lda #0
438- sta beat_count
439506 rts
440507
441508 ; ----------------------------------------------------------------------------
442509 ; Spawn Note With Frequency
443-; Input: A = track, stack has frequency
444510 ; ----------------------------------------------------------------------------
445511
446512 spawn_note_with_freq:
...
461527 lda #NOTE_SPAWN_COL
462528 sta note_col,x
463529
464- ; Get frequency from stack (caller pushed it)
465530 tsx
466- lda $0103,x ; Get frequency from stack
531+ lda $0103,x
467532 sta note_freq,x
468533
469534 jsr draw_note
...
613678
614679 draw_full_bars:
615680 lda #CHAR_BAR_FULL
616- sta SCREEN + (HEALTH_ROW * 40) + 16,x
681+ sta SCREEN + (HEALTH_ROW * 40) + 12,x
617682 lda #HEALTH_COL
618- sta COLRAM + (HEALTH_ROW * 40) + 16,x
683+ sta COLRAM + (HEALTH_ROW * 40) + 12,x
619684 inx
620685 cpx temp_health
621686 bne draw_full_bars
...
624689 cpx #8
625690 beq health_done
626691 lda #CHAR_BAR_EMPTY
627- sta SCREEN + (HEALTH_ROW * 40) + 16,x
692+ sta SCREEN + (HEALTH_ROW * 40) + 12,x
628693 lda #11
629- sta COLRAM + (HEALTH_ROW * 40) + 16,x
694+ sta COLRAM + (HEALTH_ROW * 40) + 12,x
630695 inx
631696 jmp draw_empty_bars
632697
...
874939 cpx #38
875940 bne draw_t1
876941
877- lda #CHAR_TRACK
878942 ldx #0
879943 draw_t2:
880944 sta SCREEN + (TRACK2_ROW * 40),x
...
882946 cpx #38
883947 bne draw_t2
884948
885- lda #CHAR_TRACK
886949 ldx #0
887950 draw_t3:
888951 sta SCREEN + (TRACK3_ROW * 40),x
...
9721035 draw_health_label:
9731036 lda health_label,x
9741037 beq draw_health_label_done
975- sta SCREEN + (HEALTH_ROW * 40) + 8,x
1038+ sta SCREEN + (HEALTH_ROW * 40) + 4,x
9761039 lda #5
977- sta COLRAM + (HEALTH_ROW * 40) + 8,x
1040+ sta COLRAM + (HEALTH_ROW * 40) + 4,x
9781041 inx
9791042 bne draw_health_label
9801043 draw_health_label_done:
1044+
1045+ ldx #0
1046+draw_progress_label:
1047+ lda progress_label,x
1048+ beq draw_progress_label_done
1049+ sta SCREEN + (PROGRESS_ROW * 40) + 4,x
1050+ lda #3
1051+ sta COLRAM + (PROGRESS_ROW * 40) + 4,x
1052+ inx
1053+ bne draw_progress_label
1054+draw_progress_label_done:
9811055
9821056 lda #$1A
9831057 sta SCREEN + (TRACK1_ROW * 40)
...
10101084
10111085 health_label:
10121086 !scr "health:"
1087+ !byte 0
1088+
1089+progress_label:
1090+ !scr "song:"
10131091 !byte 0
10141092
10151093 ; ----------------------------------------------------------------------------
...
11901268 rts
11911269
11921270 ; ----------------------------------------------------------------------------
1193-; Check Hit - Now stores note frequency for playback
1271+; Check Hit
11941272 ; ----------------------------------------------------------------------------
11951273
11961274 check_hit:
...
12091287 cmp #HIT_ZONE_MAX+1
12101288 bcs check_hit_next
12111289
1212- ; Store note frequency for sound playback
12131290 lda note_freq,x
12141291 sta hit_note_freq
12151292
1293+ lda note_col,x
12161294 cmp #HIT_ZONE_CENTRE
12171295 bcc hit_good
12181296 cmp #HIT_ZONE_CENTRE+2
...
14871565
14881566 ; ============================================================================
14891567 ; 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
15011568 ; ============================================================================
15021569
15031570 song_data:
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
1571+ ; Bar 1
1572+ !byte 0, 1, $47
1573+ !byte 2, 2, $2C
1574+ !byte 4, 3, $11
15081575
1509- ; Bar 2 - Descending
1510- !byte 8, 1, $3B ; A4
1511- !byte 10, 2, $27 ; D4
1512- !byte 12, 3, $13 ; D3
1576+ ; Bar 2
1577+ !byte 8, 1, $3B
1578+ !byte 10, 2, $27
1579+ !byte 12, 3, $13
15131580
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
1581+ ; Bar 3
1582+ !byte 16, 1, $35
1583+ !byte 17, 2, $2C
1584+ !byte 18, 1, $3B
1585+ !byte 20, 3, $16
15191586
1520- ; Bar 4 - Resolution
1521- !byte 24, 1, $47 ; C5
1522- !byte 26, 2, $35 ; G4
1523- !byte 28, 3, $11 ; C3
1587+ ; Bar 4
1588+ !byte 24, 1, $47
1589+ !byte 26, 2, $35
1590+ !byte 28, 3, $11
15241591
1525- ; Bar 5 - New phrase
1526- !byte 32, 2, $2F ; F4
1527- !byte 34, 1, $4F ; D5
1528- !byte 36, 3, $17 ; F3
1592+ ; Bar 5
1593+ !byte 32, 2, $2F
1594+ !byte 34, 1, $4F
1595+ !byte 36, 3, $17
15291596
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
1597+ ; Bar 6
1598+ !byte 40, 1, $58
1599+ !byte 42, 2, $2C
1600+ !byte 44, 3, $11
1601+ !byte 46, 2, $27
15351602
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
1603+ ; Bar 7
1604+ !byte 48, 1, $6A
1605+ !byte 49, 2, $35
1606+ !byte 50, 1, $58
1607+ !byte 52, 3, $1A
1608+ !byte 54, 2, $2F
15421609
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
1610+ ; Bar 8
1611+ !byte 56, 1, $47
1612+ !byte 58, 2, $2C
1613+ !byte 60, 3, $11
1614+ !byte 62, 1, $35
15481615
1549- !byte $FF ; End of song
1616+ !byte $FF
15501617
15511618 ; ----------------------------------------------------------------------------
15521619 ; Note Arrays
...
15591626 !fill MAX_NOTES, 0
15601627
15611628 note_freq:
1562- !fill MAX_NOTES, 0 ; Note frequency for each active note
1629+ !fill MAX_NOTES, 0
15631630
15641631 ; ----------------------------------------------------------------------------
15651632 ; Game Variables