lookas 1.10.0

Perception aligned terminal-based audio spectrum visualizer
Documentation
use crate::dsp::{a_weighting, ema_precomputed, ema_tc};

use super::SpectrumAnalyzer;

pub fn analyze_bands(
    sa: &mut SpectrumAnalyzer,
    dt_s: f32,
    gate_open: bool,
) {
    let filters_len = sa.filters.len();
    accumulate_band_db(sa, dt_s, filters_len);
    update_db_range(sa, filters_len, dt_s);
    normalise_targets(sa, filters_len, gate_open);
}

fn accumulate_band_db(
    sa: &mut SpectrumAnalyzer,
    dt_s: f32,
    filters_len: usize,
) {
    let alpha_eq = (-dt_s / 6.0).exp();

    for (i, tri) in sa.filters.iter().enumerate().take(filters_len) {
        let mut acc = 0.0f32;
        for &(idx, wgt) in &tri.taps {
            if let Some(&val) = sa.spec_pow_smooth.get(idx) {
                acc = val.mul_add(wgt, acc);
            }
        }
        let amp_weighted = acc.sqrt() * a_weighting(tri.center_hz);

        if let Some(eq) = sa.eq_ref.get_mut(i) {
            *eq = ema_precomputed(*eq, amp_weighted, alpha_eq)
                .max(1e-9);
            let rel = amp_weighted / *eq;
            if let Some(target) = sa.bars_target.get_mut(i) {
                *target = 20.0 * rel.max(1e-12).log10();
            }
        }
    }

    if let (Some(dst), Some(src)) = (
        sa.sort_scratch.get_mut(..filters_len),
        sa.bars_target.get(..filters_len),
    ) {
        dst.copy_from_slice(src);
    }
}

#[allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss
)]
fn update_db_range(sa: &mut SpectrumAnalyzer, len: usize, dt_s: f32) {
    if len == 0 {
        return;
    }

    let len_f = len as f32;
    let idx_low = ((len_f - 1.0) * 0.10).round().max(0.0) as usize;
    let idx_high = ((len_f - 1.0) * 0.90).round().max(0.0) as usize;

    let Some(slice) = sa.sort_scratch.get_mut(..len) else {
        return;
    };

    slice.select_nth_unstable_by(idx_low, f32::total_cmp);
    let q10 = slice.get(idx_low).copied();

    #[allow(clippy::arithmetic_side_effects)]
    let split = idx_high.saturating_sub(idx_low);
    let (_, remaining) = slice.split_at_mut(idx_low);
    remaining.select_nth_unstable_by(split, f32::total_cmp);
    let q90 = remaining.get(split).copied();

    if let (Some(q10), Some(q90)) = (q10, q90) {
        sa.db_low = ema_tc(sa.db_low, q10, 0.30, dt_s);
        sa.db_high = ema_tc(sa.db_high, q90, 0.50, dt_s);
    }
}

fn normalise_targets(
    sa: &mut SpectrumAnalyzer,
    filters_len: usize,
    gate_open: bool,
) {
    let low = sa.db_low - 3.0;
    let high = sa.db_high + 6.0;
    let range_inv = 1.0 / (high - low).max(12.0);

    if gate_open {
        for i in 0..filters_len {
            if let Some(target) = sa.bars_target.get_mut(i) {
                let mut v = (*target - low) * range_inv;
                v = v.clamp(0.0, 1.0).powf(0.85);
                *target = 1.0 - (1.0 - v).powf(1.6);
            }
        }
    } else if let Some(targets) =
        sa.bars_target.get_mut(..filters_len)
    {
        for t in targets {
            *t = 0.0;
        }
    }
}