use std::f64::consts::PI;
pub fn tone(frequency: f64, sr: u32, length: usize) -> Vec<f64> {
let phi = -PI * 0.5;
let sr_f = sr as f64;
(0..length)
.map(|n| (2.0 * PI * frequency * n as f64 / sr_f + phi).cos())
.collect()
}
pub fn chirp(fmin: f64, fmax: f64, sr: u32, length: usize, linear: bool) -> Vec<f64> {
let phi = -PI * 0.5;
let sr_f = sr as f64;
let t1 = length as f64 / sr_f;
if linear {
let beta = (fmax - fmin) / t1;
(0..length)
.map(|n| {
let t = n as f64 / sr_f;
let phase = 2.0 * PI * (fmin * t + 0.5 * beta * t * t);
(phase + phi).cos()
})
.collect()
} else {
let log_ratio = (fmax / fmin).ln();
let beta = t1 / log_ratio;
(0..length)
.map(|n| {
let t = n as f64 / sr_f;
let phase = 2.0 * PI * fmin * beta * ((log_ratio * t / t1).exp() - 1.0);
(phase + phi).cos()
})
.collect()
}
}
pub fn clicks(
times: &[f64],
sr: u32,
click_freq: f64,
click_duration: f64,
length: Option<usize>,
) -> Vec<f64> {
let sr_f = sr as f64;
let click_len = (sr_f * click_duration) as usize;
let angular_freq = 2.0 * PI * click_freq / sr_f;
let click: Vec<f64> = (0..click_len)
.map(|i| {
let t = if click_len > 1 {
i as f64 / (click_len - 1) as f64
} else {
0.0
};
let envelope = 2.0_f64.powf(-10.0 * t);
let wave = (angular_freq * i as f64).sin();
envelope * wave
})
.collect();
let positions: Vec<usize> = times.iter().map(|&t| (t * sr_f) as usize).collect();
let total_len =
length.unwrap_or_else(|| positions.iter().map(|&p| p + click_len).max().unwrap_or(0));
let mut output = vec![0.0; total_len];
for &start in &positions {
if start >= total_len {
continue;
}
let end = (start + click_len).min(total_len);
for (i, sample) in click[..end - start].iter().enumerate() {
output[start + i] += sample;
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
fn assert_approx_eq(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
assert_eq!(
actual.len(),
expected.len(),
"{name}: 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,
"{name}[{i}]: got {a}, expected {e}, diff {diff} > tol {tol}"
);
}
}
#[derive(Deserialize)]
struct ToneTestCase {
name: String,
frequency: f64,
sr: u32,
length: usize,
expected: Vec<f64>,
}
#[test]
fn test_tone_matches_librosa() {
let data = include_str!("tests/references/tone.json");
let cases: Vec<ToneTestCase> = serde_json::from_str(data).unwrap();
for case in cases {
let result = tone(case.frequency, case.sr, case.length);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
#[derive(Deserialize)]
struct ChirpTestCase {
name: String,
fmin: f64,
fmax: f64,
sr: u32,
length: usize,
linear: bool,
expected: Vec<f64>,
}
#[test]
fn test_chirp_matches_librosa() {
let data = include_str!("tests/references/chirp.json");
let cases: Vec<ChirpTestCase> = serde_json::from_str(data).unwrap();
for case in cases {
let result = chirp(case.fmin, case.fmax, case.sr, case.length, case.linear);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
#[derive(Deserialize)]
struct ClicksTestCase {
name: String,
times: Vec<f64>,
sr: u32,
click_freq: f64,
click_duration: f64,
length: usize,
expected: Vec<f64>,
}
#[test]
fn test_clicks_matches_librosa() {
let data = include_str!("tests/references/clicks.json");
let cases: Vec<ClicksTestCase> = serde_json::from_str(data).unwrap();
for case in cases {
let result = clicks(
&case.times,
case.sr,
case.click_freq,
case.click_duration,
Some(case.length),
);
assert_approx_eq(&result, &case.expected, 1e-6, &case.name);
}
}
}