File size: 3,353 Bytes
1295969
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! 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;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AudioFormat {
    Wav16,
    Wav24,
    Flac16,
    Flac24,
    Unknown(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
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,
    }
}