//
// Real spectrum analysis driving the visualizer.
//
// Each frame we pull FFT_SIZE PCM frames around the active stream's
// play_cursor, mix to mono, window with Hann, FFT, then bin the magnitudes
// into SPECTRUM_BINS log-spaced bands. The result lives in `app.spectrum`,
// where `gfx/shaders.jai` reads it for the bar visualizer.
//
// Behaviour at edges:
//   - no stream playing → bars decay smoothly to zero
//   - paused (play_cursor frozen) → spectrum still decays so the screen
//     visibly stops reacting
//   - OGG / non-PCM data → we don't have direct sample access, so spectrum
//     decays. (All our /universal-fetched tracks decode through dr_libs
//     into LINEAR_SAMPLE_ARRAY anyway, so this rarely fires in practice.)
//

MIN_VISUAL_FREQ :: 30.0;   // skip DC and very-low rumble bins

update_audio_analysis :: () {
    fft_init();

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

    sd := app.current_stream.sound_data;
    if sd.type != Sound.Sound_Data.Kind.LINEAR_SAMPLE_ARRAY {
        decay_spectrum(0.10);
        return;
    }

    nchannels := cast(s64) sd.nchannels;
    if nchannels < 1 {
        decay_spectrum(0.10);
        return;
    }
    total_frames := sd.nsamples_times_nchannels / nchannels;

    cursor_frame := cast(s64) app.current_stream.play_cursor;
    start := cursor_frame - FFT_SIZE / 2;
    if start < 0  start = 0;
    if start + FFT_SIZE > total_frames  start = total_frames - FFT_SIZE;
    if start < 0 {
        // track shorter than the FFT window
        decay_spectrum(0.10);
        return;
    }

    // Mix down to mono and apply window.
    samples := sd.samples;
    inv_chan := 1.0 / cast(float) nchannels;
    for k: 0..FFT_SIZE-1 {
        idx := (start + k) * nchannels;
        sum: float = 0;
        for ch: 0..nchannels-1  sum += cast(float) samples[idx + ch];
        mono := sum * inv_chan / 32768.0;   // s16 → [-1, 1]
        fft_re[k] = mono * fft_window[k];
        fft_im[k] = 0;
    }

    fft();

    // Bin into log-spaced bands.
    rate     := cast(float) sd.sampling_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;
        }

        // Normalize by FFT_SIZE/4 (a Hann-windowed unit sine peaks around
        // FFT_SIZE/4 in this convention) and compress with sqrt to match
        // perceptual loudness better.
        v := peak / (cast(float) FFT_SIZE * 0.25);
        v = sqrt(v);
        if v > 1  v = 1;

        // Asymmetric smoothing: fast attack, slow decay (classic
        // VU-meter feel).
        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

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