; ---------------------------------------------------------------------------
; SFX player (c) 2013 SyX
; Based in the Minimal ayFX player v0.15 by Shiru.
;
; Simply SFX player using one sound chip and the next channel selection 
; priority:
; 1.- If we select the channel, the sound already playing is replaced by 
; the new one.
;
; 2.- If the SFX is free to choose the channel and all the channels are 
; busy, then it's choosen the channel that it has been more time sounding.
;
; Initialization:
; LD   A,Sound_Chip      ; (0 -> PSG | 1 -> Left YMZ | 2 -> Right YMZ)
; LD   HL,SFX_Bank_address
; CALL SFX_Init
;
; Send SFX to be played:
; LD   A,Channel_Number  ; (0 = Free | 1 = A | 2 = B | 3 = C)
; LD   L,SFX_Number      ; (0...255)
; CALL SFX_Send
;
; For playing the SFXs, you must call during the interrupt handler:
; CALL SFX_Play
; ---------------------------------------------------------------------------

; SFX Bank structure:
; -------------------
; 1.- Number of SFXs in the bank (1 byte)
; 2.- Table with the offsets to the SFX (2 bytes * SFX)
; 3.- SFXs

; SFX format:
; -----------
; We encode each interrupt (50Hz rate) with a sequence of bytes, the length of the 
; sequence is variable and depends on the changes in the tone/noise/volume/channel.
;
; The format of bytes in the sequence is:
; %DN N P DC V3 V2 V1 V0
; V3-V0  -> New Volume
; DC = 1 -> Disable Channel
; P = 1  -> The next two bytes are the new period in that channel
; N = 1  -> The next byte is the new noise period
; DN = 1 -> Disable noise in that channel
;
; If P and N are 1, the period follows first and then the noise.
;
; The end SFX marker is the sequence $D0 (%11010000 Disable Channel, Disable Noise
; and get new noise period), $20 (%00100000 Noise period $20, no valid period).
;
; SFX_Channel_Buffer structure:
; -----------------------------
; 4 bytes for channel:
; 0-1 SFX current address (If high byte is 0, then the channel is free)
; 2-3 SFX playing time in frames

    ALIGN 4                     ; Align to an address 16 bytes multiple, for 8 bits INCs
SFX_Channel_Buffer
    DEFS 3 * 4

; ---------------------------------------------------------------------------
; Initialize SFX player.
; Turn off the channels and set the variables.
; ENTRIES: 
;    HL = Bank SFX Address
;     A = 0 -> PSG | 1 -> Left YMZ | 2 -> Right YMZ
; ---------------------------------------------------------------------------
SFX_Init
    INC  HL                     ; Skip number of SFXs in the bank
    
    ; Save the pointer to the SFX offsets table
    LD   (SFX_Send.sm_sfx_bank_address + 1),HL

    ; Mark all channels as empty
    LD   HL,SFX_Channel_Buffer
    LD   DE,$00FF
    LD   B,12
.loop_clean_sfx_channel_buffer
    LD   (HL),D
    INC  L
    LD   (HL),D                 ; Channel Empty
    INC  L
    LD   (HL),E
    INC  L
    LD   (HL),E                 ; SFX playing time maximum 
    INC  L
    DJNZ .loop_clean_sfx_channel_buffer

    ; Which sound chip need to be initialized?
    OR   A
    JR   Z,.initialize_psg
.initialize_ymzs
    ; --------------------------
    ; Initialization of the YMZs
    ; --------------------------
    DEC  A
    JR   NZ,.is_right_ymz
.is_left_ymz
    LD   HL,YMZ_SELECT_LEFT 
    JR   .set_select_ymz
.is_right_ymz
    LD   HL,YMZ_SELECT_RIGHT
.set_select_ymz
    LD   (write_ymz.sm_ymz_select + 1),HL
    
    ; Initialize the YMZ registers
    LD   B,H
    LD   C,L
    LD   A,$D
.loop_init_ymz_registers
    OUT  (C),A                  ; Register
    DEC  B
    CP   7
    JR   NZ,.send_zero_ymz
    LD   A,$3F
    OUT  (C),A
    LD   A,6
    JR   .loop_init_ymz_registers
.send_zero_ymz    
    DEFB $ED,$71                ; OUT (C),0 
    DEC  A
    JP   P,.loop_init_ymz_registers

    ; Patch the play routines for using the YMZ
    LD   HL,write_ymz
    LD   (SFX_Play.sm_write_psg_01 + 1),HL
    LD   (SFX_Play.sm_write_psg_02 + 1),HL
    LD   (SFX_Play.sm_write_psg_03 + 1),HL
    LD   (SFX_Play.sm_write_psg_04 + 1),HL
    LD   (SFX_Play.sm_write_psg_05 + 1),HL
    
    ; Reset the variable with the noise period (R6) and the mixers (R7)
    LD   HL,$3F00
    LD   (SFX_Play.sfx_noise_and_mixers + 1),HL
    RET

.initialize_psg
    ; -------------------------
    ; Initialization of the PSG
    ; -------------------------
    ; Set the PPI_A in output mode
    LD   BC,PPI_CONTROL + PPI_A_OUTPUT
    OUT  (C),C

    ; PSG Inactive (Needed in CPC+)
    DEC  B                      ; B = PPI_C
    DEFB $ED,$71                ; OUT (C),0 

    ; Using write_psg
    LD   A,PSG_REG_0D
    LD   E,0
.loop_internal_ay
    CP   PSG_REG_07
    JR   NZ,.send_zero_psg
    LD   E,$FF
    JR   .send_byte 
.send_zero_psg
    LD   E,0
.send_byte
    CALL write_psg
    DEC  A
    JP   P,.loop_internal_ay

    ; Patch routines for using the PSG
    LD   HL,write_psg
    LD   (SFX_Play.sm_write_psg_01 + 1),HL
    LD   (SFX_Play.sm_write_psg_02 + 1),HL
    LD   (SFX_Play.sm_write_psg_03 + 1),HL
    LD   (SFX_Play.sm_write_psg_04 + 1),HL
    LD   (SFX_Play.sm_write_psg_05 + 1),HL

    ; Reset the variable with the noise period (R6) and the mixers (R7)
    LD   HL,$FF00
    LD   (SFX_Play.sfx_noise_and_mixers + 1),HL
    RET

; ---------------------------------------------------------------------------
; Play the current frame of SFXs.
;
; The format of bytes in the sequence is:
; %DN N P DC V3 V2 V1 V0
; V3-V0  -> New Volume
; DC = 1 -> Disable Channel
; P = 1  -> The next two bytes are the new period in that channel
; N = 1  -> The next byte is the new noise period
; DN = 1 -> Disable noise in that channel
;
; If P and N are 1, the period follows first and then the noise.
;
; The end SFX marker is the sequence $D0 (%11010000 Disable Channel, disable Noise
; and get new noise period), $20 (%00100000 Noise period $20, no valid period).
;
; SFX_Channel_Buffer structure:
; -----------------------------
; 4 bytes for channel:
; 0-1 SFX current address (If high byte is 0, then the channel is free)
; 2-3 SFX playing time in frames
; ---------------------------------------------------------------------------
SFX_Play
    LD   BC,$03FD
    LD   IX,SFX_Channel_Buffer
.loop_sfx_play
    PUSH BC

    ; Check for END of SFX reached in a previous frame
    LD   A,11                   ; Envelope register 11 (first not used by the SFXs)
    LD   H,(IX + 1)             ; Comparison of byte address to <11
    CP   H
    JR   NC,.next_channel       ; Channel is not playing, skipping
    LD   L,(IX + 0)

    ; Get next SFX data byte
    LD   E,(HL)
    INC  HL

    ; Get channel volume register (A = 11, B = Channel processing)
    SUB  B                      ; A = Volume register | (8, 9, 10)

    ; Mask Volume
    LD   D,A

    LD   A,E
    AND  $0F
    LD   C,E                    ; Save E
    LD   E,A

    LD   A,D
   
    ; Send Volume
.sm_write_psg_01
    CALL $0000

    ; Check changes in tone period
    LD   E,C                    ; Restore E ***
    BIT  5,E                    ; Change the period of the tone?
    JR   Z,.not_change_period

    ; Get channel period tone register (D = Channel processing)
    LD   A,3
    SUB  D
    ADD  A,A                    ; A = Tone period register | (0, 2, 4)

    LD   E,(HL)
    INC  HL

    ; Send Fine Tune
.sm_write_psg_02
    CALL $0000

    ; Get coarse tune
    INC  A
    LD   E,(HL)
    INC  HL

    ; Send Coarse Tune
.sm_write_psg_03
    CALL $0000

    LD   E,C                    ; Restore E
.not_change_period

    ; Check changes in noise period
    BIT  6,E                    ; Change the period of the noise?
    JR   Z,.set_mixer_state

    LD   A,(HL)                 ; Read the value of the noise
    SUB  $20                    ; <$20, get the same lower 5 bits (Noise period)
    JR   C,.update_noise        ; Less than $20, play on
.stop_playing_sfx
    LD   H,A                    ; H = 0 (High byte 0 -> Channel Free)
    LD   B,$FF
    LD   B,C                    ; BC = $FFFF(Max. playing time)
    JR   .update_channel_buffer

.update_noise
    INC  HL
    LD   (SFX_Play.sfx_noise_and_mixers + 1),A

.set_mixer_state
    ; A = %01101111 | E = %DN N P DC V3 V2 V1 V0
    ; B = 3 : A %11110110 -> Enable noise and tone A : E %V3 V2 V1 V0 DN N P DC
    ; B = 2 : A %11101101 -> Enable noise and tone B : E %V2 V1 V0 DN N P DC V3
    ; B = 1 : A %11011011 -> Enable noise and tone C : E %V1 V0 DN N P DC V3 V2

    ; B is not changed, remain with the channel number
    INC  B                      ; Number of changes for flags TN
    LD   A,%01101111            ; Mask flags TN
.loop_get_mixer_mask_for_channel
    RRC  E
    RRCA
    DJNZ .loop_get_mixer_mask_for_channel

    ; Set Mixer state
    LD   D,A
    LD   BC,SFX_Play.sfx_noise_and_mixers + 2
    LD   A,(BC)
    XOR  E
    AND  D
    XOR  E
    LD   (BC),A

.increase_sfx_playing_time
    LD   C,(IX + 2)
    LD   B,(IX + 3)
    INC  BC

.update_channel_buffer
    LD   (IX + 2),C
    LD   (IX + 3),B             ; SFX playing time

    LD   (IX + 0),L
    LD   (IX + 1),H             ; SFX pointer

.next_channel
    LD   BC,4                   ; Go to the next channel
    ADD  IX,BC
    POP  BC
    DJNZ .loop_sfx_play

.sfx_noise_and_mixers
    LD   DE,0                   ; E = Noise period | D = Mixers
    LD   A,6
.sm_write_psg_04
    CALL $0000
    LD   E,D
    INC  A
.sm_write_psg_05
    JP   $0000

; ---------------------------------------------------------------------------
; Write a value in the PSG
; ENTRIES:
;    A = PSG Register
;    E = Value
;---------------------------------------------------------------------------                  
write_psg
    PUSH BC
    LD   B,>PPI_A
    OUT  (C),A                      ; Register
    LD   BC,PPI_C + PPI_PSG_SELECT
    OUT  (C),C
    DEFB $ED,$71                    ; OUT (C),0
    LD   B,>PPI_A
    OUT  (C),E                      ; Value
    LD   BC,PPI_C + PPI_PSG_WRITE
    OUT  (C),C
    DEFB $ED,$71                    ; OUT (C),0
    POP  BC
    RET

write_ymz
    PUSH BC
.sm_ymz_select
    LD   BC,$0000
    OUT  (C),A                  ; Register
    DEC  B
    OUT  (C),E                  ; Value
    POP  BC
    RET

; ---------------------------------------------------------------------------
; Send SFX to the channel selected or the first free channel.
;
; If we select the channel, the sound already playing is replaced by the
; new one.
;
; If the SFX is free to choose the channel and all the channels are busy, 
; then it's choosen the channel that it has been more time sounding.
;
; ENTRIES:
;     A = Channel number (0 = Free | 1 = A | 2 = B | 3 = C)
;     L = SFX number (0...255)
; ---------------------------------------------------------------------------
SFX_Send
    ; Get the SFX address
    LD   H,0
    ADD  HL,HL
.sm_sfx_bank_address
    LD   BC,0                   ; SFX offsets table
    ADD  HL,BC
    LD   C,(HL)
    INC  HL
    LD   B,(HL)                 ; BC = Offset
    DEC  HL
    ADD  HL,BC                  ; HL = SFX address

    ; Get the channel
    OR   A
    JR   Z,.find_empty_channel
    DEC  A
    JR   NZ,.is_b_or_c
.is_a
    LD   DE,SFX_Channel_Buffer
    JR   .insert_sfx

.is_b_or_c
    DEC  A
    JR   NZ,.is_c
.is_b
    LD   DE,SFX_Channel_Buffer + 4
    JR   .insert_sfx

.is_c
    DEC  A
    LD   DE,SFX_Channel_Buffer + 4 * 2
    
    ; Insert SFX in the channel selected
.insert_sfx
    EX   DE,HL
    ; Set SFX pointer
    LD   (HL),E
    INC  L
    LD   (HL),D
    INC  L
    ; Initialize playing time
    LD   (HL),A
    INC  L
    LD   (HL),A
    RET

    ; Find a free channel for the new SFX
.find_empty_channel
    PUSH HL                     ; Save the SFX address
    LD   HL,SFX_Channel_Buffer
    LD   BC,0                   ; Init value for the search
    LD   IXL,3
.loop_find_empty_channel
    PUSH HL
    INC  L
    LD   A,(HL)
    OR   A
    JR   Z,.empty_channel       ; Empty channel?
    INC  L
    INC  L
    LD   A,B
    CP   (HL)
    JR   C,.select_channel_h

    JR   NZ,.next_channel
    DEC  L
    LD   A,C
    CP   (HL)
    JR   C,.select_channel_l
    INC  L
    JR   .next_channel
    
.select_channel_h
    DEC  L

.select_channel_l    
    LD   C,(HL)                 ; Update timing to the actual largest
    INC  L
    LD   B,(HL)
    POP  DE                     ; Update pointer to the selected channel
    JR   .skip_stack_pop

.next_channel
    POP  AF                     ; Skip old pointer to the channel
    
.skip_stack_pop    
    INC  L
    DEC  IXL
    JR   NZ,.loop_find_empty_channel
.end_loop_find_empty_channel
    POP  HL                     ; DE = Selected Channel Pointer | HL = SFX Pointer
    JR   .insert_sfx
.empty_channel
    POP  DE
    JR   .end_loop_find_empty_channel
