rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Spectral rolloff matching `librosa.feature.spectral_rolloff`.

use crate::matrix::Matrix;

/// Compute roll-off frequency from a magnitude spectrogram.
///
/// Matches `librosa.feature.spectral_rolloff(S=S, freq=freq, roll_percent=roll_percent)`.
///
/// The roll-off frequency is the frequency bin such that at least
/// `roll_percent` of the energy is contained at or below that frequency.
///
/// - `s`: magnitude spectrogram (rows = frequency bins, cols = frames)
/// - `freq`: center frequencies for each bin (length = s.rows())
/// - `roll_percent`: energy threshold (default: 0.85)
///
/// # Panics
///
/// Panics if `freq.len() != s.rows()`.
pub fn spectral_rolloff(s: &Matrix, freq: &[f64], roll_percent: f64) -> Vec<f64> {
    assert_eq!(
        s.rows(),
        freq.len(),
        "freq length {} != spectrogram rows {}",
        freq.len(),
        s.rows()
    );

    let n_frames = s.cols();
    let n_bins = s.rows();
    let mut result = Vec::with_capacity(n_frames);

    for t in 0..n_frames {
        // Cumulative sum along frequency axis
        let mut total = 0.0;
        for k in 0..n_bins {
            total += s.get(k, t);
        }
        let threshold = roll_percent * total;

        // Find minimum frequency where cumsum >= threshold
        // librosa: where cumsum < threshold → NaN, else 1, then nanmin(1 * freq)
        let mut rolloff = f64::NAN;
        let mut cumsum = 0.0;
        for (k, &f) in freq.iter().enumerate().take(n_bins) {
            cumsum += s.get(k, t);
            if cumsum >= threshold {
                rolloff = f;
                break;
            }
        }
        result.push(if rolloff.is_nan() { 0.0 } else { rolloff });
    }

    result
}