aboutsummaryrefslogtreecommitdiff
;;;
;; Repeat a scale of notes on the triangle channel.

.segment "HEADER"
    .byte 'N', 'E', 'S', $1A
    .byte $02, $01
    .res $0A, $00

.segment "VECTORS"
    .addr nmi, reset, irq

.segment "CHARS"
    .incbin "../assets/basic.chr"

.segment "CODE"

.include "../shared/asm.s"
.include "../shared/apu.s"
.include "../shared/oam.s"
.include "../shared/ppu.s"
.include "../shared/clear.s"

.scope Vars
    zp_flags     = $20

    ;; Boolean value stating whether the currently indexed note should be
    ;; delivered to the triangle channel or not.
    zp_next_note = $21

    ;; Index to the 'notes_{low,high}' tables, pointing at the current note.
    zp_index     = $22

    ;; Simple counter increased/reset on NMI code.
    zp_counter   = $23
.endscope

;; Where the scale of notes should start and end.
INITIAL_NOTE = 40
HIGHEST_NOTE = 50

.proc reset
    sei
    cld

    ldx #$40
    stx APU::m_frame_counter

    ldx #$FF
    txs

    inx
    stx PPU::m_control
    stx PPU::m_mask
    stx APU::m_dmc

    bit PPU::m_status
@vblankwait1:
    bit PPU::m_status
    bpl @vblankwait1

    ldx #0
    lda #0
@ram_reset_loop:
    sta $000, x
    sta $100, x
    sta $300, x
    sta $400, x
    sta $500, x
    sta $600, x
    sta $700, x
    inx
    bne @ram_reset_loop

    lda #$EF
@sprite_reset_loop:
    sta $200, x
    inx
    bne @sprite_reset_loop

    OAM_WRITE_SPRITES

@vblankwait2:
    bit PPU::m_status
    bpl @vblankwait2

    lda #$3F
    sta PPU::m_address
    lda #$00
    sta PPU::m_address

    lda #$0F
    ldx #$20
@palettes_reset_loop:
    sta PPU::m_data
    dex
    bne @palettes_reset_loop

    __fallthrough__ main
.endproc

.proc main
    CLEAR_SCREEN

    ;; Initialize the note index, reset the counter and ensure that the first
    ;; note is delivered.
    lda #INITIAL_NOTE
    sta Vars::zp_index
    ldx #0
    stx Vars::zp_counter
    inx
    stx Vars::zp_next_note

    ;; Same as beep.s
    lda #$0F
    sta APU::m_status

    ;; Enable the triangle channel.
    lda #%10000001
    sta APU::m_triangle_control

    cli
    lda #%10001000
    sta PPU::m_control
    lda #%00011110
    sta PPU::m_mask

@main_game_loop:
    ;; Should we change the note? If not, then skip this altogether, or
    ;; otherwise sending the same note over and over will result in a
    ;; blurry/ugly sound.
    lda Vars::zp_next_note
    beq @skip_note

    ;; Fetch the new note and send it to the triangle channel. Note that notes
    ;; on both tables are defined with the square channels in mind. Sicne the
    ;; triangle channel is an octave below, we need to divide the given value by
    ;; two.
    ldx Vars::zp_index
    lda notes_low, x
    lsr
    sta APU::m_triangle_low
    lda notes_high, x
    lsr
    sta APU::m_triangle_high

@skip_note:
    lda #%10000000
    ora Vars::zp_flags
    sta Vars::zp_flags
@wait_for_render:
    bit Vars::zp_flags
    bmi @wait_for_render

    jmp @main_game_loop

    ;; NOTE: notes taken from: https://www.nesdev.org/wiki/APU_basics.
    ;; NOTE: only valid for NTSC. For PAL we would need another set of values
    ;; which can be found in other places.
notes_low:
    .byte $f1,$7f,$13,$ad,$4d,$f3,$9d,$4c,$00,$b8,$74,$34
    .byte $f8,$bf,$89,$56,$26,$f9,$ce,$a6,$80,$5c,$3a,$1a
    .byte $fb,$df,$c4,$ab,$93,$7c,$67,$52,$3f,$2d,$1c,$0c
    .byte $fd,$ef,$e1,$d5,$c9,$bd,$b3,$a9,$9f,$96,$8e,$86
    .byte $7e,$77,$70,$6a,$64,$5e,$59,$54,$4f,$4b,$46,$42
    .byte $3f,$3b,$38,$34,$31,$2f,$2c,$29,$27,$25,$23,$21
    .byte $1f,$1d,$1b,$1a,$18,$17,$15,$14
notes_high:
    .byte $07,$07,$07,$06,$06,$05,$05,$05,$05,$04,$04,$04
    .byte $03,$03,$03,$03,$03,$02,$02,$02,$02,$02,$02,$02
    .byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
    .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
    .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
    .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
    .byte $00,$00,$00,$00,$00,$00,$00,$00
.endproc

.proc nmi
    bit Vars::zp_flags
    bpl @next

    pha
    txa
    pha
    tya
    pha

    ;; Increase the counter and do nothing if we are not at the desired value yet.
    inc Vars::zp_counter
    lda Vars::zp_counter
    cmp #30
    bne @skip_reset

    ;; Reset counter, move the note index and state that we need to update the
    ;; triangle channel with this new note.
    lda #0
    sta Vars::zp_counter
    inc Vars::zp_index
    inc Vars::zp_next_note

    ;; Wrap around the note index if needed.
    lda Vars::zp_index
    cmp #HIGHEST_NOTE
    bne @done
    lda #INITIAL_NOTE
    sta Vars::zp_index
    bne @done

@skip_reset:
    ;; In the general case, avoid the current note to be delivered.
    lda #0
    sta Vars::zp_next_note

@done:
    lda #%01111111
    and Vars::zp_flags
    sta Vars::zp_flags

    pla
    tay
    pla
    tax
    pla
@next:
    rti
.endproc

.proc irq
    rti
.endproc