aboutsummaryrefslogtreecommitdiff
path: root/sound/scale.s
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-04-09 23:03:04 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-04-10 13:35:21 +0200
commita7a268c9abc34c2fdf1cbf802e625f825bf86743 (patch)
treee2dc1fd88a2f8cd6afcc59bb06b34c810529cea4 /sound/scale.s
parent00e470a4c8cefcc8e9514c6965dcd61f448d10e8 (diff)
downloadcode.nes-a7a268c9abc34c2fdf1cbf802e625f825bf86743.tar.gz
code.nes-a7a268c9abc34c2fdf1cbf802e625f825bf86743.zip
Add the sound/scale.s example
This example just repeats a scale of notes over and over. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
Diffstat (limited to 'sound/scale.s')
-rw-r--r--sound/scale.s229
1 files changed, 229 insertions, 0 deletions
diff --git a/sound/scale.s b/sound/scale.s
new file mode 100644
index 0000000..ffb679d
--- /dev/null
+++ b/sound/scale.s
@@ -0,0 +1,229 @@
+;;;
+;; 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