rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
use crate::Matrix;

/// Aggregation function for feature pooling.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Aggregate {
    /// Arithmetic mean of the values.
    Mean,
    /// Median of the values.
    Median,
}

/// Beat-synchronous feature pooling.
///
/// Input:  `data [n_features, T]`, `frames` = beat boundary frame indices
/// Output: `[n_features, n_windows]` where each column is the aggregate of
///         the input columns in the corresponding window.
///
/// Matches `librosa.util.sync` with `pad=True`: frame boundaries are padded to
/// span `[0, T]`, then consecutive pairs form windows.
pub fn sync(data: &Matrix, frames: &[usize], aggregate: Aggregate) -> Matrix {
    let n_features = data.rows();
    let t = data.cols();

    // Build boundaries matching librosa's index_to_slice(idx, idx_min=0, idx_max=T, pad=True):
    // - Prepend 0 if not already at front
    // - Append T if not already at end
    // - Create windows from consecutive pairs
    let mut boundaries: Vec<usize> = Vec::with_capacity(frames.len() + 2);
    if frames.is_empty() || frames[0] > 0 {
        boundaries.push(0);
    }
    for &f in frames {
        boundaries.push(f.min(t));
    }
    if boundaries.last().copied().unwrap_or(0) < t {
        boundaries.push(t);
    }

    let windows: Vec<(usize, usize)> = boundaries.windows(2).map(|w| (w[0], w[1])).collect();

    let n_windows = windows.len();
    let mut result = Matrix::zeros(n_features, n_windows);

    for (col, &(start, end)) in windows.iter().enumerate() {
        if end <= start {
            // empty window: leave as zeros
            continue;
        }
        let n = (end - start) as f64;
        for r in 0..n_features {
            let val = match aggregate {
                Aggregate::Mean => {
                    let sum: f64 = (start..end).map(|c| data.get(r, c)).sum();
                    sum / n
                }
                Aggregate::Median => {
                    let mut vals: Vec<f64> = (start..end).map(|c| data.get(r, c)).collect();
                    median(&mut vals)
                }
            };
            *result.get_mut(r, col) = val;
        }
    }

    result
}

fn median(v: &mut [f64]) -> f64 {
    let n = v.len();
    if n == 0 {
        return 0.0;
    }
    v.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    if n.is_multiple_of(2) {
        (v[n / 2 - 1] + v[n / 2]) / 2.0
    } else {
        v[n / 2]
    }
}

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

    #[test]
    fn test_sync_mean_basic() {
        // data: [2, 4] columns
        let data = Matrix::from_rows(&[&[1.0, 2.0, 3.0, 4.0]]);
        // frames = [2] → windows [(0,2), (2,4)]
        let result = sync(&data, &[2], Aggregate::Mean);
        assert_eq!(result.shape(), (1, 2));
        assert!((result.get(0, 0) - 1.5).abs() < 1e-12);
        assert!((result.get(0, 1) - 3.5).abs() < 1e-12);
    }

    #[test]
    fn test_sync_median_basic() {
        let data = Matrix::from_rows(&[&[1.0, 3.0, 2.0, 4.0, 5.0]]);
        let result = sync(&data, &[3], Aggregate::Median);
        assert_eq!(result.shape(), (1, 2));
        // window [0,3): [1,3,2] → sorted [1,2,3] → median=2
        assert!((result.get(0, 0) - 2.0).abs() < 1e-12);
        // window [3,5): [4,5] → sorted [4,5] → median=4.5
        assert!((result.get(0, 1) - 4.5).abs() < 1e-12);
    }

    #[test]
    fn test_sync_empty_frames() {
        let data = Matrix::from_rows(&[&[1.0, 2.0, 3.0]]);
        let result = sync(&data, &[], Aggregate::Mean);
        assert_eq!(result.shape(), (1, 1));
        assert!((result.get(0, 0) - 2.0).abs() < 1e-12);
    }
}