use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]
pub struct AudioQualityReport {
pub source: String,
pub verdict: AudioVerdict,
pub levels: AudioLevels,
pub clipping: ClippingReport,
pub silence: SilenceReport,
pub duration_secs: f64,
pub sample_rate: u32,
pub sample_count: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct AudioLevels {
pub peak: f64,
pub peak_dbfs: f64,
pub rms: f64,
pub rms_dbfs: f64,
pub dynamic_range_db: f64,
pub passed: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct ClippingReport {
pub clipped_samples: usize,
pub clipped_pct: f64,
pub passed: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct SilenceReport {
pub regions: Vec<SilenceRegion>,
pub total_silence_secs: f64,
pub silence_pct: f64,
pub passed: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct SilenceRegion {
pub start_secs: f64,
pub end_secs: f64,
pub duration_secs: f64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AudioVerdict {
Pass,
Fail,
NoAudio,
}
impl std::fmt::Display for AudioVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pass => write!(f, "PASS"),
Self::Fail => write!(f, "FAIL"),
Self::NoAudio => write!(f, "NO AUDIO"),
}
}
}
#[derive(Clone, Debug)]
pub struct AudioQualityConfig {
pub min_rms_dbfs: f64,
pub max_peak_dbfs: f64,
pub no_clipping: bool,
pub silence_threshold_dbfs: f64,
pub min_silence_duration_secs: f64,
pub max_silence_pct: f64,
}
impl Default for AudioQualityConfig {
fn default() -> Self {
Self {
min_rms_dbfs: -40.0,
max_peak_dbfs: -0.1,
no_clipping: true,
silence_threshold_dbfs: -60.0,
min_silence_duration_secs: 0.5,
max_silence_pct: 80.0,
}
}
}
impl AudioQualityConfig {
#[must_use]
pub fn with_min_rms_dbfs(mut self, dbfs: f64) -> Self {
self.min_rms_dbfs = dbfs;
self
}
#[must_use]
pub fn with_max_peak_dbfs(mut self, dbfs: f64) -> Self {
self.max_peak_dbfs = dbfs;
self
}
#[must_use]
pub const fn with_no_clipping(mut self, no_clipping: bool) -> Self {
self.no_clipping = no_clipping;
self
}
#[must_use]
pub fn with_silence_threshold_dbfs(mut self, dbfs: f64) -> Self {
self.silence_threshold_dbfs = dbfs;
self
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_audio_verdict_display() {
assert_eq!(AudioVerdict::Pass.to_string(), "PASS");
assert_eq!(AudioVerdict::Fail.to_string(), "FAIL");
assert_eq!(AudioVerdict::NoAudio.to_string(), "NO AUDIO");
}
#[test]
fn test_audio_verdict_equality() {
assert_eq!(AudioVerdict::Pass, AudioVerdict::Pass);
assert_ne!(AudioVerdict::Pass, AudioVerdict::Fail);
}
#[test]
fn test_config_defaults() {
let config = AudioQualityConfig::default();
assert!((config.min_rms_dbfs - (-40.0)).abs() < f64::EPSILON);
assert!((config.max_peak_dbfs - (-0.1)).abs() < f64::EPSILON);
assert!(config.no_clipping);
assert!((config.silence_threshold_dbfs - (-60.0)).abs() < f64::EPSILON);
}
#[test]
fn test_config_builders() {
let config = AudioQualityConfig::default()
.with_min_rms_dbfs(-30.0)
.with_max_peak_dbfs(-1.0)
.with_no_clipping(false)
.with_silence_threshold_dbfs(-50.0);
assert!((config.min_rms_dbfs - (-30.0)).abs() < f64::EPSILON);
assert!((config.max_peak_dbfs - (-1.0)).abs() < f64::EPSILON);
assert!(!config.no_clipping);
assert!((config.silence_threshold_dbfs - (-50.0)).abs() < f64::EPSILON);
}
#[test]
fn test_silence_region() {
let region = SilenceRegion {
start_secs: 1.0,
end_secs: 2.5,
duration_secs: 1.5,
};
assert!((region.duration_secs - 1.5).abs() < f64::EPSILON);
}
#[test]
fn test_audio_levels_serialization() {
let levels = AudioLevels {
peak: 0.95,
peak_dbfs: -0.45,
rms: 0.3,
rms_dbfs: -10.46,
dynamic_range_db: 50.0,
passed: true,
};
let json = serde_json::to_string(&levels).unwrap();
assert!(json.contains("\"peak\":0.95"));
}
#[test]
fn test_clipping_report() {
let report = ClippingReport {
clipped_samples: 0,
clipped_pct: 0.0,
passed: true,
};
assert!(report.passed);
}
#[test]
fn test_silence_report() {
let report = SilenceReport {
regions: vec![],
total_silence_secs: 0.0,
silence_pct: 0.0,
passed: true,
};
assert!(report.passed);
assert!(report.regions.is_empty());
}
#[test]
fn test_audio_quality_report_serialization() {
let report = AudioQualityReport {
source: "test.mp4".to_string(),
verdict: AudioVerdict::Pass,
levels: AudioLevels {
peak: 0.5,
peak_dbfs: -6.0,
rms: 0.2,
rms_dbfs: -14.0,
dynamic_range_db: 40.0,
passed: true,
},
clipping: ClippingReport {
clipped_samples: 0,
clipped_pct: 0.0,
passed: true,
},
silence: SilenceReport {
regions: vec![],
total_silence_secs: 0.0,
silence_pct: 0.0,
passed: true,
},
duration_secs: 10.0,
sample_rate: 48000,
sample_count: 480_000,
};
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("\"verdict\":\"Pass\""));
}
}