1use crate::util::{self, amp_db, pow_db};
5use ferraus_core::{db_to_lin, AudioBuffer};
6use serde::Serialize;
7
8#[derive(Clone, Debug, Serialize)]
14pub struct SilenceRegion {
15 pub start_s: f64,
16 pub end_s: f64,
17 pub duration_s: f64,
18 pub floor_db: f64,
20}
21
22#[derive(Clone, Debug, Serialize)]
24pub struct SilenceReport {
25 pub threshold_db: f64,
26 pub min_duration_s: f64,
27 pub regions: Vec<SilenceRegion>,
28 pub total_silence_s: f64,
29 pub silent_fraction: f64,
30 pub head_silence_s: f64,
32 pub tail_silence_s: f64,
34}
35
36fn frame_levels(buf: &AudioBuffer, frame_s: f64) -> (usize, Vec<f64>, Vec<f64>) {
38 let fl = ((buf.sample_rate as f64 * frame_s).round() as usize).max(1);
39 let n = buf.frames();
40 let count = n.div_ceil(fl);
41 let mut energies = vec![0.0f64; count];
42 for ch in &buf.data {
43 for (f, e) in energies.iter_mut().enumerate() {
44 let a = f * fl;
45 let b = (a + fl).min(n);
46 let ms = ch[a..b].iter().map(|v| (*v as f64).powi(2)).sum::<f64>() / (b - a) as f64;
47 *e = e.max(ms);
48 }
49 }
50 let levels = energies.iter().map(|e| pow_db(*e)).collect();
51 (fl, levels, energies)
52}
53
54pub fn detect_silence(buf: &AudioBuffer, threshold_db: f64, min_duration_s: f64) -> SilenceReport {
57 let sr = buf.sample_rate as f64;
58 let n = buf.frames();
59 let dur = buf.duration_s();
60 let (fl, levels, energies) = frame_levels(buf, 0.01);
61 let mut regions = Vec::new();
62 let mut i = 0;
63 while i < levels.len() {
64 if levels[i] < threshold_db {
65 let s = i;
66 while i < levels.len() && levels[i] < threshold_db {
67 i += 1;
68 }
69 let start = (s * fl) as f64 / sr;
70 let end = ((i * fl).min(n)) as f64 / sr;
71 if end - start + 1e-9 >= min_duration_s {
72 let e = util::mean(&energies[s..i]);
73 regions.push(SilenceRegion {
74 start_s: start,
75 end_s: end,
76 duration_s: end - start,
77 floor_db: pow_db(e),
78 });
79 }
80 } else {
81 i += 1;
82 }
83 }
84 let first = levels.iter().position(|l| *l >= threshold_db);
85 let last = levels.iter().rposition(|l| *l >= threshold_db);
86 let (head, tail) = match (first, last) {
87 (Some(f), Some(l)) => ((f * fl) as f64 / sr, (dur - ((l + 1) * fl).min(n) as f64 / sr).max(0.0)),
88 _ => (dur, dur),
89 };
90 let total: f64 = regions.iter().map(|r| r.duration_s).sum();
91 SilenceReport {
92 threshold_db,
93 min_duration_s,
94 total_silence_s: total,
95 silent_fraction: if dur > 0.0 { total / dur } else { 0.0 },
96 regions,
97 head_silence_s: head,
98 tail_silence_s: tail,
99 }
100}
101
102#[derive(Clone, Debug, Serialize)]
108pub struct ClipRun {
109 pub channel: usize,
110 pub start_s: f64,
111 pub start_sample: usize,
112 pub length: usize,
113 pub level_db: f64,
115 pub kind: &'static str,
117}
118
119#[derive(Clone, Debug, Serialize)]
121pub struct ClippingReport {
122 pub threshold_db: f64,
123 pub min_run: usize,
124 pub count: usize,
126 pub clipped_samples: usize,
127 pub clipped_fraction: f64,
128 pub runs: Vec<ClipRun>,
129 pub truncated: bool,
131}
132
133const MAX_RUNS: usize = 1000;
134
135pub fn detect_clipping(buf: &AudioBuffer, threshold_db: f64, min_run: usize) -> ClippingReport {
139 let sr = buf.sample_rate as f64;
140 let thr = (db_to_lin(threshold_db) * (1.0 - 1e-4)) as f32;
141 let min_run = min_run.max(1);
142 let mut runs: Vec<ClipRun> = Vec::new();
143 let mut count = 0usize;
144 let mut clipped = 0usize;
145 for (c, ch) in buf.data.iter().enumerate() {
146 let peak = ch.iter().fold(0.0f32, |m, v| m.max(v.abs()));
147 let mut push = |start: usize, len: usize, level: f32, kind: &'static str| {
148 count += 1;
149 clipped += len;
150 if runs.len() < MAX_RUNS {
151 runs.push(ClipRun {
152 channel: c,
153 start_s: start as f64 / sr,
154 start_sample: start,
155 length: len,
156 level_db: amp_db(level as f64),
157 kind,
158 });
159 }
160 };
161 let mut i = 0;
162 while i < ch.len() {
163 let v = ch[i];
164 if v.abs() >= thr {
165 let s = i;
166 let sign = v.signum();
167 let mut lvl = 0.0f32;
168 while i < ch.len() && ch[i].abs() >= thr && ch[i].signum() == sign {
169 lvl = lvl.max(ch[i].abs());
170 i += 1;
171 }
172 if i - s >= min_run {
173 push(s, i - s, lvl, "threshold");
174 }
175 continue;
176 }
177 if peak >= 0.25 && v.abs() >= 0.98 * peak && v.abs() < thr {
179 let s = i;
180 while i + 1 < ch.len() && (ch[i + 1] - v).abs() <= 1e-7 {
181 i += 1;
182 }
183 let len = i - s + 1;
184 if len >= min_run.max(3) {
185 push(s, len, v.abs(), "flat");
186 }
187 i += 1;
188 continue;
189 }
190 i += 1;
191 }
192 }
193 let total = buf.frames() * buf.channels();
194 ClippingReport {
195 threshold_db,
196 min_run,
197 count,
198 clipped_samples: clipped,
199 clipped_fraction: if total > 0 { clipped as f64 / total as f64 } else { 0.0 },
200 truncated: count > runs.len(),
201 runs,
202 }
203}
204
205#[derive(Clone, Debug, Serialize)]
213pub struct Histogram {
214 pub bins_db: Vec<f64>,
215 pub counts: Vec<u64>,
216}
217
218#[derive(Clone, Debug, Serialize)]
220pub struct ChannelStats {
221 pub channel: usize,
222 pub peak_db: f64,
223 pub true_peak_dbtp: f64,
224 pub rms_db: f64,
225 pub dc_offset: f64,
227 pub dc_offset_db: f64,
228 pub crest_factor_db: f64,
230 pub dynamic_range_db: f64,
232 pub min_rms_db: f64,
234 pub max_rms_db: f64,
236 pub full_scale_samples: usize,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub histogram: Option<Histogram>,
240}
241
242#[derive(Clone, Debug, Serialize)]
244pub struct OverallStats {
245 pub peak_db: f64,
246 pub true_peak_dbtp: f64,
247 pub rms_db: f64,
248 pub crest_factor_db: f64,
249 pub dynamic_range_db: f64,
250}
251
252#[derive(Clone, Debug, Serialize)]
254pub struct StatsReport {
255 pub sample_rate: u32,
256 pub channels: usize,
257 pub frames: usize,
258 pub duration_s: f64,
259 pub overall: OverallStats,
260 pub per_channel: Vec<ChannelStats>,
261}
262
263fn window_rms_db(x: &[f32], win: usize) -> Vec<f64> {
264 x.chunks(win.max(1))
265 .filter(|c| c.len() * 2 >= win)
266 .map(|c| pow_db(c.iter().map(|v| (*v as f64).powi(2)).sum::<f64>() / c.len() as f64))
267 .collect()
268}
269
270fn dynamic_range(levels: &[f64]) -> (f64, f64, f64) {
271 let active: Vec<f64> = levels.iter().copied().filter(|l| *l > -90.0).collect();
272 if active.is_empty() {
273 return (0.0, -240.0, -240.0);
274 }
275 let min = active.iter().copied().fold(f64::INFINITY, f64::min);
276 let max = active.iter().copied().fold(f64::NEG_INFINITY, f64::max);
277 (util::percentile(&active, 95.0) - util::percentile(&active, 10.0), min, max)
278}
279
280pub fn histogram(x: &[f32]) -> Histogram {
282 let mut counts = vec![0u64; 61];
283 for v in x {
284 let a = v.abs() as f64;
285 let idx = if a <= 1e-3 {
286 0
287 } else {
288 let db = 20.0 * a.log10();
289 ((db + 60.0).ceil().clamp(0.0, 60.0)) as usize
290 };
291 counts[idx] += 1;
292 }
293 Histogram { bins_db: (0..61).map(|i| -60.0 + i as f64).collect(), counts }
294}
295
296pub fn stats(buf: &AudioBuffer, with_histogram: bool) -> StatsReport {
298 let sr = buf.sample_rate;
299 let win = ((sr as f64 * 0.05).round() as usize).max(1);
300 let per_channel: Vec<ChannelStats> = buf
301 .data
302 .iter()
303 .enumerate()
304 .map(|(c, ch)| {
305 let n = ch.len().max(1) as f64;
306 let peak = ch.iter().fold(0.0f32, |m, v| m.max(v.abs())) as f64;
307 let ms = ch.iter().map(|v| (*v as f64).powi(2)).sum::<f64>() / n;
308 let dc = ch.iter().map(|v| *v as f64).sum::<f64>() / n;
309 let tp = crate::loudness::true_peak_channel(ch, sr);
310 let (dr, mn, mx) = dynamic_range(&window_rms_db(ch, win));
311 let rms_db = pow_db(ms);
312 ChannelStats {
313 channel: c,
314 peak_db: amp_db(peak),
315 true_peak_dbtp: amp_db(tp),
316 rms_db,
317 dc_offset: dc,
318 dc_offset_db: amp_db(dc.abs()),
319 crest_factor_db: if ms > 0.0 { amp_db(peak) - rms_db } else { 0.0 },
320 dynamic_range_db: dr,
321 min_rms_db: mn,
322 max_rms_db: mx,
323 full_scale_samples: ch.iter().filter(|v| v.abs() >= 0.99988).count(),
324 histogram: if with_histogram { Some(histogram(ch)) } else { None },
325 }
326 })
327 .collect();
328 let peak = buf.peak() as f64;
329 let rms = buf.rms() as f64;
330 let mono = buf.mono_mix();
331 let (dr, _, _) = dynamic_range(&window_rms_db(&mono, win));
332 let overall = OverallStats {
333 peak_db: amp_db(peak),
334 true_peak_dbtp: per_channel.iter().map(|c| c.true_peak_dbtp).fold(-240.0, f64::max),
335 rms_db: amp_db(rms),
336 crest_factor_db: if rms > 0.0 { amp_db(peak) - amp_db(rms) } else { 0.0 },
337 dynamic_range_db: dr,
338 };
339 StatsReport {
340 sample_rate: sr,
341 channels: buf.channels(),
342 frames: buf.frames(),
343 duration_s: buf.duration_s(),
344 overall,
345 per_channel,
346 }
347}
348
349pub fn crest_factor_db(buf: &AudioBuffer) -> f64 {
351 let rms = buf.rms() as f64;
352 if rms <= 0.0 {
353 return 0.0;
354 }
355 amp_db(buf.peak() as f64) - amp_db(rms)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use ferraus_core::testing::{noise, sine, stereo};
362
363 #[test]
364 fn silence_regions_and_edges() {
365 let sr = 48000;
366 let mut b = AudioBuffer::new(sr, 1, sr as usize); b.append(&sine(sr, 440.0, 1.0, 0.5));
368 b.append(&AudioBuffer::new(sr, 1, (sr / 5) as usize)); b.append(&sine(sr, 440.0, 1.0, 0.5));
370 b.append(&AudioBuffer::new(sr, 1, sr as usize / 2));
371 let r = detect_silence(&b, -40.0, 0.3);
372 assert_eq!(r.regions.len(), 2, "{:?}", r.regions);
373 assert!((r.regions[0].start_s - 0.0).abs() < 0.011);
374 assert!((r.regions[0].end_s - 1.0).abs() < 0.011);
375 assert!((r.head_silence_s - 1.0).abs() < 0.011);
376 assert!((r.tail_silence_s - 0.5).abs() < 0.011);
377 assert_eq!(r.regions[0].floor_db, -240.0);
378 let r = detect_silence(&b, -40.0, 0.1);
379 assert_eq!(r.regions.len(), 3);
380 }
381
382 #[test]
383 fn clipping_runs() {
384 let mut b = sine(48000, 100.0, 0.1, 1.4);
385 for v in b.data[0].iter_mut() {
386 *v = v.clamp(-1.0, 1.0);
387 }
388 let r = detect_clipping(&b, 0.0, 3);
389 assert_eq!(r.count, 20, "{}", r.count); assert!(r.runs.iter().all(|x| x.kind == "threshold"));
391 let mut b = sine(48000, 100.0, 0.1, 0.8);
393 for v in b.data[0].iter_mut() {
394 *v = v.clamp(-0.5, 0.5);
395 }
396 let r = detect_clipping(&b, 0.0, 3);
397 assert_eq!(r.count, 20);
398 assert!(r.runs.iter().all(|x| x.kind == "flat"));
399 let clean = sine(48000, 1000.0, 0.5, 1.0);
400 assert_eq!(detect_clipping(&clean, 0.0, 3).count, 0);
401 }
402
403 #[test]
404 fn stats_on_sine_and_noise() {
405 let b = stereo(sine(48000, 1000.0, 1.0, 0.5));
406 let r = stats(&b, true);
407 let c = &r.per_channel[0];
408 assert!((c.peak_db + 6.02).abs() < 0.01);
409 assert!((c.rms_db + 9.03).abs() < 0.01);
410 assert!((c.crest_factor_db - 3.01).abs() < 0.02);
411 assert!(c.dynamic_range_db < 0.1);
412 let h = c.histogram.as_ref().unwrap();
413 assert_eq!(h.counts.len(), 61);
414 assert_eq!(h.counts.iter().sum::<u64>(), 48000);
415 assert!(h.counts[54] > 0 && h.counts[55] == 0, "{:?}", &h.counts[50..]);
416 let mut d = noise(48000, 1.0, 0.1, 1);
417 d.data[0].iter_mut().for_each(|v| *v += 0.05);
418 let r = stats(&d, false);
419 assert!((r.per_channel[0].dc_offset - 0.05).abs() < 0.002);
420 assert!(r.per_channel[0].histogram.is_none());
421 }
422}