selene_core/library/
loudnorm.rs1use 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
20pub 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#[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 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}