aboutsummaryrefslogtreecommitdiff
path: root/sound/beep.s
blob: f9ab7a852167f2f3c8693ce738707781b15a3a36 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
;;;
;; 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