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 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
22pub 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#[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 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}