aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-04-09 13:30:34 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-04-10 13:33:30 +0200
commit00e470a4c8cefcc8e9514c6965dcd61f448d10e8 (patch)
treee9a63c8797f25292daa74f9fd8c0b53702cc30dd
parent1978afbc18ffb227f0ce36663648e00d1d2d7945 (diff)
downloadcode.nes-00e470a4c8cefcc8e9514c6965dcd61f448d10e8.tar.gz
code.nes-00e470a4c8cefcc8e9514c6965dcd61f448d10e8.zip
Add the sound/select.s example
This is an iteration over the sound/beep.s example, but it covers three channels instead of just square 1. Moreover, you can use the joypad to select which channel you want to hear and, of course, you can combine them all at will. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
-rw-r--r--Makefile3
-rw-r--r--shared/apu.s8
-rw-r--r--sound/README.md2
-rw-r--r--sound/select.s234
4 files changed, 247 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index 94fa22a..2940102 100644
--- a/Makefile
+++ b/Makefile
@@ -74,6 +74,9 @@ sound:
$(E) " CC sound/beep"
$(Q) $(CC65) $(CCOPTS) sound/beep.s -C config/nrom.cfg -o out/sound/beep.nes
+ $(E) " CC sound/select"
+ $(Q) $(CC65) $(CCOPTS) sound/select.s -C config/nrom.cfg -o out/sound/select.nes
+
.PHONY: fx
fx:
$(E) " CC fx/blink"
diff --git a/shared/apu.s b/shared/apu.s
index 814c446..f876d91 100644
--- a/shared/apu.s
+++ b/shared/apu.s
@@ -1,7 +1,15 @@
.scope APU
m_square_1_envelope = $4000
+ m_square_1_sweep = $4001
m_square_1_low = $4002
m_square_1_high = $4003
+ m_square_2_envelope = $4004
+ m_square_2_sweep = $4005
+ m_square_2_low = $4006
+ m_square_2_high = $4007
+ m_triangle_control = $4008
+ m_triangle_low = $400A
+ m_triangle_high = $400B
m_dmc = $4010
m_status = $4015
m_frame_counter = $4017
diff --git a/sound/README.md b/sound/README.md
index a099036..90fc84e 100644
--- a/sound/README.md
+++ b/sound/README.md
@@ -1 +1,3 @@
TBD
+Reference nerdynights: https://nerdy-nights.nes.science/#audio_tutorial-0
+square channels sometimes called pulse channels
diff --git a/sound/select.s b/sound/select.s
new file mode 100644
index 0000000..887a585
--- /dev/null
+++ b/sound/select.s
@@ -0,0 +1,234 @@
+;;;
+;; Enable three sound channels and make them beep depending on the button that
+;; is being pressed.
+
+;;; Nothing remarkable here, go down below until you see some comments :)
+
+.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"
+.include "../shared/joypad.s"
+
+.scope Vars
+ zp_flags = $20
+.endscope
+
+.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
+
+ ;; Just like beep.s
+ lda #$0F
+ sta APU::m_status
+
+ ;; Square 1 is set up in the same way as in beep.s but it's muted. We will
+ ;; handle playing/muting in the main game loop.
+ lda #%10110000
+ sta APU::m_square_1_envelope
+ lda #$C9
+ sta APU::m_square_1_low
+ lda #$00
+ sta APU::m_square_1_high
+ sta APU::m_square_1_sweep
+
+ ;; The square 2 channel is almost identical to the other square channel. The
+ ;; only difference is with sweeps, for which both square channels are wired
+ ;; up differently. To sum things up, when computing the target period with a
+ ;; negative value, square 1 will use one's complement, while square 2 will
+ ;; use two's complement. You can read more about this here:
+ ;; https://www.nesdev.org/wiki/APU_Sweep.
+ ;;
+ ;; In any case, here we just set the E note with a different duty. We mute
+ ;; it for the same reason as for square 1.
+ lda #%01110000
+ sta APU::m_square_2_envelope
+ lda #$A9
+ sta APU::m_square_2_low
+ lda #$00
+ sta APU::m_square_2_high
+ sta APU::m_square_2_sweep
+
+ ;; The triangle channel is a bit similar to the square ones, but triangle
+ ;; waves have no concept of "duty": there's no percentage of how much time
+ ;; it's in the "up" position because that's not how triangles work.
+ ;;
+ ;; The "control" register from the triangle channel has the most significant
+ ;; bit which, when unset, tells the channel to use the internal
+ ;; counters. The other bits hold a "value" which is only used with the
+ ;; linear counter. Here we set to 1 the most significant bit and hence we
+ ;; will control things manually instead of via the linear counter. If we set
+ ;; the "value" to zero, like in this case, then we mute the channel. To
+ ;; unmute it we can set any value (see below in the main game loop).
+ lda #%10000000
+ sta APU::m_triangle_control
+
+ ;; NOTE: contrary to what you'd expect, address $4009 is not used by the
+ ;; APU.
+
+ ;; And the low/high registers from the triangle channel work in the very
+ ;; same way as their square counterparts. Here we set a G#.
+ ;;
+ ;; That being said, take into account that the pitch of the triangle channel
+ ;; is one octave below the square channels with an equivalent timer
+ ;; value. If we were to have a table with notes for the square channels and
+ ;; we wanted to use them for the triangle channel, we would need to 'lsr'
+ ;; the value to get the same note.
+ lda #$42
+ sta APU::m_triangle_low
+ lda #$00
+ sta APU::m_triangle_high
+
+ cli
+ lda #%10001000
+ sta PPU::m_control
+ lda #%00011110
+ sta PPU::m_mask
+
+@main_game_loop:
+ ;; Read the joypad so we can mute/unmute channels depending on the button
+ ;; being pressed.
+ READ_JOYPAD1
+
+ ;; Mute/Play the square 1 channel
+ bit Joypad::zp_buttons1
+ bpl @reset_square_1
+ lda #%10111000
+ bne @set_square_1
+@reset_square_1:
+ lda #%10110000
+@set_square_1:
+ sta APU::m_square_1_envelope
+
+ ;; Mute/Play the square 2 channel
+ bit Joypad::zp_buttons1
+ bvc @reset_square_2
+ lda #%10111000
+ bne @set_square_2
+@reset_square_2:
+ lda #%10110000
+@set_square_2:
+ sta APU::m_square_2_envelope
+
+ ;; Mute/Play the triangle channel
+ lda Joypad::zp_buttons1
+ and #%00100000
+ beq @reset_triangle
+ lda #%10000001
+ bne @set_triangle
+@reset_triangle:
+ lda #%10000000
+@set_triangle:
+ sta APU::m_triangle_control
+
+ ;; NOTE: done :)
+
+ 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
+.endproc
+
+.proc nmi
+ bit Vars::zp_flags
+ bpl @next
+
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ lda #%01111111
+ and Vars::zp_flags
+ sta Vars::zp_flags
+
+ pla
+ tay
+ pla
+ tax
+ pla
+@next:
+ rti
+.endproc
+
+.proc irq
+ rti
+.endproc