rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
#![allow(dead_code)]

use hound::WavReader;

/// Path to the project root.
fn project_root() -> String {
    env!("CARGO_MANIFEST_DIR").to_string()
}

/// Resolve path to a WAV test file in audio_test_files/.
pub fn wav_path(filename: &str) -> String {
    format!("{}/audio_test_files/{}", project_root(), filename)
}

/// Load a JSON reference file from python/audio_references/.
pub fn load_audio_reference(filename: &str) -> serde_json::Value {
    let path = format!("{}/python/audio_references/{}", project_root(), filename);
    let data = std::fs::read_to_string(&path).unwrap_or_else(|e| {
        panic!(
            "Failed to read {path}: {e}\n\
             Run: cd python && uv run generate_audio_references.py"
        )
    });
    serde_json::from_str(&data).expect("Failed to parse JSON")
}

/// Read a mono WAV file into f64 samples normalized to [-1, 1].
///
/// Matches soundfile/librosa normalization: divide by 2^(bits-1).
pub fn read_wav_mono(path: &str) -> (Vec<f64>, u32) {
    let reader = WavReader::open(path).unwrap_or_else(|e| panic!("Failed to open WAV {path}: {e}"));
    let spec = reader.spec();
    let sample_rate = spec.sample_rate;
    let bits = spec.bits_per_sample;
    let max_val = (1i64 << (bits - 1)) as f64;

    let samples: Vec<f64> = reader
        .into_samples::<i32>()
        .map(|s| s.unwrap() as f64 / max_val)
        .collect();

    (samples, sample_rate)
}

/// Extract a frame from audio samples.
///
/// Matches `librosa.util.frame(y, frame_length, hop_length)[:, frame_index]`.
pub fn extract_frame(
    samples: &[f64],
    frame_length: usize,
    hop_length: usize,
    frame_index: usize,
) -> Vec<f64> {
    let start = frame_index * hop_length;
    let end = (start + frame_length).min(samples.len());
    samples[start..end].to_vec()
}

/// Assert two f64 slices are approximately equal.
pub fn assert_approx_eq(actual: &[f64], expected: &[f64], tol: f64, label: &str) {
    assert_eq!(
        actual.len(),
        expected.len(),
        "{label}: length mismatch: got {} expected {}",
        actual.len(),
        expected.len()
    );
    for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
        let diff = (a - e).abs();
        assert!(
            diff < tol,
            "{label}[{i}]: got {a}, expected {e}, diff {diff} > tol {tol}"
        );
    }
}