rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
/// Detect zero crossings in a signal.
///
/// Matches `librosa.zero_crossings(y, threshold, pad, zero_pos)`.
///
/// - `threshold`: values with `|y[i]| <= threshold` are treated as 0
/// - `pad`: if true, first element is always a crossing
/// - `zero_pos`: if true, exact zero is treated as positive (uses signbit semantics)
pub fn zero_crossings(y: &[f64], threshold: f64, pad: bool, zero_pos: bool) -> Vec<bool> {
    let n = y.len();
    if n == 0 {
        return Vec::new();
    }

    // Apply threshold: values within threshold of zero become zero
    let thresholded: Vec<f64> = y
        .iter()
        .map(|&v| if v.abs() <= threshold { 0.0 } else { v })
        .collect();

    let sign = |val: f64| -> i8 {
        if zero_pos {
            // signbit semantics: 0 and positive → same sign, negative → different
            if val.is_sign_negative() && val != 0.0 {
                -1
            } else {
                1
            }
        } else {
            // strict sign: -1, 0, +1 are all distinct
            if val > 0.0 {
                1
            } else if val < 0.0 {
                -1
            } else {
                0
            }
        }
    };

    let mut result = vec![false; n];

    for i in 1..n {
        result[i] = sign(thresholded[i - 1]) != sign(thresholded[i]);
    }

    result[0] = pad;

    result
}

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

    #[derive(Deserialize)]
    struct ZeroCrossingsTestCase {
        name: String,
        input: Vec<f64>,
        threshold: f64,
        pad: bool,
        zero_pos: bool,
        expected: Vec<bool>,
    }

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

    #[test]
    fn test_zero_crossings_matches_librosa() {
        for case in load_references() {
            let result = zero_crossings(&case.input, case.threshold, case.pad, case.zero_pos);
            assert_eq!(
                result, case.expected,
                "FAIL: {}\nresult:   {:?}\nexpected: {:?}",
                case.name, result, case.expected
            );
        }
    }
}