From 5703db0b80eba55b9d24662b4c3bf5c56c52f737 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:24:47 +0200 Subject: [PATCH] Fix normalize producing inf samples for subnormal peaks Found while testing crates/dsp/src/offline.rs:62-79 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/dsp/src/offline.rs | 18 ++++++++++++++---- crates/dsp/tests/offline_normalize.rs | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 crates/dsp/tests/offline_normalize.rs diff --git a/crates/dsp/src/offline.rs b/crates/dsp/src/offline.rs index 8bbc80a6..337f3589 100644 --- a/crates/dsp/src/offline.rs +++ b/crates/dsp/src/offline.rs @@ -65,19 +65,29 @@ pub fn normalize(ch: &mut [Vec], target_db: f32, per_channel: bool) { for c in ch.iter_mut() { let p = peak(std::slice::from_ref(c)); if p > 0.0 { - let g = target / p; - c.iter_mut().for_each(|s| *s *= g); + scale_to(c.iter_mut(), target, p); } } } else { let p = peak(ch); if p > 0.0 { - let g = target / p; - ch.iter_mut().flatten().for_each(|s| *s *= g); + scale_to(ch.iter_mut().flatten(), target, p); } } } +/// Scales samples whose peak is `p` to `target`. A subnormal `p` makes the f32 gain overflow to +/// infinity, so that case scales in f64. +fn scale_to<'a>(samples: impl Iterator, target: f32, p: f32) { + let g = target / p; + if g.is_finite() { + samples.for_each(|s| *s *= g); + } else { + let g = f64::from(target) / f64::from(p); + samples.for_each(|s| *s = (f64::from(*s) * g) as f32); + } +} + /// Applies a gain change in dB. pub fn gain(ch: &mut [Vec], db: f32) { let g = db_to_gain(if db.is_nan() { 0.0 } else { db.min(96.0) }); diff --git a/crates/dsp/tests/offline_normalize.rs b/crates/dsp/tests/offline_normalize.rs new file mode 100644 index 00000000..6ed47973 --- /dev/null +++ b/crates/dsp/tests/offline_normalize.rs @@ -0,0 +1,19 @@ +use soundcraft_dsp::offline::{normalize, peak}; + +#[test] +fn normalize_subnormal_peak_stays_finite() { + for per_channel in [false, true] { + let mut c = vec![vec![1e-44f32, -1e-44]]; + normalize(&mut c, 0.0, per_channel); + assert!(c[0].iter().all(|s| s.is_finite()), "{c:?}"); + assert!((peak(&c) - 1.0).abs() < 1e-3, "{c:?}"); + } +} + +#[test] +fn normalize_silent_and_normal_channels() { + let mut c = vec![vec![0.0f32; 4], vec![0.25, -0.5]]; + normalize(&mut c, 0.0, true); + assert_eq!(c[0], vec![0.0; 4]); + assert!((peak(&c[1..]) - 1.0).abs() < 1e-5); +}