use crate::error::{Error, Result};
use std::path::Path;
use std::sync::atomic::AtomicBool;
#[cfg(feature = "replaygain")]
use crate::mp4meta;
#[cfg(feature = "replaygain")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "replaygain")]
use std::sync::Arc;
#[cfg(feature = "replaygain")]
use symphonia::core::audio::{Audio, GenericAudioBufferRef};
#[cfg(feature = "replaygain")]
use symphonia::core::codecs::audio::{AudioDecoderOptions, CODEC_ID_NULL_AUDIO};
#[cfg(feature = "replaygain")]
use symphonia::core::formats::probe::Hint;
#[cfg(feature = "replaygain")]
use symphonia::core::formats::FormatOptions;
#[cfg(feature = "replaygain")]
use symphonia::core::io::{MediaSource, MediaSourceStream};
#[cfg(feature = "replaygain")]
use symphonia::core::meta::MetadataOptions;
pub const REPLAYGAIN_REFERENCE_DB: f64 = 89.0;
const PINK_REF: f64 = 64.82;
pub const RG2_REFERENCE_LUFS: f64 = -18.0;
pub const R128_REFERENCE_LUFS: f64 = -23.0;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AnalysisMode {
#[default]
Rg1,
Rg2,
R128,
}
impl AnalysisMode {
pub fn target_lufs(&self) -> Option<f64> {
match self {
AnalysisMode::Rg1 => None,
AnalysisMode::Rg2 => Some(RG2_REFERENCE_LUFS),
AnalysisMode::R128 => Some(R128_REFERENCE_LUFS),
}
}
pub fn unit(&self) -> &'static str {
if self.target_lufs().is_some() {
"LUFS"
} else {
"dB"
}
}
pub fn algorithm_tag(&self) -> Option<&'static str> {
match self {
AnalysisMode::Rg1 => None,
AnalysisMode::Rg2 | AnalysisMode::R128 => Some("ITU-R BS.1770"),
}
}
pub fn name(&self) -> &'static str {
match self {
AnalysisMode::Rg1 => "rg1",
AnalysisMode::Rg2 => "rg2",
AnalysisMode::R128 => "r128",
}
}
}
impl std::fmt::Display for AnalysisMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AnalysisMode::Rg1 => f.write_str("ReplayGain 1.0"),
AnalysisMode::Rg2 => f.write_str("ReplayGain 2.0"),
AnalysisMode::R128 => f.write_str("EBU R128"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AudioFileType {
Mp3,
Aac,
}
impl std::fmt::Display for AudioFileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AudioFileType::Mp3 => f.write_str("MP3"),
AudioFileType::Aac => f.write_str("AAC"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ReplayGainResult {
loudness_db: f64,
gain_db: f64,
peak: f64,
sample_rate: u32,
file_type: AudioFileType,
#[cfg_attr(feature = "serde", serde(default))]
analysis_mode: AnalysisMode,
#[cfg_attr(feature = "serde", serde(default))]
true_peak: bool,
}
impl ReplayGainResult {
#[allow(dead_code)]
pub(crate) fn new(
loudness_db: f64,
gain_db: f64,
peak: f64,
sample_rate: u32,
file_type: AudioFileType,
analysis_mode: AnalysisMode,
) -> Self {
Self {
loudness_db,
gain_db,
peak,
sample_rate,
file_type,
analysis_mode,
true_peak: false,
}
}
pub fn from_stored_tags(
gain_db: f64,
peak: f64,
file_type: AudioFileType,
analysis_mode: AnalysisMode,
) -> Self {
let target = analysis_mode
.target_lufs()
.unwrap_or(REPLAYGAIN_REFERENCE_DB);
Self {
loudness_db: target - gain_db,
gain_db,
peak,
sample_rate: 0,
file_type,
analysis_mode,
true_peak: false,
}
}
pub fn loudness_db(&self) -> f64 {
self.loudness_db
}
pub fn gain_db(&self) -> f64 {
self.gain_db
}
pub fn peak(&self) -> f64 {
self.peak
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn file_type(&self) -> AudioFileType {
self.file_type
}
pub fn analysis_mode(&self) -> AnalysisMode {
self.analysis_mode
}
pub fn is_true_peak(&self) -> bool {
self.true_peak
}
pub fn loudness_lufs(&self) -> Option<f64> {
self.analysis_mode.target_lufs().map(|_| self.loudness_db)
}
pub fn gain_steps(&self) -> i32 {
crate::gain::db_to_steps(self.gain_db)
}
pub fn with_peak(mut self, peak: f64) -> Self {
self.peak = peak;
self
}
}
impl std::fmt::Display for ReplayGainResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:+.2} dB (peak: {:.6})", self.gain_db, self.peak)
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlbumGainResult {
tracks: Vec<ReplayGainResult>,
album_loudness_db: f64,
album_gain_db: f64,
album_peak: f64,
}
impl AlbumGainResult {
#[allow(dead_code)]
pub(crate) fn new(
tracks: Vec<ReplayGainResult>,
album_loudness_db: f64,
album_gain_db: f64,
album_peak: f64,
) -> Self {
Self {
tracks,
album_loudness_db,
album_gain_db,
album_peak,
}
}
pub fn from_stored_tags(
tracks: Vec<ReplayGainResult>,
album_gain_db: f64,
album_peak: f64,
analysis_mode: AnalysisMode,
) -> Self {
let target = analysis_mode
.target_lufs()
.unwrap_or(REPLAYGAIN_REFERENCE_DB);
Self {
tracks,
album_loudness_db: target - album_gain_db,
album_gain_db,
album_peak,
}
}
pub fn tracks(&self) -> &[ReplayGainResult] {
&self.tracks
}
pub fn album_loudness_db(&self) -> f64 {
self.album_loudness_db
}
pub fn album_gain_db(&self) -> f64 {
self.album_gain_db
}
pub fn album_peak(&self) -> f64 {
self.album_peak
}
pub fn album_gain_steps(&self) -> i32 {
crate::gain::db_to_steps(self.album_gain_db)
}
}
impl std::fmt::Display for AlbumGainResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Album: {:+.2} dB (peak: {:.6}, {} tracks)",
self.album_gain_db,
self.album_peak,
self.tracks.len()
)
}
}
#[derive(Debug, Clone)]
pub struct AlbumAnalysisReport {
pub album: AlbumGainResult,
pub failures: Vec<(usize, String)>,
pub successful_indices: Vec<usize>,
}
#[cfg(feature = "replaygain")]
mod filter_coeffs {
pub(super) const YULE_A_96000: [f64; 11] = [
1.0,
-7.22103125152679,
24.7034187975904,
-52.6825833623896,
77.4825736677539,
-82.0074753444205,
63.1566097101925,
-34.889569769245,
13.2126852760198,
-3.09445623301669,
0.340344741393305,
];
pub(super) const YULE_B_96000: [f64; 11] = [
0.006471345933032,
-0.02567678242161,
0.049805860704367,
-0.05823001743528,
0.040611847441914,
-0.010912036887501,
-0.00901635868667,
0.012448886238123,
-0.007206683749426,
0.002167156433951,
-0.000261819276949,
];
pub(super) const BUTTER_A_96000: [f64; 3] = [1.0, -1.98611621154089, 0.986211929160751];
pub(super) const BUTTER_B_96000: [f64; 3] =
[0.99308203517541, -1.98616407035082, 0.99308203517541];
pub(super) const YULE_A_88200: [f64; 11] = [
1.0,
-7.19001570087017,
24.4109412087159,
-51.6306373580801,
75.3978476863163,
-79.4164552507386,
61.0373661948115,
-33.7446462547014,
12.8168791146274,
-3.01332198541437,
0.223619893831468,
];
pub(super) const YULE_B_88200: [f64; 11] = [
0.015415414474287,
-0.07691359399407,
0.196677418516518,
-0.338855114128061,
0.430094579594561,
-0.415015413747894,
0.304942508151101,
-0.166191795926663,
0.063198189938739,
-0.015003978694525,
0.001748085184539,
];
pub(super) const BUTTER_A_88200: [f64; 3] = [1.0, -1.98488843762334, 0.979389350028798];
pub(super) const BUTTER_B_88200: [f64; 3] =
[0.992472550461293, -1.98494510092258, 0.992472550461293];
pub(super) const YULE_A_64000: [f64; 11] = [
1.0,
-5.74819833657784,
16.246507961894,
-29.9691822642542,
40.027597579378,
-40.3209196052655,
30.8542077487718,
-17.5965138737281,
7.10690214103873,
-1.82175564515191,
0.223619893831468,
];
pub(super) const YULE_B_64000: [f64; 11] = [
0.021776466467053,
-0.062376961003801,
0.107731165328514,
-0.150994515142316,
0.170334807313632,
-0.157984942890531,
0.121639833268721,
-0.074094040816409,
0.031282852041061,
-0.00755421235941,
0.00117925454213,
];
pub(super) const BUTTER_A_64000: [f64; 3] = [1.0, -1.97917472731008, 0.979389350028798];
pub(super) const BUTTER_B_64000: [f64; 3] =
[0.989641019334721, -1.97928203866944, 0.989641019334721];
pub(super) const YULE_A_48000: [f64; 11] = [
1.0,
-3.84664617118067,
7.81501653005538,
-11.34170355132042,
13.05504219327545,
-12.28759895145294,
9.48293806319790,
-5.87257861775999,
2.75465861874613,
-0.86984376593551,
0.13919314567432,
];
pub(super) const YULE_B_48000: [f64; 11] = [
0.03857599435200,
-0.02160367184185,
-0.00123395316851,
-0.00009291677959,
-0.01655260341619,
0.02161526843274,
-0.02074045215285,
0.00594298065125,
0.00306428023191,
0.00012025322027,
0.00288463683916,
];
pub(super) const BUTTER_A_48000: [f64; 3] = [1.0, -1.97223372919527, 0.97261396931306];
pub(super) const BUTTER_B_48000: [f64; 3] =
[0.98621192462708, -1.97242384925416, 0.98621192462708];
pub(super) const YULE_A_44100: [f64; 11] = [
1.0,
-3.47845948550071,
6.36317777566148,
-8.54751527471874,
9.47693607801280,
-8.81498681370155,
6.85401540936998,
-4.39470996079559,
2.19611684890774,
-0.75104302451432,
0.13149317958808,
];
pub(super) const YULE_B_44100: [f64; 11] = [
0.05418656406430,
-0.02911007808948,
-0.00848709379851,
-0.00851165645469,
-0.00834990904936,
0.02245293253339,
-0.02596338512915,
0.01624864962975,
-0.00240879051584,
0.00674613682247,
-0.00187763777362,
];
pub(super) const BUTTER_A_44100: [f64; 3] = [1.0, -1.96977855582618, 0.97022847566350];
pub(super) const BUTTER_B_44100: [f64; 3] =
[0.98500175787242, -1.97000351574484, 0.98500175787242];
pub(super) const YULE_A_32000: [f64; 11] = [
1.0,
-2.37898834973084,
2.84868151156327,
-2.64577170229825,
2.23697657451713,
-1.67148153367602,
1.00595954808547,
-0.45953458054983,
0.16378164858596,
-0.05032077717131,
0.02347897407020,
];
pub(super) const YULE_B_32000: [f64; 11] = [
0.15457299681924,
-0.09331049056315,
-0.06247880153653,
0.02163541888798,
-0.05588393329856,
0.04781476674921,
0.00222312597743,
0.03174092540049,
-0.01390589421898,
0.00651420667831,
-0.00881362733839,
];
pub(super) const BUTTER_A_32000: [f64; 3] = [1.0, -1.95835380975398, 0.95920349965459];
pub(super) const BUTTER_B_32000: [f64; 3] =
[0.97938932735214, -1.95877865470428, 0.97938932735214];
pub(super) const YULE_A_24000: [f64; 11] = [
1.0,
-1.61273165137247,
1.07977492259970,
-0.25656257754070,
-0.16276719120440,
-0.22638893773906,
0.39120800788284,
-0.22138138954925,
0.04500235387352,
0.02005851806501,
0.00302439095741,
];
pub(super) const YULE_B_24000: [f64; 11] = [
0.30296907319327,
-0.22613988682123,
-0.08587323730772,
0.03282930172664,
-0.00915702933434,
-0.02364141202522,
-0.00584456039913,
0.06276101321749,
-0.00000828086748,
0.00205861885564,
-0.02950134983287,
];
pub(super) const BUTTER_A_24000: [f64; 3] = [1.0, -1.95002759149878, 0.95124613669835];
pub(super) const BUTTER_B_24000: [f64; 3] =
[0.97531843204928, -1.95063686409857, 0.97531843204928];
pub(super) const YULE_A_22050: [f64; 11] = [
1.0,
-1.49858979367799,
0.87350271418188,
0.12205022308084,
-0.80774944671438,
0.47854794562326,
-0.12453458140019,
-0.04067510197014,
0.08333755284107,
-0.04237348025746,
0.02977207319925,
];
pub(super) const YULE_B_22050: [f64; 11] = [
0.33642304856132,
-0.25572241425570,
-0.11828570177555,
0.11921148675203,
-0.07834489609479,
-0.00469977914380,
-0.00589500224440,
0.05724228140351,
0.00832043980773,
-0.01635381384540,
-0.01760176568150,
];
pub(super) const BUTTER_A_22050: [f64; 3] = [1.0, -1.94561023566527, 0.94705070426118];
pub(super) const BUTTER_B_22050: [f64; 3] =
[0.97316523498161, -1.94633046996323, 0.97316523498161];
pub(super) const YULE_A_16000: [f64; 11] = [
1.0,
-0.62820619233671,
0.29661783706366,
-0.37256372942400,
0.00213767857124,
-0.42029820170918,
0.22199650564824,
0.00613424350682,
0.06747620744683,
0.05784820375801,
0.03222754072173,
];
pub(super) const YULE_B_16000: [f64; 11] = [
0.44915256608450,
-0.14351757464547,
-0.22784394429749,
-0.01419140100551,
0.04078262797139,
-0.12398163381748,
0.04078565135648,
0.10478503600251,
-0.01863887810927,
-0.03193428438915,
0.00541907748707,
];
pub(super) const BUTTER_A_16000: [f64; 3] = [1.0, -1.92783286977036, 0.93034775234268];
pub(super) const BUTTER_B_16000: [f64; 3] =
[0.96454515552826, -1.92909031105652, 0.96454515552826];
pub(super) const YULE_A_12000: [f64; 11] = [
1.0,
-1.04800335126349,
0.29156311971249,
-0.26806001042947,
0.00819999645858,
0.45054734505008,
-0.33032403314006,
0.06739368333110,
-0.04784254229033,
0.01639907836189,
0.01807364323573,
];
pub(super) const YULE_B_12000: [f64; 11] = [
0.56619470757641,
-0.75464456939302,
0.16242137742230,
0.16744243493672,
-0.18901604199609,
0.30931782841830,
-0.27562961986224,
0.00647310677246,
0.08647503780351,
-0.03788984554840,
-0.00588215443421,
];
pub(super) const BUTTER_A_12000: [f64; 3] = [1.0, -1.91858953033784, 0.92177618768381];
pub(super) const BUTTER_B_12000: [f64; 3] =
[0.96009142950541, -1.92018285901082, 0.96009142950541];
pub(super) const YULE_A_11025: [f64; 11] = [
1.0,
-0.51035327095184,
-0.31863563325245,
-0.20256413484477,
0.14728154134330,
0.38952639978999,
-0.23313271880868,
-0.05246019024463,
-0.02505961724053,
0.02442357316099,
0.01818801111503,
];
pub(super) const YULE_B_11025: [f64; 11] = [
0.58100494960553,
-0.53174909058578,
-0.14289799034253,
0.17520704835522,
0.02377945217615,
0.15558449135573,
-0.25344790059353,
0.01628462406333,
0.06920467763959,
-0.03721611395801,
-0.00749618797172,
];
pub(super) const BUTTER_A_11025: [f64; 3] = [1.0, -1.91542108074780, 0.91885558323625];
pub(super) const BUTTER_B_11025: [f64; 3] =
[0.95856916599601, -1.91713833199203, 0.95856916599601];
pub(super) const YULE_A_8000: [f64; 11] = [
1.0,
-0.25049871956020,
-0.43193942311114,
-0.03424681017675,
-0.04678328784242,
0.26408300200955,
0.15113130533216,
-0.17556493366449,
-0.18823009262115,
0.05477720428674,
0.04704409688120,
];
pub(super) const YULE_B_8000: [f64; 11] = [
0.53648789255105,
-0.42163034350696,
-0.00275953611929,
0.04267842219415,
-0.10214864179676,
0.14590772289388,
-0.02459864859345,
-0.11202315195388,
-0.04060034127000,
0.04788665548180,
-0.02217936801134,
];
pub(super) const BUTTER_A_8000: [f64; 3] = [1.0, -1.88903307939452, 0.89487434461664];
pub(super) const BUTTER_B_8000: [f64; 3] =
[0.94597685600279, -1.89195371200558, 0.94597685600279];
}
const DENORMAL_PREVENTION: f64 = 1e-10;
#[cfg(feature = "replaygain")]
struct EqualLoudnessFilter {
yule_a: [f64; 11],
yule_b: [f64; 11],
butter_a: [f64; 3],
butter_b: [f64; 3],
yule_x: [f64; 16],
yule_y: [f64; 16],
butter_x: [f64; 4],
butter_y: [f64; 4],
pos: usize,
}
#[cfg(feature = "replaygain")]
impl EqualLoudnessFilter {
fn new(sample_rate: u32) -> Option<Self> {
use filter_coeffs::*;
let (yule_a, yule_b, butter_a, butter_b) = match sample_rate {
96000 => (YULE_A_96000, YULE_B_96000, BUTTER_A_96000, BUTTER_B_96000),
88200 => (YULE_A_88200, YULE_B_88200, BUTTER_A_88200, BUTTER_B_88200),
64000 => (YULE_A_64000, YULE_B_64000, BUTTER_A_64000, BUTTER_B_64000),
48000 => (YULE_A_48000, YULE_B_48000, BUTTER_A_48000, BUTTER_B_48000),
44100 => (YULE_A_44100, YULE_B_44100, BUTTER_A_44100, BUTTER_B_44100),
32000 => (YULE_A_32000, YULE_B_32000, BUTTER_A_32000, BUTTER_B_32000),
24000 => (YULE_A_24000, YULE_B_24000, BUTTER_A_24000, BUTTER_B_24000),
22050 => (YULE_A_22050, YULE_B_22050, BUTTER_A_22050, BUTTER_B_22050),
16000 => (YULE_A_16000, YULE_B_16000, BUTTER_A_16000, BUTTER_B_16000),
12000 => (YULE_A_12000, YULE_B_12000, BUTTER_A_12000, BUTTER_B_12000),
11025 => (YULE_A_11025, YULE_B_11025, BUTTER_A_11025, BUTTER_B_11025),
8000 => (YULE_A_8000, YULE_B_8000, BUTTER_A_8000, BUTTER_B_8000),
_ => return None, };
Some(Self {
yule_a,
yule_b,
butter_a,
butter_b,
yule_x: [0.0; 16],
yule_y: [0.0; 16],
butter_x: [0.0; 4],
butter_y: [0.0; 4],
pos: 0,
})
}
fn process(&mut self, sample: f64) -> f64 {
self.pos = (self.pos + 15) & 15;
let pos = self.pos;
self.yule_x[pos] = sample;
let mut yule_out = DENORMAL_PREVENTION + self.yule_b[0] * sample;
for i in 1..11 {
let j = (pos + i) & 15;
yule_out += self.yule_b[i] * self.yule_x[j] - self.yule_a[i] * self.yule_y[j];
}
self.yule_y[pos] = yule_out;
let bpos = pos & 3;
self.butter_x[bpos] = yule_out;
let mut butter_out = DENORMAL_PREVENTION + self.butter_b[0] * yule_out;
for i in 1..3 {
let j = (bpos + i) & 3;
butter_out += self.butter_b[i] * self.butter_x[j] - self.butter_a[i] * self.butter_y[j];
}
self.butter_y[bpos] = butter_out;
butter_out
}
}
const STEPS_PER_DB: f64 = 100.0;
const HISTOGRAM_SIZE: usize = 12000;
const RMS_PERCENTILE: f64 = 0.95;
#[cfg(feature = "replaygain")]
#[derive(Clone)]
struct LoudnessHistogram {
data: Vec<u32>,
}
#[cfg(feature = "replaygain")]
impl LoudnessHistogram {
fn new() -> Self {
Self {
data: vec![0; HISTOGRAM_SIZE],
}
}
fn accumulate(&mut self, other: &LoudnessHistogram) {
for (a, &b) in self.data.iter_mut().zip(other.data.iter()) {
*a += b;
}
}
fn get_loudness(&self) -> f64 {
let total: u64 = self.data.iter().map(|&x| x as u64).sum();
if total == 0 {
return -20.0; }
let threshold = ((total as f64) * (1.0 - RMS_PERCENTILE)).ceil() as u64;
let mut count = 0u64;
for i in (0..HISTOGRAM_SIZE).rev() {
count += self.data[i] as u64;
if count >= threshold {
return i as f64 / STEPS_PER_DB;
}
}
-20.0 }
}
#[cfg(feature = "replaygain")]
struct ReplayGainAnalyzer {
lsum: f64,
rsum: f64,
totsamp: usize,
window_samples: usize,
histogram: LoudnessHistogram,
}
#[cfg(feature = "replaygain")]
impl ReplayGainAnalyzer {
fn new(sample_rate: u32) -> Self {
let window_samples = (sample_rate as usize * 50) / 1000;
Self {
lsum: 0.0,
rsum: 0.0,
totsamp: 0,
window_samples,
histogram: LoudnessHistogram::new(),
}
}
fn into_histogram(self) -> LoudnessHistogram {
self.histogram
}
fn add_sample(&mut self, left: f64, right: f64) {
self.lsum += left * left;
self.rsum += right * right;
self.totsamp += 1;
if self.totsamp >= self.window_samples {
self.finish_window();
}
}
fn add_mono_sample(&mut self, sample: f64) {
let sq = sample * sample;
self.lsum += sq;
self.rsum += sq;
self.totsamp += 1;
if self.totsamp >= self.window_samples {
self.finish_window();
}
}
fn finish_window(&mut self) {
if self.totsamp == 0 {
return;
}
let mean_square = (self.lsum + self.rsum) / self.totsamp as f64 * 0.5;
let val = STEPS_PER_DB * 10.0 * (mean_square + 1e-37).log10();
let idx = (val as i32).clamp(0, HISTOGRAM_SIZE as i32 - 1) as usize;
self.histogram.data[idx] += 1;
self.lsum = 0.0;
self.rsum = 0.0;
self.totsamp = 0;
}
fn get_loudness(&self) -> f64 {
self.histogram.get_loudness()
}
}
#[cfg(feature = "replaygain")]
fn detect_file_type(file_path: &Path) -> AudioFileType {
if mp4meta::is_aac_file(file_path) {
AudioFileType::Aac
} else {
AudioFileType::Mp3
}
}
#[cfg(feature = "replaygain")]
struct ProgressMediaSource {
inner: std::fs::File,
position: Arc<AtomicU64>,
total_size: u64,
}
#[cfg(feature = "replaygain")]
impl std::io::Read for ProgressMediaSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.inner.read(buf)?;
self.position.fetch_add(n as u64, Ordering::Relaxed);
Ok(n)
}
}
#[cfg(feature = "replaygain")]
impl std::io::Seek for ProgressMediaSource {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
let new_pos = self.inner.seek(pos)?;
self.position.store(new_pos, Ordering::Relaxed);
Ok(new_pos)
}
}
#[cfg(feature = "replaygain")]
impl MediaSource for ProgressMediaSource {
fn is_seekable(&self) -> bool {
true
}
fn byte_len(&self) -> Option<u64> {
Some(self.total_size)
}
}
#[cfg(feature = "replaygain")]
enum LoudnessState {
Rg1(LoudnessHistogram),
Bs1770(crate::bs1770::BlockEnergies),
}
#[cfg(feature = "replaygain")]
impl LoudnessState {
fn new(mode: AnalysisMode) -> Self {
match mode {
AnalysisMode::Rg1 => LoudnessState::Rg1(LoudnessHistogram::new()),
_ => LoudnessState::Bs1770(crate::bs1770::BlockEnergies::new()),
}
}
fn accumulate(&mut self, other: &LoudnessState) {
match (self, other) {
(LoudnessState::Rg1(a), LoudnessState::Rg1(b)) => a.accumulate(b),
(LoudnessState::Bs1770(a), LoudnessState::Bs1770(b)) => a.accumulate(b),
_ => debug_assert!(false, "mixed analysis modes in album accumulation"),
}
}
fn loudness_and_gain(&self, mode: AnalysisMode) -> (f64, f64) {
match self {
LoudnessState::Rg1(histogram) => {
let loudness = histogram.get_loudness();
(loudness, PINK_REF - loudness)
}
LoudnessState::Bs1770(blocks) => lufs_loudness_and_gain(blocks.integrated_lufs(), mode),
}
}
}
#[cfg(feature = "replaygain")]
fn lufs_loudness_and_gain(lufs: f64, mode: AnalysisMode) -> (f64, f64) {
let target = mode
.target_lufs()
.expect("BS.1770 gain calculation requires an RG2/R128 mode");
if lufs.is_finite() {
(lufs, target - lufs)
} else {
(target, 0.0)
}
}
#[cfg(feature = "replaygain")]
struct TrackAnalysisInternal {
result: ReplayGainResult,
state: LoudnessState,
}
#[cfg(feature = "replaygain")]
enum TrackAnalyzer {
Rg1 {
filters: Vec<EqualLoudnessFilter>,
analyzer: ReplayGainAnalyzer,
},
Bs1770 {
analyzer: crate::bs1770::Bs1770Analyzer,
},
}
#[cfg(feature = "replaygain")]
fn analyze_track_internal(
file_path: &Path,
track_index: Option<u32>,
progress: Option<&dyn Fn(u64, u64)>,
mode: AnalysisMode,
true_peak: bool,
) -> Result<TrackAnalysisInternal> {
let file_type = detect_file_type(file_path);
let file = std::fs::File::open(file_path).map_err(|e| Error::io_open(file_path, e))?;
let file_size = file.metadata().map(|m| m.len()).unwrap_or(0);
let position_tracker = progress.map(|_| Arc::new(AtomicU64::new(0)));
let mss = if let Some(ref tracker) = position_tracker {
let source = ProgressMediaSource {
inner: file,
position: Arc::clone(tracker),
total_size: file_size,
};
MediaSourceStream::new(Box::new(source), Default::default())
} else {
MediaSourceStream::new(Box::new(file), Default::default())
};
let mut hint = Hint::new();
if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let mut format = symphonia::default::get_probe()
.probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(|e| Error::ProbeFailed {
path: file_path.to_path_buf(),
source: Box::new(e),
})?;
let audio_tracks: Vec<_> = format
.tracks()
.iter()
.filter(|t| {
t.codec_params
.as_ref()
.and_then(|p| p.audio())
.is_some_and(|a| a.codec != CODEC_ID_NULL_AUDIO)
})
.collect();
if audio_tracks.is_empty() {
return Err(Error::NoAudioTrack);
}
let track = match track_index {
Some(idx) => {
let idx = idx as usize;
if idx >= audio_tracks.len() {
return Err(Error::TrackIndexOutOfRange {
index: idx as u32,
count: audio_tracks.len(),
});
}
audio_tracks[idx]
}
None => audio_tracks[0],
};
let track_id = track.id;
let audio_params = track
.codec_params
.as_ref()
.and_then(|p| p.audio())
.ok_or(Error::NoAudioTrack)?;
let sample_rate = audio_params
.sample_rate
.ok_or(Error::UnsupportedSampleRate(0))?;
let channels = audio_params
.channels
.as_ref()
.map(|c| c.count())
.unwrap_or(2);
let mut decoder = symphonia::default::get_codecs()
.make_audio_decoder(audio_params, &AudioDecoderOptions::default())
.map_err(|e| Error::Decode(Box::new(e)))?;
let mut track_analyzer = match mode {
AnalysisMode::Rg1 => {
let filters: Vec<EqualLoudnessFilter> = (0..channels)
.map(|_| {
EqualLoudnessFilter::new(sample_rate)
.ok_or(Error::UnsupportedSampleRate(sample_rate))
})
.collect::<Result<Vec<_>>>()?;
TrackAnalyzer::Rg1 {
filters,
analyzer: ReplayGainAnalyzer::new(sample_rate),
}
}
_ if true_peak => TrackAnalyzer::Bs1770 {
analyzer: crate::bs1770::Bs1770Analyzer::new_with_true_peak(sample_rate, channels),
},
_ => TrackAnalyzer::Bs1770 {
analyzer: crate::bs1770::Bs1770Analyzer::new(sample_rate, channels),
},
};
let mut peak: f64 = 0.0;
loop {
let packet = match format.next_packet() {
Ok(Some(p)) => p,
Ok(None) => break,
Err(e) => return Err(Error::Decode(Box::new(e))),
};
if packet.track_id != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(symphonia::core::errors::Error::DecodeError(_)) => continue,
Err(e) => return Err(Error::Decode(Box::new(e))),
};
match &mut track_analyzer {
TrackAnalyzer::Rg1 { filters, analyzer } => {
process_audio_buffer(&decoded, filters, analyzer, &mut peak)
}
TrackAnalyzer::Bs1770 { analyzer } => {
process_audio_buffer_bs1770(&decoded, analyzer, &mut peak)
}
}
if let (Some(cb), Some(ref tracker)) = (progress, &position_tracker) {
cb(tracker.load(Ordering::Relaxed), file_size);
}
}
if let Some(cb) = progress {
cb(file_size, file_size);
}
let mut is_true_peak = false;
let (loudness_db, gain_db, state) = match track_analyzer {
TrackAnalyzer::Rg1 { mut analyzer, .. } => {
analyzer.finish_window();
let loudness_db = analyzer.get_loudness();
(
loudness_db,
PINK_REF - loudness_db,
LoudnessState::Rg1(analyzer.into_histogram()),
)
}
TrackAnalyzer::Bs1770 { analyzer, .. } => {
if let Some(tp) = analyzer.true_peak() {
peak = peak.max(tp);
is_true_peak = true;
}
let blocks = analyzer.into_blocks();
let (loudness_db, gain_db) = lufs_loudness_and_gain(blocks.integrated_lufs(), mode);
(loudness_db, gain_db, LoudnessState::Bs1770(blocks))
}
};
let mut result =
ReplayGainResult::new(loudness_db, gain_db, peak, sample_rate, file_type, mode);
result.true_peak = is_true_peak;
Ok(TrackAnalysisInternal { result, state })
}
#[cfg(feature = "replaygain")]
pub fn analyze_track(file_path: &Path) -> Result<ReplayGainResult> {
analyze_track_with_index(file_path, None)
}
#[cfg(feature = "replaygain")]
pub fn analyze_track_with_index(
file_path: &Path,
track_index: Option<u32>,
) -> Result<ReplayGainResult> {
analyze_track_with_mode(file_path, track_index, AnalysisMode::default(), None)
}
#[cfg(feature = "replaygain")]
pub fn analyze_track_with_mode(
file_path: &Path,
track_index: Option<u32>,
mode: AnalysisMode,
on_progress: Option<&dyn Fn(u64, u64)>,
) -> Result<ReplayGainResult> {
analyze_track_with_options(
file_path,
&TrackAnalysisOptions {
track_index,
mode,
on_progress,
..Default::default()
},
)
}
#[derive(Default)]
pub struct TrackAnalysisOptions<'a> {
pub track_index: Option<u32>,
pub mode: AnalysisMode,
pub true_peak: bool,
pub on_progress: Option<&'a dyn Fn(u64, u64)>,
}
#[cfg(feature = "replaygain")]
pub fn analyze_track_with_options(
file_path: &Path,
opts: &TrackAnalysisOptions,
) -> Result<ReplayGainResult> {
let internal = analyze_track_internal(
file_path,
opts.track_index,
opts.on_progress,
opts.mode,
opts.true_peak,
)?;
Ok(internal.result)
}
#[cfg(feature = "replaygain")]
pub fn analyze_track_with_progress(
file_path: &Path,
track_index: Option<u32>,
on_progress: &dyn Fn(u64, u64),
) -> Result<ReplayGainResult> {
analyze_track_with_mode(
file_path,
track_index,
AnalysisMode::default(),
Some(on_progress),
)
}
const SAMPLE_SCALE_16BIT: f64 = 32768.0;
const SAMPLE_SCALE_32BIT: f64 = 2147483648.0;
#[cfg(feature = "replaygain")]
fn process_audio_buffer(
buffer: &GenericAudioBufferRef,
filters: &mut [EqualLoudnessFilter],
analyzer: &mut ReplayGainAnalyzer,
peak: &mut f64,
) {
match buffer {
GenericAudioBufferRef::F32(buf) => {
let channels = buf.num_planes();
let frames = buf.frames();
let left_plane = buf.plane(0).unwrap();
let right_plane = (channels >= 2).then(|| buf.plane(1).unwrap());
for frame in 0..frames {
let left_norm = left_plane[frame] as f64;
*peak = peak.max(left_norm.abs());
let left_filtered = filters[0].process(left_norm * SAMPLE_SCALE_16BIT);
if let Some(right_plane) = right_plane {
let right_norm = right_plane[frame] as f64;
*peak = peak.max(right_norm.abs());
let right_filtered = filters[1].process(right_norm * SAMPLE_SCALE_16BIT);
analyzer.add_sample(left_filtered, right_filtered);
} else {
analyzer.add_mono_sample(left_filtered);
}
}
}
GenericAudioBufferRef::S16(buf) => {
let channels = buf.num_planes();
let frames = buf.frames();
let left_plane = buf.plane(0).unwrap();
let right_plane = (channels >= 2).then(|| buf.plane(1).unwrap());
for frame in 0..frames {
let left = left_plane[frame] as f64;
*peak = peak.max((left / SAMPLE_SCALE_16BIT).abs());
let left_filtered = filters[0].process(left);
if let Some(right_plane) = right_plane {
let right = right_plane[frame] as f64;
*peak = peak.max((right / SAMPLE_SCALE_16BIT).abs());
let right_filtered = filters[1].process(right);
analyzer.add_sample(left_filtered, right_filtered);
} else {
analyzer.add_mono_sample(left_filtered);
}
}
}
GenericAudioBufferRef::S32(buf) => {
let channels = buf.num_planes();
let frames = buf.frames();
let scale = SAMPLE_SCALE_16BIT / SAMPLE_SCALE_32BIT;
let left_plane = buf.plane(0).unwrap();
let right_plane = (channels >= 2).then(|| buf.plane(1).unwrap());
for frame in 0..frames {
let left = left_plane[frame] as f64 * scale;
*peak = peak.max((left / SAMPLE_SCALE_16BIT).abs());
let left_filtered = filters[0].process(left);
if let Some(right_plane) = right_plane {
let right = right_plane[frame] as f64 * scale;
*peak = peak.max((right / SAMPLE_SCALE_16BIT).abs());
let right_filtered = filters[1].process(right);
analyzer.add_sample(left_filtered, right_filtered);
} else {
analyzer.add_mono_sample(left_filtered);
}
}
}
_ => {
}
}
}
#[cfg(feature = "replaygain")]
fn process_audio_buffer_bs1770(
buffer: &GenericAudioBufferRef,
analyzer: &mut crate::bs1770::Bs1770Analyzer,
peak: &mut f64,
) {
fn feed<T: Copy>(
planes: &[&[T]],
frames: usize,
conv: impl Fn(T) -> f64,
analyzer: &mut crate::bs1770::Bs1770Analyzer,
peak: &mut f64,
) {
match planes {
[mono] => {
for &s in &mono[..frames] {
let v = conv(s);
*peak = peak.max(v.abs());
analyzer.add_frame(&[v]);
}
}
[left, right] => {
for (&l, &r) in left[..frames].iter().zip(&right[..frames]) {
let l = conv(l);
let r = conv(r);
*peak = peak.max(l.abs()).max(r.abs());
analyzer.add_frame(&[l, r]);
}
}
_ => {
let mut frame_buf = vec![0.0; planes.len()];
for frame in 0..frames {
for (dst, plane) in frame_buf.iter_mut().zip(planes) {
let v = conv(plane[frame]);
*peak = peak.max(v.abs());
*dst = v;
}
analyzer.add_frame(&frame_buf);
}
}
}
}
match buffer {
GenericAudioBufferRef::F32(buf) => {
let planes: Vec<&[f32]> = (0..buf.num_planes())
.map(|i| buf.plane(i).unwrap())
.collect();
feed(&planes, buf.frames(), |s| s as f64, analyzer, peak);
}
GenericAudioBufferRef::S16(buf) => {
let planes: Vec<&[i16]> = (0..buf.num_planes())
.map(|i| buf.plane(i).unwrap())
.collect();
feed(
&planes,
buf.frames(),
|s| s as f64 / SAMPLE_SCALE_16BIT,
analyzer,
peak,
);
}
GenericAudioBufferRef::S32(buf) => {
let planes: Vec<&[i32]> = (0..buf.num_planes())
.map(|i| buf.plane(i).unwrap())
.collect();
feed(
&planes,
buf.frames(),
|s| s as f64 / SAMPLE_SCALE_32BIT,
analyzer,
peak,
);
}
_ => {
}
}
}
pub type AlbumProgressFn<'a> = &'a dyn Fn(usize, u64, u64);
pub type AlbumCompleteFn<'a> = &'a (dyn Fn(usize, &Path) + Sync);
#[derive(Default)]
pub struct AlbumAnalysisOptions<'a> {
pub track_index: Option<u32>,
pub threads: usize,
pub skip_errors: bool,
pub on_progress: Option<AlbumProgressFn<'a>>,
pub on_complete: Option<AlbumCompleteFn<'a>>,
pub cancel: Option<&'a AtomicBool>,
pub mode: AnalysisMode,
pub true_peak: bool,
}
#[cfg(feature = "replaygain")]
pub fn analyze_album(files: &[&Path]) -> Result<AlbumGainResult> {
Ok(analyze_album_with_options(files, &AlbumAnalysisOptions::default())?.album)
}
#[cfg(feature = "replaygain")]
pub fn analyze_album_with_options(
files: &[&Path],
opts: &AlbumAnalysisOptions,
) -> Result<AlbumAnalysisReport> {
if opts.threads <= 1 || files.len() <= 1 {
analyze_album_serial(files, opts)
} else {
analyze_album_parallel_internal(files, opts)
}
}
#[cfg(feature = "replaygain")]
fn analyze_album_serial(
files: &[&Path],
opts: &AlbumAnalysisOptions,
) -> Result<AlbumAnalysisReport> {
let AlbumAnalysisOptions {
track_index,
on_progress,
on_complete,
skip_errors,
cancel,
mode,
true_peak,
..
} = *opts;
let mut track_results = Vec::with_capacity(files.len());
let mut album_peak: f64 = 0.0;
let mut album_state = LoudnessState::new(mode);
let mut failures: Vec<(usize, String)> = Vec::new();
let mut successful_indices: Vec<usize> = Vec::with_capacity(files.len());
for (i, file) in files.iter().enumerate() {
if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
return Err(Error::Cancelled);
}
let file_progress: Option<Box<dyn Fn(u64, u64) + '_>> =
on_progress.map(|cb| Box::new(move |bytes, total| cb(i, bytes, total)) as _);
let track =
analyze_track_internal(file, track_index, file_progress.as_deref(), mode, true_peak);
if let Some(cb) = on_complete {
cb(i, file);
}
match track {
Ok(internal) => {
album_peak = album_peak.max(internal.result.peak);
album_state.accumulate(&internal.state);
track_results.push(internal.result);
successful_indices.push(i);
}
Err(e) => {
if skip_errors {
failures.push((i, format!("{}", e)));
} else {
return Err(e);
}
}
}
}
if track_results.is_empty() && !files.is_empty() {
return Err(Error::AllFilesFailed { count: files.len() });
}
let (album_loudness_db, album_gain_db) = album_state.loudness_and_gain(mode);
let album = AlbumGainResult::new(track_results, album_loudness_db, album_gain_db, album_peak);
Ok(AlbumAnalysisReport {
album,
failures,
successful_indices,
})
}
#[cfg(feature = "replaygain")]
fn analyze_album_parallel_internal(
files: &[&Path],
opts: &AlbumAnalysisOptions,
) -> Result<AlbumAnalysisReport> {
use rayon::prelude::*;
let AlbumAnalysisOptions {
track_index,
on_complete,
skip_errors,
cancel,
mode,
true_peak,
..
} = *opts;
let mut track_results = Vec::with_capacity(files.len());
let mut album_peak: f64 = 0.0;
let mut album_state = LoudnessState::new(mode);
let mut failures: Vec<(usize, String)> = Vec::new();
let mut successful_indices: Vec<usize> = Vec::with_capacity(files.len());
if skip_errors {
let internals: Vec<Result<TrackAnalysisInternal>> = files
.par_iter()
.enumerate()
.map(|(i, file)| {
if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
return Err(Error::Cancelled);
}
let r = analyze_track_internal(file, track_index, None, mode, true_peak);
if let Some(cb) = on_complete {
cb(i, file);
}
r
})
.collect();
if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
return Err(Error::Cancelled);
}
for (i, r) in internals.into_iter().enumerate() {
match r {
Ok(internal) => {
album_peak = album_peak.max(internal.result.peak);
album_state.accumulate(&internal.state);
track_results.push(internal.result);
successful_indices.push(i);
}
Err(e) => failures.push((i, format!("{}", e))),
}
}
} else {
let internals: Vec<TrackAnalysisInternal> = files
.par_iter()
.enumerate()
.map(|(i, file)| {
if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
return Err(Error::Cancelled);
}
let r = analyze_track_internal(file, track_index, None, mode, true_peak);
if let Some(cb) = on_complete {
cb(i, file);
}
r
})
.collect::<Result<Vec<_>>>()?;
for (i, internal) in internals.into_iter().enumerate() {
album_peak = album_peak.max(internal.result.peak);
album_state.accumulate(&internal.state);
track_results.push(internal.result);
successful_indices.push(i);
}
}
if track_results.is_empty() && !files.is_empty() {
return Err(Error::AllFilesFailed { count: files.len() });
}
let (album_loudness_db, album_gain_db) = album_state.loudness_and_gain(mode);
let album = AlbumGainResult::new(track_results, album_loudness_db, album_gain_db, album_peak);
Ok(AlbumAnalysisReport {
album,
failures,
successful_indices,
})
}
#[cfg(not(feature = "replaygain"))]
pub fn analyze_track(_file_path: &Path) -> Result<ReplayGainResult> {
Err(Error::FeatureNotAvailable {
feature: "ReplayGain analysis",
feature_flag: "replaygain",
})
}
#[cfg(not(feature = "replaygain"))]
pub fn analyze_track_with_index(
_file_path: &Path,
_track_index: Option<u32>,
) -> Result<ReplayGainResult> {
Err(Error::FeatureNotAvailable {
feature: "ReplayGain analysis",
feature_flag: "replaygain",
})
}
#[cfg(not(feature = "replaygain"))]
pub fn analyze_track_with_mode(
_file_path: &Path,
_track_index: Option<u32>,
_mode: AnalysisMode,
_on_progress: Option<&dyn Fn(u64, u64)>,
) -> Result<ReplayGainResult> {
Err(Error::FeatureNotAvailable {
feature: "ReplayGain analysis",
feature_flag: "replaygain",
})
}
#[cfg(not(feature = "replaygain"))]
pub fn analyze_track_with_options(
_file_path: &Path,
_opts: &TrackAnalysisOptions,
) -> Result<ReplayGainResult> {
Err(Error::FeatureNotAvailable {
feature: "ReplayGain analysis",
feature_flag: "replaygain",
})
}
#[cfg(not(feature = "replaygain"))]
pub fn analyze_track_with_progress(
_file_path: &Path,
_track_index: Option<u32>,
_on_progress: &dyn Fn(u64, u64),
) -> Result<ReplayGainResult> {
Err(Error::FeatureNotAvailable {
feature: "ReplayGain analysis",
feature_flag: "replaygain",
})
}
#[cfg(not(feature = "replaygain"))]
pub fn analyze_album(_files: &[&Path]) -> Result<AlbumGainResult> {
Err(Error::FeatureNotAvailable {
feature: "ReplayGain analysis",
feature_flag: "replaygain",
})
}
#[cfg(not(feature = "replaygain"))]
pub fn analyze_album_with_options(
_files: &[&Path],
_opts: &AlbumAnalysisOptions,
) -> Result<AlbumAnalysisReport> {
Err(Error::FeatureNotAvailable {
feature: "ReplayGain analysis",
feature_flag: "replaygain",
})
}
pub fn is_available() -> bool {
cfg!(feature = "replaygain")
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PeakAmplitudeResult {
peak: f64,
peak_pcm: f64,
sample_rate: u32,
}
impl PeakAmplitudeResult {
#[allow(dead_code)]
pub(crate) fn new(peak: f64, peak_pcm: f64, sample_rate: u32) -> Self {
Self {
peak,
peak_pcm,
sample_rate,
}
}
pub fn peak(&self) -> f64 {
self.peak
}
pub fn peak_pcm(&self) -> f64 {
self.peak_pcm
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
}
impl std::fmt::Display for PeakAmplitudeResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "peak: {:.6} ({:.1} PCM)", self.peak, self.peak_pcm)
}
}
#[cfg(feature = "replaygain")]
pub fn find_peak_amplitude(file_path: &Path) -> Result<PeakAmplitudeResult> {
let file = std::fs::File::open(file_path).map_err(|e| Error::io_open(file_path, e))?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let mut format = symphonia::default::get_probe()
.probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(|e| Error::ProbeFailed {
path: file_path.to_path_buf(),
source: Box::new(e),
})?;
let track = format
.tracks()
.iter()
.find(|t| {
t.codec_params
.as_ref()
.and_then(|p| p.audio())
.is_some_and(|a| a.codec != CODEC_ID_NULL_AUDIO)
})
.ok_or(Error::NoAudioTrack)?;
let track_id = track.id;
let audio_params = track
.codec_params
.as_ref()
.and_then(|p| p.audio())
.ok_or(Error::NoAudioTrack)?;
let sample_rate = audio_params
.sample_rate
.ok_or(Error::UnsupportedSampleRate(0))?;
let mut decoder = symphonia::default::get_codecs()
.make_audio_decoder(audio_params, &AudioDecoderOptions::default())
.map_err(|e| Error::Decode(Box::new(e)))?;
let mut max_peak: f64 = 0.0;
loop {
let packet = match format.next_packet() {
Ok(Some(p)) => p,
Ok(None) => break,
Err(e) => return Err(Error::Decode(Box::new(e))),
};
if packet.track_id != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(symphonia::core::errors::Error::DecodeError(_)) => continue,
Err(e) => return Err(Error::Decode(Box::new(e))),
};
match &decoded {
GenericAudioBufferRef::F32(buf) => {
for ch in 0..buf.num_planes() {
for &sample in buf.plane(ch).unwrap() {
max_peak = max_peak.max((sample as f64).abs());
}
}
}
GenericAudioBufferRef::S16(buf) => {
for ch in 0..buf.num_planes() {
for &sample in buf.plane(ch).unwrap() {
let s = (sample as f64).abs() / SAMPLE_SCALE_16BIT;
max_peak = max_peak.max(s);
}
}
}
GenericAudioBufferRef::S32(buf) => {
for ch in 0..buf.num_planes() {
for &sample in buf.plane(ch).unwrap() {
let s = (sample as f64).abs() / SAMPLE_SCALE_32BIT;
max_peak = max_peak.max(s);
}
}
}
_ => {}
}
}
Ok(PeakAmplitudeResult::new(
max_peak,
crate::gain::peak_to_pcm_sample(max_peak),
sample_rate,
))
}
#[cfg(not(feature = "replaygain"))]
pub fn find_peak_amplitude(_file_path: &Path) -> Result<PeakAmplitudeResult> {
Err(Error::FeatureNotAvailable {
feature: "Peak amplitude analysis",
feature_flag: "replaygain",
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replaygain_availability() {
let available = is_available();
#[cfg(feature = "replaygain")]
assert!(available);
#[cfg(not(feature = "replaygain"))]
assert!(!available);
}
#[test]
fn with_peak_replaces_peak_and_preserves_other_fields() {
let original = ReplayGainResult::new(
-15.0,
6.0,
0.5,
44_100,
AudioFileType::Mp3,
AnalysisMode::Rg1,
);
let updated = original.clone().with_peak(0.8);
assert_eq!(updated.peak(), 0.8);
assert_eq!(updated.gain_db(), original.gain_db());
assert_eq!(updated.loudness_db(), original.loudness_db());
assert_eq!(updated.sample_rate(), original.sample_rate());
assert_eq!(updated.file_type(), original.file_type());
}
#[cfg(feature = "replaygain")]
#[test]
fn test_filter_creation() {
let supported_rates = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000,
];
for rate in supported_rates {
let filter = EqualLoudnessFilter::new(rate);
assert!(filter.is_some(), "Sample rate {} should be supported", rate);
let filter = filter.unwrap();
assert_eq!(filter.yule_a.len(), 11);
assert_eq!(filter.butter_a.len(), 3);
}
let unsupported = EqualLoudnessFilter::new(99999);
assert!(
unsupported.is_none(),
"Unsupported sample rate should return None"
);
}
#[cfg(feature = "replaygain")]
#[test]
fn test_rms_calculation() {
let sample_rate = 44100u32;
let mut filter = EqualLoudnessFilter::new(sample_rate).unwrap();
let mut analyzer = ReplayGainAnalyzer::new(sample_rate);
let frequency = 1000.0;
let amplitude_normalized = 0.5; let amplitude = amplitude_normalized * SAMPLE_SCALE_16BIT; let duration_samples = sample_rate as usize;
for i in 0..duration_samples {
let t = i as f64 / sample_rate as f64;
let sample = amplitude * (2.0 * std::f64::consts::PI * frequency * t).sin();
let filtered = filter.process(sample);
analyzer.add_mono_sample(filtered);
}
let loudness = analyzer.get_loudness();
assert!(
loudness > 50.0,
"Loudness should be above 50 dB: {}",
loudness
);
assert!(
loudness < 100.0,
"Loudness should be below 100 dB: {}",
loudness
);
}
#[cfg(feature = "replaygain")]
#[test]
fn lenient_album_skips_failed_files() {
let good = std::path::PathBuf::from("tests/fixtures/test_stereo.mp3");
let bad = std::path::PathBuf::from("tests/fixtures/this-file-does-not-exist.mp3");
let files = vec![good.as_path(), bad.as_path()];
let strict = analyze_album(&files);
assert!(
strict.is_err(),
"strict mode must fail when any file is unreadable"
);
let lenient = AlbumAnalysisOptions {
skip_errors: true,
..Default::default()
};
let report =
analyze_album_with_options(&files, &lenient).expect("lenient must skip the bad file");
assert_eq!(report.album.tracks().len(), 1);
assert_eq!(report.successful_indices, vec![0]);
assert_eq!(report.failures.len(), 1);
assert_eq!(report.failures[0].0, 1);
}
#[cfg(feature = "replaygain")]
#[test]
fn lenient_album_errors_when_all_fail() {
let bad1 = std::path::PathBuf::from("tests/fixtures/missing-1.mp3");
let bad2 = std::path::PathBuf::from("tests/fixtures/missing-2.mp3");
let files = vec![bad1.as_path(), bad2.as_path()];
let lenient = AlbumAnalysisOptions {
skip_errors: true,
..Default::default()
};
let result = analyze_album_with_options(&files, &lenient);
assert!(matches!(result, Err(Error::AllFilesFailed { count: 2 })));
}
#[cfg(feature = "replaygain")]
#[test]
fn test_loudness_calculation() {
let sample_rate = 44100u32;
let mut filter = EqualLoudnessFilter::new(sample_rate).unwrap();
let mut analyzer = ReplayGainAnalyzer::new(sample_rate);
let frequency = 1000.0;
let amplitude_normalized = 0.1; let amplitude = amplitude_normalized * SAMPLE_SCALE_16BIT; let duration_samples = sample_rate as usize;
for i in 0..duration_samples {
let t = i as f64 / sample_rate as f64;
let sample = amplitude * (2.0 * std::f64::consts::PI * frequency * t).sin();
let filtered = filter.process(sample);
analyzer.add_mono_sample(filtered);
}
let loudness = analyzer.get_loudness();
assert!(
loudness > 50.0 && loudness < 80.0,
"Loudness {} should be between 50 and 80 dB for a 0.1 amplitude 1kHz sine",
loudness
);
}
#[cfg(feature = "replaygain")]
#[test]
fn silent_file_matches_reference_zero_db() {
let sample_rate = 44100u32;
let mut filter = EqualLoudnessFilter::new(sample_rate).unwrap();
let mut analyzer = ReplayGainAnalyzer::new(sample_rate);
for _ in 0..sample_rate {
let filtered = filter.process(0.0);
analyzer.add_mono_sample(filtered);
}
let loudness = analyzer.get_loudness();
assert_eq!(loudness, 0.0, "silent file loudness must clamp to 0 dB");
assert!(
(PINK_REF - loudness - 64.82).abs() < 1e-12,
"silent file gain must be +64.82 dB (mp3gain reference)"
);
}
#[cfg(feature = "replaygain")]
#[test]
fn near_silent_file_matches_reference_zero_db() {
let sample_rate = 44100u32;
let window = (sample_rate as usize * 50) / 1000;
let mut filter = EqualLoudnessFilter::new(sample_rate).unwrap();
let mut analyzer = ReplayGainAnalyzer::new(sample_rate);
let frequency = 1000.0;
let amplitude = 0.5 * SAMPLE_SCALE_16BIT;
for i in 0..(window * 100) {
let sample = if i < window * 96 {
0.0
} else {
let t = i as f64 / sample_rate as f64;
amplitude * (2.0 * std::f64::consts::PI * frequency * t).sin()
};
let filtered = filter.process(sample);
analyzer.add_mono_sample(filtered);
}
let loudness = analyzer.get_loudness();
assert_eq!(
loudness, 0.0,
"96%-silent file loudness must clamp to 0 dB, got {}",
loudness
);
assert!(
(PINK_REF - loudness - 64.82).abs() < 1e-12,
"96%-silent file gain must be +64.82 dB (mp3gain reference)"
);
}
#[cfg(feature = "replaygain")]
const GOLDEN_PCM_SAMPLE_RATE: u32 = 44_100;
#[cfg(feature = "replaygain")]
const GOLDEN_PCM_FRAMES: usize = 2205 * 80;
#[cfg(feature = "replaygain")]
fn golden_pcm() -> (Vec<f64>, Vec<f64>) {
let win = (GOLDEN_PCM_SAMPLE_RATE as usize * 50) / 1000; let n = GOLDEN_PCM_FRAMES;
let mut left = Vec::with_capacity(n);
let mut right = Vec::with_capacity(n);
let mut ls: u64 = 0x1234_5678_9abc_def0;
let mut rs: u64 = 0x0fed_cba9_8765_4321;
let lcg = |s: &mut u64| -> f64 {
*s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((*s >> 11) as f64 / (1u64 << 53) as f64) * 2.0 - 1.0
};
for i in 0..n {
let w = i / win;
let amp = if w < 50 {
0.0
} else {
0.06 + 0.01 * (w - 50) as f64
};
let l = (lcg(&mut ls) * amp * 30000.0).round() as i32;
let r = (lcg(&mut rs) * amp * 30000.0).round() as i32;
left.push(l as f64 / SAMPLE_SCALE_16BIT);
right.push(r as f64 / SAMPLE_SCALE_16BIT);
}
(left, right)
}
#[cfg(feature = "replaygain")]
#[test]
#[ignore = "one-time: regenerates the PCM dump for the reference C harness (#201)"]
fn dump_golden_pcm() {
let path =
std::env::var("RG_PCM_DUMP").unwrap_or_else(|_| "/tmp/rg_golden_pcm.bin".to_string());
let (left, right) = golden_pcm();
let mut buf = Vec::with_capacity(8 + left.len() * 16);
buf.extend_from_slice(&GOLDEN_PCM_SAMPLE_RATE.to_le_bytes());
buf.extend_from_slice(&(left.len() as u32).to_le_bytes());
for &x in &left {
buf.extend_from_slice(&(x * SAMPLE_SCALE_16BIT).to_le_bytes());
}
for &x in &right {
buf.extend_from_slice(&(x * SAMPLE_SCALE_16BIT).to_le_bytes());
}
std::fs::write(&path, &buf).expect("write PCM dump");
eprintln!(
"wrote {} frames ({} bytes) to {}",
left.len(),
buf.len(),
path
);
}
#[cfg(feature = "replaygain")]
#[test]
fn analysis_matches_reference_c_to_float_precision() {
const GOLDEN_GAIN_DB: f64 = -0.83000000000001251;
let (left, right) = golden_pcm();
let sr = GOLDEN_PCM_SAMPLE_RATE;
let mut filter_l = EqualLoudnessFilter::new(sr).unwrap();
let mut filter_r = EqualLoudnessFilter::new(sr).unwrap();
let mut analyzer = ReplayGainAnalyzer::new(sr);
for (&l, &r) in left.iter().zip(right.iter()) {
let lf = filter_l.process(l * SAMPLE_SCALE_16BIT);
let rf = filter_r.process(r * SAMPLE_SCALE_16BIT);
analyzer.add_sample(lf, rf);
}
analyzer.finish_window(); let gain = PINK_REF - analyzer.get_loudness();
let delta = (gain - GOLDEN_GAIN_DB).abs();
assert!(
delta < 1e-6,
"ReplayGain analysis diverged from reference gain_analysis.c: \
mp3rgain {gain:.12} dB vs reference {GOLDEN_GAIN_DB:.12} dB (Δ {delta:.3e} dB)"
);
}
}