use std::path::Path;
use audiofp::classical::{Haitsma, Panako, PanakoHash, Wang, WangHash};
use audiofp::{AudioBuffer, Fingerprinter, SampleRate};
const TONE_LO: f32 = 880.0;
const TONE_HI: f32 = 1320.0;
const SECS: f32 = 6.0;
const GOLDEN_DIR: &str = "tests/goldens";
const MAGIC_WANG: &[u8; 8] = b"AFPWANG\0";
const MAGIC_PANAKO: &[u8; 8] = b"AFPPANK\0";
const MAGIC_HAITSMA: &[u8; 8] = b"AFPHAIT\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 * TONE_LO * t).sin()
+ 0.3 * (2.0 * std::f32::consts::PI * TONE_HI * t).sin()
+ noise;
out.push(s);
}
out
}
fn with_magic(magic: &[u8; 8], bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(magic.len() + bytes.len());
out.extend_from_slice(magic);
out.extend_from_slice(bytes);
out
}
fn write_golden(path: &Path, payload: &[u8]) {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).unwrap();
}
std::fs::write(path, payload).unwrap();
eprintln!(
"[goldens] wrote {} ({} bytes)",
path.display(),
payload.len()
);
}
fn read_golden(path: &Path, magic: &[u8; 8], label: &str) -> Vec<u8> {
let bytes = std::fs::read(path).unwrap_or_else(|e| {
panic!(
"golden file {} missing or unreadable: {e}\n\
Run with UPDATE_GOLDENS=1 cargo test --test regression to (re)generate.",
path.display(),
)
});
if bytes.len() < magic.len() || &bytes[..magic.len()] != magic {
panic!(
"{label} golden {} is missing the {} magic header\n \
got: {:?}\n \
want: {:?}\n\
This usually means the file was generated against an older\n\
version of this test. Re-run with\n \
UPDATE_GOLDENS=1 cargo test --test regression",
path.display(),
label,
&bytes[..bytes.len().min(magic.len())],
magic.as_slice(),
);
}
bytes[magic.len()..].to_vec()
}
fn assert_bytes_equal(actual: &[u8], expected: &[u8], label: &str) {
if actual == expected {
return;
}
let first_diff = actual
.iter()
.zip(expected.iter())
.position(|(a, b)| a != b)
.unwrap_or(actual.len().min(expected.len()));
panic!(
"{label} hash output drift detected\n \
actual: {} bytes\n \
expected: {} bytes\n \
first byte mismatch at offset {first_diff}\n\
If this change is intentional, regenerate with:\n \
UPDATE_GOLDENS=1 cargo test --test regression",
actual.len(),
expected.len(),
);
}
#[test]
fn wang_v1_golden() {
let samples = synth(0xCAFE, 8_000);
let mut wang = Wang::default();
let buf = AudioBuffer {
samples: &samples,
rate: SampleRate::HZ_8000,
};
let fp = wang.extract(buf).unwrap();
let bytes = bytemuck::cast_slice::<WangHash, u8>(&fp.hashes);
let path = Path::new(GOLDEN_DIR).join("wang_v1.bin");
if std::env::var_os("UPDATE_GOLDENS").is_some() {
write_golden(&path, &with_magic(MAGIC_WANG, bytes));
}
let expected = read_golden(&path, MAGIC_WANG, "Wang");
assert_bytes_equal(bytes, &expected, "Wang");
}
#[test]
fn panako_v2_golden() {
let samples = synth(0xCAFE, 8_000);
let mut panako = Panako::default();
let buf = AudioBuffer {
samples: &samples,
rate: SampleRate::HZ_8000,
};
let fp = panako.extract(buf).unwrap();
let bytes = bytemuck::cast_slice::<PanakoHash, u8>(&fp.hashes);
let path = Path::new(GOLDEN_DIR).join("panako_v2.bin");
if std::env::var_os("UPDATE_GOLDENS").is_some() {
write_golden(&path, &with_magic(MAGIC_PANAKO, bytes));
}
let expected = read_golden(&path, MAGIC_PANAKO, "Panako");
assert_bytes_equal(bytes, &expected, "Panako");
}
#[test]
fn haitsma_v1_golden() {
let samples = synth(0xCAFE, 5_000);
let mut h = Haitsma::default();
let buf = AudioBuffer {
samples: &samples,
rate: SampleRate::HZ_5000,
};
let fp = h.extract(buf).unwrap();
let bytes = bytemuck::cast_slice::<u32, u8>(&fp.frames);
let path = Path::new(GOLDEN_DIR).join("haitsma_v1.bin");
if std::env::var_os("UPDATE_GOLDENS").is_some() {
write_golden(&path, &with_magic(MAGIC_HAITSMA, bytes));
}
let expected = read_golden(&path, MAGIC_HAITSMA, "Haitsma");
assert_bytes_equal(bytes, &expected, "Haitsma");
}