resonators 0.1.1

Rust implementation of the Resonate algorithm for low-latency spectral analysis.
Documentation
/// Returns a reasonable default `alpha` (EWMA coefficient) for a resonator at
/// the given frequency, using the per-frequency heuristic from the [paper].
///
/// Higher frequencies receive a larger `alpha` (shorter time constant).
///
/// [paper]: https://alexandrefrancois.org/assets/publications/FrancoisARJ-ICMC2025.pdf
pub fn heuristic_alpha(freq: f32, sample_rate: f32) -> f32 {
    debug_assert!(freq.is_finite() && freq > 0.0, "freq must be positive");
    debug_assert!(
        sample_rate.is_finite() && sample_rate > 0.0,
        "sample_rate must be positive"
    );
    let dt = 1.0 / sample_rate;
    1.0 - (-dt * freq / (1.0 + freq).log10()).exp()
}

/// Returns [`heuristic_alpha`] applied to each frequency in `freqs`.
pub fn heuristic_alphas(freqs: &[f32], sample_rate: f32) -> Vec<f32> {
    freqs
        .iter()
        .map(|&f| heuristic_alpha(f, sample_rate))
        .collect()
}

/// Converts an EWMA time constant `tau` (in seconds) to an `alpha` coefficient.
pub fn alpha_from_tau(tau: f32, sample_rate: f32) -> f32 {
    let dt = 1.0 / sample_rate;
    1.0 - (-dt / tau).exp()
}

/// Converts an `alpha` coefficient to an EWMA time constant `tau`, in seconds.
pub fn tau_from_alpha(alpha: f32, sample_rate: f32) -> f32 {
    let dt = 1.0 / sample_rate;
    -dt / (1.0 - alpha).ln()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn heuristic_alpha_in_valid_range() {
        for &freq in &[27.5, 440.0, 4186.0] {
            let a = heuristic_alpha(freq, 44100.0);
            assert!(a > 0.0 && a < 1.0, "alpha({freq}) = {a}");
        }
    }

    #[test]
    fn heuristic_alpha_increases_with_freq() {
        let sr = 44100.0;
        assert!(heuristic_alpha(4000.0, sr) > heuristic_alpha(100.0, sr));
    }

    #[test]
    fn heuristic_alphas_matches_scalar() {
        let sr = 44100.0;
        let freqs = vec![27.5, 440.0, 4186.0];
        let alphas = heuristic_alphas(&freqs, sr);
        for (i, &f) in freqs.iter().enumerate() {
            assert_eq!(alphas[i], heuristic_alpha(f, sr));
        }
    }

    #[test]
    fn alpha_tau_roundtrip() {
        let sr = 44100.0;
        let tau = 0.05;
        let alpha = alpha_from_tau(tau, sr);
        let tau_back = tau_from_alpha(alpha, sr);
        assert!((tau - tau_back).abs() < 1e-6);
    }
}