iigs-game-engine/demos/smb/apu.s
2023-06-27 21:24:59 -05:00

1265 lines
43 KiB
ArmAsm

; Init
sound_control = $3c ;really at $e1c03c
sound_data = $3d ;really at $e1c03d
sound_address = $3e ;really at $e1c03e
sound_interrupt_ptr = $e1002c
irq_volume = $e100ca
osc_interrupt = $e100cc
mx %10
access_doc_registers = *
ldal irq_volume
sta sound_control
rts
access_doc_ram = *
ldal irq_volume
ora #%0110_0000
sta sound_control
rts
access_doc_ram_no_inc = *
ldal irq_volume
ora #%0100_0000
sta sound_control
rts
mx %00
APUStartUp
sei
phd
pea $c000
pld
jsr copy_instruments_to_doc
jsr setup_doc_registers
jsr setup_interrupt
pld
cli
rts
APUShutDown = *
sei
phd
lda #$c000
tcd
jsr stop_playing
lda backup_interrupt_ptr ; restore old interrupt ptr
stal sound_interrupt_ptr
lda backup_interrupt_ptr+2
stal sound_interrupt_ptr+2
cli
pld
clc
rts
stop_playing = *
ldy #7 ; Number of oscillators
sep #$20
mx %10
jsr access_doc_registers
lda #$a0 ; stop all oscillators in use
sta sound_address
lda #%11
]loop sta sound_data
inc sound_address
dey
bne ]loop
lda #$a0+interrupt_oscillator ; stop interrupt oscillator
sta sound_address
lda #3
sta sound_data
rep #$20
mx %00
rts
; Copy in 4 different square wave duty cycles and a triangle wave
copy_instruments_to_doc
jsr setup_docram
lda #$0100
jsr make_eigth_pulse
lda #$0200
jsr make_quarter_pulse
lda #$0300
jsr make_half_pulse
lda #$0400
jsr make_inv_quarter_pulse
lda #$0500
jsr copy_triangle
lda #$0600
jsr copy_noise
lda #$8000
; jsr gen_noise
rts
;--------------------------
setup_docram
sep #$20
mx %10
jsr access_doc_ram
stz sound_address
lda #$80
ldx #256 ;make sure that page 00 has nonzero data for interrupt
:loop sta sound_data
dex
bne :loop
rep #$20
mx %00
rts
;--------------------------
make_eigth_pulse
ldy #32
jmp make_pulse
make_quarter_pulse
ldy #64
jmp make_pulse
make_half_pulse
ldy #128
jmp make_pulse
make_inv_quarter_pulse
ldy #192
jmp make_pulse
make_pulse
sep #$30
mx %11
stz sound_address
xba
sta sound_address+1
ldx #0
:loop1
lda #$01
sta sound_data
inx
dey
bne :loop1
:loop2
lda #$FF
sta sound_data
inx
bne :loop2
rep #$30
mx %00
rts
copy_triangle
sep #$30
mx %11
stz sound_address
xba
sta sound_address+1
ldx #0
:loop
lda triangle_wave,x
sta sound_data
inx
bne :loop
rep #$30
mx %00
rts
; Generate random data from the NES APU LFSR. Make it long enough to sound good.
gen_noise
copy_noise
sep #$30
mx %11
stz sound_address
xba
sta sound_address+1
ldx #0
:loop
lda noise_wave,x
sta sound_data
inx
bne :loop
rep #$30
mx %00
rts
;--------------------------
triangle_wave
hex 80828486888a8c8e90929496989a9c9e
hex a0a2a4a6a8aaacaeb0b2b4b6b8babcbe
hex c0c1c3c5c7c9cbcdcfd1d3d5d7d9dbdd
hex dfe1e3e5e7e9ebedeff1f3f5f7f9fbfd
hex fffdfbf9f7f5f3f1efedebe9e7e5e3e1
hex dfdddbd9d7d5d3d1cfcdcbc9c7c5c3c1
hex c0bebcbab8b6b4b2b0aeacaaa8a6a4a2
hex a09e9c9a98969492908e8c8a88868482
hex 807e7c7a78767472706e6c6a68666462
hex 605e5c5a58565452504e4c4a48464442
hex 413f3d3b39373533312f2d2b29272523
hex 211f1d1b19171513110f0d0b09070503
hex 01030507090b0d0f11131517191b1d1f
hex 21232527292b2d2f31333537393b3d3f
hex 41424446484a4c4e50525456585a5c5e
hex 60626466686a6c6e70727476787a7c7e
noise_wave
hex 8f968f763e6fd49ab1e564e295a9bcc9
hex 717b6629e6970b865dc0e0d840d32a96
hex 3bd4c5d407b78923d8c9766bea128e8a
hex c9ee5ddbed3119ff14b4d9a44bfbb7c4
hex 7a56e26e8aac9ebf1653c0260446231b
hex 73431495fc585e943edacf8f5bb970e6
hex 118dc361bee99c98f32d25f06a33715a
hex 585344f7f3e2f3c36c37cfd78e40147f
hex a4b20624ac633b42b3aac5407fac4ba9
hex a4d71a1d020a7757ea244b103f0b7a76
hex 9b533a60cda31e0fa2ce3491b55c4f26
hex ea47a61f661deec128129372c3471a9b
hex f85c3c077168d413184a139440460950
hex dee3f9bdb65e162b08ed9231a72fb943
hex 1ba599be80dc2812afa63cc2317cdb1a
hex 8d99d56327bc50dc975bee94754f561b
; hex 01ffffff0101ffffff01ffff01ff0101
; hex ffffffffffff0101ff0101ff01ffff01
; hex 01ff0101ffff01ffff0101ff01ff01ff
; hex ffffffff0101010101ffff0101ff0101
; hex ffffff0101ff01ff010101ff01010101
; hex 0101ffffff01ffff01ff01ffff01ffff
; hex ff01ffff0101ffff01ffffffffff01ff
; hex ffffffffffff010101ffff01ff01ffff
; hex 01ffffffff0101ffffffff0101ffff01
; hex ff01ff01ff01ffff0101ff01ffffffff
; hex ffff010101ffffffff01010101ff0101
; hex ffffffffffffff01ff0101ffffff0101
; hex 01ff010101ff01ffffffffff01ffffff
; hex 01ffffffff010101ff01ffff01ff01ff
; hex ffffffff0101ff010101ff01ffffff01
; hex 0101010101ffff01ffff01010101ffff
;--------------------------
setup_doc_registers
sep #$20
mx %10
jsr access_doc_registers
ldx #pulse1_sound_settings
jsr copy_register_config
ldx #pulse2_sound_settings
jsr copy_register_config
ldx #triangle_sound_settings
jsr copy_register_config
ldx #noise_sound_settings
jsr copy_register_config
rep #$20
mx %00
rts
copy_register_config
ldy #0
:loop lda: 0,x ; Set DOC registers for the NES channels
sta sound_address
inx
lda: 0,x
sta sound_data
inx
iny
cpy #6 ; 6 pairs to describe this oscillator
bne :loop
rts
;--------------------------
setup_interrupt = *
ldal sound_interrupt_ptr
sta backup_interrupt_ptr
ldal sound_interrupt_ptr+2
sta backup_interrupt_ptr+2
lda #$5c
stal sound_interrupt_ptr
phk
phk
pla
stal sound_interrupt_ptr+2
lda #interrupt_handler
stal sound_interrupt_ptr+1
sep #$20
mx %10
jsr access_doc_registers
ldy #0
:loop lda timer_sound_settings,y ; Set DOC registers for the interrupt oscillator
sta sound_address
iny
lda timer_sound_settings,y
sta sound_data
iny
cpy #7*2
bne :loop
rep #$20
mx %00
rts
interrupt_oscillator = 31
;reference_freq = 299 ; interrupt frequence (60Hz)
;reference_freq = 598 ; interrupt frequence (120Hz)
reference_freq = 1195 ; interrupt frequence (240Hz)
timer_sound_settings = * ; set up oscillator 30 for interrupts
dfb $00+interrupt_oscillator,reference_freq ; frequency low register
dfb $20+interrupt_oscillator,reference_freq/256 ; frequency high register
dfb $40+interrupt_oscillator,0 ; volume register, volume = 0
dfb $80+interrupt_oscillator,0 ; wavetable pointer register, point to 0
dfb $c0+interrupt_oscillator,0 ; wavetable size register, 256 byte length
dfb $e1,$3e ; oscillator enable register
dfb $a0+interrupt_oscillator,$08 ; mode register, set to free run
pulse1_oscillator = 0
pulse2_oscillator = 2
triangle_oscillator = 4
noise_oscillator = 6
default_freq = 800
pulse1_sound_settings = *
dfb $00+pulse1_oscillator,default_freq ; frequency low register
dfb $20+pulse1_oscillator,default_freq/256 ; frequency high register
dfb $40+pulse1_oscillator,0 ; volume register, volume = 0
dfb $80+pulse1_oscillator,3 ; wavetable pointer register, point to $0300 by default (50% duty cycle)
dfb $c0+pulse1_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+pulse1_oscillator,0 ; mode register, set to free run
pulse2_sound_settings = *
dfb $00+pulse2_oscillator,default_freq ; frequency low register
dfb $20+pulse2_oscillator,default_freq/256 ; frequency high register
dfb $40+pulse2_oscillator,0 ; volume register, volume = 0
dfb $80+pulse2_oscillator,3 ; wavetable pointer register, point to $0300 by default (50% duty cycle)
dfb $c0+pulse2_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+pulse2_oscillator,0 ; mode register, set to free run
triangle_sound_settings = *
dfb $00+triangle_oscillator,default_freq ; frequency low register
dfb $20+triangle_oscillator,default_freq/256 ; frequency high register
dfb $40+triangle_oscillator,0 ; volume register, volume = 0
dfb $80+triangle_oscillator,5 ; wavetable pointer register, point to $0500
dfb $c0+triangle_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+triangle_oscillator,0 ; mode register, set to free run
noise_sound_settings = *
dfb $00+noise_oscillator,default_freq ; frequency low register
dfb $20+noise_oscillator,default_freq/256 ; frequency high register
dfb $40+noise_oscillator,128 ; volume register, volume = 0
dfb $80+noise_oscillator,6 ; wavetable pointer register, point to $0600
dfb $c0+noise_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+noise_oscillator,0 ; mode register, set to free run
backup_interrupt_ptr ds 4
;-----------------------------------------------------------------------------------------
; APU internals
;-----------------------------------------------------------------------------------------
mx %11
clock_length_counter mac
lda ]1+{APU_PULSE1_REG1-APU_PULSE1}
bit ]2
bne no_count
lda ]1+{APU_PULSE1_LENGTH_COUNTER-APU_PULSE1}
beq no_count
dec
sta ]1+{APU_PULSE1_LENGTH_COUNTER-APU_PULSE1}
no_count <<<
clock_linear_counter mac
lda ]1+{APU_TRIANGLE_START_FLAG-APU_TRIANGLE}
beq do_clock
lda ]1+{APU_TRIANGLE_REG1-APU_TRIANGLE}
and #$7F
sta ]1+{APU_TRIANGLE_LINEAR_COUNTER-APU_TRIANGLE}
bra check_reset
do_clock lda ]1+{APU_TRIANGLE_LINEAR_COUNTER-APU_TRIANGLE}
beq check_reset
dec
sta ]1+{APU_TRIANGLE_LINEAR_COUNTER-APU_TRIANGLE}
check_reset
lda ]1+{APU_TRIANGLE_REG1-APU_TRIANGLE}
bmi no_reset
stz ]1+{APU_TRIANGLE_START_FLAG-APU_TRIANGLE}
no_reset <<<
clock_sweep mac
lda ]1+{APU_PULSE1_SWEEP_DIVIDER-APU_PULSE1}
dec
sta ]1+{APU_PULSE1_SWEEP_DIVIDER-APU_PULSE1}
bpl no_sweep
lda #1
sta ]1+{APU_PULSE1_RELOAD_FLAG-APU_PULSE1}
lda ]1+{APU_PULSE1_REG2-APU_PULSE1} ; get the barrel shift argument from the register
bpl no_sweep ; if sweep is not enabled, do nothing
and #$07
beq no_sweep ; shift must be != 0
asl
tax
lda ]1+{APU_PULSE1_REG2-APU_PULSE1} ; put the negate flag in the y register
and #$08
tay
rep #$20
lda ]1+{APU_PULSE1_CURRENT_PERIOD-APU_PULSE1}
cmp #8
bcc no_sweep0 ; current period must be >= 8
jmp (bitshift,x) ; shift it by the shifter amount
bitshift da bitshift_0,bitshift_1,bitshift_2,bitshift_3,bitshift_4,bitshift_5,bitshift_6,bitshift_7
bitshift_7 lsr
bitshift_6 lsr
bitshift_5 lsr
bitshift_4 lsr
bitshift_3 lsr
bitshift_2 lsr
bitshift_1 lsr
bitshift_0
cpy #0 ; check if the negate flag was set
beq no_negate
eor #$FFFF ; pulse 1 uses 1's complement
DO ]2
inc
FIN
no_negate clc
adc ]1+{APU_PULSE1_CURRENT_PERIOD-APU_PULSE1}
cmp #$800
bcs no_sweep0
sta ]1+{APU_PULSE1_CURRENT_PERIOD-APU_PULSE1}
no_sweep0
sep #$20
no_sweep
lda ]1+{APU_PULSE1_RELOAD_FLAG-APU_PULSE1} ; check if we need to reload the sweep delay
beq no_reload
stz ]1+{APU_PULSE1_RELOAD_FLAG-APU_PULSE1}
lda ]1+{APU_PULSE1_REG2-APU_PULSE1}
lsr
lsr
lsr
lsr
and #7
sta ]1+{APU_PULSE1_SWEEP_DIVIDER-APU_PULSE1}
no_reload <<<
clock_envelope mac
lda ]1+{APU_PULSE1_START_FLAG-APU_PULSE1}
beq no_start
stz ]1+{APU_PULSE1_START_FLAG-APU_PULSE1} ; clear the start flag
lda #15
sta ]1+{APU_PULSE1_ENVELOPE-APU_PULSE1} ; reset the envelope saw wave decay value
lda ]1+{APU_PULSE1_REG1-APU_PULSE1}
and #$0F
sta ]1+{APU_PULSE1_ENVELOPE_DIVIDER-APU_PULSE1} ; reset the divider value
bra envelope_out ; nothing else to do
no_start
lda ]1+{APU_PULSE1_ENVELOPE_DIVIDER-APU_PULSE1} ; clock the divider
dec
sta ]1+{APU_PULSE1_ENVELOPE_DIVIDER-APU_PULSE1}
bpl envelope_out ; as long as divider is >=0, nothing to do
lda ]1+{APU_PULSE1_REG1-APU_PULSE1} ; reset the divider to the volume/envelope value
and #$0F
sta ]1+{APU_PULSE1_ENVELOPE_DIVIDER-APU_PULSE1}
lda ]1+{APU_PULSE1_ENVELOPE-APU_PULSE1}
bne tick_envelope
lda ]1+{APU_PULSE1_REG1-APU_PULSE1} ; if decay level counter is 0, check the loop bit and set counter to 15 if loop bit is set
bit #PULSE_HALT_FLAG
beq envelope_out
lda #16 ; Set to 15
tick_envelope
dec
sta ]1+{APU_PULSE1_ENVELOPE-APU_PULSE1}
envelope_out <<<
half_frame_clock
; clock the length counters
clock_length_counter APU_PULSE1;#PULSE_HALT_FLAG
clock_length_counter APU_PULSE2;#PULSE_HALT_FLAG
clock_length_counter APU_TRIANGLE;#TRIANGLE_HALT_FLAG
clock_length_counter APU_NOISE;#NOISE_HALT_FLAG
; clock the sweep units
clock_sweep APU_PULSE1;0
clock_sweep APU_PULSE2;1
rts
quarter_frame_clock
; clock the envelopes and triangle linear counter
clock_linear_counter APU_TRIANGLE
clock_envelope APU_PULSE1
clock_envelope APU_PULSE2
clock_envelope APU_NOISE
rts
;-----------------------------------------------------------------------------------------
; interupt handler
;-----------------------------------------------------------------------------------------
apu_frame_steps equ 5
PULSE_HALT_FLAG equ $20
NOISE_HALT_FLAG equ $20 ; noise and pulse channels have halt flagin same bit position in REG1
PULSE_CONST_VOL_FLAG equ $10
NOISE_CONST_VOL_FLAG equ $10
TRIANGLE_HALT_FLAG equ $80
mx %11
interrupt_handler = *
ldal show_border
beq :no_show
ldal $E0C034 ; save the border color
stal border_color
lda #1
jsr setborder
:no_show
phb
phd
phk
plb
clc
xce
pea $c000
pld
; Make sure it's the oscillator we care about
ldal osc_interrupt ; which oscillator generated the interrupt?
and #%00111110
cmp #2*interrupt_oscillator
beq *+5
brl :not_timer ; Only service timer interrupts
; Update the frame counter. We double-count so that frame counter can be used directly to dispatch to the
; appropriate tick handler
ldx apu_frame_counter
inx
inx
cpx #2*apu_frame_steps ; TODO: This is set by MSB in $4017 (4 or 5). 4 = PAL, 5 = NTSC.
bcc *+4
ldx #0
stx apu_frame_counter
jmp (:frame_counter_proc,x)
:frame_counter_proc da :quarter_frame,:half_frame,:quarter_frame,:no_frame,:half_frame
; Full speed emulation (240Hz)
:half_frame jsr half_frame_clock
:quarter_frame jsr quarter_frame_clock
; Half-speed interrupts (120Hz) -- clock twice in each handler
;:half_frame
;:quarter_frame
; jsr half_frame_clock
; jsr quarter_frame_clock
; jsr quarter_frame_clock
; Quarter-speed interrupts (60Hz) -- clock four times in each handler
;:half_frame
;:quarter_frame
; jsr half_frame_clock
; jsr quarter_frame_clock
; jsr quarter_frame_clock
; jsr half_frame_clock
; jsr quarter_frame_clock
; jsr quarter_frame_clock
; Apply any changes to the DOC registers
jsr access_doc_registers
; Set the parameters for the first square wave channel.
;
; First, set the frequency, if the period is <8 then the pulse channel is muted,
; to test that first
lda APU_PULSE1_MUTE ; If the sweep muted the channel, no output
bne :mute_pulse1
lda APU_PULSE1_LENGTH_COUNTER ; If the length counter is zero, no output
beq :mute_pulse1
rep #$30
lda APU_PULSE1_CURRENT_PERIOD
cmp #8
bcc :mute_pulse1
cmp _apu_pulse1_last_period ; it's expensive to recalc frequencies, so avoid it when possible
beq :freq_end_pulse1
sta _apu_pulse1_last_period
jsr get_pulse_freq ; return freq in 16-bit accumulator
sep #$30
ldx #$00+pulse1_oscillator
stx sound_address
sta sound_data
ldx #$20+pulse1_oscillator
stx sound_address
xba
sta sound_data
:freq_end_pulse1 sep #$30 ; redundent, but avoids extra branches
lda #$80+pulse1_oscillator
sta sound_address
lda APU_PULSE1_REG1 ; Get the cycle duty bits
jsr set_pulse_duty_cycle
lda #$40+pulse1_oscillator
sta sound_address
lda APU_PULSE1_REG1
bit #PULSE_CONST_VOL_FLAG ; Check the constant volume bit
bne :set_volume_pulse1
lda APU_PULSE1_ENVELOPE
bra :set_volume_pulse1
:mute_pulse1
sep #$30
lda #$40+pulse1_oscillator
sta sound_address
lda #0
:set_volume_pulse1 jsr set_pulse_volume
; Now do the second square wave
lda APU_PULSE2_MUTE ; If the sweep muted the channel, no output
bne :mute_pulse2
lda APU_PULSE2_LENGTH_COUNTER ; If the length counter is zero, no output
beq :mute_pulse2
rep #$30
lda APU_PULSE2_CURRENT_PERIOD
cmp #8
bcc :mute_pulse2
cmp _apu_pulse2_last_period
beq :freq_end_pulse2
sta _apu_pulse2_last_period
jsr get_pulse_freq ; return freq in 16-bic accumulator
sep #$30
ldx #$00+pulse2_oscillator
stx sound_address
sta sound_data
ldx #$20+pulse2_oscillator
stx sound_address
xba
sta sound_data
:freq_end_pulse2 sep #$30
lda #$80+pulse2_oscillator
sta sound_address
lda APU_PULSE2_REG1 ; Get the cycle duty bits
jsr set_pulse_duty_cycle
lda #$40+pulse2_oscillator
sta sound_address
lda APU_PULSE2_REG1
bit #PULSE_CONST_VOL_FLAG ; Check the constant volume bit
bne :set_volume_pulse2
lda APU_PULSE2_ENVELOPE
bra :set_volume_pulse2
:mute_pulse2
sep #$30
lda #$40+pulse2_oscillator
sta sound_address
lda #0
:set_volume_pulse2 jsr set_pulse_volume
; Now the triangle wave. This wave needs linear counter support to be silenced
lda APU_TRIANGLE_LENGTH_COUNTER ; If the length counter is zero, no output
beq :mute_triangle
lda APU_TRIANGLE_LINEAR_COUNTER ; If the linear counter is zero, no output
beq :mute_triangle
rep #$30
lda APU_TRIANGLE_CURRENT_PERIOD
cmp #2
bcc :mute_triangle
; NOTE on Triangle channel frequence from https://www.nesdev.org/wiki/APU_Triangle
;
; Unlike the pulse channels, the triangle channel supports frequencies up to the maximum frequency the
; timer will allow, meaning frequencies up to fCPU/32 (about 55.9 kHz for NTSC) are possible - far above
; the audible range. Some games, e.g. Mega Man 2, "silence" the triangle channel by setting the timer to
; zero, which produces a popping sound when an audible frequency is resumed, easily heard e.g. in Crash
; Man's stage. At the expense of accuracy, these can be eliminated in an emulator e.g. by halting the
; triangle channel when an ultrasonic frequency is set (a timer value less than 2).
cmp _apu_triangle_last_period
beq :freq_end_triangle
sta _apu_triangle_last_period
jsr get_pulse_freq ; return freq in 16-bic accumulator
lsr
sep #$30
ldx #$00+triangle_oscillator
stx sound_address
sta sound_data
ldx #$20+triangle_oscillator
stx sound_address
xba
sta sound_data
:freq_end_triangle sep #$30
lda #$40+triangle_oscillator
sta sound_address
lda #12 ; Triangle is a bit softer than pulse channels
bra :set_volume_triangle
:mute_triangle
sep #$30
lda #$40+triangle_oscillator
sta sound_address
lda #0
:set_volume_triangle jsr set_pulse_volume
; Now the noise channel. It's mixer volume output is ~half of the pulse channels
lda APU_NOISE_LENGTH_COUNTER ; If the length counter is zero, no output
beq :mute_noise
ldx #$00+noise_oscillator
stx sound_address
lda APU_NOISE_CURRENT_PERIOD
sta sound_data
ldx #$20+noise_oscillator
stx sound_address
lda APU_NOISE_CURRENT_PERIOD+1
sta sound_data
lda #$40+noise_oscillator
sta sound_address
lda APU_NOISE_REG1
bit #NOISE_CONST_VOL_FLAG ; Check the constant volume bit
bne :set_volume_noise
lda APU_NOISE_ENVELOPE
bra :set_volume_noise
:mute_noise
lda #$40+noise_oscillator
sta sound_address
lda #0
:set_volume_noise
and #$0F
asl
asl
asl
pha
lda APU_NOISE_REG3 ; Up the volume for low sounds
bit #$08
beq :high_pitch
pla
asl
pha
:high_pitch pla
sta sound_data
:no_frame
:not_timer
ldal show_border
beq :no_show2
ldal border_color
jsr setborder
:no_show2
pld
plb
clc
rtl
set_pulse_duty_cycle
mx %11
rol
rol
rol
and #$03
tax
lda duty_cycle_page,x
sta sound_data
rts
set_pulse_volume
and #$0F
asl
asl
asl
asl
sta sound_data
rts
; This is a bit different because we actually calculate a scan rate directly to match the
; rate at which new samples are read form DOC RAM to the period of the noise channel
;
; IIgs Scan Rate (SR) = 894886 Hz / (OSC + 2) = 894886 Hz / 34 = 26320.1765 samples / sec
; IIgs Sample Rate = 51.406 * F_HL samples / sec
; We have 256 samples
; NES Noise Sample Rate = 1789772 Hz / P
;
; An as example, let P = 8, so a new sample should be output
; Solving for F_HL: F_HL = 1789772 / (51.406 * 8) = 4352
get_noise_freq
; NES freq = f_CPU / (16 * (t + 1))
; = 1.789773 MHz / (16 * (t + 1))
; = 111860.812 Hz / (t + 1)
;
; IIgs freq = 0.200807 * F_HL (for 32 oscillators with DOC RES = 0)
;
; Solving for F_HL = (1 / 0.200807) * 111860.812 / (t + 1)
; = 557056.338 / (t + 1)
;
; if t < 8 this value is out of range and the oscillator should be silenced
;
; otherwise, break apart the ratio
;
; f_HL = 10 * (55706 / (t + 1))
;
get_pulse_freq
mx %00
and #$7FF ; prevent overflow...
inc
sta divisor
lda #55706
sta dividend
lda #0
ldx #16 ; 16 bits of division
asl dividend
:dl1 rol
cmp divisor
bcc :dl2
sbc divisor
:dl2 rol dividend
dex
bne :dl1
lda dividend
sta dividend
asl
asl
clc
adc dividend ; multiple by 10 to get the DOC value
asl
rts
turn_off_interrupts
php
sep #$20
lda #$a0+interrupt_oscillator
sta sound_address
lda #0
sta sound_data
plp
rts
; Internal APU registers.
;
; These variables track the internal flags, counters and other status bits that make up
; the core functionality of the different channel hardware
apu_frame_counter dw 0 ; frame counter, clocked at 240Hz from the interrupt handler
duty_cycle_page dfb $01,$02,$03,$04 ; Page of DOC RAM that holds the different duty cycle wavforms
show_border dw 0
border_color dw 0
dividend dw 0 ; Used when converting from NES APU values to DOC values
divisor dw 0
; Pulse Channel 1
APU_PULSE1
APU_PULSE1_REG1 ds 1 ; DDLC NNNN - Duty, length counter halt, constant volume/evelope, envelope period/volume
APU_PULSE1_REG2 ds 1 ; EPPP NSSS - Sweep unit: enabled, period, negative, shift count
APU_PULSE1_REG3 ds 1 ; LLLL LLLL - Timer Low
APU_PULSE1_REG4 ds 1 ; llll lHHH - Length counter load, timer high (also resets duty and starts envelope)
APU_PULSE1_LENGTH_COUNTER dfb 0 ; internal register for the length counter
APU_PULSE1_RELOAD_FLAG dfb 0 ; internal register to reload the sweep divider value
APU_PULSE1_SWEEP_DIVIDER dfb 0 ; internal register to track the sweep divider value
APU_PULSE1_TARGET_PERIOD dw 0 ; internal register to hold the sweep unit target period
APU_PULSE1_CURRENT_PERIOD dw 0 ; internal register to hold the current period driving the oscillator
APU_PULSE1_MUTE dfb 0
APU_PULSE1_START_FLAG dfb 0
APU_PULSE1_ENVELOPE_DIVIDER dfb 0
APU_PULSE1_ENVELOPE dfb 0
_apu_pulse1_last_period dw $FFFF ; optimization
APU_PULSE2
APU_PULSE2_REG1 ds 1 ; DDLC NNNN - Duty, length counter halt, constant volume/evelope, envelope period/volume
APU_PULSE2_REG2 ds 1 ; EPPP NSSS - Sweep unit: enabled, period, negative, shift count
APU_PULSE2_REG3 ds 1 ; LLLL LLLL - Timer Low
APU_PULSE2_REG4 ds 1 ; llll lHHH - Length counter load, timer high (also resets duty and starts envelope)
APU_PULSE2_LENGTH_COUNTER dfb 0 ; internal register for the length counter
APU_PULSE2_RELOAD_FLAG dfb 0 ; internal register to reload the sweep divider value
APU_PULSE2_SWEEP_DIVIDER dfb 0 ; internal register to track the sweep divider value
APU_PULSE2_TARGET_PERIOD dw 0 ; internal register to hold the sweep unit target period
APU_PULSE2_CURRENT_PERIOD dw 0 ; internal register to hold the current period driving the oscillator
APU_PULSE2_MUTE dfb 0
APU_PULSE2_START_FLAG dfb 0
APU_PULSE2_ENVELOPE_DIVIDER dfb 0
APU_PULSE2_ENVELOPE dfb 0
_apu_pulse2_last_period dw $FFFF ; optimization
APU_TRIANGLE
APU_TRIANGLE_REG1 ds 1 ; DDLC NNNN - Duty, loop envelope/disable length counter, constant volume, envelope period/volume
APU_TRIANGLE_REG2 ds 1 ; EPPP NSSS - Sweep unit: enabled, period, negative, shift count
APU_TRIANGLE_REG3 ds 1 ; LLLL LLLL - Timer Low
APU_TRIANGLE_REG4 ds 1 ; llll lHHH - Length counter load, timer high (also resets duty and starts envelope)
APU_TRIANGLE_LENGTH_COUNTER dfb 0
APU_TRIANGLE_CURRENT_PERIOD dw 0
APU_TRIANGLE_START_FLAG dfb 0
APU_TRIANGLE_LINEAR_COUNTER dfb 0
_apu_triangle_last_period dw $FFFF ; optimization
APU_NOISE
APU_NOISE_REG1 ds 1 ; --LC NNNN - length counter halt, constant volume/evelope, envelope period/volume
APU_NOISE_REG2 ds 1 ; ---- ---- - Unused
APU_NOISE_REG3 ds 1 ; M--- PPPP - Mode and period lookup
APU_NOISE_REG4 ds 1 ; llll l--- - Length counter load
APU_NOISE_LENGTH_COUNTER dfb 0 ; internal register for the length counter
APU_NOISE_RELOAD_FLAG dfb 0 ; unused
APU_NOISE_SWEEP_DIVIDER dfb 0 ; unused
APU_NOISE_TARGET_PERIOD dw 0 ; unused
APU_NOISE_CURRENT_PERIOD dw 0 ; internal register to hold the current period driving the oscillator
APU_NOISE_MUTE dfb 0 ; unused
APU_NOISE_START_FLAG dfb 0
APU_NOISE_ENVELOPE_DIVIDER dfb 0
APU_NOISE_ENVELOPE dfb 0
_apu_noise_last_period dw $FFFF ; optimization
APU_STATUS ds 1
mx %11
APU_PULSE1_REG1_WRITE ENT
stal APU_PULSE1_REG1
rtl
APU_PULSE1_REG2_WRITE ENT
php
pha
stal APU_PULSE1_REG2
lda #1
stal APU_PULSE1_RELOAD_FLAG ; mark that this register was written to
pla
plp
rtl
APU_PULSE1_REG3_WRITE ENT
stal APU_PULSE1_CURRENT_PERIOD
stal APU_PULSE1_REG3
rtl
APU_PULSE1_REG4_WRITE ENT
php
phx
pha
stal APU_PULSE1_REG4
and #$07
stal APU_PULSE1_CURRENT_PERIOD+1
; If the APU_STATUS bit is enabled, then load the length counter
ldal APU_STATUS
bit #$01
beq :no_reload
ldal APU_PULSE1_REG4
and #$F8
lsr
lsr
lsr
tax
ldal LengthTable,x
stal APU_PULSE1_LENGTH_COUNTER ; Immediately start the counter
lda #1
stal APU_PULSE1_START_FLAG
:no_reload
pla
plx
plp
rtl
; From https://www.nesdev.org/wiki/APU_Length_Counter
LengthTable
db 10,254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14
db 12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30
APU_PULSE2_REG1_WRITE ENT
stal APU_PULSE2_REG1
rtl
APU_PULSE2_REG2_WRITE ENT
php
pha
stal APU_PULSE2_REG2
lda #1
stal APU_PULSE2_RELOAD_FLAG
pla
plp
rtl
APU_PULSE2_REG3_WRITE ENT
stal APU_PULSE2_CURRENT_PERIOD
stal APU_PULSE2_REG3
rtl
APU_PULSE2_REG4_WRITE ENT
php
phx
pha
stal APU_PULSE2_REG4
and #$07
stal APU_PULSE2_CURRENT_PERIOD+1
ldal APU_STATUS
bit #$02
beq :no_reload
ldal APU_PULSE2_REG4
and #$F8
lsr
lsr
lsr
tax
ldal LengthTable,x
stal APU_PULSE2_LENGTH_COUNTER ; Immediately start the counter
lda #1
stal APU_PULSE2_START_FLAG
:no_reload
pla
plx
plp
rtl
APU_TRIANGLE_REG1_WRITE ENT
stal APU_TRIANGLE_REG1
rtl
APU_TRIANGLE_REG2_WRITE ENT
stal APU_TRIANGLE_REG2
rtl
APU_TRIANGLE_REG3_WRITE ENT
stal APU_TRIANGLE_CURRENT_PERIOD
stal APU_TRIANGLE_REG3
rtl
APU_TRIANGLE_REG4_WRITE ENT
php
phx
pha
stal APU_TRIANGLE_REG4
and #$07
stal APU_TRIANGLE_CURRENT_PERIOD+1
ldal APU_STATUS
bit #$04
beq :no_reload
ldal APU_TRIANGLE_REG4
and #$F8
lsr
lsr
lsr
tax
ldal LengthTable,x
stal APU_TRIANGLE_LENGTH_COUNTER ; Immediately start the counter
lda #1
stal APU_TRIANGLE_START_FLAG
:no_reload
pla
plx
plp
rtl
APU_NOISE_REG1_WRITE ENT
stal APU_NOISE_REG1
rtl
APU_NOISE_REG2_WRITE ENT
stal APU_NOISE_REG2
rtl
APU_NOISE_REG3_WRITE ENT
php
phx
pha
stal APU_NOISE_REG3
and #$0F
asl
tax
; ldal NoisePeriodTable,x
ldal EsqNoiseFreqTable,x
stal APU_NOISE_CURRENT_PERIOD
; ldal NoisePeriodTable+1,x
ldal EsqNoiseFreqTable+1,x
stal APU_NOISE_CURRENT_PERIOD+1
pla
plx
plp
rtl
APU_NOISE_REG4_WRITE ENT
php
phx
pha
stal APU_NOISE_REG4
ldal APU_STATUS
bit #$08
beq :no_reload
ldal APU_NOISE_REG4
and #$F8
lsr
lsr
lsr
tax
ldal LengthTable,x
stal APU_NOISE_LENGTH_COUNTER ; Immediately start the counter
lda #1
stal APU_NOISE_START_FLAG
:no_reload
pla
plx
plp
rtl
; Lookup from bottom 4 bits of NOISE_REG3 and pre-calculated ensoniq parameters
NoisePeriodTable dw 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068
;EsqNoiseFreqTable dw 8704, 4352, 2176, 1088, 544, 363, 272, 218, 172, 137, 92, 69, 46, 34, 17, 9
EsqNoiseFreqTable dw 1088, 544, 272, 136, 68, 45,34,27,22,17,12,9,6,4,2,1
APU_FORCE_OFF dw 0
APU_STATUS_FORCE
phb
phk
plb
pha
bra force_entry
APU_STATUS_WRITE ENT
phb
phk
plb
pha
phx
ldx APU_FORCE_OFF
bne force_exit
plx
force_entry
sta APU_STATUS
; From NESDev Wiki: When the enabled bit is cleared (via $4015), the length counter is forced to 0
; and cannot be changed until enabled is set again (the length counter's previous value is lost).
; There is no immediate effect when enabled is set.
; Pulse 1
bit #$01
bne :pulse1_on
stz APU_PULSE1_LENGTH_COUNTER
:pulse1_on
; Pulse 2
bit #$02
bne :pulse2_on
stz APU_PULSE2_LENGTH_COUNTER
:pulse2_on
; Triangle
bit #$04
bne :triangle_on
stz APU_TRIANGLE_LENGTH_COUNTER
:triangle_on
; Noise
bit #$08
bne :noise_on
stz APU_NOISE_LENGTH_COUNTER
:noise_on
pla
plb
rtl
force_exit
plx
pla
plb
rtl