use std::path::Path;
use symphonia::{
core::{
codecs::{DecoderOptions, CODEC_TYPE_NULL},
formats::FormatOptions,
io::MediaSourceStream,
meta::MetadataOptions,
probe::Hint,
},
default::get_probe,
};
const TARGET_RMS: f32 = 0.2;
const MIN_GAIN: f32 = 0.5;
const MAX_GAIN: f32 = 3.0;
const MAX_PEAK: f32 = 0.99;
pub fn compute_normalization_gain(path: &Path) -> Option<f32> {
let file = std::fs::File::open(path).ok()?;
let mss = MediaSourceStream::new(
Box::new(file),
symphonia::core::io::MediaSourceStreamOptions::default(),
);
let mut probed = get_probe()
.format(
&Hint::new(),
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.ok()?;
let track = probed
.format
.tracks()
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)?;
let track_id = track.id;
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.ok()?;
let mut sum_squares: f64 = 0.0;
let mut n_samples: u64 = 0;
let mut peak: f32 = 0.0;
while let Ok(packet) = probed.format.next_packet() {
if packet.track_id() != track_id {
continue;
}
let Ok(audio_buf) = decoder.decode(&packet) else {
continue;
};
let spec = audio_buf.spec().to_owned();
let mut buf = symphonia::core::audio::SampleBuffer::<i16>::new(
symphonia::core::units::Duration::from(audio_buf.capacity() as u64),
spec,
);
buf.copy_interleaved_ref(audio_buf);
for &s in buf.samples() {
let v = f32::from(s) / 32768.0;
sum_squares += f64::from(v) * f64::from(v);
n_samples += 1;
let a = v.abs();
if a > peak {
peak = a;
}
}
}
if n_samples == 0 {
return None;
}
let rms = (sum_squares / n_samples as f64).sqrt() as f32;
if rms <= 0.0 {
return None;
}
let mut gain = TARGET_RMS / rms;
gain = gain.clamp(MIN_GAIN, MAX_GAIN);
if peak * gain > MAX_PEAK {
gain = MAX_PEAK / peak;
}
Some(gain)
}