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();
}
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 {
if val.is_sign_negative() && val != 0.0 {
-1
} else {
1
}
} else {
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
);
}
}
}