Skip to main content

selene_core/library/
loudnorm.rs

1use 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
25/// Filters a slice of [`TrackId`]'s using the input
26pub 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/// [`LoudnormAnalysis`] represents the measured values from the first-pass of EBU R 128 loudnorm
130#[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    /// Uses ffmpeg to measure the input file and return a [`LoudnormAnalysis`]
166    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}