selene_core/library/
loudnorm.rs1use std::{f64, sync::Arc};
2
3use barber::{ProgressBar, ProgressRenderer};
4use ebur128::{EbuR128, Mode};
5use lunar_lib::database::{
6 DatabaseEntry, DatabaseError, DbHandle, TransactionError, db_transaction,
7 writer::DatabaseWriter,
8};
9use rayon::{
10 ThreadPoolBuilder,
11 iter::{IntoParallelIterator, ParallelIterator},
12};
13use serde::{Deserialize, Serialize};
14use symphonia::core::audio::{Audio, GenericAudioBufferRef};
15use thiserror::Error;
16
17use crate::{
18 config::common_config,
19 database::LibraryDb,
20 library::track::Track,
21 media_container::MediaContainer,
22 symphonia_helpers::raw_decoder::{DecodingError, RawDecoder},
23};
24
25pub fn find_needs_loudnorm_analysis(db: &LibraryDb) -> Result<Vec<Track>, DatabaseError> {
27 let tracks = Track::db_get_all(db)?;
28 let accurate_true_peak = common_config().loudnorm.accurate_true_peak;
29
30 Ok(tracks
31 .into_iter()
32 .filter(|t| {
33 t.loudnorm_analysis
34 .is_none_or(|a| !a.accurate_true_peak && accurate_true_peak)
35 })
36 .collect())
37}
38
39pub fn analyze_loudorm(
40 tracks: Vec<Track>,
41 progress_renderer: Arc<dyn ProgressRenderer>,
42 dry: bool,
43) -> Result<(), LoudnormAnalysisError> {
44 if tracks.is_empty() {
45 return Ok(());
46 }
47
48 let progress_bar = ProgressBar::new(0, tracks.len(), progress_renderer);
49 progress_bar.set_label("Extracting loudnorm analysis from source files...");
50
51 if common_config().main.multithreading {
52 let writer = DatabaseWriter::<LibraryDb>::spawn();
53 let max_threads = (rayon::current_num_threads() as f32 * 0.66).ceil() as usize;
54 let thread_pool = ThreadPoolBuilder::new()
55 .num_threads(max_threads)
56 .build()
57 .unwrap();
58
59 thread_pool.install(|| {
60 tracks
61 .into_par_iter()
62 .try_for_each(|mut track| -> Result<(), LoudnormAnalysisError> {
63 if writer.is_closed() {
64 return Ok(());
65 }
66
67 let analysis = LoudnormAnalysis::from_container(track.container())?;
68
69 if writer.is_closed() {
70 return Ok(());
71 }
72
73 track.loudnorm_analysis = Some(analysis);
74
75 let title = track.metadata.safe_title().to_owned();
76
77 if !dry {
78 writer.transaction(move |cas_tx| cas_tx.tx_upsert(track.clone()));
79 }
80
81 progress_bar.set_label(&format!("Extracted loudnorm for '{title}'",));
82 progress_bar.increment();
83
84 Ok(())
85 })
86 })?;
87 writer.finish()?;
88 } else {
89 for mut track in tracks {
90 let analysis = LoudnormAnalysis::from_container(track.container())?;
91 let title = track.metadata.safe_title().to_owned();
92
93 track.loudnorm_analysis = Some(analysis);
94
95 if !dry {
96 db_transaction(
97 |cas_tx| cas_tx.tx_upsert(track.clone()).map_err(Into::into),
98 DbHandle::<LibraryDb>::open()?.clone(),
99 false,
100 )
101 .map_err(TransactionError::from)?;
102 }
103
104 progress_bar.set_label(&format!("Extracted loudnorm for '{title}'",));
105 progress_bar.increment();
106 }
107 }
108
109 progress_bar.flush();
110
111 Ok(())
112}
113
114#[derive(Debug, Error)]
115pub enum LoudnormAnalysisError {
116 #[error("{0}")]
117 Ebur128(#[from] ebur128::Error),
118
119 #[error("DatabaseError: {0}")]
120 Database(#[from] DatabaseError),
121
122 #[error("Transaction Error: {0}")]
123 Transaction(#[from] TransactionError),
124
125 #[error("Decoding Error: {0}")]
126 Decoding(#[from] DecodingError),
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Copy)]
131pub struct LoudnormAnalysis {
132 pub(crate) accurate_true_peak: bool,
133
134 pub(crate) i: f64,
135 pub(crate) tp: f64,
136}
137
138impl LoudnormAnalysis {
139 #[must_use]
140 pub fn measured_i(&self) -> f64 {
141 self.i
142 }
143
144 #[must_use]
145 pub fn measured_tp(&self) -> f64 {
146 self.tp
147 }
148
149 #[must_use]
150 pub fn calculated_gain_db(&self) -> f64 {
151 let config = common_config().loudnorm;
152 (config.target_i - self.i - config.target_offset).min(config.target_tp - self.tp)
153 }
154
155 #[must_use]
156 pub fn calculated_gain(&self) -> f64 {
157 10f64.powf(self.calculated_gain_db() / 20.0)
158 }
159
160 #[must_use]
161 pub fn calculated_replay_gain_peak(&self) -> f64 {
162 10.0_f64.powf(self.tp / 20.0)
163 }
164
165 pub fn from_container(container: &MediaContainer) -> Result<Self, LoudnormAnalysisError> {
167 let accurate_true_peak = common_config().loudnorm.accurate_true_peak;
168
169 let peak_mode = if accurate_true_peak {
170 Mode::TRUE_PEAK
171 } else {
172 Mode::SAMPLE_PEAK
173 };
174
175 let mut analyzer = EbuR128::new(
176 container.stream().codec_params.channels as u32,
177 container.stream().codec_params.sample_rate,
178 Mode::I | peak_mode,
179 )?;
180
181 let mut raw_decoder = RawDecoder::from_container(container, 512 * 1024)?;
182
183 while let Some(packet) = raw_decoder.decode_next_packet()? {
184 match packet {
185 GenericAudioBufferRef::S16(buf) => {
186 let mut frames = vec![0; buf.samples_interleaved()];
187 buf.copy_to_slice_interleaved(&mut frames);
188 analyzer.add_frames_i16(&frames)?;
189 }
190 GenericAudioBufferRef::S32(buf) => {
191 let mut frames = vec![0; buf.samples_interleaved()];
192 buf.copy_to_slice_interleaved(&mut frames);
193 analyzer.add_frames_i32(&frames)?;
194 }
195 GenericAudioBufferRef::F64(buf) => {
196 let mut frames = vec![0.0; buf.samples_interleaved()];
197 buf.copy_to_slice_interleaved(&mut frames);
198 analyzer.add_frames_f64(&frames)?;
199 }
200 buf => {
201 let mut frames = vec![0.0; buf.samples_interleaved()];
202 buf.copy_to_slice_interleaved(&mut frames);
203 analyzer.add_frames_f32(&frames)?;
204 }
205 }
206 }
207
208 let i = analyzer.loudness_global()?;
209 let tp = (0..container.stream().codec_params.channels as u32).try_fold(
210 f64::NEG_INFINITY,
211 |max, c| {
212 let peak = if accurate_true_peak {
213 analyzer.true_peak(c)
214 } else {
215 analyzer.sample_peak(c)
216 };
217 peak.map(|tp| f64::max(max, tp))
218 },
219 )?;
220
221 Ok(LoudnormAnalysis {
222 accurate_true_peak,
223 i,
224 tp,
225 })
226 }
227}