use xmrsplayer::xmrsplayer::XmrsPlayer;
const FRAME: usize = 1024;
const HOP: usize = 512;
const SPEC_BINS: usize = 24; const MAX_LAG_FRAMES: i64 = 8;
fn render_dut(path: &str, rate: u32, frames: usize) -> Vec<f32> {
let bytes = std::fs::read(path).expect("read module");
let module = xmrs::core::module::Module::load(&bytes).expect("Module::load");
let mut player = XmrsPlayer::new(&module, rate, 0);
let mut out = Vec::with_capacity(frames);
for _ in 0..frames {
match player.sample(true) {
Some((l, r)) => out.push((l as f32 + r as f32) * 0.5),
None => break,
}
}
out.resize(frames, 0.0);
out
}
fn read_raw_mono(path: &str, rate: u32, channels: usize) -> (u32, Vec<f32>) {
let b = std::fs::read(path).expect("read ref raw");
let ch = channels.max(1);
let mut out = Vec::with_capacity(b.len() / (2 * ch));
let mut j = 0;
while j + 2 * ch <= b.len() {
let mut acc = 0f32;
for c in 0..ch {
acc += i16::from_le_bytes([b[j + 2 * c], b[j + 2 * c + 1]]) as f32;
}
out.push(acc / ch as f32);
j += 2 * ch;
}
(rate, out)
}
fn read_wav_mono(path: &str) -> (u32, Vec<f32>) {
let b = std::fs::read(path).expect("read ref wav");
assert!(
&b[0..4] == b"RIFF" && &b[8..12] == b"WAVE",
"not a RIFF/WAVE"
);
let (mut channels, mut rate, mut bits) = (2u16, 0u32, 16u16);
let mut data: &[u8] = &[];
let mut i = 12;
while i + 8 <= b.len() {
let id = &b[i..i + 4];
let sz = u32::from_le_bytes([b[i + 4], b[i + 5], b[i + 6], b[i + 7]]) as usize;
let body = &b[i + 8..(i + 8 + sz).min(b.len())];
if id == b"fmt " {
channels = u16::from_le_bytes([body[2], body[3]]);
rate = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
bits = u16::from_le_bytes([body[14], body[15]]);
} else if id == b"data" {
data = body;
}
i += 8 + sz + (sz & 1); }
assert_eq!(bits, 16, "only 16-bit PCM reference supported");
let ch = channels.max(1) as usize;
let mut out = Vec::with_capacity(data.len() / (2 * ch));
let mut j = 0;
while j + 2 * ch <= data.len() {
let mut acc = 0f32;
for c in 0..ch {
let s = i16::from_le_bytes([data[j + 2 * c], data[j + 2 * c + 1]]);
acc += s as f32;
}
out.push(acc / ch as f32);
j += 2 * ch;
}
(rate, out)
}
fn rms(x: &[f32]) -> f32 {
if x.is_empty() {
return 0.0;
}
(x.iter().map(|v| v * v).sum::<f32>() / x.len() as f32).sqrt()
}
fn log_energy_envelope(x: &[f32]) -> Vec<f32> {
let mut env = Vec::new();
let mut k = 0;
while k + FRAME <= x.len() {
let e = rms(&x[k..k + FRAME]);
env.push((e + 1e-9).ln());
k += HOP;
}
env
}
fn pearson(a: &[f32], b: &[f32]) -> f32 {
let n = a.len().min(b.len());
if n < 2 {
return 0.0;
}
let (a, b) = (&a[..n], &b[..n]);
let ma = a.iter().sum::<f32>() / n as f32;
let mb = b.iter().sum::<f32>() / n as f32;
let mut num = 0f32;
let mut da = 0f32;
let mut db = 0f32;
for i in 0..n {
let (x, y) = (a[i] - ma, b[i] - mb);
num += x * y;
da += x * x;
db += y * y;
}
if da <= 0.0 || db <= 0.0 {
return 0.0;
}
num / (da.sqrt() * db.sqrt())
}
fn env_corr_best(dut: &[f32], reference: &[f32]) -> f32 {
let mut best = -1f32;
for lag in -MAX_LAG_FRAMES..=MAX_LAG_FRAMES {
let c = if lag >= 0 {
pearson(&dut[lag as usize..], reference)
} else {
pearson(dut, &reference[(-lag) as usize..])
};
if c > best {
best = c;
}
}
best
}
fn fft(re: &mut [f32], im: &mut [f32]) {
let n = re.len();
let mut j = 0;
for i in 1..n {
let mut bit = n >> 1;
while j & bit != 0 {
j ^= bit;
bit >>= 1;
}
j ^= bit;
if i < j {
re.swap(i, j);
im.swap(i, j);
}
}
let mut len = 2;
while len <= n {
let ang = -2.0 * core::f32::consts::PI / len as f32;
let (wr, wi) = (ang.cos(), ang.sin());
let mut i = 0;
while i < n {
let (mut cr, mut ci) = (1f32, 0f32);
for k in 0..len / 2 {
let a = i + k;
let b = i + k + len / 2;
let tr = re[b] * cr - im[b] * ci;
let ti = re[b] * ci + im[b] * cr;
re[b] = re[a] - tr;
im[b] = im[a] - ti;
re[a] += tr;
im[a] += ti;
let ncr = cr * wr - ci * wi;
ci = cr * wi + ci * wr;
cr = ncr;
}
i += len;
}
len <<= 1;
}
}
fn spectra(x: &[f32]) -> Vec<[f32; SPEC_BINS]> {
let mut frames = Vec::new();
let half = FRAME / 2;
let edges: Vec<usize> = (0..=SPEC_BINS)
.map(|i| {
let t = i as f32 / SPEC_BINS as f32;
((half as f32).powf(t)).round() as usize
})
.collect();
let mut k = 0;
while k + FRAME <= x.len() {
let mut re = vec![0f32; FRAME];
let mut im = vec![0f32; FRAME];
for n in 0..FRAME {
let w = 0.5 - 0.5 * (2.0 * core::f32::consts::PI * n as f32 / FRAME as f32).cos();
re[n] = x[k + n] * w;
}
fft(&mut re, &mut im);
let mut bands = [0f32; SPEC_BINS];
for (bi, band) in bands.iter_mut().enumerate() {
let (lo, hi) = (edges[bi].max(1), edges[bi + 1].max(edges[bi] + 1).min(half));
let mut acc = 0f32;
for bin in lo..hi {
acc += (re[bin] * re[bin] + im[bin] * im[bin]).sqrt();
}
*band = acc / (hi - lo).max(1) as f32;
}
frames.push(bands);
k += HOP;
}
frames
}
fn cosine(a: &[f32; SPEC_BINS], b: &[f32; SPEC_BINS]) -> f32 {
let mut dot = 0f32;
let mut na = 0f32;
let mut nb = 0f32;
for i in 0..SPEC_BINS {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
if na <= 0.0 || nb <= 0.0 {
return 0.0; }
dot / (na.sqrt() * nb.sqrt())
}
fn main() {
let mut args = std::env::args().skip(1);
let it = args
.next()
.expect("usage: <in.it> <ref.wav> [seconds] [rate]");
let refwav = args
.next()
.expect("usage: <in.it> <ref.wav> [seconds] [rate]");
let seconds: f64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(30.0);
let rate: u32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(48_000);
let (rrate, reference) = if refwav.to_ascii_lowercase().ends_with(".raw") {
read_raw_mono(&refwav, rate, 2)
} else {
read_wav_mono(&refwav)
};
assert_eq!(rrate, rate, "ref rate {} != requested {}", rrate, rate);
let frames = (seconds * rate as f64) as usize;
let dut = render_dut(&it, rate, frames);
let n = dut.len().min(reference.len());
let (dut, reference) = (&dut[..n], &reference[..n]);
let (rd, rr) = (rms(dut), rms(reference));
let level_db = if rr > 0.0 && rd > 0.0 {
20.0 * (rd / rr).log10()
} else {
f32::NAN
};
let env_corr = env_corr_best(&log_energy_envelope(dut), &log_energy_envelope(reference));
let sd = spectra(dut);
let sr = spectra(reference);
let fcount = sd.len().min(sr.len());
let mut ssum = 0f32;
let mut scnt = 0usize;
for i in 0..fcount {
let energetic = sd[i].iter().sum::<f32>() > 1.0 || sr[i].iter().sum::<f32>() > 1.0;
if energetic {
ssum += cosine(&sd[i], &sr[i]);
scnt += 1;
}
}
let spec_sim = if scnt > 0 {
ssum / scnt as f32
} else {
f32::NAN
};
let third = n / 3;
let level_window = |a: usize, b: usize| {
let rd = rms(&dut[a..b.min(dut.len())]);
let rr = rms(&reference[a..b.min(reference.len())]);
if rr > 0.0 && rd > 0.0 {
20.0 * (rd / rr).log10()
} else {
f32::NAN
}
};
let (l0, l1, l2) = (
level_window(0, third),
level_window(third, 2 * third),
level_window(2 * third, n),
);
let name = std::path::Path::new(&it)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("?");
println!(
"{:<40} env_corr={:+.3} spec_sim={:.3} level_db={:+.2} [{:+.1}/{:+.1}/{:+.1}] frames={}",
name, env_corr, spec_sim, level_db, l0, l1, l2, n
);
}