aboutsummaryrefslogtreecommitdiff
;;;
;; 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