From 072b4ff3dabf5f54df26ae807d40cd0c2831632d Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:59:45 +0200 Subject: [PATCH 1/2] Guard all playback output paths against NaN from interpolation overflow Fixes #918 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/playback/src/lib.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/playback/src/lib.rs b/crates/playback/src/lib.rs index 41bb89ed..bb77e13d 100644 --- a/crates/playback/src/lib.rs +++ b/crates/playback/src/lib.rs @@ -363,7 +363,7 @@ impl AudioState { let ch = self.pending.get(c); let a = ch.and_then(|v| v.get(i0)).copied().unwrap_or(0.0); let b = ch.and_then(|v| v.get(i0 + 1)).copied().unwrap_or(a); - a + (b - a) * fr + a * (1.0 - fr) + b * fr }; if channels < nch { // Fold surround to stereo first; a mono device averages the two sides. @@ -381,18 +381,15 @@ impl AudioState { } for (c, o) in frame.iter_mut().enumerate() { *o = match c { - 0 if channels == 1 => { - let mono = (0.5 * l + 0.5 * r).clamp(-1.0, 1.0); - if mono.is_finite() { mono } else { 0.0 } - } - 0 => l.clamp(-1.0, 1.0), - 1 => r.clamp(-1.0, 1.0), + 0 if channels == 1 => out_sample(0.5 * l + 0.5 * r), + 0 => out_sample(l), + 1 => out_sample(r), _ => 0.0, }; } } else { for (c, o) in frame.iter_mut().enumerate() { - *o = if c < nch.max(1) && sounding { sample(c).clamp(-1.0, 1.0) } else { 0.0 }; + *o = if c < nch.max(1) && sounding { out_sample(sample(c)) } else { 0.0 }; } } self.read += step; @@ -400,6 +397,12 @@ impl AudioState { } } +/// Clamp to the device range, mapping NaN and infinities to silence. +fn out_sample(v: f32) -> f32 { + let v = v.clamp(-1.0, 1.0); + if v.is_finite() { v } else { 0.0 } +} + /// UI-thread bookkeeping of the third-party instances the audio thread holds. struct Side { /// What was last created for each slot: (plugin id, channels, created ok). @@ -1253,6 +1256,18 @@ mod tests { assert!(out.iter().all(|x| *x == 0.0), "overflow in the stereo fold-down must not send NaN to a mono device"); } + #[test] + fn interpolation_overflow_between_huge_samples_never_reaches_the_device() { + use soundcraft_model::ChannelFormat; + let alternating: Vec = (0..512).map(|i| if i % 2 == 0 { -3.4e38 } else { 3.4e38 }).collect(); + for (format, nch, device) in [(ChannelFormat::Stereo, 2, 2), (ChannelFormat::Stereo, 2, 8), (ChannelFormat::Mono, 1, 1)] { + let mut st = audio_output_state(format, vec![alternating.clone(); nch]); + let mut out = vec![0.5; 64 * device]; + st.fill(&mut out, device); + assert!(out.iter().all(|x| x.is_finite() && (-1.0..=1.0).contains(x)), "{format:?} on {device} channels: {out:?}"); + } + } + #[test] fn surround_main_maps_to_device_channels_or_folds_down() { use soundcraft_model::{ChannelFormat, Clip, SourceAudio, SourceId, TrackKind}; From 0888ca8ac3b220a239be2d4221ef13999d9151bb Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 24 Oct 2026 00:54:53 -0400 Subject: [PATCH 2/2] Playback: an infinite sample goes out as silence, not full scale Co-Authored-By: Claude Opus 5.5 --- crates/playback/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/playback/src/lib.rs b/crates/playback/src/lib.rs index bb77e13d..8195640c 100644 --- a/crates/playback/src/lib.rs +++ b/crates/playback/src/lib.rs @@ -399,8 +399,7 @@ impl AudioState { /// Clamp to the device range, mapping NaN and infinities to silence. fn out_sample(v: f32) -> f32 { - let v = v.clamp(-1.0, 1.0); - if v.is_finite() { v } else { 0.0 } + if v.is_finite() { v.clamp(-1.0, 1.0) } else { 0.0 } } /// UI-thread bookkeeping of the third-party instances the audio thread holds.