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 thiserror::Error;
12
13use crate::{
14    database::{LibraryDb, tx_extensions::CasTxExtensions},
15    decoding::{DecodingError, RawDecoder},
16    library::track::Track,
17    media_container::MediaContainer,
18};
19
20/// Filters a slice of [`TrackId`]'s using the input
21pub fn find_needs_loudnorm_analysis() -> Result<Vec<Track>, DatabaseError> {
22    let tracks = Track::db_get_all()?;
23
24    Ok(tracks
25        .into_iter()
26        .filter(|t| t.loudnorm_analysis.is_none())
27        .collect())
28}
29
30pub fn analyze_loudorm(
31    tracks: Vec<Track>,
32    progress_renderer: Arc<dyn ProgressRenderer>,
33) -> Result<(), LoudnormAnalysisError> {
34    if tracks.is_empty() {
35        return Ok(());
36    }
37
38    let progress_bar = ProgressBar::new(0, tracks.len(), progress_renderer);
39    progress_bar.set_label("Extracting loudnorm analysis from source files...");
40
41    let writer = DatabaseWriter::<LibraryDb>::spawn();
42
43    let max_threads = (rayon::current_num_threads() as f32 * 0.66).ceil() as usize;
44    let thread_pool = ThreadPoolBuilder::new()
45        .num_threads(max_threads)
46        .build()
47        .unwrap();
48
49    thread_pool.install(|| {
50        tracks
51            .into_par_iter()
52            .try_for_each(|mut track| -> Result<(), LoudnormAnalysisError> {
53                if writer.is_closed() {
54                    return Ok(());
55                }
56
57                let analysis = LoudnormAnalysis::from_container(track.src_container())?;
58
59                if writer.is_closed() {
60                    return Ok(());
61                }
62
63                track.loudnorm_analysis = Some(analysis);
64
65                let title = track.metadata.safe_title().to_owned();
66                writer.transaction(move |cas_tx| cas_tx.tx_patch(track.clone()));
67
68                progress_bar.set_label(&format!("Extracted loudnorm for '{title}'",));
69                progress_bar.increment();
70
71                Ok(())
72            })
73    })?;
74
75    writer.finish()?;
76
77    progress_bar.flush();
78
79    Ok(())
80}
81
82#[derive(Debug, Error)]
83pub enum LoudnormAnalysisError {
84    #[error("{0}")]
85    Ebur128(#[from] ebur128::Error),
86
87    #[error("DatabaseError: {0}")]
88    Database(#[from] DatabaseError),
89
90    #[error("Transaction Error: {0}")]
91    Transaction(#[from] TransactionError),
92
93    #[error("Decoding Error: {0}")]
94    Decoding(#[from] DecodingError),
95}
96
97/// [`LoudnormAnalysis`] represents the measured values from the first-pass of EBU R 128 loudnorm
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Copy)]
99pub struct LoudnormAnalysis {
100    pub(crate) i: f64,
101    pub(crate) tp: f64,
102    pub(crate) lra: f64,
103}
104
105impl LoudnormAnalysis {
106    /// Uses ffmpeg to measure the input file and return a [`LoudnormAnalysis`]
107    pub fn from_container(container: &MediaContainer) -> Result<Self, LoudnormAnalysisError> {
108        let mut analyzer = EbuR128::new(
109            container.stream().channels as u32,
110            container.stream().sample_rate,
111            Mode::I | Mode::LRA | Mode::TRUE_PEAK,
112        )?;
113
114        let mut raw_decoder = RawDecoder::from_container(container, 512 * 1024)?;
115
116        while let Some(packet) = raw_decoder.decode_next_packet()? {
117            let samples = packet.sample_buf.samples();
118            analyzer.add_frames_f32(samples)?;
119        }
120
121        let i = analyzer.loudness_global()?;
122        let lra = analyzer.loudness_range()?;
123        let tp = (0..container.stream().channels)
124            .map(|c| analyzer.true_peak(c as u32))
125            .collect::<Result<Vec<_>, _>>()?
126            .into_iter()
127            .fold(f64::NEG_INFINITY, f64::max);
128
129        Ok(LoudnormAnalysis { i, tp, lra })
130    }
131}