;;;
;; 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
;; The noise channel produces noise with a pseudo-random bit generator. Its
;; envelope register is pretty much the same as with the square channels,
;; but the two most significant bits are unused as "duty" is, like with the
;; triangle channel, not a thing on this context.
lda #%00110000
sta APU::m_noise_envelope
;; NOTE: address $400D is unused from the APU.
;; You then configure the mode of noise by writing to the $400E address. In the
;; low nibble you configure the "noise period", which in turn is the index
;; for a table that the APU will use as the real noise period (see:
;; https://www.nesdev.org/wiki/APU_Noise for specifics). Finally, only the
;; most significant bit is used on the high nibble, which sets the
;; "mode". Long story short, it dramatically shortens the period if set,
;; which will give you a more "metallic" sound. Change the current value
;; from "$0A" to "$8A" and test it by yourself ;-)
lda #$0A
sta APU::m_noise_mode
;; Finally, there is the counter register for the noise channel. Given that
;; we have disabled when we configured 'APU::m_noise_envelope', you'd think
;; that you wouldn't have to touch it, but you'd be wrong. This register
;; needs to be written at least once so to trigger the enablement of this
;; channel. We do it now with a zero value, which also does the trick.
lda #0
sta APU::m_noise_counter
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
;; Mute/Play the noise channel
lda Joypad::zp_buttons1
and #%00010000
beq @reset_noise
lda #%00111000
bne @set_noise
@reset_noise:
lda #%00110000
@set_noise:
sta APU::m_noise_envelope
;; 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