rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Tempogram ratio matching `librosa.feature.rhythm.tempogram_ratio`.

use crate::dsp::util::Norm;
use crate::matrix::Matrix;

use super::tempo::{TempoParams, tempo};
use super::tempogram::{TempogramParams, tempogram};

/// Default metric factors (matches librosa).
pub const DEFAULT_FACTORS: [f64; 13] = [
    4.0,
    8.0 / 3.0,
    3.0,
    2.0,
    4.0 / 3.0,
    3.0 / 2.0,
    1.0,
    2.0 / 3.0,
    3.0 / 4.0,
    1.0 / 2.0,
    1.0 / 3.0,
    3.0 / 8.0,
    1.0 / 4.0,
];

/// Parameters for tempogram ratio computation.
pub struct TempogramRatioParams {
    pub sr: f64,
    pub hop_length: usize,
    pub win_length: usize,
    pub start_bpm: f64,
    pub std_bpm: f64,
    pub max_tempo: Option<f64>,
    pub factors: Option<Vec<f64>>,
    pub norm: Option<Norm>,
}

impl TempogramRatioParams {
    pub fn new(sr: f64) -> Self {
        Self {
            sr,
            hop_length: 512,
            win_length: 384,
            start_bpm: 120.0,
            std_bpm: 1.0,
            max_tempo: Some(320.0),
            factors: None,
            norm: Some(Norm::Linf),
        }
    }
}

/// Compute tempogram ratio — autocorrelation strength at metric multiples of tempo.
///
/// Returns Matrix of shape `(n_factors, n_frames)`.
/// Matches `librosa.feature.tempogram_ratio`.
pub fn tempogram_ratio(onset_envelope: &[f64], params: &TempogramRatioParams) -> Matrix {
    let n_frames = onset_envelope.len();
    if n_frames == 0 {
        let n_factors = params
            .factors
            .as_ref()
            .map(|f| f.len())
            .unwrap_or(DEFAULT_FACTORS.len());
        return Matrix::zeros(n_factors, 0);
    }

    let factors = params.factors.as_deref().unwrap_or(&DEFAULT_FACTORS);
    let n_factors = factors.len();

    // Compute tempogram
    let tg_params = TempogramParams {
        sr: params.sr,
        hop_length: params.hop_length,
        win_length: params.win_length,
        center: true,
        norm: Some(Norm::Linf),
    };
    let tg = tempogram(onset_envelope, &tg_params);

    let tg_rows = tg.rows(); // = win_length (number of lag bins)
    let tg_cols = tg.cols();

    // Estimate global tempo
    let tempo_params = TempoParams {
        sr: params.sr,
        hop_length: params.hop_length,
        start_bpm: params.start_bpm,
        std_bpm: params.std_bpm,
        ac_size: 8.0,
        max_tempo: params.max_tempo,
    };
    let bpm = tempo(onset_envelope, &tempo_params);

    // For each factor and each frame, interpolate the tempogram at factor * bpm
    let mut data = vec![0.0; n_factors * tg_cols];

    for (fi, &factor) in factors.iter().enumerate() {
        let target_bpm = factor * bpm;
        if target_bpm <= 0.0 || !target_bpm.is_finite() {
            continue;
        }

        // Convert target BPM to lag: lag = 60 * sr / (hop_length * bpm)
        let target_lag = 60.0 * params.sr / (params.hop_length as f64 * target_bpm);

        if target_lag < 0.0 || target_lag >= (tg_rows - 1) as f64 {
            continue;
        }

        // Linear interpolation between floor and ceil lag bins
        let lag_floor = target_lag.floor() as usize;
        let lag_ceil = (lag_floor + 1).min(tg_rows - 1);
        let frac = target_lag - lag_floor as f64;

        for t in 0..tg_cols {
            let v0 = tg.get(lag_floor, t);
            let v1 = tg.get(lag_ceil, t);
            data[fi * tg_cols + t] = v0 + frac * (v1 - v0);
        }
    }

    let mut result = Matrix::from_vec(data, n_factors, tg_cols);

    // Normalize each column
    if let Some(norm) = params.norm {
        for t in 0..tg_cols {
            let mut col: Vec<f64> = (0..n_factors).map(|f| result.get(f, t)).collect();
            crate::dsp::util::normalize(&mut col, norm);
            for (f, &val) in col.iter().enumerate() {
                *result.get_mut(f, t) = val;
            }
        }
    }

    result
}