;
; This program can stream a regular .wav file from the sdcard.
; It can be uncompressed or IMA-adpcm compressed (factor 4 lossy compression).
; See the "adpcm" module source for tips how to create those files.
;
; Note that 8 bit wav files are *unsigned* values whereas Vera wants *signed* values
; so these have to be converted on the fly. 16 bit wav files are signed already.
;
; The playback is done via AFLOW irq handler that fills the audio fifo buffer
; with around 1 Kb of new audio data. (copies raw pcm data or decodes adpcm block)
; In the meantime the main program loop reads new data blocks from the wav file
; as it is being played.
;
; NOTE: stripping the wav header and just having the raw pcm data in the file
; is slightly more efficient because the data blocks are then sector-aligned on the disk
;

%import diskio
%import floats
%import adpcm
%import wavfile
%import textio
%option no_sysinit

main {

    str MUSIC_FILENAME = "?"*32
    uword vera_rate_hz
    ubyte vera_rate

    sub start() {
        txt.print("name of .wav file to play on drive 8: ")
        while 0==txt.input_chars(MUSIC_FILENAME) {
            ; until user types a name...
        }
        prepare_music()
        txt.print("\ngood file! playback starts! ")
        cx16.rombank(0)                         ; activate kernal bank for faster calls
        interrupts.wait()
        interrupts.set_handler()
        play_stuff()
        txt.print("done!\n")
        repeat { }
    }

    sub error(str msg) {
        txt.print(msg)
        repeat { }
    }

    sub prepare_music() {
        txt.print("\nchecking ")
        txt.print(MUSIC_FILENAME)
        txt.nl()
        bool wav_ok = false
        if diskio.f_open(MUSIC_FILENAME) {
            void diskio.f_read(music.buffer, 128)
            wav_ok = wavfile.parse_header(music.buffer)
            diskio.f_close()
        }
        if not wav_ok
            error("no good wav file!")

        calculate_vera_rate()

        txt.print("wav format: ")
        txt.print_ub(wavfile.wavefmt)
        txt.print("\nchannels: ")
        txt.print_ub(wavfile.nchannels)
        txt.print("\nsample rate: ")
        txt.print_uw(wavfile.sample_rate)
        txt.print("\nbits per sample: ")
        txt.print_uw(wavfile.bits_per_sample)
        txt.print("\ndata size: ")
        txt.print_uwhex(wavfile.data_size_hi, true)
        txt.print_uwhex(wavfile.data_size_lo, false)
        txt.print("\nvera rate: ")
        txt.print_ub(vera_rate)
        txt.print(" = ")
        txt.print_uw(vera_rate_hz)
        txt.print(" hz\n")
        if wavfile.wavefmt==wavfile.WAVE_FORMAT_DVI_ADPCM {
            txt.print("adpcm block size: ")
            txt.print_uw(wavfile.block_align)
            txt.nl()
        }

        if wavfile.nchannels>2 or
           (wavfile.wavefmt!=wavfile.WAVE_FORMAT_DVI_ADPCM and wavfile.wavefmt!=wavfile.WAVE_FORMAT_PCM) or
           wavfile.sample_rate > 48828 or
           wavfile.bits_per_sample>16
                error("unsupported format!")

        if wavfile.wavefmt==wavfile.WAVE_FORMAT_DVI_ADPCM {
            if(wavfile.block_align!=256) {
                error("unsupported block alignment!")
            }
        }

        cx16.VERA_AUDIO_RATE = 0                ; halt playback
        cx16.VERA_AUDIO_CTRL = %10101011        ; mono 16 bit, volume 11
        if wavfile.nchannels==2
            cx16.VERA_AUDIO_CTRL = %10111011    ; stereo 16 bit, volume 11
        if(wavfile.bits_per_sample==8)
            cx16.VERA_AUDIO_CTRL &= %11011111    ; set to 8 bit instead
        repeat 1024
            cx16.VERA_AUDIO_DATA = 0            ; fill buffer with short silence
    }

    sub calculate_vera_rate() {
        const float vera_freq_factor = 25e6 / 65536.0
        vera_rate = (wavfile.sample_rate as float / vera_freq_factor) + 1.0 as ubyte
        vera_rate_hz = (vera_rate as float) * vera_freq_factor as uword
    }

    sub play_stuff() {
        if diskio.f_open(MUSIC_FILENAME) {
            uword block_size = 1024
            if wavfile.wavefmt==wavfile.WAVE_FORMAT_DVI_ADPCM
                block_size = wavfile.block_align * 2      ; read 2 adpcm blocks at a time (512 bytes)
            void diskio.f_read(music.buffer, wavfile.data_offset)       ; skip to actual sample data start
            music.pre_buffer(block_size)
            cx16.VERA_AUDIO_RATE = vera_rate    ; start audio playback

            str progress_chars = "-\\|/-\\|/"
            ubyte progress = 0

            repeat {
                interrupts.wait()
                if interrupts.vsync {
                    interrupts.vsync=false
                    ; ...do something triggered by vsync irq...
                }
                if interrupts.aflow {
                    interrupts.aflow=false
                    if not music.load_next_block(block_size)
                        break
                    ; Note: copying the samples into the fifo buffer is done by the aflow interrupt handler itself.
                    txt.chrout(progress_chars[progress/2 & 7])
                    txt.chrout($9d)     ; cursor left
                    progress++
                }
            }

            diskio.f_close()
        } else {
            error("load error")
        }

        cx16.VERA_AUDIO_RATE = 0                ; halt playback
    }

}


interrupts {

    sub set_handler() {
        sys.set_irqd()
        cx16.CINV = &handler         ; handles both AFLOW and VSYNC
        cx16.VERA_IEN = %00001001    ; enable AFLOW + VSYNC
        sys.clear_irqd()
    }

    bool aflow
    bool vsync

    asmsub wait() {
        %asm {{
            wai
        }}
    }

    sub handler() {
        if cx16.VERA_ISR & %00001000 {
            ; Filling the fifo is the only way to clear the Aflow irq.
            ; So we do this here, otherwise the aflow irq will keep triggering.
            ; Note that filling the buffer with fresh audio samples is NOT done here,
            ; but instead in the main program code that triggers on the 'aflow' being true!
            cx16.save_virtual_registers()
            music.aflow_play_block()
            cx16.restore_virtual_registers()
            aflow = true
        }
        if cx16.VERA_ISR & %00000001 {
            cx16.VERA_ISR = %00000001
            vsync = true
        }

        %asm {{
            ply
            plx
            pla
            rti
        }}
    }

}


music {
    uword @requirezp nibblesptr
    uword buffer = memory("buffer", 1024, 256)

    sub pre_buffer(uword block_size) {
        ; pre-buffer first block
        void diskio.f_read(buffer, block_size)
    }

    sub aflow_play_block() {
        ; play block that is currently in the buffer
        if wavfile.wavefmt==wavfile.WAVE_FORMAT_DVI_ADPCM {
            nibblesptr = buffer
            if wavfile.nchannels==2 {
                adpcm_block_stereo()
                adpcm_block_stereo()
            }
            else {
                adpcm_block_mono()
                adpcm_block_mono()
            }
        }
        else if wavfile.bits_per_sample==16
            uncompressed_block_16()
        else
            uncompressed_block_8()
    }

    sub load_next_block(uword block_size) -> bool {
        ; read next block from disk into the buffer, for next time the irq triggers
        return diskio.f_read(buffer, block_size) == block_size
    }

    asmsub uncompressed_block_8() {
        ; optimized loop to put 1024 bytes of data into the fifo as fast as possible
        ; converting unsigned wav 8 bit samples to signed 8 bit on the fly
        %asm {{
            lda  p8_buffer
            sta  cx16.r0L
            lda  p8_buffer+1
            sta  cx16.r0H
            ldx  #4
-           ldy  #0
-           lda  (cx16.r0),y
            eor  #$80       ; convert to signed 8-bit
            sta  cx16.VERA_AUDIO_DATA
            iny
            bne  -
            inc  cx16.r0H
            dex
            bne  --
            rts
        }}

; original prog8 code:
;        uword @requirezp ptr = main.start.buffer
;        ubyte @requirezp sample
;        repeat 1024 {
;            sample = @(ptr) - 128
;            cx16.VERA_AUDIO_DATA = sample
;            ptr++
;        }
    }

    asmsub uncompressed_block_16() {
        ; optimized loop to put 1024 bytes of data into the fifo as fast as possible
        %asm {{
            lda  p8_buffer
            sta  cx16.r0L
            lda  p8_buffer+1
            sta  cx16.r0H
            ldx  #4
-           ldy  #0
-           lda  (cx16.r0),y
            sta  cx16.VERA_AUDIO_DATA
            iny
            bne  -
            inc  cx16.r0H
            dex
            bne  --
            rts
        }}
; original prog8 code:
;        uword @requirezp ptr = main.start.buffer
;        repeat 1024 {
;            cx16.VERA_AUDIO_DATA = @(ptr)
;            ptr++
;        }
    }

    sub adpcm_block_mono() {
        ; refill the fifo buffer with one decoded adpcm block (1010 bytes of pcm data)
        adpcm.init(peekw(nibblesptr), @(nibblesptr+2))
        cx16.VERA_AUDIO_DATA = lsb(adpcm.predict)
        cx16.VERA_AUDIO_DATA = msb(adpcm.predict)
        nibblesptr += 4
        ubyte @zp nibble
        repeat 252/2 {
            unroll 2 {
                nibble = @(nibblesptr)
                adpcm.decode_nibble(nibble & 15)     ; first word
                cx16.VERA_AUDIO_DATA = lsb(adpcm.predict)
                cx16.VERA_AUDIO_DATA = msb(adpcm.predict)
                adpcm.decode_nibble(nibble>>4)       ; second word
                cx16.VERA_AUDIO_DATA = lsb(adpcm.predict)
                cx16.VERA_AUDIO_DATA = msb(adpcm.predict)
                nibblesptr++
            }
        }
    }

    sub adpcm_block_stereo() {
        ; refill the fifo buffer with one decoded adpcm block (1010 bytes of pcm data)
        adpcm.init(peekw(nibblesptr), @(nibblesptr+2))            ; left channel
        cx16.VERA_AUDIO_DATA = lsb(adpcm.predict)
        cx16.VERA_AUDIO_DATA = msb(adpcm.predict)
        adpcm.init_second(peekw(nibblesptr+4), @(nibblesptr+6))   ; right channel
        cx16.VERA_AUDIO_DATA = lsb(adpcm.predict_2)
        cx16.VERA_AUDIO_DATA = msb(adpcm.predict_2)
        nibblesptr += 8
        repeat 248/8
            decode_nibbles_unrolled()
    }

    sub decode_nibbles_unrolled() {
        ; decode 4 left channel nibbles
        uword[8] left
        uword[8] right
        ubyte @requirezp nibble = @(nibblesptr)
        adpcm.decode_nibble(nibble & 15)     ; first word
        left[0] = adpcm.predict
        adpcm.decode_nibble(nibble>>4)       ; second word
        left[1] = adpcm.predict
        nibble = @(nibblesptr+1)
        adpcm.decode_nibble(nibble & 15)     ; first word
        left[2] = adpcm.predict
        adpcm.decode_nibble(nibble>>4)       ; second word
        left[3] = adpcm.predict
        nibble = @(nibblesptr+2)
        adpcm.decode_nibble(nibble & 15)     ; first word
        left[4] = adpcm.predict
        adpcm.decode_nibble(nibble>>4)       ; second word
        left[5] = adpcm.predict
        nibble = @(nibblesptr+3)
        adpcm.decode_nibble(nibble & 15)     ; first word
        left[6] = adpcm.predict
        adpcm.decode_nibble(nibble>>4)       ; second word
        left[7] = adpcm.predict

        ; decode 4 right channel nibbles
        nibble = @(nibblesptr+4)
        adpcm.decode_nibble_second(nibble & 15)     ; first word
        right[0] = adpcm.predict_2
        adpcm.decode_nibble_second(nibble>>4)       ; second word
        right[1] = adpcm.predict_2
        nibble = @(nibblesptr+5)
        adpcm.decode_nibble_second(nibble & 15)     ; first word
        right[2] = adpcm.predict_2
        adpcm.decode_nibble_second(nibble>>4)       ; second word
        right[3] = adpcm.predict_2
        nibble = @(nibblesptr+6)
        adpcm.decode_nibble_second(nibble & 15)     ; first word
        right[4] = adpcm.predict_2
        adpcm.decode_nibble_second(nibble>>4)       ; second word
        right[5] = adpcm.predict_2
        nibble = @(nibblesptr+7)
        adpcm.decode_nibble_second(nibble & 15)     ; first word
        right[6] = adpcm.predict_2
        adpcm.decode_nibble_second(nibble>>4)       ; second word
        right[7] = adpcm.predict_2
        nibblesptr += 8

        %asm {{
            ; copy to vera PSG fifo buffer
            ldy  #0
-           lda  p8_left,y
            sta  cx16.VERA_AUDIO_DATA
            lda  p8_left+1,y
            sta  cx16.VERA_AUDIO_DATA
            lda  p8_right,y
            sta  cx16.VERA_AUDIO_DATA
            lda  p8_right+1,y
            sta  cx16.VERA_AUDIO_DATA
            iny
            iny
            cpy  #16
            bne  -
        }}
    }

}
