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)
}
pub fn load_gains() -> std::collections::HashMap<String, f32> {
let path = crate::data::cache_path("normalization.json");
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save_gains(gains: &std::collections::HashMap<String, f32>) {
let path = crate::data::cache_path("normalization.json");
if let Some(dir) = path.parent() {
if std::fs::create_dir_all(dir).is_err() {
return;
}
}
if let Ok(s) = serde_json::to_string(gains) {
let tmp = path.with_extension("tmp");
if std::fs::write(&tmp, s).is_ok() {
let _ = std::fs::rename(&tmp, &path);
}
}
}