From 1978afbc18ffb227f0ce36663648e00d1d2d7945 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Thu, 9 Apr 2026 00:33:04 +0200 Subject: Add the sound/beep.s example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the most simple example you can come up when it comes to producing sound on the NES/Famicom. Basically, a single note is delivered to the APU which is continuously on. Signed-off-by: Miquel Sabaté Solà --- sound/README.md | 1 + sound/beep.s | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 sound/README.md create mode 100644 sound/beep.s (limited to 'sound') diff --git a/sound/README.md b/sound/README.md new file mode 100644 index 0000000..a099036 --- /dev/null +++ b/sound/README.md @@ -0,0 +1 @@ +TBD diff --git a/sound/beep.s b/sound/beep.s new file mode 100644 index 0000000..cae6ec6 --- /dev/null +++ b/sound/beep.s @@ -0,0 +1,196 @@ +;;; +;; Make an annoying "beep" sound. + +;;; 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" + +.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 + + ;; In order to get sound, you need to enable/disable the channels you + ;; want. One typical setup is to enable all of them except for DMC. You do + ;; this by writing into $4015 (APU Status). Note that this register can also + ;; be read. Reading from this register will give you the state of + ;; interrupts, and the length of the counter for each channel. Yes, each + ;; channel has an internal counter, but we can discuss this later. + ;; + ;; Anyways, if we want to enable all channels except DMC, we are in luck + ;; because then we just need to set all bits for the low nibble. + lda #$0F + sta APU::m_status + + ;; This was the configuration for the APU. Now let's produce sound on the + ;; Square 1 channel (registers: $4000-$4003). + + ;; Let's configure the Square 1 channel before producing any sound. + ;; + ;; The low nibble controls the volume: 0 for silent, 1 very low, F + ;; maximum. Just to be annoying we will be setting the volume at a maximum + ;; level. + ;; + ;; Then we have two bits which seem quite odd at first. Bit 4 sets/unsets + ;; whether the volume is to be kept constant. If unset, then an internal + ;; counter will tune it down when running out. Similarly, bit 5 sets/unsets + ;; whether the length counter is to be accounted or not. See this counter + ;; down below. All in all, here we make the sound constant, so the beep + ;; never stops until we mute it (which we don't in this example). + ;; + ;; Finally we have the "duty" bits, which has four possibilities + ;; available. These regulate the tone for the note, and it's basically the + ;; percentage of time the square wave is in the "up" position. + lda #%10111111 + sta APU::m_square_1_envelope + + ;; The $4001 address contains the sweep register, which is a way that the + ;; APU has in order to produce different pitch effects, or workaround known + ;; issues on some tones. I'm not touching it here other than resetting to 0. + lda #0 + sta APU::m_square_1_sweep + + ;; The note to play is 11 bits long. This means that we need two bytes for + ;; it, which for square 1 are $4002 and $4003. $4002 is the least + ;; significant bits, and the three least significant bits from $4003 are the + ;; most significant bits from this 11-bit note definition. How to know which + ;; note corresponds to what value is easy via: + ;; https://www.nesdev.org/wiki/APU_period_table. Thus, in NTSC, setting $0C9 + ;; to this 11-bit value gives us a C#. + ;; + ;; The 5 other bits from $4003 correspond to the length counter. That is, + ;; you can regulate for how long this note has to be reproduced, and the APU + ;; will (magically) track things for you. This is not available on this + ;; configuration because we disabled the option when we configured + ;; 'APU::m_square_1_envelope' (see above). Hence, we will set these bits to + ;; 0. + lda #$C9 + sta APU::m_square_1_low + lda #$00 + sta APU::m_square_1_high + + ;; NOTE: and that's it :) + + cli + lda #%10001000 + sta PPU::m_control + lda #%00011110 + sta PPU::m_mask + +@main_game_loop: + + 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 -- cgit v1.2.3