132 lines
6.4 KiB
Rust
132 lines
6.4 KiB
Rust
//! Динамический левеллер громкости (fallback, когда в файле нет тега ReplayGain):
|
||
//! постоянно измеряет громкость последних секунд звука и плавно подстраивает усиление
|
||
//! к целевому уровню. В отличие от настоящего ReplayGain (посчитан заранее по всему
|
||
//! треку), это адаптация "на лету" — первые секунды трека выравниваются не идеально,
|
||
//! дальше подстройка сходится к цели.
|
||
|
||
const TARGET_RMS: f32 = 0.1; // примерно -20 dBFS
|
||
const MIN_GAIN: f32 = 0.1; // макс. ослабление -20 дБ (для "громких" мастерингов, близких к 0dBFS)
|
||
const MAX_GAIN: f32 = 4.0; // макс. усиление +12 дБ (не тащим почти-тишину слишком громко)
|
||
const POWER_TIME_CONSTANT_SECS: f32 = 2.0;
|
||
const GAIN_TIME_CONSTANT_SECS: f32 = 1.0;
|
||
|
||
pub struct Leveler {
|
||
ema_power: f32,
|
||
current_gain: f32,
|
||
}
|
||
|
||
impl Leveler {
|
||
pub fn new() -> Self {
|
||
Leveler {
|
||
ema_power: TARGET_RMS * TARGET_RMS,
|
||
current_gain: 1.0,
|
||
}
|
||
}
|
||
|
||
/// Обрабатывает interleaved-буфер на месте. `channels`/`sample_rate` нужны, чтобы
|
||
/// сглаживание было привязано ко времени, а не к размеру пакета декодера (иначе
|
||
/// скорость адаптации плясала бы в зависимости от формата/битрейта файла).
|
||
pub fn process(&mut self, samples: &mut [f32], channels: usize, sample_rate: f32) {
|
||
if samples.is_empty() || channels == 0 || sample_rate <= 0.0 {
|
||
return;
|
||
}
|
||
let n = samples.len() as f32;
|
||
let block_power: f32 = samples.iter().map(|s| s * s).sum::<f32>() / n;
|
||
let frames = n / channels as f32;
|
||
let block_duration = (frames / sample_rate).max(0.0001);
|
||
|
||
let power_alpha = 1.0 - (-block_duration / POWER_TIME_CONSTANT_SECS).exp();
|
||
self.ema_power = self.ema_power * (1.0 - power_alpha) + block_power * power_alpha;
|
||
|
||
let rms = self.ema_power.sqrt().max(1e-6);
|
||
let desired_gain = (TARGET_RMS / rms).clamp(MIN_GAIN, MAX_GAIN);
|
||
|
||
let gain_alpha = 1.0 - (-block_duration / GAIN_TIME_CONSTANT_SECS).exp();
|
||
self.current_gain = self.current_gain * (1.0 - gain_alpha) + desired_gain * gain_alpha;
|
||
|
||
for s in samples.iter_mut() {
|
||
*s *= self.current_gain;
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for Leveler {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn rms(samples: &[f32]) -> f32 {
|
||
(samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt()
|
||
}
|
||
|
||
/// Прогоняет несколько секунд синусоиды заданной амплитуды через левеллер и
|
||
/// возвращает RMS последнего блока (когда сглаживание уже сошлось).
|
||
fn converged_rms(amplitude: f32) -> f32 {
|
||
let sample_rate = 44100.0;
|
||
let block_frames = 4096;
|
||
let mut leveler = Leveler::new();
|
||
let mut last_block_rms = 0.0;
|
||
// ~28 секунд звука — оба каскада сглаживания (мощность 2с + усиление 1с) успевают
|
||
// сойтись за много постоянных времени каждый (в реальном прослушивании счёт на минуты)
|
||
for block in 0..300 {
|
||
let mut samples: Vec<f32> = (0..block_frames)
|
||
.map(|i| {
|
||
let t = (block * block_frames + i) as f32 / sample_rate;
|
||
amplitude * (2.0 * std::f32::consts::PI * 440.0 * t).sin()
|
||
})
|
||
.collect();
|
||
leveler.process(&mut samples, 1, sample_rate);
|
||
last_block_rms = rms(&samples);
|
||
}
|
||
last_block_rms
|
||
}
|
||
|
||
#[test]
|
||
fn loud_and_quiet_signals_converge_to_similar_output_level() {
|
||
let loud_out = converged_rms(0.9); // громкий сигнал (близко к клиппингу)
|
||
let quiet_out = converged_rms(0.04); // тихий, но не настолько, чтобы упереться в MAX_GAIN
|
||
|
||
// оба должны приблизиться к целевому RMS, а не остаться на исходных уровнях
|
||
assert!(
|
||
(loud_out - TARGET_RMS).abs() < TARGET_RMS * 0.5,
|
||
"loud_out={loud_out}, target={TARGET_RMS}"
|
||
);
|
||
assert!(
|
||
(quiet_out - TARGET_RMS).abs() < TARGET_RMS * 0.5,
|
||
"quiet_out={quiet_out}, target={TARGET_RMS}"
|
||
);
|
||
// и главное — они должны оказаться близко друг к другу после выравнивания
|
||
assert!(
|
||
(loud_out - quiet_out).abs() < TARGET_RMS * 0.3,
|
||
"loud_out={loud_out}, quiet_out={quiet_out}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn does_not_amplify_near_silence_excessively() {
|
||
let sample_rate = 44100.0;
|
||
let mut leveler = Leveler::new();
|
||
let mut samples = vec![0.0001f32; 4096]; // почти тишина
|
||
leveler.process(&mut samples, 1, sample_rate);
|
||
// усиление ограничено MAX_GAIN, а не улетает в бесконечность на почти-нулевом сигнале
|
||
assert!(samples.iter().all(|s| s.abs() <= 0.0001 * MAX_GAIN + 1e-6));
|
||
}
|
||
|
||
#[test]
|
||
fn extremely_quiet_signal_hits_max_gain_ceiling_by_design() {
|
||
// Сигнал настолько тихий, что даже максимальное разрешённое усиление (+12дБ)
|
||
// не дотягивает до целевого RMS — это осознанное ограничение (не тащим шум/тишину
|
||
// до полной громкости), а не недостижение сходимости.
|
||
let out = converged_rms(0.02);
|
||
let expected_ceiling = (0.02f32 / std::f32::consts::SQRT_2) * MAX_GAIN;
|
||
assert!(
|
||
(out - expected_ceiling).abs() < expected_ceiling * 0.1,
|
||
"expected to converge to the MAX_GAIN ceiling ~{expected_ceiling}, got {out}"
|
||
);
|
||
}
|
||
}
|