Spaces:
Build error
Build error
| //! LUFS loudness + format QC. Target: -14±1 LUFS, stereo WAV/FLAC, 44.1–96kHz. | |
| use serde::{Deserialize, Serialize}; | |
| use tracing::{info, warn}; | |
| pub const TARGET_LUFS: f64 = -14.0; | |
| pub const LUFS_TOLERANCE: f64 = 1.0; | |
| pub const TRUE_PEAK_MAX: f64 = -1.0; | |
| pub enum AudioFormat { | |
| Wav16, | |
| Wav24, | |
| Flac16, | |
| Flac24, | |
| Unknown(String), | |
| } | |
| pub struct AudioQcReport { | |
| pub passed: bool, | |
| pub format: AudioFormat, | |
| pub sample_rate_hz: u32, | |
| pub channels: u8, | |
| pub duration_secs: f64, | |
| pub integrated_lufs: Option<f64>, | |
| pub true_peak_dbfs: Option<f64>, | |
| pub lufs_ok: bool, | |
| pub format_ok: bool, | |
| pub channels_ok: bool, | |
| pub sample_rate_ok: bool, | |
| pub defects: Vec<String>, | |
| } | |
| pub fn detect_format(b: &[u8]) -> AudioFormat { | |
| if b.len() < 4 { | |
| return AudioFormat::Unknown("too short".into()); | |
| } | |
| match &b[..4] { | |
| b"RIFF" => AudioFormat::Wav24, | |
| b"fLaC" => AudioFormat::Flac24, | |
| _ => AudioFormat::Unknown(format!("{:02x?}", &b[..4])), | |
| } | |
| } | |
| pub fn parse_wav_header(b: &[u8]) -> (u32, u8, u16) { | |
| if b.len() < 36 { | |
| return (44100, 2, 16); | |
| } | |
| let ch = u16::from_le_bytes([b[22], b[23]]) as u8; | |
| let sr = u32::from_le_bytes([b[24], b[25], b[26], b[27]]); | |
| let bd = u16::from_le_bytes([b[34], b[35]]); | |
| (sr, ch, bd) | |
| } | |
| pub fn run_qc(bytes: &[u8], lufs: Option<f64>, true_peak: Option<f64>) -> AudioQcReport { | |
| let fmt = detect_format(bytes); | |
| let (sr, ch, _) = parse_wav_header(bytes); | |
| let duration = | |
| (bytes.len().saturating_sub(44)) as f64 / (sr.max(1) as f64 * ch.max(1) as f64 * 3.0); | |
| let mut defects = Vec::new(); | |
| let fmt_ok = matches!( | |
| fmt, | |
| AudioFormat::Wav16 | AudioFormat::Wav24 | AudioFormat::Flac16 | AudioFormat::Flac24 | |
| ); | |
| if !fmt_ok { | |
| defects.push("unsupported format".into()); | |
| } | |
| let sr_ok = (44100..=96000).contains(&sr); | |
| if !sr_ok { | |
| defects.push(format!("sample rate {sr}Hz out of range")); | |
| } | |
| let ch_ok = ch == 2; | |
| if !ch_ok { | |
| defects.push(format!("{ch} channels — stereo required")); | |
| } | |
| let lufs_ok = match lufs { | |
| Some(l) => { | |
| let ok = (l - TARGET_LUFS).abs() <= LUFS_TOLERANCE; | |
| if !ok { | |
| defects.push(format!( | |
| "{l:.1} LUFS — target {TARGET_LUFS:.1}±{LUFS_TOLERANCE:.1}" | |
| )); | |
| } | |
| ok | |
| } | |
| None => true, | |
| }; | |
| let peak_ok = match true_peak { | |
| Some(p) => { | |
| let ok = p <= TRUE_PEAK_MAX; | |
| if !ok { | |
| defects.push(format!("true peak {p:.1} dBFS > {TRUE_PEAK_MAX:.1}")); | |
| } | |
| ok | |
| } | |
| None => true, | |
| }; | |
| let passed = fmt_ok && sr_ok && ch_ok && lufs_ok && peak_ok; | |
| if !passed { | |
| warn!(defects=?defects, "Audio QC failed"); | |
| } else { | |
| info!(sr=%sr, "Audio QC passed"); | |
| } | |
| AudioQcReport { | |
| passed, | |
| format: fmt, | |
| sample_rate_hz: sr, | |
| channels: ch, | |
| duration_secs: duration, | |
| integrated_lufs: lufs, | |
| true_peak_dbfs: true_peak, | |
| lufs_ok, | |
| format_ok: fmt_ok, | |
| channels_ok: ch_ok, | |
| sample_rate_ok: sr_ok, | |
| defects, | |
| } | |
| } | |