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