From a354fee79215be5f09793d3909caf6fc22ac679a Mon Sep 17 00:00:00 2001 From: Smittix Date: Tue, 10 Feb 2026 20:24:51 +0000 Subject: [PATCH] fix: Resolve listening post audio stuttering introduced in v2.15.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Throttle audio waterfall rendering (50ms→200ms), eliminate per-frame Array.from() allocation, drain stale pipe buffer before streaming, increase chunk size to 8192, and remove debug logging from animation hot paths. Co-Authored-By: Claude Opus 4.6 --- routes/listening_post.py | 21 +++++++++++++++++++-- static/js/modes/listening-post.js | 30 +++--------------------------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/routes/listening_post.py b/routes/listening_post.py index cb43aec..2907f88 100644 --- a/routes/listening_post.py +++ b/routes/listening_post.py @@ -1456,13 +1456,30 @@ def stream_audio() -> Response: if not proc or not proc.stdout: return try: - # First byte timeout to avoid hanging clients forever + # Drain stale audio that accumulated in the pipe buffer + # between pipeline start and stream connection. Keep the + # first chunk (contains WAV header) and discard the rest + # so the browser starts close to real-time. + header_chunk = None + while True: + ready, _, _ = select.select([proc.stdout], [], [], 0) + if not ready: + break + chunk = proc.stdout.read(8192) + if not chunk: + break + if header_chunk is None: + header_chunk = chunk + if header_chunk: + yield header_chunk + + # Stream real-time audio first_chunk_deadline = time.time() + 3.0 while audio_running and proc.poll() is None: # Use select to avoid blocking forever ready, _, _ = select.select([proc.stdout], [], [], 2.0) if ready: - chunk = proc.stdout.read(4096) + chunk = proc.stdout.read(8192) if chunk: yield chunk else: diff --git a/static/js/modes/listening-post.js b/static/js/modes/listening-post.js index 98c7339..0dfd755 100644 --- a/static/js/modes/listening-post.js +++ b/static/js/modes/listening-post.js @@ -1742,9 +1742,6 @@ function initSynthesizer() { drawSynthesizer(); } -// Debug: log signal level periodically -let lastSynthDebugLog = 0; - function drawSynthesizer() { if (!synthCtx || !synthCanvas) return; @@ -1760,19 +1757,6 @@ function drawSynthesizer() { let activityLevel = 0; let signalIntensity = 0; - // Debug logging every 2 seconds - const now = Date.now(); - if (now - lastSynthDebugLog > 2000) { - console.log('[SYNTH] State:', { - isScannerRunning, - isDirectListening, - scannerSignalActive, - currentSignalLevel, - visualizerAnalyser: !!visualizerAnalyser - }); - lastSynthDebugLog = now; - } - if (isScannerRunning && !isScannerPaused) { // Use actual signal level data (0-5000 range, normalize to 0-1) signalIntensity = Math.min(1, currentSignalLevel / 3000); @@ -1864,13 +1848,6 @@ function drawSynthesizer() { synthCtx.lineTo(width, height / 2); synthCtx.stroke(); - // Debug: show signal level value - if (isScannerRunning || isDirectListening) { - synthCtx.fillStyle = 'rgba(255, 255, 255, 0.5)'; - synthCtx.font = '9px monospace'; - synthCtx.fillText(`lvl:${Math.round(currentSignalLevel)}`, 4, 10); - } - synthAnimationId = requestAnimationFrame(drawSynthesizer); } @@ -3109,7 +3086,7 @@ let waterfallEndFreq = 108; let waterfallRowImage = null; let waterfallPalette = null; let lastWaterfallDraw = 0; -const WATERFALL_MIN_INTERVAL_MS = 50; +const WATERFALL_MIN_INTERVAL_MS = 200; let waterfallInteractionBound = false; let waterfallResizeObserver = null; let waterfallMode = 'rf'; @@ -3436,9 +3413,8 @@ function startAudioWaterfall() { if (ts - lastAudioWaterfallDraw >= WATERFALL_MIN_INTERVAL_MS) { lastAudioWaterfallDraw = ts; visualizerAnalyser.getByteFrequencyData(dataArray); - const bins = Array.from(dataArray, v => v); - drawWaterfallRow(bins); - drawSpectrumLine(bins, 0, maxFreqKhz, 'kHz'); + drawWaterfallRow(dataArray); + drawSpectrumLine(dataArray, 0, maxFreqKhz, 'kHz'); } audioWaterfallAnimId = requestAnimationFrame(drawFrame); };