rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Tempogram computation matching `librosa.feature.rhythm.tempogram` and
//! `librosa.feature.rhythm.fourier_tempogram`.

use crate::autocorrelate::autocorrelate;
use crate::dsp::util::{Norm, pad_linear_ramp};
use crate::matrix::{FrameIter, Matrix};

/// Parameters for tempogram computation.
pub struct TempogramParams {
    pub sr: f64,
    pub hop_length: usize,
    /// Autocorrelation window length in frames (default 384 ≈ 8s at sr=22050/hop=512).
    pub win_length: usize,
    pub center: bool,
    /// Normalization mode per column. Default: Some(Linf).
    pub norm: Option<Norm>,
}

impl TempogramParams {
    pub fn new(sr: f64) -> Self {
        Self {
            sr,
            hop_length: 512,
            win_length: 384,
            center: true,
            norm: Some(Norm::Linf),
        }
    }
}

/// Compute local autocorrelation of onset strength envelope.
///
/// Returns Matrix of shape `(win_length, n_frames)`.
/// Matches `librosa.feature.tempogram`.
pub fn tempogram(onset_envelope: &[f64], params: &TempogramParams) -> Matrix {
    let win_length = params.win_length;
    let original_len = onset_envelope.len();

    if original_len == 0 || win_length == 0 {
        return Matrix::zeros(win_length, 0);
    }

    // Center padding with linear ramp
    let padded;
    let env = if params.center {
        let pad = win_length / 2;
        padded = pad_linear_ramp(onset_envelope, pad, pad);
        &padded[..]
    } else {
        onset_envelope
    };

    // Frame the onset envelope with hop_length=1
    let frame_iter = FrameIter::new(env, win_length, 1);
    let total_frames = frame_iter.n_frames();

    // When centered, truncate to original length
    let n_frames = if params.center {
        total_frames.min(original_len)
    } else {
        total_frames
    };

    if n_frames == 0 {
        return Matrix::zeros(win_length, 0);
    }

    // Build hann window
    let window = crate::windows::hann(win_length, true);

    // For each frame: window, autocorrelate, store as column
    let mut data = vec![0.0; win_length * n_frames];

    for t in 0..n_frames {
        let frame = frame_iter.frame(t);

        // Apply window
        let windowed: Vec<f64> = frame
            .iter()
            .zip(window.iter())
            .map(|(s, w)| s * w)
            .collect();

        // Autocorrelate
        let ac = autocorrelate(&windowed, None);

        // Store as column (row-major: data[lag * n_frames + t])
        for (lag, &val) in ac.iter().enumerate().take(win_length) {
            data[lag * n_frames + t] = val;
        }
    }

    let mut result = Matrix::from_vec(data, win_length, n_frames);

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

    result
}

/// Parameters for Fourier tempogram computation.
pub struct FourierTempogramParams {
    pub sr: f64,
    pub hop_length: usize,
    pub win_length: usize,
    pub center: bool,
}

impl FourierTempogramParams {
    pub fn new(sr: f64) -> Self {
        Self {
            sr,
            hop_length: 512,
            win_length: 384,
            center: true,
        }
    }
}

/// Compute STFT of onset strength envelope.
///
/// Returns `(real, imag)` matrices of shape `(win_length/2+1, n_frames)`.
/// Matches `librosa.feature.fourier_tempogram`.
pub fn fourier_tempogram(
    onset_envelope: &[f64],
    params: &FourierTempogramParams,
) -> (Matrix, Matrix) {
    use crate::stft::{StftParams, stft_as_real_imag};

    let stft_params = StftParams {
        n_fft: params.win_length,
        hop_length: 1,
        win_length: params.win_length,
        center: params.center,
        window: None,
    };

    stft_as_real_imag(onset_envelope, &stft_params)
}