//! Динамический левеллер громкости (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::() / 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::() / 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 = (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}" ); } }