Skip to main content

max / audiofiles

Fix fade off-by-one, gain NaN guard, normalize clamp, and other core bugs Fade: use (frames-1) denominator so final frame reaches target gain. Gain: reject NaN/Inf dB values. Normalize LUFS: clamp output to [-1,1]. Spectral: use consistent 1e-10 floor for geometric mean log fallback. VP-tree: cap recursion depth at 64 to prevent stack overflow on degenerate input. Audio/instrument: guard against zero sample rate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-26 19:38 UTC
Commit: f19a9ec409ce8d9436c4a3950a20f77073816ea2
Parent: c22a046
8 files changed, +34 insertions, -24 deletions
@@ -130,7 +130,7 @@ pub(crate) fn fill_preview(
130 130 return; // GUI thread holds lock — leave buffer unchanged (already zeroed)
131 131 };
132 132
133 - if !guard.playing {
133 + if !guard.playing || device_sample_rate == 0 {
134 134 return;
135 135 }
136 136
@@ -234,7 +234,7 @@ pub fn render_voices(
234 234 channels: usize,
235 235 device_sr: u32,
236 236 ) {
237 - if !inst.active || inst.zone_buffers.is_empty() || channels == 0 {
237 + if !inst.active || inst.zone_buffers.is_empty() || channels == 0 || device_sr == 0 {
238 238 return;
239 239 }
240 240
@@ -153,7 +153,7 @@ fn compute_spectral_inner(
153 153 let arithmetic_mean = mag_sum / n;
154 154 let log_sum: f64 = magnitudes
155 155 .iter()
156 - .map(|&m| if m > 1e-10 { m.ln() } else { (-10.0f64).ln() * 2.3 })
156 + .map(|&m| if m > 1e-10 { m.ln() } else { (1e-10_f64).ln() })
157 157 .sum();
158 158 let geometric_mean = (log_sum / n).exp();
159 159 let flat = if arithmetic_mean > 0.0 {
@@ -37,8 +37,9 @@ pub fn apply_fade_in(samples: &mut [f32], channels: u16, frames: usize, curve: F
37 37 let total_frames = samples.len() / ch;
38 38 let fade_frames = frames.min(total_frames);
39 39
40 + let denom = (fade_frames - 1).max(1) as f32;
40 41 for i in 0..fade_frames {
41 - let t = i as f32 / fade_frames as f32;
42 + let t = i as f32 / denom;
42 43 let gain = curve.gain(t);
43 44 for c in 0..ch {
44 45 samples[i * ch + c] *= gain;
@@ -57,8 +58,9 @@ pub fn apply_fade_out(samples: &mut [f32], channels: u16, frames: usize, curve:
57 58 let fade_frames = frames.min(total_frames);
58 59 let start_frame = total_frames - fade_frames;
59 60
61 + let denom = (fade_frames - 1).max(1) as f32;
60 62 for i in 0..fade_frames {
61 - let t = 1.0 - (i as f32 / fade_frames as f32);
63 + let t = 1.0 - (i as f32 / denom);
62 64 let gain = curve.gain(t);
63 65 let frame_idx = start_frame + i;
64 66 for c in 0..ch {
@@ -75,22 +77,22 @@ mod tests {
75 77 fn fade_in_linear_mono() {
76 78 let mut samples = vec![1.0; 4];
77 79 apply_fade_in(&mut samples, 1, 4, FadeCurve::Linear);
78 - // Frame 0: gain=0.0, Frame 1: gain=0.25, Frame 2: gain=0.5, Frame 3: gain=0.75
80 + // Frame 0: gain=0/3, Frame 1: gain=1/3, Frame 2: gain=2/3, Frame 3: gain=3/3
79 81 assert!((samples[0] - 0.0).abs() < 0.001);
80 - assert!((samples[1] - 0.25).abs() < 0.001);
81 - assert!((samples[2] - 0.5).abs() < 0.001);
82 - assert!((samples[3] - 0.75).abs() < 0.001);
82 + assert!((samples[1] - 1.0 / 3.0).abs() < 0.001);
83 + assert!((samples[2] - 2.0 / 3.0).abs() < 0.001);
84 + assert!((samples[3] - 1.0).abs() < 0.001);
83 85 }
84 86
85 87 #[test]
86 88 fn fade_out_linear_mono() {
87 89 let mut samples = vec![1.0; 4];
88 90 apply_fade_out(&mut samples, 1, 4, FadeCurve::Linear);
89 - // Frame 0: gain=1.0, Frame 1: gain=0.75, Frame 2: gain=0.5, Frame 3: gain=0.25
91 + // Frame 0: gain=3/3, Frame 1: gain=2/3, Frame 2: gain=1/3, Frame 3: gain=0/3
90 92 assert!((samples[0] - 1.0).abs() < 0.001);
91 - assert!((samples[1] - 0.75).abs() < 0.001);
92 - assert!((samples[2] - 0.5).abs() < 0.001);
93 - assert!((samples[3] - 0.25).abs() < 0.001);
93 + assert!((samples[1] - 2.0 / 3.0).abs() < 0.001);
94 + assert!((samples[2] - 1.0 / 3.0).abs() < 0.001);
95 + assert!((samples[3] - 0.0).abs() < 0.001);
94 96 }
95 97
96 98 #[test]
@@ -98,11 +100,13 @@ mod tests {
98 100 // 3 frames of stereo, all 1.0
99 101 let mut samples = vec![1.0; 6];
100 102 apply_fade_in(&mut samples, 2, 3, FadeCurve::Linear);
101 - // Frame 0: gain=0.0 → [0,0], Frame 1: gain=0.333 → [0.333,0.333], Frame 2: gain=0.667
103 + // Frame 0: gain=0/2, Frame 1: gain=1/2, Frame 2: gain=2/2
102 104 assert!((samples[0]).abs() < 0.001); // L0
103 105 assert!((samples[1]).abs() < 0.001); // R0
104 - assert!((samples[2] - 1.0 / 3.0).abs() < 0.01);
105 - assert!((samples[3] - 1.0 / 3.0).abs() < 0.01);
106 + assert!((samples[2] - 0.5).abs() < 0.01);
107 + assert!((samples[3] - 0.5).abs() < 0.01);
108 + assert!((samples[4] - 1.0).abs() < 0.01);
109 + assert!((samples[5] - 1.0).abs() < 0.01);
106 110 }
107 111
108 112 #[test]
@@ -2,7 +2,7 @@
2 2
3 3 /// Apply a gain offset in dB to all samples, clamping to [-1.0, 1.0].
4 4 pub fn apply_gain(samples: &mut [f32], db: f64) {
5 - if samples.is_empty() || db == 0.0 {
5 + if samples.is_empty() || db == 0.0 || !db.is_finite() {
6 6 return;
7 7 }
8 8
@@ -152,8 +152,8 @@ mod tests {
152 152 curve: FadeCurve::Linear,
153 153 };
154 154 apply_edit(&mut samples, 1, 44100, &op).unwrap();
155 - // Last frame should be near zero
156 - assert!((samples[3] - 0.25).abs() < 0.001);
155 + // Last frame should be zero (fade reaches silence)
156 + assert!((samples[3]).abs() < 0.001);
157 157 }
158 158
159 159 #[test]
@@ -58,7 +58,7 @@ pub fn apply_normalize_lufs(
58 58 let scale = 10.0_f64.powf(gain_db / 20.0) as f32;
59 59
60 60 for sample in samples.iter_mut() {
61 - *sample *= scale;
61 + *sample = (*sample * scale).clamp(-1.0, 1.0);
62 62 }
63 63
64 64 Ok(())
@@ -51,7 +51,7 @@ impl<T> VpTree<T> {
51 51 }
52 52 let mut indices: Vec<usize> = (0..n).collect();
53 53 let mut nodes = Vec::with_capacity(n);
54 - let root = build_recursive(&items, &mut indices, &dist, &mut nodes);
54 + let root = build_recursive(&items, &mut indices, &dist, &mut nodes, 0);
55 55 Self {
56 56 items,
57 57 root: Some(root),
@@ -138,16 +138,22 @@ impl<T> VpTree<T> {
138 138
139 139 // --- Build ---
140 140
141 + /// Maximum recursion depth to prevent stack overflow on degenerate input
142 + /// (e.g. all-identical feature vectors causing O(n)-deep recursion).
143 + const MAX_BUILD_DEPTH: usize = 64;
144 +
141 145 fn build_recursive<T>(
142 146 items: &[T],
143 147 indices: &mut [usize],
144 148 dist: &impl Fn(&T, &T) -> f64,
145 149 nodes: &mut Vec<VpNode>,
150 + depth: usize,
146 151 ) -> usize {
147 152 debug_assert!(!indices.is_empty());
148 153
149 154 let vp_idx = indices[0];
150 - if indices.len() == 1 {
155 + // Leaf node: single item or depth cap reached
156 + if indices.len() == 1 || depth >= MAX_BUILD_DEPTH {
151 157 let node_idx = nodes.len();
152 158 nodes.push(VpNode {
153 159 item_idx: vp_idx,
@@ -192,12 +198,12 @@ fn build_recursive<T>(
192 198 let left = if inside.is_empty() {
193 199 None
194 200 } else {
195 - Some(build_recursive(items, &mut inside, dist, nodes))
201 + Some(build_recursive(items, &mut inside, dist, nodes, depth + 1))
196 202 };
197 203 let right = if outside.is_empty() {
198 204 None
199 205 } else {
200 - Some(build_recursive(items, &mut outside, dist, nodes))
206 + Some(build_recursive(items, &mut outside, dist, nodes, depth + 1))
201 207 };
202 208
203 209 nodes[node_idx].left = left;