;;; ;; 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. After that, all enabled channels ;; can produce output and it's going to be combined using a non-linear ;; mixing scheme (https://www.nesdev.org/wiki/APU_Mixer). Long story short: ;; all channels work independently and they are going to be properly ;; combined in the end, so a programmer can focus on each channel ;; separately. ;; This example uses the Square 1 channel to produce sound. Let's see how it ;; can be configured, and 'sound/select.s' will deal with the other ;; channels. ;; ;; The $4000 register configures how the square 1 channel will produce its ;; output via the envelope. The **envelope** is a hardware feature that ;; automatically controls the volume of a sound over time. The APU has two ;; modes of operation: constant volume or decay envelope mode. This is ;; regulated via bit 4 of the $4000 register: 1 for constant volume; 0 for ;; decay envelope mode. With this in mind, the low nibble of this register ;; works like this: ;; ;; - Bit 4 = 1: the low nibble contains the **volume** of the sound, which ;; will be frozen in time (i.e. constant). Set the volume to ;; 0 for a silent channel; 1 for very low volume, F for ;; maximum volume. ;; - Bit 4 = 0: the low nibble contains the **period** of decay of the ;; volume. That is, it makes the volume to decay every V ;; frames, where V is the value on the low nibble. This is in ;; the end possible because of the internal frame counter ;; from the APU. In this context, the volume will always ;; start at F, and it will decay until reaching zero at the ;; given period. With all of that in mind, then, note that ;; higher number means decaying the volume slower. ;; ;; Another important bit is 5, which is the "looping" bit. That is, whether ;; the configured envelope should be repeated after a shot has been done. If ;; this bit is set to 1, then this envelope will be repeated over and ;; over. If, otherwise, the bit is set to 0, then it's a one-shot note. For ;; how long this one-shot should happen? This is regulated in $4003 as you ;; will see below. ;; ;; With this in mind note that in this example we set these two bits to ;; 1. Hence, it's an continuous beep, and the low nibble contains the volume ;; that will be constant over time. The value for the volume is F, hence ;; maximum volume. ;; ;; Finally we have the two most significant bits, which configure the "duty" ;; cycle. That is, on a square wave, what percentage of time should the wave ;; spend in the "up" position. We have two bits, so we can select four ;; different configurations of the wave's shape (see: ;; https://www.nesdev.org/wiki/APU_Pulse). Here we pick "10", which ;; corresponds to the 50% duty cycle, meaning that we want a square wave ;; that spends the same amount of time up and down. 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. Configuring the sweep register so we tend towards ;; lower frequencies (longer periods) can also be a way of muting the ;; channel. 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 contains 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. Funnily enough, this "counter" is ;; not really a counter, but an index to a table with the actual values. See ;; the table here: https://www.nesdev.org/wiki/APU_Length_Counter. 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. ;; ;; NOTE: one gotcha from writing into $4003 is that it implicitely resets ;; the envelope. Hence, if you write to this every frame for whatever ;; reason, then you will never notice the fading. 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