rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Complex number helpers matching `librosa.util`.
//!
//! Uses `(f64, f64)` tuples for complex numbers: `(real, imaginary)`.

/// Squared magnitude of a complex number: `re² + im²`.
///
/// More efficient than `abs()` when you don't need the square root.
/// Matches `librosa.util.abs2`.
pub fn abs2(re: f64, im: f64) -> f64 {
    re * re + im * im
}

/// Squared magnitude for a slice of complex numbers.
///
/// Returns `re² + im²` for each element.
/// Matches `np.abs(x)**2` for complex arrays.
pub fn abs2_slice(complex: &[(f64, f64)]) -> Vec<f64> {
    complex.iter().map(|&(re, im)| abs2(re, im)).collect()
}

/// Complex phasor from angles: `(cos(θ), sin(θ))`.
///
/// Equivalent to `exp(1j * angles)` in numpy.
/// Matches `librosa.util.phasor`.
pub fn phasor(angles: &[f64]) -> Vec<(f64, f64)> {
    angles.iter().map(|&a| (a.cos(), a.sin())).collect()
}

/// Complex phasor with magnitude: `(mag * cos(θ), mag * sin(θ))`.
///
/// Equivalent to `mag * exp(1j * angles)` in numpy.
///
/// # Panics
///
/// Panics if `angles` and `mag` have different lengths.
pub fn phasor_mag(angles: &[f64], mag: &[f64]) -> Vec<(f64, f64)> {
    assert_eq!(
        angles.len(),
        mag.len(),
        "phasor_mag: angles and mag must have the same length"
    );
    angles
        .iter()
        .zip(mag.iter())
        .map(|(&a, &m)| (m * a.cos(), m * a.sin()))
        .collect()
}

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

    #[derive(Deserialize)]
    struct ComplexTestCase {
        name: String,
        function: String,
        #[serde(default)]
        input: Option<Vec<(f64, f64)>>,
        #[serde(default)]
        angles: Option<Vec<f64>>,
        #[serde(default)]
        magnitudes: Option<Vec<f64>>,
        expected: serde_json::Value,
    }

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

    #[test]
    fn test_abs2_matches_numpy() {
        let refs = load_complex_references();
        for case in refs.iter().filter(|c| c.function == "abs2") {
            let input = case.input.as_ref().unwrap();
            let expected: Vec<f64> = serde_json::from_value(case.expected.clone()).unwrap();
            let result = abs2_slice(input);
            assert_eq!(result.len(), expected.len(), "{}", case.name);
            for (i, (a, e)) in result.iter().zip(expected.iter()).enumerate() {
                let diff = (a - e).abs();
                assert!(diff < 1e-12, "{}[{i}]: {a} != {e}", case.name);
            }
        }
    }

    #[test]
    fn test_phasor_matches_numpy() {
        let refs = load_complex_references();
        for case in refs.iter().filter(|c| c.function == "phasor") {
            let angles = case.angles.as_ref().unwrap();
            let expected: Vec<(f64, f64)> = serde_json::from_value(case.expected.clone()).unwrap();
            let result = phasor(angles);
            assert_eq!(result.len(), expected.len(), "{}", case.name);
            for (i, ((ar, ai), (er, ei))) in result.iter().zip(expected.iter()).enumerate() {
                assert!(
                    (ar - er).abs() < 1e-10,
                    "{}[{i}] re: {ar} != {er}",
                    case.name
                );
                assert!(
                    (ai - ei).abs() < 1e-10,
                    "{}[{i}] im: {ai} != {ei}",
                    case.name
                );
            }
        }
    }

    #[test]
    fn test_phasor_mag_matches_numpy() {
        let refs = load_complex_references();
        for case in refs.iter().filter(|c| c.function == "phasor_mag") {
            let angles = case.angles.as_ref().unwrap();
            let mags = case.magnitudes.as_ref().unwrap();
            let expected: Vec<(f64, f64)> = serde_json::from_value(case.expected.clone()).unwrap();
            let result = phasor_mag(angles, mags);
            assert_eq!(result.len(), expected.len(), "{}", case.name);
            for (i, ((ar, ai), (er, ei))) in result.iter().zip(expected.iter()).enumerate() {
                assert!(
                    (ar - er).abs() < 1e-10,
                    "{}[{i}] re: {ar} != {er}",
                    case.name
                );
                assert!(
                    (ai - ei).abs() < 1e-10,
                    "{}[{i}] im: {ai} != {ei}",
                    case.name
                );
            }
        }
    }

    #[test]
    fn test_abs2_basic() {
        assert_eq!(abs2(3.0, 4.0), 25.0);
        assert_eq!(abs2(0.0, 0.0), 0.0);
        assert_eq!(abs2(1.0, 0.0), 1.0);
    }

    #[test]
    fn test_phasor_at_zero() {
        let result = phasor(&[0.0]);
        assert!((result[0].0 - 1.0).abs() < 1e-15);
        assert!(result[0].1.abs() < 1e-15);
    }
}