; 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