1use crate::levels::{detect_clipping, detect_silence};
6use crate::loudness::{self, loudness_outliers};
7use crate::spectrum::{band_power_fraction, ltas};
8use crate::stereo;
9use crate::util::{self, amp_db, pow_db};
10use ferraus_core::AudioBuffer;
11use serde::Serialize;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
15#[serde(rename_all = "lowercase")]
16pub enum Severity {
17 Low,
18 Medium,
19 High,
20}
21
22#[derive(Clone, Debug, Serialize)]
24pub struct Problem {
25 pub problem: String,
29 pub severity: Severity,
30 pub details: String,
31 pub suggested_fix: String,
34}
35
36#[derive(Clone, Debug, Default, Serialize)]
38pub struct SeverityCounts {
39 pub high: usize,
40 pub medium: usize,
41 pub low: usize,
42}
43
44#[derive(Clone, Debug, Serialize)]
46pub struct ProblemsReport {
47 pub clean: bool,
49 pub counts: SeverityCounts,
50 pub problems: Vec<Problem>,
52}
53
54fn problem(code: &str, severity: Severity, details: String, fix: &str) -> Problem {
55 Problem { problem: code.into(), severity, details, suggested_fix: fix.into() }
56}
57
58fn fmt_times(t: &[f64]) -> String {
59 let shown: Vec<String> = t.iter().take(5).map(|v| format!("{v:.2}s")).collect();
60 let more = if t.len() > 5 { format!(" (+{} more)", t.len() - 5) } else { String::new() };
61 format!("{}{more}", shown.join(", "))
62}
63
64pub fn dc_offsets(buf: &AudioBuffer) -> Vec<f64> {
66 buf.data.iter().map(|c| c.iter().map(|v| *v as f64).sum::<f64>() / c.len().max(1) as f64).collect()
67}
68
69pub fn detect_clicks(buf: &AudioBuffer) -> Vec<f64> {
72 let sr = buf.sample_rate as f64;
73 let merge = (sr * 0.002) as usize;
74 let mut times: Vec<usize> = Vec::new();
75 for ch in &buf.data {
76 let n = ch.len();
77 if n < 300 {
78 continue;
79 }
80 let e = |i: usize| -> f64 {
82 if i < 2 {
83 0.0
84 } else {
85 ch[i] as f64 - 2.0 * ch[i - 1] as f64 + ch[i - 2] as f64
86 }
87 };
88 let mut sum: f64 = (0..129.min(n)).map(|j| e(j).powi(2)).sum();
90 let mut last: Option<usize> = None;
91 for i in 0..n {
92 if i > 0 {
93 if i + 128 < n {
94 sum += e(i + 128).powi(2);
95 }
96 if i > 128 {
97 sum -= e(i - 129).powi(2);
98 }
99 }
100 if i < 2 {
101 continue;
102 }
103 let a = i.saturating_sub(128);
104 let b = (i + 129).min(n);
105 let rms = (sum.max(0.0) / (b - a) as f64).sqrt();
106 let ei = e(i);
107 if ei.abs() > (10.0 * rms).max(0.01) {
108 if let Some(l) = last {
109 if i - l <= merge {
110 last = Some(i);
111 continue;
112 }
113 }
114 times.push(i);
115 last = Some(i);
116 }
117 }
118 }
119 times.sort_unstable();
120 times.dedup_by(|a, b| a.abs_diff(*b) <= merge);
121 times.into_iter().map(|i| i as f64 / sr).collect()
122}
123
124#[derive(Clone, Debug, Serialize)]
126pub struct HumInfo {
127 pub freq_hz: f64,
129 pub harmonics: Vec<usize>,
131 pub relative_db: f64,
133 pub level_db: f64,
135}
136
137pub fn detect_hum(buf: &AudioBuffer) -> Option<HumInfo> {
139 let sr = buf.sample_rate as f64;
140 let n_fft = util::next_pow2((sr / 1.5) as usize).min(util::next_pow2(buf.frames()) / 2).max(4096);
141 if buf.frames() < n_fft / 2 {
142 return None;
143 }
144 let l = ltas(buf, n_fft);
145 let df = l.bin_hz;
146 let total: f64 = l.power.iter().skip(1).sum();
147 if total <= 1e-20 {
148 return None;
149 }
150 let mut best: Option<HumInfo> = None;
151 for base in [50.0, 60.0] {
152 let mut harmonics = Vec::new();
153 let mut hum_power = 0.0;
154 for h in 1..=8usize {
155 let f = base * h as f64;
156 if f > sr / 2.0 - 20.0 {
157 break;
158 }
159 let k0 = ((f - 1.5) / df).floor().max(1.0) as usize;
160 let k1 = ((f + 1.5) / df).ceil() as usize;
161 let (kp, peak) = (k0..=k1).fold((k0, 0.0f64), |m, k| if l.power[k] > m.1 { (k, l.power[k]) } else { m });
162 let mut bg: Vec<f64> = Vec::new();
163 let lo = ((f - 15.0) / df).floor().max(1.0) as usize;
164 let hi = (((f + 15.0) / df).ceil() as usize).min(l.power.len() - 1);
165 for k in lo..=hi {
166 if (k as f64 * df - f).abs() > 4.0 {
167 bg.push(l.power[k]);
168 }
169 }
170 let background = util::median(&bg).max(1e-30);
171 let lobe: f64 = (kp.saturating_sub(2)..=(kp + 2).min(l.power.len() - 1)).map(|k| l.power[k]).sum();
173 let prominence = pow_db(peak / background);
174 if prominence > 15.0 && pow_db(lobe) > -100.0 {
175 harmonics.push(h);
176 hum_power += lobe;
177 }
178 }
179 let strong_fundamental = harmonics.contains(&1) || harmonics.contains(&2);
180 if harmonics.len() >= 2 || (strong_fundamental && !harmonics.is_empty() && hum_power / total > 1e-4) {
181 let info = HumInfo {
182 freq_hz: base,
183 relative_db: pow_db(hum_power / total),
184 level_db: pow_db(hum_power) + 3.0103,
185 harmonics,
186 };
187 if best.as_ref().map(|b| info.relative_db > b.relative_db).unwrap_or(true) {
188 best = Some(info);
189 }
190 }
191 }
192 best
193}
194
195#[derive(Clone, Debug, Serialize)]
197pub struct NoiseFloor {
198 pub hiss_db: f64,
200 pub hiss_flatness: f64,
202 pub floor_db: f64,
204 pub floor_flatness: f64,
206 pub loud_db: f64,
208}
209
210pub fn noise_floor(buf: &AudioBuffer) -> NoiseFloor {
212 let sr = buf.sample_rate as f64;
213 let x = buf.mono_mix();
214 let n_fft = 2048usize;
215 let frames = util::stft_mag(&x, n_fft, n_fft / 2);
216 let df = sr / n_fft as f64;
217 let wss = 3.0 * n_fft as f64 / 8.0;
218 let scale = 2.0 / (n_fft as f64 * wss);
219 let k6 = (6000.0 / df) as usize;
220 let k16 = ((16000.0f64.min(sr / 2.0 * 0.95)) / df) as usize;
221 let mut rows: Vec<(f64, f64, f64, f64)> = frames
222 .iter()
223 .skip(1)
224 .take(frames.len().saturating_sub(2))
225 .map(|f| {
226 let p: Vec<f64> = f.iter().map(|m| (*m as f64).powi(2) * scale).collect();
227 let total: f64 = p[1..].iter().sum();
228 let flat = |s: &[f64]| -> f64 {
229 if s.is_empty() {
230 return 0.0;
231 }
232 let am = s.iter().sum::<f64>() / s.len() as f64;
233 if am <= 1e-30 {
234 return 0.0;
235 }
236 let gm = (s.iter().map(|v| (v + 1e-30).ln()).sum::<f64>() / s.len() as f64).exp();
237 (gm / am).clamp(0.0, 1.0)
238 };
239 let (hi, hflat) =
240 if k16 > k6 + 4 { (p[k6..k16].iter().sum::<f64>(), flat(&p[k6..k16])) } else { (0.0, 0.0) };
241 (total, flat(&p[1..]), hi, hflat)
242 })
243 .collect();
244 if rows.is_empty() {
245 return NoiseFloor {
246 hiss_db: -240.0,
247 hiss_flatness: 0.0,
248 floor_db: -240.0,
249 floor_flatness: 0.0,
250 loud_db: -240.0,
251 };
252 }
253 rows.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
254 let q = (rows.len() / 10).max(1);
255 let quiet = &rows[..q];
256 let loud = &rows[rows.len() - (rows.len() / 20).max(1)..];
257 let med = |v: Vec<f64>| util::median(&v);
258 NoiseFloor {
259 hiss_db: pow_db(med(quiet.iter().map(|r| r.2).collect())),
260 hiss_flatness: med(quiet.iter().map(|r| r.3).collect()),
261 floor_db: pow_db(med(quiet.iter().map(|r| r.0).collect())),
262 floor_flatness: med(quiet.iter().map(|r| r.1).collect()),
263 loud_db: pow_db(med(loud.iter().map(|r| r.0).collect())),
264 }
265}
266
267#[derive(Clone, Debug, Serialize)]
269pub struct Sibilance {
270 pub sibilant_fraction: f64,
272 pub peak_db: f64,
274}
275
276pub fn sibilance(buf: &AudioBuffer) -> Sibilance {
278 let sr = buf.sample_rate as f64;
279 let none = Sibilance { sibilant_fraction: 0.0, peak_db: -240.0 };
280 if sr < 22050.0 {
281 return none;
282 }
283 let x = buf.mono_mix();
284 let n_fft = 1024usize;
285 let frames = util::stft_mag(&x, n_fft, n_fft / 2);
286 let df = sr / n_fft as f64;
287 let scale = 2.0 / (n_fft as f64 * (3.0 * n_fft as f64 / 8.0));
288 let (k5, k10) = ((5000.0 / df) as usize, ((10000.0f64.min(sr / 2.0)) / df) as usize);
289 let rows: Vec<(f64, f64)> = frames
290 .iter()
291 .map(|f| {
292 let p: Vec<f64> = f.iter().map(|m| (*m as f64).powi(2) * scale).collect();
293 (p[1..].iter().sum(), p[k5..k10].iter().sum())
294 })
295 .collect();
296 let max = rows.iter().map(|r| r.0).fold(0.0, f64::max);
297 if max <= 0.0 {
298 return none;
299 }
300 let loud: Vec<&(f64, f64)> = rows.iter().filter(|r| pow_db(r.0) > (pow_db(max) - 30.0).max(-45.0)).collect();
301 if loud.is_empty() {
302 return none;
303 }
304 let sib: Vec<&&(f64, f64)> = loud.iter().filter(|r| r.1 > 0.5 * r.0).collect();
305 Sibilance {
306 sibilant_fraction: sib.len() as f64 / loud.len() as f64,
307 peak_db: sib.iter().map(|r| pow_db(r.1) + 3.0103).fold(-240.0, f64::max),
308 }
309}
310
311pub fn diagnose(buf: &AudioBuffer) -> ProblemsReport {
313 let mut out: Vec<Problem> = Vec::new();
314 let dur = buf.duration_s();
315 let sp = buf.peak() as f64;
316 if buf.is_empty() || sp == 0.0 {
317 out.push(problem("silent", Severity::High, "the file contains only digital silence".into(), ""));
318 return finish(out);
319 }
320
321 let clip = detect_clipping(buf, 0.0, 3);
323 if clip.count > 0 {
324 let sev = if clip.clipped_fraction >= 1e-3 || clip.count > 50 {
325 Severity::High
326 } else if clip.count >= 5 {
327 Severity::Medium
328 } else {
329 Severity::Low
330 };
331 let times: Vec<f64> = clip.runs.iter().map(|r| r.start_s).collect();
332 out.push(problem(
333 "clipping",
334 sev,
335 format!(
336 "{} clipped run(s), {} samples ({:.3}%), at {}",
337 clip.count,
338 clip.clipped_samples,
339 clip.clipped_fraction * 100.0,
340 fmt_times(×)
341 ),
342 "declip",
343 ));
344 }
345
346 let dcs = dc_offsets(buf);
348 let worst = dcs.iter().fold(0.0f64, |m, v| m.max(v.abs()));
349 if worst >= 0.001 {
350 let sev = if worst >= 0.02 {
351 Severity::High
352 } else if worst >= 0.005 {
353 Severity::Medium
354 } else {
355 Severity::Low
356 };
357 let per: Vec<String> = dcs.iter().enumerate().map(|(c, d)| format!("ch{} {:+.4}", c + 1, d)).collect();
358 out.push(problem(
359 "dc_offset",
360 sev,
361 format!("DC offset up to {:.4} ({:.1} dBFS): {}", worst, amp_db(worst), per.join(", ")),
362 "dc-remove",
363 ));
364 }
365
366 let tp = loudness::true_peak(buf);
368 let (tp_db, sp_db) = (amp_db(tp), amp_db(sp));
369 if tp_db > -1.0 && tp_db > sp_db + 0.3 {
370 let sev = if tp_db > 0.0 {
371 Severity::High
372 } else if tp_db > -0.5 {
373 Severity::Medium
374 } else {
375 Severity::Low
376 };
377 out.push(problem(
378 "inter_sample_peaks",
379 sev,
380 format!("true peak {tp_db:.2} dBTP is above the sample peak {sp_db:.2} dBFS"),
381 "limit:ceiling=-1dBTP",
382 ));
383 }
384
385 let clicks = detect_clicks(buf);
387 if !clicks.is_empty() {
388 let rate = clicks.len() as f64 / (dur / 60.0).max(1e-9);
389 let sev = if clicks.len() >= 20 || (rate > 10.0 && clicks.len() >= 5) {
390 Severity::High
391 } else if clicks.len() >= 3 {
392 Severity::Medium
393 } else {
394 Severity::Low
395 };
396 out.push(problem("clicks", sev, format!("{} click(s) at {}", clicks.len(), fmt_times(&clicks)), "declick"));
397 }
398
399 if let Some(h) = detect_hum(buf) {
401 let sev = if h.relative_db > -20.0 {
402 Severity::High
403 } else if h.relative_db > -40.0 {
404 Severity::Medium
405 } else {
406 Severity::Low
407 };
408 out.push(problem(
409 "hum",
410 sev,
411 format!(
412 "{:.0} Hz hum (harmonics {:?}) at {:.1} dB relative to the signal",
413 h.freq_hz, h.harmonics, h.relative_db
414 ),
415 if h.freq_hz == 50.0 { "dehum:freq=50Hz" } else { "dehum:freq=60Hz" },
416 ));
417 }
418
419 let nf = noise_floor(buf);
421 if nf.hiss_db > -72.0 && nf.hiss_flatness > 0.3 {
422 let sev = if nf.hiss_db > -45.0 {
423 Severity::High
424 } else if nf.hiss_db > -58.0 {
425 Severity::Medium
426 } else {
427 Severity::Low
428 };
429 out.push(problem(
430 "hiss",
431 sev,
432 format!("high-frequency noise floor of {:.1} dBFS (6–16 kHz) in quiet passages", nf.hiss_db),
433 "dehiss",
434 ));
435 }
436 if nf.floor_db > -50.0 && nf.floor_flatness > 0.4 && nf.loud_db - nf.floor_db < 30.0 {
437 out.push(problem(
438 "noise_floor",
439 if nf.loud_db - nf.floor_db < 15.0 { Severity::High } else { Severity::Medium },
440 format!(
441 "broadband noise floor of {:.1} dBFS, only {:.1} dB below the loud passages",
442 nf.floor_db,
443 nf.loud_db - nf.floor_db
444 ),
445 "denoise:adaptive",
446 ));
447 }
448
449 if buf.channels() >= 2 {
451 let st = stereo::stereo_analysis(buf);
452 if st.correlation < -0.3 {
453 out.push(problem(
454 "phase",
455 Severity::High,
456 format!(
457 "left and right are largely out of phase (correlation {:.2}); the mono sum cancels",
458 st.correlation
459 ),
460 "channels mix --invert R",
461 ));
462 } else if st.correlation < 0.1 && st.width > 0.45 {
463 out.push(problem(
464 "phase",
465 Severity::Medium,
466 format!("weak L/R correlation ({:.2}); mono playback loses {:.1} dB", st.correlation, -st.mono_loss_db),
467 "phase-align",
468 ));
469 }
470 let l = ltas(buf, util::next_pow2((buf.sample_rate / 4) as usize).max(2048));
471 let low_share = band_power_fraction(&l, 20.0, 120.0);
472 let cl = st.correlation_below_120hz;
473 if low_share > 0.01 && cl < 0.8 {
474 let sev = if cl < 0.0 {
475 Severity::High
476 } else if cl < 0.5 {
477 Severity::Medium
478 } else {
479 Severity::Low
480 };
481 out.push(problem(
482 "out_of_phase_bass",
483 sev,
484 format!("L/R correlation below 120 Hz is {cl:.2}; bass will thin out or cancel in mono"),
485 "widen:mono-below=120Hz",
486 ));
487 }
488 }
489
490 let sil = detect_silence(buf, -60.0, 0.1);
492 if sil.head_silence_s > 0.5 {
493 out.push(problem(
494 "silence_head",
495 if sil.head_silence_s >= 2.0 { Severity::Medium } else { Severity::Low },
496 format!("{:.2} s of silence (below -60 dBFS) at the start", sil.head_silence_s),
497 "silence trim --edges",
498 ));
499 }
500 if sil.tail_silence_s > 1.0 {
501 out.push(problem(
502 "silence_tail",
503 if sil.tail_silence_s >= 3.0 { Severity::Medium } else { Severity::Low },
504 format!("{:.2} s of silence (below -60 dBFS) at the end", sil.tail_silence_s),
505 "silence trim --edges",
506 ));
507 }
508
509 if dur >= 20.0 {
511 let o = loudness_outliers(buf, 8.0);
512 if !o.is_empty() {
513 let worst = o.iter().fold(0.0f64, |m, s| m.max(s.delta_lu.abs()));
514 let desc: Vec<String> =
515 o.iter().take(5).map(|s| format!("{:.1}-{:.1}s {:+.1} LU", s.start_s, s.end_s, s.delta_lu)).collect();
516 out.push(problem(
517 "loudness_outliers",
518 if worst > 12.0 { Severity::Medium } else { Severity::Low },
519 format!("sections far from the integrated loudness: {}", desc.join(", ")),
520 "leveler",
521 ));
522 }
523 }
524
525 let lr = ltas(buf, util::next_pow2((buf.sample_rate / 2) as usize).max(4096));
527 let frac = band_power_fraction(&lr, 8.0, 30.0);
528 let rel = pow_db(frac);
529 let rumble_level = pow_db(frac * lr.power.iter().skip(1).sum::<f64>());
530 if rel > -30.0 && rumble_level > -70.0 {
531 out.push(problem(
532 "rumble",
533 if rel > -10.0 {
534 Severity::High
535 } else if rel > -20.0 {
536 Severity::Medium
537 } else {
538 Severity::Low
539 },
540 format!("energy below 30 Hz is {rel:.1} dB relative to the whole signal"),
541 "highpass:freq=30Hz",
542 ));
543 }
544
545 let s = sibilance(buf);
547 if s.sibilant_fraction >= 0.02 && s.sibilant_fraction <= 0.3 && s.peak_db > -25.0 {
548 out.push(problem(
549 "sibilance",
550 if s.sibilant_fraction > 0.1 && s.peak_db > -15.0 { Severity::Medium } else { Severity::Low },
551 format!(
552 "{:.0}% of loud frames are dominated by 5-10 kHz energy (peak {:.1} dBFS)",
553 s.sibilant_fraction * 100.0,
554 s.peak_db
555 ),
556 "deess",
557 ));
558 }
559 finish(out)
560}
561
562fn finish(mut out: Vec<Problem>) -> ProblemsReport {
563 out.sort_by_key(|p| std::cmp::Reverse(p.severity));
564 let mut counts = SeverityCounts::default();
565 for p in &out {
566 match p.severity {
567 Severity::High => counts.high += 1,
568 Severity::Medium => counts.medium += 1,
569 Severity::Low => counts.low += 1,
570 }
571 }
572 ProblemsReport { clean: counts.high == 0 && counts.medium == 0, counts, problems: out }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use ferraus_core::testing::{add, noise, sine, stereo};
579
580 fn has(r: &ProblemsReport, code: &str) -> bool {
581 r.problems.iter().any(|p| p.problem == code)
582 }
583
584 #[test]
585 fn clean_sine_has_no_serious_problems() {
586 for f in [440.0, 1000.0] {
587 let b = stereo(sine(48000, f, 4.0, 0.5));
588 let r = diagnose(&b);
589 assert!(r.problems.iter().all(|p| p.severity != Severity::High), "{r:?}");
590 assert!(r.clean, "{r:?}");
591 }
592 }
593
594 #[test]
595 fn detects_clipping_and_dc() {
596 let mut b = sine(48000, 200.0, 2.0, 1.6);
597 b.data[0].iter_mut().for_each(|v| *v = v.clamp(-1.0, 1.0));
598 let r = diagnose(&b);
599 assert!(has(&r, "clipping"), "{r:?}");
600 assert_eq!(r.problems[0].suggested_fix, "declip");
601 let mut d = stereo(sine(48000, 440.0, 2.0, 0.4));
602 d.data.iter_mut().for_each(|c| c.iter_mut().for_each(|v| *v += 0.05));
603 let r = diagnose(&d);
604 let p = r.problems.iter().find(|p| p.problem == "dc_offset").expect("dc");
605 assert_eq!(p.severity, Severity::High);
606 assert_eq!(p.suggested_fix, "dc-remove");
607 }
608
609 #[test]
610 fn detects_hum() {
611 let mut hum = sine(48000, 60.0, 3.0, 0.03);
612 for h in 2..=4 {
613 hum = add(&hum, &sine(48000, 60.0 * h as f64, 3.0, 0.03 / h as f32));
614 }
615 let b = add(&sine(48000, 440.0, 3.0, 0.3), &hum);
616 let r = diagnose(&b);
617 let p = r.problems.iter().find(|p| p.problem == "hum").expect("hum");
618 assert_eq!(p.suggested_fix, "dehum:freq=60Hz");
619 let b = add(&sine(48000, 1000.0, 3.0, 0.3), &sine(48000, 50.0, 3.0, 0.05));
621 let r = diagnose(&b);
622 assert!(r.problems.iter().any(|p| p.suggested_fix == "dehum:freq=50Hz"), "{r:?}");
623 }
624
625 #[test]
626 fn detects_head_silence() {
627 let mut b = AudioBuffer::new(48000, 2, 48000 * 3);
628 b.append(&stereo(sine(48000, 440.0, 2.0, 0.5)));
629 let r = diagnose(&b);
630 let p = r.problems.iter().find(|p| p.problem == "silence_head").expect("head");
631 assert_eq!(p.severity, Severity::Medium);
632 assert_eq!(p.suggested_fix, "silence trim --edges");
633 }
634
635 #[test]
636 fn detects_out_of_phase_bass() {
637 let bass = sine(48000, 60.0, 3.0, 0.4).data[0].clone();
638 let hi = sine(48000, 1500.0, 3.0, 0.3).data[0].clone();
639 let l: Vec<f32> = bass.iter().zip(&hi).map(|(b, h)| b + h).collect();
640 let r: Vec<f32> = bass.iter().zip(&hi).map(|(b, h)| -b + h).collect();
641 let rep = diagnose(&AudioBuffer::from_planar(48000, vec![l, r]));
642 let p = rep.problems.iter().find(|p| p.problem == "out_of_phase_bass").expect("bass");
643 assert_eq!(p.severity, Severity::High);
644 assert_eq!(p.suggested_fix, "widen:mono-below=120Hz");
645 }
646
647 #[test]
648 fn detects_clicks_hiss_rumble_and_silence() {
649 let mut b = sine(48000, 440.0, 3.0, 0.3);
650 for t in [0.5, 1.2, 2.1, 2.7] {
651 b.data[0][(t * 48000.0) as usize] += 0.6;
652 }
653 let r = diagnose(&b);
654 let p = r.problems.iter().find(|p| p.problem == "clicks").expect("clicks");
655 assert!(p.details.starts_with("4 click"), "{}", p.details);
656 let h = add(&sine(48000, 440.0, 3.0, 0.3), &noise(48000, 3.0, 0.02, 3));
657 assert!(has(&diagnose(&h), "hiss"));
658 let rb = add(&sine(48000, 440.0, 3.0, 0.2), &sine(48000, 18.0, 3.0, 0.3));
659 assert!(has(&diagnose(&rb), "rumble"));
660 let s = diagnose(&AudioBuffer::new(48000, 2, 4800));
661 assert!(has(&s, "silent"));
662 assert!(!s.clean);
663 }
664
665 #[test]
666 fn inter_sample_peaks() {
667 let x: Vec<f32> = (0..48000)
668 .map(|i| (0.99 * (std::f64::consts::PI / 2.0 * i as f64 + std::f64::consts::PI / 4.0).sin()) as f32)
669 .collect();
670 let r = diagnose(&AudioBuffer::from_mono(48000, x));
671 let p = r.problems.iter().find(|p| p.problem == "inter_sample_peaks").expect("isp");
672 assert_eq!(p.severity, Severity::Medium);
673 }
674}