use std::collections::HashSet;
use audiofp::classical::{Haitsma, Panako, Wang};
use audiofp::{AudioBuffer, Fingerprinter, SampleRate};
const SECS: f32 = 10.0;
fn synth(seed: u32, sr: u32) -> Vec<f32> {
let n = (sr as f32 * SECS) as usize;
let mut out = Vec::with_capacity(n);
let mut x = seed.max(1);
for i in 0..n {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
let noise = (x as i32 as f32) / (i32::MAX as f32) * 0.05;
let t = i as f32 / sr as f32;
let s = 0.5 * (2.0 * std::f32::consts::PI * 880.0 * t).sin()
+ 0.3 * (2.0 * std::f32::consts::PI * 1320.0 * t).sin()
+ noise;
out.push(s);
}
out
}
fn add_noise(samples: &[f32], snr_db: f32, seed: u32) -> Vec<f32> {
let signal_power: f32 = samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32;
let signal_rms = signal_power.sqrt();
let noise_rms = signal_rms / 10f32.powf(snr_db / 20.0);
let noise_amp = noise_rms * 3f32.sqrt();
let mut x = seed.max(1);
samples
.iter()
.map(|s| {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
let n = (x as i32 as f32) / (i32::MAX as f32) * noise_amp;
s + n
})
.collect()
}
fn lowpass(samples: &[f32], cutoff_normalised: f32) -> Vec<f32> {
let alpha = 1.0 - (-2.0 * std::f32::consts::PI * cutoff_normalised).exp();
let mut y = 0.0_f32;
samples
.iter()
.map(|&s| {
y = alpha * s + (1.0 - alpha) * y;
y
})
.collect()
}
fn jaccard<T: std::hash::Hash + Eq>(a: &HashSet<T>, b: &HashSet<T>) -> f32 {
let union = a.union(b).count();
if union == 0 {
return 0.0;
}
a.intersection(b).count() as f32 / union as f32
}
fn wang_hash_set(samples: &[f32]) -> HashSet<u32> {
let mut wang = Wang::default();
let buf = AudioBuffer {
samples,
rate: SampleRate::HZ_8000,
};
wang.extract(buf)
.unwrap()
.hashes
.into_iter()
.map(|h| h.hash)
.collect()
}
fn panako_hash_set(samples: &[f32]) -> HashSet<u32> {
let mut p = Panako::default();
let buf = AudioBuffer {
samples,
rate: SampleRate::HZ_8000,
};
p.extract(buf)
.unwrap()
.hashes
.into_iter()
.map(|h| h.hash)
.collect()
}
fn haitsma_frames(samples: &[f32]) -> Vec<u32> {
let mut h = Haitsma::default();
let buf = AudioBuffer {
samples,
rate: SampleRate::new(5_000).unwrap(),
};
h.extract(buf).unwrap().frames
}
fn haitsma_similarity(clean: &[u32], dirty: &[u32]) -> f32 {
let n = clean.len().min(dirty.len());
if n == 0 {
return 0.0;
}
let total_bits = (n as u32) * 32;
let matching: u32 = clean[..n]
.iter()
.zip(dirty[..n].iter())
.map(|(a, b)| 32 - (a ^ b).count_ones())
.sum();
matching as f32 / total_bits as f32
}
#[test]
fn wang_robust_to_30db_noise() {
let clean = synth(0xCAFE, 8_000);
let dirty = add_noise(&clean, 30.0, 0xBEEF);
let overlap = jaccard(&wang_hash_set(&clean), &wang_hash_set(&dirty));
assert!(
overlap >= 0.05,
"Wang Jaccard at 30 dB SNR = {overlap:.3} (threshold 0.05)",
);
}
#[test]
fn wang_robust_to_lowpass() {
let clean = synth(0xCAFE, 8_000);
let dirty = lowpass(&clean, 0.20);
let overlap = jaccard(&wang_hash_set(&clean), &wang_hash_set(&dirty));
assert!(
overlap >= 0.20,
"Wang Jaccard under lowpass = {overlap:.3} (threshold 0.20)",
);
}
#[test]
fn panako_robust_to_30db_noise() {
let clean = synth(0xCAFE, 8_000);
let dirty = add_noise(&clean, 30.0, 0xBEEF);
let overlap = jaccard(&panako_hash_set(&clean), &panako_hash_set(&dirty));
assert!(
overlap >= 0.03,
"Panako Jaccard at 30 dB SNR = {overlap:.3} (threshold 0.03)",
);
}
#[test]
fn panako_robust_to_lowpass() {
let clean = synth(0xCAFE, 8_000);
let dirty = lowpass(&clean, 0.20);
let overlap = jaccard(&panako_hash_set(&clean), &panako_hash_set(&dirty));
assert!(
overlap >= 0.10,
"Panako Jaccard under lowpass = {overlap:.3} (threshold 0.10)",
);
}
#[test]
fn haitsma_robust_to_30db_noise() {
let clean = synth(0xCAFE, 5_000);
let dirty = add_noise(&clean, 30.0, 0xBEEF);
let sim = haitsma_similarity(&haitsma_frames(&clean), &haitsma_frames(&dirty));
assert!(
sim >= 0.75,
"Haitsma bit similarity at 30 dB SNR = {sim:.3} (threshold 0.75)",
);
}
#[test]
fn haitsma_robust_to_lowpass() {
let clean = synth(0xCAFE, 5_000);
let dirty = lowpass(&clean, 0.25);
let sim = haitsma_similarity(&haitsma_frames(&clean), &haitsma_frames(&dirty));
assert!(
sim >= 0.80,
"Haitsma bit similarity under lowpass = {sim:.3} (threshold 0.80)",
);
}