aboutsummaryrefslogtreecommitdiff
path: root/sound
diff options
context:
space:
mode:
Diffstat (limited to 'sound')
-rw-r--r--sound/README.md1
-rw-r--r--sound/beep.s196
2 files changed, 197 insertions, 0 deletions
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