1use crate::loudness::{self, LoudnessReport, LoudnessSection};
7use crate::problems::{self, ProblemsReport};
8use crate::spectrum::{self, BandLevel, ToneBalance};
9use crate::stereo::{self, StereoReport};
10use crate::tempo::{self, TempoOptions};
11use ferraus_core::AudioBuffer;
12use serde::Serialize;
13
14#[derive(Clone, Debug, Serialize)]
16pub struct MasteredHeuristic {
17 pub looks_mastered: bool,
18 pub score: f64,
20 pub reasons: Vec<String>,
21}
22
23#[derive(Clone, Debug, Serialize)]
25pub struct DynamicsReport {
26 pub crest_factor_db: f64,
28 pub plr_db: f64,
30 pub loudness_range_lu: f64,
31 pub transient_density_per_s: f64,
33 pub headroom_db: f64,
35 pub sections: Vec<LoudnessSection>,
37 pub mastered: MasteredHeuristic,
38}
39
40pub fn plr_db(buf: &AudioBuffer) -> f64 {
42 let r = loudness::loudness(buf);
43 r.plr_db
44}
45
46pub fn looks_mastered(l: &LoudnessReport, crest_factor_db: f64) -> MasteredHeuristic {
48 let mut reasons = Vec::new();
49 let mut hits = 0.0;
50 let total = 5.0;
51 if l.integrated_lufs.is_finite() && l.integrated_lufs > -14.0 {
52 hits += 1.0;
53 reasons.push(format!("loud: {:.1} LUFS integrated", l.integrated_lufs));
54 }
55 if l.true_peak_dbtp > -1.5 {
56 hits += 1.0;
57 reasons.push(format!("little headroom: true peak {:.1} dBTP", l.true_peak_dbtp));
58 }
59 if l.plr_db.is_finite() && l.plr_db < 11.0 {
60 hits += 1.0;
61 reasons.push(format!("low peak-to-loudness ratio: {:.1} dB", l.plr_db));
62 }
63 if crest_factor_db < 10.0 {
64 hits += 1.0;
65 reasons.push(format!("low crest factor: {crest_factor_db:.1} dB (limited)"));
66 }
67 if l.loudness_range_lu < 6.0 && l.duration_s >= 10.0 {
68 hits += 1.0;
69 reasons.push(format!("narrow loudness range: {:.1} LU", l.loudness_range_lu));
70 }
71 let score = hits / total;
72 MasteredHeuristic {
73 looks_mastered: score >= 0.6 && l.integrated_lufs.is_finite() && l.integrated_lufs > -16.0,
74 score,
75 reasons,
76 }
77}
78
79pub fn dynamics(buf: &AudioBuffer) -> DynamicsReport {
81 let l = loudness::loudness(buf);
82 dynamics_with(buf, &l)
83}
84
85fn dynamics_with(buf: &AudioBuffer, l: &LoudnessReport) -> DynamicsReport {
86 let crest = crate::levels::crest_factor_db(buf);
87 DynamicsReport {
88 crest_factor_db: crest,
89 plr_db: l.plr_db,
90 loudness_range_lu: l.loudness_range_lu,
91 transient_density_per_s: tempo::transient_density(buf),
92 headroom_db: -l.true_peak_dbtp,
93 sections: loudness::loudness_outliers(buf, 6.0),
94 mastered: looks_mastered(l, crest),
95 }
96}
97
98#[derive(Clone, Debug, Serialize)]
100pub struct MasteringContext {
101 pub duration_s: f64,
102 pub sample_rate: u32,
103 pub channels: usize,
104 pub tempo_bpm: f64,
105 pub tempo_confidence: f64,
106}
107
108#[derive(Clone, Debug, Serialize)]
111pub struct MasteringAnalysis {
112 pub loudness: LoudnessReport,
113 pub third_octave: Vec<BandLevel>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub tone_balance: Option<ToneBalance>,
117 pub dynamics: DynamicsReport,
118 pub stereo: StereoReport,
119 pub problems: ProblemsReport,
120 pub context: MasteringContext,
121}
122
123pub fn mastering_analysis(buf: &AudioBuffer, reference: Option<&AudioBuffer>) -> MasteringAnalysis {
125 let l = loudness::loudness(buf);
126 let bands = spectrum::third_octave_levels(buf);
127 let tone_balance = reference.map(|r| spectrum::tone_balance(&bands, &spectrum::third_octave_levels(r)));
128 let t = tempo::detect_tempo(buf, &TempoOptions::default());
129 MasteringAnalysis {
130 dynamics: dynamics_with(buf, &l),
131 loudness: l,
132 third_octave: bands,
133 tone_balance,
134 stereo: stereo::stereo_analysis(buf),
135 problems: problems::diagnose(buf),
136 context: MasteringContext {
137 duration_s: buf.duration_s(),
138 sample_rate: buf.sample_rate,
139 channels: buf.channels(),
140 tempo_bpm: t.bpm,
141 tempo_confidence: t.confidence,
142 },
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use ferraus_core::testing::{noise, sine, stereo};
150
151 #[test]
152 fn heuristic_flags_loud_limited_audio() {
153 let mut b = noise(48000, 12.0, 1.4, 1);
155 b.data[0].iter_mut().for_each(|v| *v = v.clamp(-0.98, 0.98));
156 let d = dynamics(&stereo(b));
157 assert!(d.mastered.looks_mastered, "{:?}", d.mastered);
158 let q = stereo(sine(48000, 440.0, 12.0, 0.05));
160 let d = dynamics(&q);
161 assert!(!d.mastered.looks_mastered, "{:?}", d.mastered);
162 assert!(d.headroom_db > 20.0);
163 }
164
165 #[test]
166 fn bundle_with_reference() {
167 let a = stereo(sine(48000, 440.0, 3.0, 0.3));
168 let r = stereo(noise(48000, 3.0, 0.2, 2));
169 let m = mastering_analysis(&a, Some(&r));
170 assert!(m.tone_balance.is_some());
171 assert!(m.loudness.integrated_lufs.is_finite());
172 assert_eq!(m.context.channels, 2);
173 let v = serde_json::to_value(&m).unwrap();
174 assert!(v["stereo"]["correlation"].as_f64().unwrap() > 0.99);
175 }
176}