Files
yellowjacket/.planning/quick/15-fix-audio-glitches-and-skips-in-decoding/15-SUMMARY.md
T
yonlu 6cb22359c8 docs(quick-15): complete audio glitch fix plan
- Add 15-SUMMARY.md with implementation details
- Update STATE.md with quick task 15 completion
2026-03-05 15:29:39 -05:00

3.3 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
15-fix-audio-glitches 01 player
audio
buffering
performance
glitch-fix
requires provides affects
BufferedStreamer
read-ahead-buffering
player-pipeline
speaker-init
added patterns
ring-buffer
goroutine-read-ahead
channel-signaling
created modified
backend/player/buffered_streamer.go
backend/player/buffered_streamer_test.go
backend/player/player.go
2-second ring buffer (88200 samples at 44100 Hz) provides sufficient runway for I/O stalls and GC pauses
Source reads happen outside mutex lock to avoid blocking speaker callback
Empty buffer returns silence rather than blocking or signaling end-of-stream
Speaker buffer doubled from 100ms to 200ms as secondary underrun protection
duration completed tasks files_changed
10m 38s 2026-03-05 2 3

Quick Task 15: Fix Audio Glitches and Skips in Decoding Summary

Ring-buffer BufferedStreamer with goroutine read-ahead between decoder/resampler and speaker output, plus 200ms speaker buffer — eliminates glitches from disk I/O stalls, GC pauses, and CPU scheduling delays.

Tasks Completed

# Task Commit Key Changes
1 Create BufferedStreamer with goroutine read-ahead 85b23ac New BufferedStreamer type with ring buffer, read-ahead goroutine, silence-on-empty, Close() cleanup; 5 unit tests
2 Insert BufferedStreamer into player pipeline and increase speaker buffer 8a0b16a Chain: decode→resample→BufferedStreamer→ctrl→volume→speaker; speaker buffer 100ms→200ms; Close on unload/track-change

Implementation Details

BufferedStreamer Design

  • Ring buffer: Pre-allocated [][2]float64 of configurable size (default 88200 samples ≈ 2 seconds at 44100 Hz)
  • Read-ahead goroutine: Reads from source in 512-sample chunks outside the mutex, copies into ring under lock
  • Thread safety: Mutex protects ring metadata only; source I/O never holds the lock, so the speaker goroutine is never blocked by disk
  • Empty buffer handling: Returns silence (len(samples), true) when buffer temporarily empty — brief silence is far better than a glitch or premature track end
  • Shutdown: Close() signals goroutine via channel; safe to call multiple times; called on track change and unload

Player Pipeline Changes

  • buffered *BufferedStreamer field added to Player struct
  • Inserted between beep.Resample and beep.Ctrl in updateStreamers()
  • loadFileLocked() closes old BufferedStreamer before loading new track
  • UnloadTrack() closes and nils BufferedStreamer to prevent goroutine leaks
  • Speaker buffer changed from time.Second/10 (100ms) to time.Second/5 (200ms)

Lock Safety

No changes to lock ordering or mutex-sensitive code paths. The BufferedStreamer is self-contained and does not interact with speaker.Lock() or p.mu.

Verification Results

  • go build ./backend/... — PASS
  • go vet ./backend/player/... — PASS
  • go test ./backend/player/ -v — all 12 tests pass (5 BufferedStreamer + 7 existing)

Deviations from Plan

None — plan executed exactly as written.

Self-Check: PASSED

All created files exist, all commits verified.