Skip to main content

max / audiofiles

Improve preview decode: generation counter, NaN sanitization, OOM cap Replace decode_cancel boolean with generation counter so rapid decode starts don't race (old thread exits when its generation is stale). Sanitize NaN/Inf samples to silence in interleaved_to_stereo. Cap decode at 10 min of stereo 48kHz to prevent OOM on corrupted metadata. Guard against zero sample rate in estimate_duration. 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:39 UTC
Commit: a07c72f9069c395ecc85d0d86ccfc5090147cc82
Parent: c72605b
1 file changed, +30 insertions, -12 deletions
@@ -119,6 +119,10 @@ pub fn decode_to_f32(path: &Path) -> Result<PreviewBuffer, PreviewError> {
119 119
120 120 let mut all_samples: Vec<f32> = Vec::new();
121 121
122 + // Cap at 10 minutes of stereo 48kHz to prevent OOM on files with bad metadata
123 + // that bypass the streaming threshold.
124 + const MAX_SAMPLES: usize = 10 * 60 * 48_000 * 2;
125 +
122 126 loop {
123 127 let packet = match format.next_packet() {
124 128 Ok(p) => p,
@@ -153,6 +157,11 @@ pub fn decode_to_f32(path: &Path) -> Result<PreviewBuffer, PreviewError> {
153 157
154 158 // Convert to interleaved stereo
155 159 interleaved_to_stereo(samples, num_channels, num_frames, &mut all_samples);
160 +
161 + if all_samples.len() >= MAX_SAMPLES {
162 + tracing::warn!("decode_to_f32: hit {MAX_SAMPLES} sample cap, truncating");
163 + break;
164 + }
156 165 }
157 166
158 167 if all_samples.is_empty() {
@@ -199,6 +208,9 @@ pub fn estimate_duration(path: &Path) -> Option<f64> {
199 208 if is_wav {
200 209 let reader = hound::WavReader::open(path).ok()?;
201 210 let spec = reader.spec();
211 + if spec.sample_rate == 0 {
212 + return None;
213 + }
202 214 let frames = reader.len() as f64 / spec.channels as f64;
203 215 return Some(frames / spec.sample_rate as f64);
204 216 }
@@ -277,8 +289,8 @@ pub fn start_streaming_decode(
277 289 .make(&codec_params, &DecoderOptions::default())
278 290 .map_err(|e| PreviewError::Decoder(e.to_string()))?;
279 291
280 - // Cancel any previous streaming decode thread
281 - shared.decode_cancel.store(true, Ordering::Release);
292 + // Increment generation to cancel any previous streaming decode thread
293 + let my_generation = shared.decode_generation.fetch_add(1, Ordering::AcqRel) + 1;
282 294
283 295 // Set up the initial buffer and playback state (not yet playing)
284 296 {
@@ -296,17 +308,14 @@ pub fn start_streaming_decode(
296 308 guard.total_frames_estimate = n_frames_estimate;
297 309 }
298 310
299 - // Clear cancel flag for the new decode thread
300 - shared.decode_cancel.store(false, Ordering::Release);
301 -
302 311 let shared = std::sync::Arc::clone(shared);
303 312 std::thread::spawn(move || {
304 313 let mut total_frames = 0usize;
305 314 let mut started = false;
306 315
307 316 loop {
308 - // Check cancellation before each packet
309 - if shared.decode_cancel.load(Ordering::Acquire) {
317 + // Check if a newer decode has started — if so, this thread exits
318 + if shared.decode_generation.load(Ordering::Acquire) != my_generation {
310 319 let mut guard = shared.preview.lock();
311 320 guard.streaming = false;
312 321 return;
@@ -391,19 +400,28 @@ fn interleaved_to_stereo(
391 400 num_frames: usize,
392 401 out: &mut Vec<f32>,
393 402 ) {
403 + // Sanitize: replace NaN/Inf with silence to prevent downstream propagation
404 + // (NaN.clamp() returns NaN, so the audio output clamp won't catch it).
405 + let clean = |s: f32| if s.is_finite() { s } else { 0.0 };
406 +
394 407 match num_channels {
395 408 1 => {
396 409 for &s in samples {
397 - out.push(s);
398 - out.push(s);
410 + let v = clean(s);
411 + out.push(v);
412 + out.push(v);
413 + }
414 + }
415 + 2 => {
416 + for &s in samples {
417 + out.push(clean(s));
399 418 }
400 419 }
401 - 2 => out.extend_from_slice(samples),
402 420 n => {
403 421 for frame in 0..num_frames {
404 422 let base = frame * n;
405 - let left = samples.get(base).copied().unwrap_or(0.0);
406 - let right = samples.get(base + 1).copied().unwrap_or(0.0);
423 + let left = clean(samples.get(base).copied().unwrap_or(0.0));
424 + let right = clean(samples.get(base + 1).copied().unwrap_or(0.0));
407 425 out.push(left);
408 426 out.push(right);
409 427 }