aede_core/playback/
loudness.rs1use std::error::Error;
8use std::path::Path;
9
10use aede_dsp::{PcmFormat, loudness::LoudnessProgramme};
11
12use crate::analysis::FileAnalysis;
13
14use super::decoder::FileDecoder;
15
16pub use aede_dsp::loudness::Measurement;
17
18#[derive(Clone, Debug, PartialEq)]
20pub struct CachedTrack {
21 pub size: u64,
23 pub mtime: u64,
25 pub mtime_subseconds: Option<u32>,
28 pub measurement: Option<Measurement>,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct ProgrammeFile {
35 pub path: String,
37 pub size: u64,
39 pub mtime: u64,
41 pub mtime_subseconds: Option<u32>,
43}
44
45impl CachedTrack {
46 pub(crate) fn matches(&self, file: &ProgrammeFile) -> bool {
47 self.size == file.size
48 && self.mtime == file.mtime
49 && self.mtime_subseconds == file.mtime_subseconds
50 }
51}
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct CachedProgramme {
56 pub files: Vec<ProgrammeFile>,
58 pub measurement: Option<Measurement>,
60}
61
62pub fn identity(path: &Path) -> Result<ProgrammeFile, std::io::Error> {
70 let stored_path = path.to_str().ok_or_else(|| {
71 std::io::Error::new(
72 std::io::ErrorKind::InvalidInput,
73 "playback loudness path must be valid UTF-8",
74 )
75 })?;
76 let metadata = std::fs::metadata(path)?;
77 if !metadata.is_file() {
78 return Err(std::io::Error::new(
79 std::io::ErrorKind::InvalidInput,
80 "playback loudness source must be a regular file",
81 ));
82 }
83 let modified = metadata
84 .modified()?
85 .duration_since(std::time::UNIX_EPOCH)
86 .map_err(|_| {
87 std::io::Error::new(
88 std::io::ErrorKind::InvalidData,
89 "playback loudness source timestamp predates the epoch",
90 )
91 })?;
92 Ok(ProgrammeFile {
93 path: stored_path.to_owned(),
94 size: metadata.len(),
95 mtime: modified.as_secs(),
96 mtime_subseconds: Some(modified.subsec_nanos()),
97 })
98}
99
100pub fn from_flaccompagnon(analyses: &[FileAnalysis], file: &ProgrammeFile) -> Option<Measurement> {
106 analyses.iter().rev().find_map(|analysis| {
107 if analysis.path != file.path
108 || analysis.source != "flaccompagnon"
109 || analysis.error.is_some()
110 || !analysis.still_applies(file.size, file.mtime)
111 {
112 return None;
113 }
114 let loudness = analysis.integrated_lufs?;
115 if !loudness.is_finite() || !(-100.0..=20.0).contains(&loudness) {
116 return None;
117 }
118 let linear_peak = |db: f64| {
119 if !db.is_finite() {
120 return None;
121 }
122 let peak = 10f64.powf(db / 20.0) as f32;
123 peak.is_finite().then_some(peak)
124 };
125 let peak = analysis.true_peak_dbtp.and_then(linear_peak).map(|peak| {
126 analysis
127 .peak_dbfs
128 .and_then(linear_peak)
129 .map_or(peak, |sample_peak| peak.max(sample_peak))
130 });
131 Some(Measurement {
132 integrated_lufs: loudness as f32,
133 true_peak: peak,
134 })
135 })
136}
137
138fn decode_into(
139 decoder: &mut FileDecoder,
140 meter: &mut LoudnessProgramme,
141) -> Result<(), Box<dyn Error>> {
142 let channels = usize::from(decoder.channels());
143 let format = PcmFormat::new(decoder.sample_rate(), decoder.channels())?;
144 let mut samples = vec![0.0; 4096 * channels];
145 loop {
146 let frames = decoder.read_frames(&mut samples)?;
147 if frames == 0 {
148 break;
149 }
150 meter.push(format, &samples[..frames * channels])?;
151 }
152 Ok(())
153}
154
155pub fn measure_track(path: &Path) -> Result<Option<Measurement>, Box<dyn Error>> {
157 let mut decoder = FileDecoder::open(path)?;
158 if !matches!(decoder.channels(), 1 | 2) {
159 return Ok(None);
160 }
161 let mut meter = LoudnessProgramme::new();
162 decode_into(&mut decoder, &mut meter)?;
163 Ok(meter.measurement()?)
164}
165
166pub fn measure_programme(paths: &[&Path]) -> Result<Option<Measurement>, Box<dyn Error>> {
169 if paths.is_empty() {
170 return Ok(None);
171 }
172 let mut meter = LoudnessProgramme::new();
173 for path in paths {
174 let mut decoder = FileDecoder::open(path)?;
175 if !matches!(decoder.channels(), 1 | 2) {
176 return Ok(None);
177 }
178 decode_into(&mut decoder, &mut meter)?;
179 }
180 Ok(meter.measurement()?)
181}
182
183#[cfg(test)]
184#[path = "loudness_tests.rs"]
185mod tests;