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::{DatabaseEntry, DatabaseError, TransactionError, writer::DatabaseWriter};
6use rayon::{
7    ThreadPoolBuilder,
8    iter::{IntoParallelIterator, ParallelIterator},
9};
10use serde::{Deserialize, Serialize};
11use symphonia::core::audio::{Audio, AudioBuffer, GenericAudioBuffer};
12use thiserror::Error;
13
14use crate::{
15    config::common::common_config,
16    database::LibraryDb,
17    library::track::Track,
18    media_container::MediaContainer,
19    symphonia_helpers::raw_decoder::{DecodingError, RawDecoder},
20};
21
22/// Filters a slice of [`TrackId`]'s using the input
23pub fn find_needs_loudnorm_analysis() -> Result<Vec<Track>, DatabaseError> {
24    let tracks = Track::db_get_all()?;
25    let accurate_true_peak = common_config().loudnorm.accurate_true_peak;
26
27    Ok(tracks
28        .into_iter()
29        .filter(|t| {
30            t.loudnorm_analysis
31                .is_none_or(|a| !a.accurate_true_peak && accurate_true_peak)
32        })
33        .collect())
34}
35
36pub fn analyze_loudorm(
37    tracks: Vec<Track>,
38    progress_renderer: Arc<dyn ProgressRenderer>,
39    dry: bool,
40) -> Result<(), LoudnormAnalysisError> {
41    if tracks.is_empty() {
42        return Ok(());
43    }
44
45    let progress_bar = ProgressBar::new(0, tracks.len(), progress_renderer);
46    progress_bar.set_label("Extracting loudnorm analysis from source files...");
47
48    let writer = DatabaseWriter::<LibraryDb>::spawn();
49
50    let max_threads = (rayon::current_num_threads() as f32 * 0.66).ceil() as usize;
51    let thread_pool = ThreadPoolBuilder::new()
52        .num_threads(max_threads)
53        .build()
54        .unwrap();
55
56    thread_pool.install(|| {
57        tracks
58            .into_par_iter()
59            .try_for_each(|mut track| -> Result<(), LoudnormAnalysisError> {
60                if writer.is_closed() {
61                    return Ok(());
62                }
63
64                let analysis = LoudnormAnalysis::from_container(track.container())?;
65
66                if writer.is_closed() {
67                    return Ok(());
68                }
69
70                track.loudnorm_analysis = Some(analysis);
71
72                let title = track.metadata.safe_title().to_owned();
73
74                if !dry {
75                    writer.transaction(move |cas_tx| {
76                        cas_tx.tx_upsert(track.id(), Some(track.clone()))
77                    });
78                }
79
80                progress_bar.set_label(&format!("Extracted loudnorm for '{title}'",));
81                progress_bar.increment();
82
83                Ok(())
84            })
85    })?;
86
87    writer.finish()?;
88
89    progress_bar.flush();
90
91    Ok(())
92}
93
94#[derive(Debug, Error)]
95pub enum LoudnormAnalysisError {
96    #[error("{0}")]
97    Ebur128(#[from] ebur128::Error),
98
99    #[error("DatabaseError: {0}")]
100    Database(#[from] DatabaseError),
101
102    #[error("Transaction Error: {0}")]
103    Transaction(#[from] TransactionError),
104
105    #[error("Decoding Error: {0}")]
106    Decoding(#[from] DecodingError),
107}
108
109/// [`LoudnormAnalysis`] represents the measured values from the first-pass of EBU R 128 loudnorm
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Copy)]
111pub struct LoudnormAnalysis {
112    pub(crate) accurate_true_peak: bool,
113
114    pub(crate) i: f64,
115    pub(crate) tp: f64,
116}
117
118impl LoudnormAnalysis {
119    pub fn measured_i(&self) -> f64 {
120        self.i
121    }
122
123    pub fn measured_tp(&self) -> f64 {
124        self.tp
125    }
126
127    /// Uses ffmpeg to measure the input file and return a [`LoudnormAnalysis`]
128    pub fn from_container(container: &MediaContainer) -> Result<Self, LoudnormAnalysisError> {
129        let accurate_true_peak = common_config().loudnorm.accurate_true_peak;
130
131        let peak_mode = if accurate_true_peak {
132            Mode::TRUE_PEAK
133        } else {
134            Mode::SAMPLE_PEAK
135        };
136
137        let mut analyzer = EbuR128::new(
138            container.stream().codec_params.channels as u32,
139            container.stream().codec_params.sample_rate,
140            Mode::I | peak_mode,
141        )?;
142
143        let mut raw_decoder = RawDecoder::from_container(container, 512 * 1024)?;
144
145        while let Some(packet) = raw_decoder.decode_next_packet()? {
146            match packet {
147                GenericAudioBuffer::S16(buf) => {
148                    analyzer.add_frames_i16(&buf.iter_interleaved().collect::<Vec<i16>>())?
149                }
150                GenericAudioBuffer::S32(buf) => {
151                    analyzer.add_frames_i32(&buf.iter_interleaved().collect::<Vec<i32>>())?
152                }
153                GenericAudioBuffer::F32(buf) => {
154                    analyzer.add_frames_f32(&buf.iter_interleaved().collect::<Vec<f32>>())?
155                }
156                GenericAudioBuffer::F64(buf) => {
157                    analyzer.add_frames_f64(&buf.iter_interleaved().collect::<Vec<f64>>())?
158                }
159                other => {
160                    let mut buf = AudioBuffer::<f32>::new(other.spec().clone(), other.frames());
161                    buf.render_silence(Some(other.frames()));
162                    other.copy_to(&mut buf);
163                    analyzer.add_frames_f32(&buf.iter_interleaved().collect::<Vec<f32>>())?
164                }
165            }
166        }
167
168        let i = analyzer.loudness_global()?;
169        let tp = (0..container.stream().codec_params.channels as u32).try_fold(
170            f64::NEG_INFINITY,
171            |max, c| {
172                let peak = if accurate_true_peak {
173                    analyzer.true_peak(c)
174                } else {
175                    analyzer.sample_peak(c)
176                };
177                peak.map(|tp| f64::max(max, tp))
178            },
179        )?;
180
181        Ok(LoudnormAnalysis {
182            i,
183            tp,
184            accurate_true_peak,
185        })
186    }
187}