File size: 5,582 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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! DSP delivery spec validation — Spotify, Apple Music, Amazon, YouTube, TikTok, Tidal.
use crate::audio_qc::AudioQcReport;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Dsp {
    Spotify,
    AppleMusic,
    AmazonMusic,
    YouTubeMusic,
    TikTok,
    Tidal,
}

impl Dsp {
    pub fn all() -> &'static [Dsp] {
        &[
            Dsp::Spotify,
            Dsp::AppleMusic,
            Dsp::AmazonMusic,
            Dsp::YouTubeMusic,
            Dsp::TikTok,
            Dsp::Tidal,
        ]
    }
    pub fn name(&self) -> &'static str {
        match self {
            Dsp::Spotify => "Spotify",
            Dsp::AppleMusic => "Apple Music",
            Dsp::AmazonMusic => "Amazon Music",
            Dsp::YouTubeMusic => "YouTube Music",
            Dsp::TikTok => "TikTok Music",
            Dsp::Tidal => "Tidal",
        }
    }
}

#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct DspSpec {
    pub dsp: Dsp,
    pub lufs_target: f64,
    pub lufs_tol: f64,
    pub true_peak_max: f64,
    pub sample_rates: Vec<u32>,
    pub stereo: bool,
    pub isrc_req: bool,
    pub upc_req: bool,
    pub cover_art_min_px: u32,
}

impl DspSpec {
    pub fn for_dsp(d: &Dsp) -> Self {
        match d {
            Dsp::Spotify => Self {
                dsp: Dsp::Spotify,
                lufs_target: -14.0,
                lufs_tol: 1.0,
                true_peak_max: -1.0,
                sample_rates: vec![44100, 48000],
                stereo: true,
                isrc_req: true,
                upc_req: true,
                cover_art_min_px: 3000,
            },
            Dsp::AppleMusic => Self {
                dsp: Dsp::AppleMusic,
                lufs_target: -16.0,
                lufs_tol: 1.0,
                true_peak_max: -1.0,
                sample_rates: vec![44100, 48000, 96000],
                stereo: true,
                isrc_req: true,
                upc_req: true,
                cover_art_min_px: 3000,
            },
            Dsp::AmazonMusic => Self {
                dsp: Dsp::AmazonMusic,
                lufs_target: -14.0,
                lufs_tol: 1.0,
                true_peak_max: -2.0,
                sample_rates: vec![44100, 48000],
                stereo: true,
                isrc_req: true,
                upc_req: true,
                cover_art_min_px: 3000,
            },
            Dsp::YouTubeMusic => Self {
                dsp: Dsp::YouTubeMusic,
                lufs_target: -14.0,
                lufs_tol: 2.0,
                true_peak_max: -1.0,
                sample_rates: vec![44100, 48000],
                stereo: false,
                isrc_req: true,
                upc_req: false,
                cover_art_min_px: 1400,
            },
            Dsp::TikTok => Self {
                dsp: Dsp::TikTok,
                lufs_target: -14.0,
                lufs_tol: 2.0,
                true_peak_max: -1.0,
                sample_rates: vec![44100, 48000],
                stereo: false,
                isrc_req: true,
                upc_req: false,
                cover_art_min_px: 1400,
            },
            Dsp::Tidal => Self {
                dsp: Dsp::Tidal,
                lufs_target: -14.0,
                lufs_tol: 1.0,
                true_peak_max: -1.0,
                sample_rates: vec![44100, 48000, 96000],
                stereo: true,
                isrc_req: true,
                upc_req: true,
                cover_art_min_px: 3000,
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DspValidationResult {
    pub dsp: String,
    pub passed: bool,
    pub defects: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct TrackMeta {
    pub isrc: Option<String>,
    pub upc: Option<String>,
    pub explicit: bool,
    pub territory_rights: bool,
    pub contributor_meta: bool,
    pub cover_art_px: Option<u32>,
}

pub fn validate_all(qc: &AudioQcReport, meta: &TrackMeta) -> Vec<DspValidationResult> {
    Dsp::all()
        .iter()
        .map(|d| validate_for(d, qc, meta))
        .collect()
}

pub fn validate_for(dsp: &Dsp, qc: &AudioQcReport, meta: &TrackMeta) -> DspValidationResult {
    let spec = DspSpec::for_dsp(dsp);
    let mut def = Vec::new();
    if !qc.format_ok {
        def.push("unsupported format".into());
    }
    if !qc.channels_ok && spec.stereo {
        def.push("stereo required".into());
    }
    if !qc.sample_rate_ok {
        def.push(format!("{}Hz not accepted", qc.sample_rate_hz));
    }
    if let Some(l) = qc.integrated_lufs {
        if (l - spec.lufs_target).abs() > spec.lufs_tol {
            def.push(format!(
                "{:.1} LUFS (need {:.1}±{:.1})",
                l, spec.lufs_target, spec.lufs_tol
            ));
        }
    }
    if spec.isrc_req && meta.isrc.is_none() {
        def.push("ISRC required".into());
    }
    if spec.upc_req && meta.upc.is_none() {
        def.push("UPC required".into());
    }
    if let Some(px) = meta.cover_art_px {
        if px < spec.cover_art_min_px {
            def.push(format!(
                "cover art {}px — need {}px",
                px, spec.cover_art_min_px
            ));
        }
    } else {
        def.push(format!(
            "cover art missing — {} needs {}px",
            spec.dsp.name(),
            spec.cover_art_min_px
        ));
    }
    DspValidationResult {
        dsp: spec.dsp.name().into(),
        passed: def.is_empty(),
        defects: def,
    }
}