//
// Real spectrum analysis driving the visualizer.
//
// Since we now play OGG_COMPRESSED (Sound_Player has no public PCM output),
// we maintain a separate stb_vorbis decoder in app.analysis_vorbis. Each
// frame we seek it to the current play_cursor position and decode FFT_SIZE
// samples for the FFT. Forward seeks (normal playback) are near-instant since
// they just continue from the current bitstream position.
//
// play_cursor is in "virtual 44100 Hz sample" units (Sound_Player default);
// we convert to OGG sample position using the vorbis file's actual rate.
//

MIN_VISUAL_FREQ :: 30.0;

update_audio_analysis :: () {
    fft_init();

    if !app.current_stream || !app.current_stream.sound_data || app.paused {
        decay_spectrum(0.10);
        return;
    }

    if !app.analysis_vorbis {
        decay_spectrum(0.10);
        return;
    }

    vorbis := cast(*Stb_Vorbis.stb_vorbis) app.analysis_vorbis;
    info   := Stb_Vorbis.stb_vorbis_get_info(vorbis);
    if info.sample_rate == 0 || info.channels == 0 {
        decay_spectrum(0.10);
        return;
    }

    // Convert play_cursor to real elapsed seconds, then to OGG sample position.
    // play_cursor advances at play_rate * current_rate per second, where
    // current_rate = play_rate / ogg_rate (set by make_stream). So:
    //   time_s  = play_cursor / (play_rate * current_rate)
    //   ogg_pos = time_s * ogg_rate
    sd           := app.current_stream.sound_data;
    play_rate    := cast(float) sd.sampling_rate;     // 44100 default for OGG_COMPRESSED
    ogg_rate     := cast(float) info.sample_rate;     // actual OGG sample rate
    current_rate := app.current_stream.current_rate;
    if current_rate <= 0  { decay_spectrum(0.10); return; }
    time_s     := app.current_stream.play_cursor / (play_rate * current_rate);
    ogg_frame  := cast(s64)(time_s * ogg_rate);
    seek_frame := max(0, ogg_frame - FFT_SIZE / 2);

    Stb_Vorbis.stb_vorbis_seek_frame(vorbis, cast(u32) seek_frame);

    nch     := cast(s32) min(info.channels, 2);
    decoded := Stb_Vorbis.stb_vorbis_get_samples_short_interleaved(
        vorbis, nch, analysis_pcm_buf.data, FFT_SIZE * nch);

    if decoded < FFT_SIZE / 4 {
        decay_spectrum(0.10);
        return;
    }

    // Mix to mono and apply Hann window.
    inv_chan := 1.0 / cast(float) nch;
    for k: 0..FFT_SIZE-1 {
        if k >= decoded { fft_re[k] = 0; fft_im[k] = 0; continue; }
        sum: float = 0;
        for ch: 0..nch-1  sum += cast(float) analysis_pcm_buf[k * nch + ch];
        mono     := sum * inv_chan / 32768.0;
        fft_re[k] = mono * fft_window[k];
        fft_im[k] = 0;
    }

    fft();

    rate     := ogg_rate;
    nyquist  := rate * 0.5;
    log_lo   := log(MIN_VISUAL_FREQ);
    log_hi   := log(nyquist);
    log_step := (log_hi - log_lo) / cast(float) SPECTRUM_BINS;

    for b: 0..SPECTRUM_BINS-1 {
        f_lo := exp(log_lo + log_step * cast(float) b);
        f_hi := exp(log_lo + log_step * cast(float) (b + 1));

        bin_lo := cast(int) (f_lo * FFT_SIZE / rate);
        bin_hi := cast(int) (f_hi * FFT_SIZE / rate);
        if bin_hi <= bin_lo  bin_hi = bin_lo + 1;
        if bin_hi > HALF_FFT  bin_hi = HALF_FFT;
        if bin_lo >= HALF_FFT  continue;

        peak: float = 0;
        for k: bin_lo..bin_hi-1 {
            mag := sqrt(fft_re[k] * fft_re[k] + fft_im[k] * fft_im[k]);
            if mag > peak  peak = mag;
        }

        v := peak / (cast(float) FFT_SIZE * 0.25);
        v = sqrt(v);
        if v > 1  v = 1;

        prev      := app.spectrum[b];
        rate_lerp := ifx v > prev then 0.45 else 0.10;
        app.spectrum[b] = lerp(prev, v, rate_lerp);
    }
}

#scope_file

analysis_pcm_buf: [FFT_SIZE * 2] s16;

decay_spectrum :: (rate: float) {
    for 0..SPECTRUM_BINS-1 {
        app.spectrum[it] = lerp(app.spectrum[it], 0, rate);
    }
}
