1use crate::util::{self, amp_db, pow_db};
4use ferraus_core::resample::{resample, Quality};
5use ferraus_core::AudioBuffer;
6use serde::Serialize;
7
8#[derive(Clone, Debug)]
10pub struct CompareOptions {
11 pub max_offset_s: f64,
13 pub no_align: bool,
15}
16
17impl Default for CompareOptions {
18 fn default() -> Self {
19 CompareOptions { max_offset_s: 1.0, no_align: false }
20 }
21}
22
23#[derive(Clone, Debug, Serialize)]
25pub struct CompareReport {
26 pub offset_samples: i64,
28 pub offset_s: f64,
29 pub alignment_correlation: f64,
31 pub polarity_inverted: bool,
33 pub compared_frames: usize,
35 pub length_difference_s: f64,
37 pub rms_diff_db: f64,
39 pub null_depth_db: f64,
41 pub diff_peak_db: f64,
43 pub peak_delta_db: f64,
45 pub rms_delta_db: f64,
47 pub loudness_delta_lu: f64,
49 pub gain_db: f64,
51 pub si_sdr_db: f64,
53 pub identical: bool,
55 pub verdict: String,
57 #[serde(skip_serializing_if = "Vec::is_empty")]
58 pub notes: Vec<String>,
59}
60
61fn conform(a: &AudioBuffer, b: &AudioBuffer, notes: &mut Vec<String>) -> AudioBuffer {
63 let mut b2 = b.clone();
64 if b2.sample_rate != a.sample_rate {
65 notes.push(format!("resampled b from {} Hz to {} Hz", b2.sample_rate, a.sample_rate));
66 b2 = resample(&b2, a.sample_rate, Quality::High);
67 }
68 if b2.channels() != a.channels() {
69 notes.push(format!("converted b from {} to {} channel(s)", b2.channels(), a.channels()));
70 b2 = b2.with_channels(a.channels());
71 }
72 b2
73}
74
75pub fn find_offset(a: &AudioBuffer, b: &AudioBuffer, max_offset_s: f64) -> (i64, f64) {
78 let sr = a.sample_rate as f64;
79 let max_lag = (max_offset_s.max(0.0) * sr).round() as usize;
80 let span = (30.0 * sr) as usize + max_lag;
81 let ma = a.mono_mix();
82 let mb = b.mono_mix();
83 let la = ma.len().min(span);
84 let lb = mb.len().min(span);
85 let (lag, _, norm) = util::xcorr_peak(&ma[..la], &mb[..lb], max_lag);
86 (lag as i64, norm)
87}
88
89fn si_sdr(reference: &[f64], estimate: &[f64]) -> f64 {
90 let dot: f64 = reference.iter().zip(estimate).map(|(r, e)| r * e).sum();
91 let rr: f64 = reference.iter().map(|r| r * r).sum();
92 if rr <= 1e-30 {
93 return if estimate.iter().all(|e| *e == 0.0) { 200.0 } else { -200.0 };
94 }
95 let alpha = dot / rr;
96 let (mut st, mut en) = (0.0f64, 0.0f64);
97 for (r, e) in reference.iter().zip(estimate) {
98 let t = alpha * r;
99 st += t * t;
100 en += (e - t).powi(2);
101 }
102 if en <= 0.0 {
103 return 200.0;
104 }
105 if st <= 0.0 {
106 return -200.0;
107 }
108 (10.0 * (st / en).log10()).clamp(-200.0, 200.0)
109}
110
111pub fn compare(a: &AudioBuffer, b: &AudioBuffer, opts: &CompareOptions) -> CompareReport {
113 let mut notes = Vec::new();
114 let b = conform(a, b, &mut notes);
115 let (lag, corr) = if opts.no_align { (0, 0.0) } else { find_offset(a, &b, opts.max_offset_s) };
116 let (a0, b0) = if lag >= 0 { (0usize, lag as usize) } else { ((-lag) as usize, 0usize) };
118 let len = a.frames().saturating_sub(a0).min(b.frames().saturating_sub(b0));
119 let ch = a.channels();
120 let mut ra = Vec::with_capacity(len * ch);
121 let mut rb = Vec::with_capacity(len * ch);
122 for c in 0..ch {
123 ra.extend(a.data[c][a0..a0 + len].iter().map(|v| *v as f64));
124 rb.extend(b.data[c][b0..b0 + len].iter().map(|v| *v as f64));
125 }
126 let n = ra.len().max(1) as f64;
127 let (mut sd, mut pd, mut sa, mut sb, mut ab) = (0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64);
128 for (x, y) in ra.iter().zip(&rb) {
129 let d = x - y;
130 sd += d * d;
131 pd = pd.max(d.abs());
132 sa += x * x;
133 sb += y * y;
134 ab += x * y;
135 }
136 let rms_diff_db = pow_db(sd / n);
137 let rms_a_db = pow_db(sa / n);
138 let la = crate::loudness::integrated_lufs(a);
139 let lb = crate::loudness::integrated_lufs(&b);
140 let gain = if sa > 0.0 { ab / sa } else { 0.0 };
141 let si = si_sdr(&ra, &rb);
142 let identical = lag == 0 && a.frames() == b.frames() && pd == 0.0 && notes.is_empty();
143 let null_depth = if sa > 0.0 { rms_diff_db - rms_a_db } else { rms_diff_db };
144 let gain_db = amp_db(gain.abs());
145 let verdict = if identical {
146 "identical"
147 } else if null_depth <= -90.0 {
148 "transparent"
149 } else if si >= 60.0 {
150 "gain_only"
151 } else if si >= 20.0 {
152 "similar"
153 } else {
154 "different"
155 };
156 CompareReport {
157 offset_samples: lag,
158 offset_s: lag as f64 / a.sample_rate as f64,
159 alignment_correlation: corr,
160 polarity_inverted: gain < 0.0,
161 compared_frames: len,
162 length_difference_s: b.duration_s() - a.duration_s(),
163 rms_diff_db,
164 null_depth_db: null_depth,
165 diff_peak_db: amp_db(pd),
166 peak_delta_db: amp_db(b.peak() as f64) - amp_db(a.peak() as f64),
167 rms_delta_db: pow_db(sb / n) - rms_a_db,
168 loudness_delta_lu: if la.is_finite() && lb.is_finite() { lb - la } else { f64::NAN },
169 gain_db,
170 si_sdr_db: si,
171 identical,
172 verdict: verdict.into(),
173 notes,
174 }
175}
176
177#[derive(Clone, Debug, Serialize)]
183pub struct SimilarityReport {
184 pub score: f64,
186 pub spectral_similarity: f64,
188 pub spectral_distance_db: f64,
190 pub loudness_difference_lu: f64,
192 pub mfcc_distance: f64,
194 pub mfcc_similarity: f64,
195 pub verdict: String,
197}
198
199pub fn mfcc(buf: &AudioBuffer, n_coeffs: usize) -> Vec<Vec<f32>> {
202 let sr = buf.sample_rate as f64;
203 let x = buf.mono_mix();
204 let n_fft = util::next_pow2((sr * 0.046) as usize).max(256);
205 let hop = n_fft / 2;
206 let bank = util::mel_filterbank(sr, n_fft, 40, 30.0, 16000.0f64.min(sr / 2.0));
207 let frames = util::stft_mag(&x, n_fft, hop);
208 let energies: Vec<f32> = frames.iter().map(|f| f.iter().map(|m| m * m).sum()).collect();
209 let emax = energies.iter().copied().fold(0.0f32, f32::max);
210 frames
211 .iter()
212 .zip(&energies)
213 .filter(|(_, e)| emax > 0.0 && **e > emax * 1e-6)
214 .map(|(f, _)| {
215 let p: Vec<f32> = f.iter().map(|m| m * m).collect();
216 let mel: Vec<f32> = util::apply_bank(&bank, &p).into_iter().map(|e| 10.0 * (e + 1e-10).log10()).collect();
217 util::dct2(&mel, n_coeffs)
218 })
219 .collect()
220}
221
222fn mean_mfcc(buf: &AudioBuffer) -> Option<Vec<f64>> {
223 let m = mfcc(buf, 13);
224 if m.is_empty() {
225 return None;
226 }
227 let mut acc = vec![0.0f64; 12];
228 for f in &m {
229 for (a, v) in acc.iter_mut().zip(&f[1..]) {
230 *a += *v as f64;
231 }
232 }
233 Some(acc.into_iter().map(|v| v / m.len() as f64).collect())
234}
235
236pub fn similarity(a: &AudioBuffer, b: &AudioBuffer) -> SimilarityReport {
238 let sa = crate::spectrum::third_octave_levels(a);
239 let sb = crate::spectrum::third_octave_levels(b);
240 let pairs: Vec<(f64, f64)> = sa
241 .iter()
242 .filter_map(|x| sb.iter().find(|y| y.center_hz == x.center_hz).map(|y| (x.center_hz, x.db, y.db)))
243 .filter(|(c, x, y)| *c >= 31.0 && *c <= 16500.0 && (*x > -120.0 || *y > -120.0))
244 .map(|(_, x, y)| (x.max(-120.0), y.max(-120.0)))
245 .collect();
246 let (spec_sim, spec_dist) = if pairs.len() >= 3 {
247 let xa: Vec<f64> = pairs.iter().map(|p| p.0).collect();
248 let xb: Vec<f64> = pairs.iter().map(|p| p.1).collect();
249 let ma = util::mean(&xa);
250 let mb = util::mean(&xb);
251 let (mut dot, mut na, mut nb, mut dd) = (0.0, 0.0, 0.0, 0.0);
252 for (x, y) in xa.iter().zip(&xb) {
253 let (p, q) = (x - ma, y - mb);
254 dot += p * q;
255 na += p * p;
256 nb += q * q;
257 dd += (p - q).powi(2);
258 }
259 let cos = if na > 1e-12 && nb > 1e-12 {
260 dot / (na * nb).sqrt()
261 } else if na <= 1e-12 && nb <= 1e-12 {
262 1.0
263 } else {
264 0.0
265 };
266 (cos.clamp(0.0, 1.0), (dd / xa.len() as f64).sqrt())
267 } else {
268 (if pairs.is_empty() { 1.0 } else { 0.0 }, 0.0)
269 };
270 let la = crate::loudness::integrated_lufs(a);
271 let lb = crate::loudness::integrated_lufs(b);
272 let ldiff = if la.is_finite() && lb.is_finite() {
273 lb - la
274 } else if la.is_finite() != lb.is_finite() {
275 f64::INFINITY
276 } else {
277 0.0
278 };
279 let (mdist, msim) = match (mean_mfcc(a), mean_mfcc(b)) {
280 (Some(x), Some(y)) => {
281 let d = x.iter().zip(&y).map(|(p, q)| (p - q).powi(2)).sum::<f64>().sqrt();
282 (d, (-d / 25.0).exp())
283 }
284 (None, None) => (0.0, 1.0),
285 _ => (f64::INFINITY, 0.0),
286 };
287 let lsim = if ldiff.is_finite() { (-ldiff.abs() / 6.0).exp() } else { 0.0 };
288 let score = (0.45 * spec_sim + 0.4 * msim + 0.15 * lsim).clamp(0.0, 1.0);
289 let verdict = if score >= 0.9 {
290 "same"
291 } else if score >= 0.7 {
292 "similar"
293 } else {
294 "different"
295 };
296 SimilarityReport {
297 score,
298 spectral_similarity: spec_sim,
299 spectral_distance_db: spec_dist,
300 loudness_difference_lu: ldiff,
301 mfcc_distance: mdist,
302 mfcc_similarity: msim,
303 verdict: verdict.into(),
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use ferraus_core::testing::{add, chirp, noise, sine, stereo};
311
312 fn music_like() -> AudioBuffer {
313 let a = add(&sine(48000, 220.0, 2.0, 0.3), &noise(48000, 2.0, 0.05, 7));
314 stereo(add(&a, &chirp(48000, 300.0, 3000.0, 2.0, 0.2)))
315 }
316
317 #[test]
318 fn identical_files() {
319 let a = music_like();
320 let r = compare(&a, &a, &CompareOptions::default());
321 assert!(r.identical);
322 assert_eq!(r.offset_samples, 0);
323 assert_eq!(r.rms_diff_db, -240.0);
324 assert!(r.si_sdr_db >= 150.0, "{}", r.si_sdr_db);
325 assert_eq!(r.verdict, "identical");
326 }
327
328 #[test]
329 fn delayed_and_scaled() {
330 let a = music_like();
331 let mut b = AudioBuffer::new(48000, 2, 100);
332 b.append(&a);
333 b.scale(0.5);
334 let r = compare(&a, &b, &CompareOptions::default());
335 assert_eq!(r.offset_samples, 100);
336 assert!(r.si_sdr_db > 100.0, "{}", r.si_sdr_db);
337 assert!((r.gain_db + 6.02).abs() < 0.01, "{}", r.gain_db);
338 assert!(!r.polarity_inverted);
339 assert_eq!(r.verdict, "gain_only");
340 let r = compare(&b, &a, &CompareOptions::default());
342 assert_eq!(r.offset_samples, -100);
343 let mut inv = a.clone();
345 inv.scale(-1.0);
346 let r = compare(&a, &inv, &CompareOptions::default());
347 assert!(r.polarity_inverted);
348 assert_eq!(r.offset_samples, 0);
349 }
350
351 #[test]
352 fn different_signals() {
353 let a = music_like();
354 let b = stereo(noise(48000, 2.0, 0.3, 99));
355 let r = compare(&a, &b, &CompareOptions::default());
356 assert_eq!(r.verdict, "different");
357 assert!(r.si_sdr_db < 0.0);
358 }
359
360 #[test]
361 fn similarity_scores() {
362 let a = music_like();
363 let mut a2 = a.clone();
364 a2.scale(0.9);
365 let s = similarity(&a, &a2);
366 assert!(s.score > 0.9, "{s:?}");
367 assert_eq!(s.verdict, "same");
368 let b = stereo(sine(48000, 5000.0, 2.0, 0.1));
369 let s = similarity(&a, &b);
370 assert!(s.score < 0.6, "{s:?}");
371 assert_eq!(s.verdict, "different");
372 }
373}