use anyhow::{Context, Result};
use std::path::Path;
pub const TONE_THRESHOLD: f64 = 0.2;
pub fn tone_score(samples: &[i16], sample_rate: u32, freq: f64) -> f64 {
let n = samples.len();
if n == 0 || sample_rate == 0 {
return 0.0;
}
let omega = 2.0 * std::f64::consts::PI * freq / f64::from(sample_rate);
let coeff = 2.0 * omega.cos();
let (mut s1, mut s2) = (0.0f64, 0.0f64);
let mut energy = 0.0f64;
for &x in samples {
let xf = f64::from(x);
energy += xf * xf;
let s = xf + coeff * s1 - s2;
s2 = s1;
s1 = s;
}
if energy == 0.0 {
return 0.0;
}
let power = s2 * s2 + s1 * s1 - coeff * s1 * s2;
(2.0 * power) / (n as f64 * energy)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ToneAnalysis {
pub score: f64,
pub rms: f64,
pub samples: usize,
}
pub fn analyze_tone_samples(
samples: &[i16],
sample_rate: u32,
freq: u32,
window: std::time::Duration,
) -> ToneAnalysis {
let want = (f64::from(sample_rate) * window.as_secs_f64()) as usize;
let tail = &samples[samples.len().saturating_sub(want)..];
let rms = if tail.is_empty() {
0.0
} else {
(tail.iter().map(|&x| f64::from(x).powi(2)).sum::<f64>() / tail.len() as f64).sqrt()
};
ToneAnalysis {
score: tone_score(tail, sample_rate, f64::from(freq)),
rms,
samples: tail.len(),
}
}
pub fn write_wav(path: &Path, samples: &[i16], sample_rate: u32) -> Result<()> {
let data_len = samples.len() * 2;
let byte_rate = sample_rate * 2; let mut v = Vec::with_capacity(44 + data_len);
v.extend_from_slice(b"RIFF");
v.extend_from_slice(&((36 + data_len) as u32).to_le_bytes());
v.extend_from_slice(b"WAVE");
v.extend_from_slice(b"fmt ");
v.extend_from_slice(&16u32.to_le_bytes()); v.extend_from_slice(&1u16.to_le_bytes()); v.extend_from_slice(&1u16.to_le_bytes()); v.extend_from_slice(&sample_rate.to_le_bytes());
v.extend_from_slice(&byte_rate.to_le_bytes());
v.extend_from_slice(&2u16.to_le_bytes()); v.extend_from_slice(&16u16.to_le_bytes()); v.extend_from_slice(b"data");
v.extend_from_slice(&(data_len as u32).to_le_bytes());
for s in samples {
v.extend_from_slice(&s.to_le_bytes());
}
std::fs::write(path, v).with_context(|| format!("write WAV {}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn sine(freq: f64, sr: u32, n: usize) -> Vec<i16> {
(0..n)
.map(|i| (8000.0 * (2.0 * PI * freq * i as f64 / f64::from(sr)).sin()) as i16)
.collect()
}
#[test]
fn detects_matching_tone_rejects_others() {
let sr = 8000;
let s = sine(440.0, sr, 8000);
assert!(tone_score(&s, sr, 440.0) > 0.8, "expected strong 440 match");
assert!(
tone_score(&s, sr, 1000.0) < 0.1,
"1000 Hz should not match a 440 tone"
);
}
#[test]
fn silence_and_empty_score_zero() {
assert_eq!(tone_score(&[], 8000, 440.0), 0.0);
assert_eq!(tone_score(&[0i16; 4000], 8000, 440.0), 0.0);
}
#[test]
fn write_wav_roundtrips_header_and_samples() {
let s = sine(440.0, 8000, 8000);
let dir = std::env::temp_dir();
let path = dir.join("ringo-flow-write-wav-test.wav");
write_wav(&path, &s, 8000).unwrap();
let b = std::fs::read(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(&b[0..4], b"RIFF");
assert_eq!(&b[8..12], b"WAVE");
assert_eq!(u32::from_le_bytes([b[24], b[25], b[26], b[27]]), 8000); assert_eq!(b.len(), 44 + s.len() * 2);
let parsed: Vec<i16> = b[44..]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
assert_eq!(parsed, s);
}
#[test]
fn threshold_separates_tone_from_noise() {
let sr = 8000;
let noise: Vec<i16> = (0..8000)
.map(|i| if i % 2 == 0 { 4000 } else { -4000 })
.collect();
assert!(tone_score(&noise, sr, 440.0) < TONE_THRESHOLD);
assert!(tone_score(&sine(440.0, sr, 8000), sr, 440.0) >= TONE_THRESHOLD);
}
}