jugar_probar/audio_quality/
types.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Serialize)]
10pub struct AudioQualityReport {
11 pub source: String,
13 pub verdict: AudioVerdict,
15 pub levels: AudioLevels,
17 pub clipping: ClippingReport,
19 pub silence: SilenceReport,
21 pub duration_secs: f64,
23 pub sample_rate: u32,
25 pub sample_count: usize,
27}
28
29#[derive(Clone, Debug, Serialize)]
31pub struct AudioLevels {
32 pub peak: f64,
34 pub peak_dbfs: f64,
36 pub rms: f64,
38 pub rms_dbfs: f64,
40 pub dynamic_range_db: f64,
42 pub passed: bool,
44}
45
46#[derive(Clone, Debug, Serialize)]
48pub struct ClippingReport {
49 pub clipped_samples: usize,
51 pub clipped_pct: f64,
53 pub passed: bool,
55}
56
57#[derive(Clone, Debug, Serialize)]
59pub struct SilenceReport {
60 pub regions: Vec<SilenceRegion>,
62 pub total_silence_secs: f64,
64 pub silence_pct: f64,
66 pub passed: bool,
68}
69
70#[derive(Clone, Debug, Serialize)]
72pub struct SilenceRegion {
73 pub start_secs: f64,
75 pub end_secs: f64,
77 pub duration_secs: f64,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
83pub enum AudioVerdict {
84 Pass,
86 Fail,
88 NoAudio,
90}
91
92impl std::fmt::Display for AudioVerdict {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 Self::Pass => write!(f, "PASS"),
96 Self::Fail => write!(f, "FAIL"),
97 Self::NoAudio => write!(f, "NO AUDIO"),
98 }
99 }
100}
101
102#[derive(Clone, Debug)]
104pub struct AudioQualityConfig {
105 pub min_rms_dbfs: f64,
107 pub max_peak_dbfs: f64,
109 pub no_clipping: bool,
111 pub silence_threshold_dbfs: f64,
113 pub min_silence_duration_secs: f64,
115 pub max_silence_pct: f64,
117}
118
119impl Default for AudioQualityConfig {
120 fn default() -> Self {
121 Self {
122 min_rms_dbfs: -40.0,
123 max_peak_dbfs: -0.1,
124 no_clipping: true,
125 silence_threshold_dbfs: -60.0,
126 min_silence_duration_secs: 0.5,
127 max_silence_pct: 80.0,
128 }
129 }
130}
131
132impl AudioQualityConfig {
133 #[must_use]
135 pub fn with_min_rms_dbfs(mut self, dbfs: f64) -> Self {
136 self.min_rms_dbfs = dbfs;
137 self
138 }
139
140 #[must_use]
142 pub fn with_max_peak_dbfs(mut self, dbfs: f64) -> Self {
143 self.max_peak_dbfs = dbfs;
144 self
145 }
146
147 #[must_use]
149 pub const fn with_no_clipping(mut self, no_clipping: bool) -> Self {
150 self.no_clipping = no_clipping;
151 self
152 }
153
154 #[must_use]
156 pub fn with_silence_threshold_dbfs(mut self, dbfs: f64) -> Self {
157 self.silence_threshold_dbfs = dbfs;
158 self
159 }
160}
161
162#[cfg(test)]
163#[allow(clippy::unwrap_used)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_audio_verdict_display() {
169 assert_eq!(AudioVerdict::Pass.to_string(), "PASS");
170 assert_eq!(AudioVerdict::Fail.to_string(), "FAIL");
171 assert_eq!(AudioVerdict::NoAudio.to_string(), "NO AUDIO");
172 }
173
174 #[test]
175 fn test_audio_verdict_equality() {
176 assert_eq!(AudioVerdict::Pass, AudioVerdict::Pass);
177 assert_ne!(AudioVerdict::Pass, AudioVerdict::Fail);
178 }
179
180 #[test]
181 fn test_config_defaults() {
182 let config = AudioQualityConfig::default();
183 assert!((config.min_rms_dbfs - (-40.0)).abs() < f64::EPSILON);
184 assert!((config.max_peak_dbfs - (-0.1)).abs() < f64::EPSILON);
185 assert!(config.no_clipping);
186 assert!((config.silence_threshold_dbfs - (-60.0)).abs() < f64::EPSILON);
187 }
188
189 #[test]
190 fn test_config_builders() {
191 let config = AudioQualityConfig::default()
192 .with_min_rms_dbfs(-30.0)
193 .with_max_peak_dbfs(-1.0)
194 .with_no_clipping(false)
195 .with_silence_threshold_dbfs(-50.0);
196 assert!((config.min_rms_dbfs - (-30.0)).abs() < f64::EPSILON);
197 assert!((config.max_peak_dbfs - (-1.0)).abs() < f64::EPSILON);
198 assert!(!config.no_clipping);
199 assert!((config.silence_threshold_dbfs - (-50.0)).abs() < f64::EPSILON);
200 }
201
202 #[test]
203 fn test_silence_region() {
204 let region = SilenceRegion {
205 start_secs: 1.0,
206 end_secs: 2.5,
207 duration_secs: 1.5,
208 };
209 assert!((region.duration_secs - 1.5).abs() < f64::EPSILON);
210 }
211
212 #[test]
213 fn test_audio_levels_serialization() {
214 let levels = AudioLevels {
215 peak: 0.95,
216 peak_dbfs: -0.45,
217 rms: 0.3,
218 rms_dbfs: -10.46,
219 dynamic_range_db: 50.0,
220 passed: true,
221 };
222 let json = serde_json::to_string(&levels).unwrap();
223 assert!(json.contains("\"peak\":0.95"));
224 }
225
226 #[test]
227 fn test_clipping_report() {
228 let report = ClippingReport {
229 clipped_samples: 0,
230 clipped_pct: 0.0,
231 passed: true,
232 };
233 assert!(report.passed);
234 }
235
236 #[test]
237 fn test_silence_report() {
238 let report = SilenceReport {
239 regions: vec![],
240 total_silence_secs: 0.0,
241 silence_pct: 0.0,
242 passed: true,
243 };
244 assert!(report.passed);
245 assert!(report.regions.is_empty());
246 }
247
248 #[test]
249 fn test_audio_quality_report_serialization() {
250 let report = AudioQualityReport {
251 source: "test.mp4".to_string(),
252 verdict: AudioVerdict::Pass,
253 levels: AudioLevels {
254 peak: 0.5,
255 peak_dbfs: -6.0,
256 rms: 0.2,
257 rms_dbfs: -14.0,
258 dynamic_range_db: 40.0,
259 passed: true,
260 },
261 clipping: ClippingReport {
262 clipped_samples: 0,
263 clipped_pct: 0.0,
264 passed: true,
265 },
266 silence: SilenceReport {
267 regions: vec![],
268 total_silence_secs: 0.0,
269 silence_pct: 0.0,
270 passed: true,
271 },
272 duration_secs: 10.0,
273 sample_rate: 48000,
274 sample_count: 480_000,
275 };
276 let json = serde_json::to_string(&report).unwrap();
277 assert!(json.contains("\"verdict\":\"Pass\""));
278 }
279}