rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Time, sample, and frame conversion utilities matching librosa.core.convert.

/// Convert time values (seconds) to sample indices.
///
/// Matches `librosa.time_to_samples(times, sr=sr)`.
///
/// # Examples
///
/// ```
/// let samples = rosa::convert::time_to_samples(&[0.0, 0.5, 1.0], 22050);
/// assert_eq!(samples, vec![0, 11025, 22050]);
/// ```
pub fn time_to_samples(times: &[f64], sr: u32) -> Vec<usize> {
    let sr = sr as f64;
    times.iter().map(|&t| (t * sr) as usize).collect()
}

/// Convert sample indices to time values (seconds).
///
/// Matches `librosa.samples_to_time(samples, sr=sr)`.
pub fn samples_to_time(samples: &[usize], sr: u32) -> Vec<f64> {
    let sr = sr as f64;
    samples.iter().map(|&s| s as f64 / sr).collect()
}

/// Convert frame indices to sample indices.
///
/// If `n_fft` is provided, adds `n_fft / 2` offset to compensate
/// for centered STFT windowing.
///
/// Matches `librosa.frames_to_samples(frames, hop_length, n_fft)`.
pub fn frames_to_samples(frames: &[usize], hop_length: usize, n_fft: Option<usize>) -> Vec<usize> {
    let offset = n_fft.map_or(0, |n| n / 2);
    frames.iter().map(|&f| f * hop_length + offset).collect()
}

/// Convert sample indices to frame indices.
///
/// If `n_fft` is provided, subtracts `n_fft / 2` offset.
/// Returns `i64` because negative frame indices are possible
/// when the offset exceeds the sample index.
///
/// Matches `librosa.samples_to_frames(samples, hop_length, n_fft)`.
pub fn samples_to_frames(samples: &[usize], hop_length: usize, n_fft: Option<usize>) -> Vec<i64> {
    let offset = n_fft.map_or(0, |n| n / 2) as i64;
    let hop = hop_length as i64;
    samples
        .iter()
        .map(|&s| {
            let adjusted = s as i64 - offset;
            adjusted.div_euclid(hop)
        })
        .collect()
}

/// Convert frame indices to time values (seconds).
///
/// Matches `librosa.frames_to_time(frames, sr, hop_length, n_fft)`.
pub fn frames_to_time(
    frames: &[usize],
    sr: u32,
    hop_length: usize,
    n_fft: Option<usize>,
) -> Vec<f64> {
    let sample_indices = frames_to_samples(frames, hop_length, n_fft);
    samples_to_time(&sample_indices, sr)
}

/// Convert time values (seconds) to frame indices.
///
/// Returns `i64` because negative frame indices are possible
/// when `n_fft` offset is provided.
///
/// Matches `librosa.time_to_frames(times, sr, hop_length, n_fft)`.
pub fn time_to_frames(times: &[f64], sr: u32, hop_length: usize, n_fft: Option<usize>) -> Vec<i64> {
    let sample_indices = time_to_samples(times, sr);
    samples_to_frames(&sample_indices, hop_length, n_fft)
}

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

    #[derive(Deserialize)]
    struct ConvertTestCase {
        function: String,
        name: String,
        #[serde(default)]
        times: Option<Vec<f64>>,
        #[serde(default)]
        samples: Option<Vec<i64>>,
        #[serde(default)]
        frames: Option<Vec<i64>>,
        #[serde(default)]
        sr: Option<u32>,
        #[serde(default)]
        hop_length: Option<usize>,
        #[serde(default)]
        n_fft: Option<usize>,
        expected: serde_json::Value,
    }

    fn load_references() -> Vec<ConvertTestCase> {
        let data = include_str!("../../tests/references/convert.json");
        serde_json::from_str(data).expect("Failed to parse convert.json")
    }

    #[test]
    fn test_time_to_samples() {
        for case in load_references()
            .into_iter()
            .filter(|c| c.function == "time_to_samples")
        {
            let times = case.times.unwrap();
            let sr = case.sr.unwrap();
            let result = time_to_samples(&times, sr);
            let expected: Vec<usize> = serde_json::from_value(case.expected).unwrap();
            assert_eq!(result, expected, "FAIL: {}", case.name);
        }
    }

    #[test]
    fn test_samples_to_time() {
        for case in load_references()
            .into_iter()
            .filter(|c| c.function == "samples_to_time")
        {
            let samples: Vec<usize> = case
                .samples
                .unwrap()
                .into_iter()
                .map(|s| s as usize)
                .collect();
            let sr = case.sr.unwrap();
            let result = samples_to_time(&samples, sr);
            let expected: Vec<f64> = serde_json::from_value(case.expected).unwrap();
            assert_eq!(result, expected, "FAIL: {}", case.name);
        }
    }

    #[test]
    fn test_frames_to_samples() {
        for case in load_references()
            .into_iter()
            .filter(|c| c.function == "frames_to_samples")
        {
            let frames: Vec<usize> = case
                .frames
                .unwrap()
                .into_iter()
                .map(|f| f as usize)
                .collect();
            let hop = case.hop_length.unwrap();
            let n_fft = case.n_fft;
            let result = frames_to_samples(&frames, hop, n_fft);
            let expected: Vec<usize> = serde_json::from_value(case.expected).unwrap();
            assert_eq!(result, expected, "FAIL: {}", case.name);
        }
    }

    #[test]
    fn test_samples_to_frames() {
        for case in load_references()
            .into_iter()
            .filter(|c| c.function == "samples_to_frames")
        {
            let samples: Vec<usize> = case
                .samples
                .unwrap()
                .into_iter()
                .map(|s| s as usize)
                .collect();
            let hop = case.hop_length.unwrap();
            let n_fft = case.n_fft;
            let result = samples_to_frames(&samples, hop, n_fft);
            let expected: Vec<i64> = serde_json::from_value(case.expected).unwrap();
            assert_eq!(result, expected, "FAIL: {}", case.name);
        }
    }

    #[test]
    fn test_frames_to_time() {
        for case in load_references()
            .into_iter()
            .filter(|c| c.function == "frames_to_time")
        {
            let frames: Vec<usize> = case
                .frames
                .unwrap()
                .into_iter()
                .map(|f| f as usize)
                .collect();
            let sr = case.sr.unwrap();
            let hop = case.hop_length.unwrap();
            let n_fft = case.n_fft;
            let result = frames_to_time(&frames, sr, hop, n_fft);
            let expected: Vec<f64> = serde_json::from_value(case.expected).unwrap();
            for (i, (a, e)) in result.iter().zip(expected.iter()).enumerate() {
                let diff = (a - e).abs();
                assert!(
                    diff < 1e-12,
                    "FAIL: {}[{}]: got {}, expected {}, diff {}",
                    case.name,
                    i,
                    a,
                    e,
                    diff
                );
            }
        }
    }

    #[test]
    fn test_time_to_frames() {
        for case in load_references()
            .into_iter()
            .filter(|c| c.function == "time_to_frames")
        {
            let times = case.times.unwrap();
            let sr = case.sr.unwrap();
            let hop = case.hop_length.unwrap();
            let n_fft = case.n_fft;
            let result = time_to_frames(&times, sr, hop, n_fft);
            let expected: Vec<i64> = serde_json::from_value(case.expected).unwrap();
            assert_eq!(result, expected, "FAIL: {}", case.name);
        }
    }
}