clawft_plugin/voice/
quality.rs1#[derive(Debug, Clone, Default)]
8pub struct AudioMetrics {
9 pub rms_level: f32,
11 pub peak_level: f32,
13 pub clipping_detected: bool,
15 pub clipped_sample_count: usize,
17 pub estimated_snr_db: f32,
19}
20
21pub fn analyze_frame(samples: &[f32], noise_floor: f32) -> AudioMetrics {
30 if samples.is_empty() {
31 return AudioMetrics::default();
32 }
33
34 let rms = (samples.iter().map(|s| s * s).sum::<f32>()
35 / samples.len() as f32)
36 .sqrt();
37 let peak = samples
38 .iter()
39 .map(|s| s.abs())
40 .fold(0.0f32, f32::max);
41
42 let clipping_threshold = 0.99;
43 let clipped = samples
44 .iter()
45 .filter(|s| s.abs() >= clipping_threshold)
46 .count();
47
48 let snr = if noise_floor > 0.0001 {
49 20.0 * (rms / noise_floor).log10()
50 } else {
51 60.0 };
53
54 AudioMetrics {
55 rms_level: rms,
56 peak_level: peak,
57 clipping_detected: clipped > 0,
58 clipped_sample_count: clipped,
59 estimated_snr_db: snr.clamp(-20.0, 80.0),
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn silence_metrics() {
69 let silence = vec![0.0f32; 160];
70 let metrics = analyze_frame(&silence, 0.0);
71 assert!((metrics.rms_level - 0.0).abs() < f32::EPSILON);
72 assert!((metrics.peak_level - 0.0).abs() < f32::EPSILON);
73 assert!(!metrics.clipping_detected);
74 assert_eq!(metrics.clipped_sample_count, 0);
75 assert!((metrics.estimated_snr_db - 60.0).abs() < f32::EPSILON);
77 }
78
79 #[test]
80 fn sine_wave_metrics() {
81 let samples: Vec<f32> = (0..160)
83 .map(|i| {
84 let t = i as f32 / 16000.0;
85 (2.0 * std::f32::consts::PI * 1000.0 * t).sin() * 0.5
86 })
87 .collect();
88
89 let metrics = analyze_frame(&samples, 0.01);
90
91 assert!(
93 (metrics.rms_level - 0.354).abs() < 0.02,
94 "RMS should be ~0.354, got {}",
95 metrics.rms_level
96 );
97 assert!(
99 (metrics.peak_level - 0.5).abs() < 0.01,
100 "Peak should be ~0.5, got {}",
101 metrics.peak_level
102 );
103 assert!(!metrics.clipping_detected);
104 assert!(metrics.estimated_snr_db > 20.0);
106 }
107
108 #[test]
109 fn clipping_detection() {
110 let mut samples = vec![0.5f32; 160];
111 samples[0] = 1.0;
113 samples[1] = -1.0;
114 samples[2] = 0.995;
115
116 let metrics = analyze_frame(&samples, 0.0);
117 assert!(metrics.clipping_detected);
118 assert_eq!(metrics.clipped_sample_count, 3);
119 }
120
121 #[test]
122 fn snr_estimation() {
123 let samples = vec![0.5f32; 160];
125 let metrics = analyze_frame(&samples, 0.01);
126
127 assert!(
129 (metrics.estimated_snr_db - 34.0).abs() < 1.0,
130 "SNR should be ~34 dB, got {}",
131 metrics.estimated_snr_db
132 );
133 }
134
135 #[test]
136 fn empty_frame_returns_defaults() {
137 let metrics = analyze_frame(&[], 0.0);
138 assert!((metrics.rms_level - 0.0).abs() < f32::EPSILON);
139 assert!((metrics.peak_level - 0.0).abs() < f32::EPSILON);
140 assert!(!metrics.clipping_detected);
141 assert_eq!(metrics.clipped_sample_count, 0);
142 assert!((metrics.estimated_snr_db - 0.0).abs() < f32::EPSILON);
143 }
144
145 #[test]
146 fn snr_clamped_to_range() {
147 let samples = vec![0.0001f32; 160];
149 let metrics = analyze_frame(&samples, 1.0);
150 assert!(
151 metrics.estimated_snr_db >= -20.0,
152 "SNR should be clamped at -20, got {}",
153 metrics.estimated_snr_db
154 );
155 }
156}