From 6cb22359c8d1331975a7617445e12031ebde7a0f Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Thu, 5 Mar 2026 15:29:39 -0500 Subject: [PATCH] docs(quick-15): complete audio glitch fix plan - Add 15-SUMMARY.md with implementation details - Update STATE.md with quick task 15 completion --- .planning/STATE.md | 7 +- .../15-SUMMARY.md | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 .planning/quick/15-fix-audio-glitches-and-skips-in-decoding/15-SUMMARY.md diff --git a/.planning/STATE.md b/.planning/STATE.md index a7f1ba6..fd520dc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -63,17 +63,18 @@ Decisions from v1.0 are archived in PROJECT.md Key Decisions table. Key patterns | 012 | Add favorite icon to album dropdown track rows | 2026-03-05 | 12a0bbc | | 013 | Fix all golangci-lint issues (zero issues) | 2026-03-05 | e1a95e6 | | 014 | Fix queue/player desync after track load failure | 2026-03-05 | 2820de2 | +| 015 | Fix audio glitches with BufferedStreamer read-ahead | 2026-03-05 | 8a0b16a | ## Session Continuity ### Last Session **Date:** 2026-03-05 -**What happened:** Quick task 14 — fixed queue/player desync after track load failure. Added roll-back-on-failure semantics to all queue index advancement paths (Next, Previous, OnPlaybackFinished, PlayIndex, playFromStart). `playCurrentTrack` and `playOrLoadCurrentTrack` now return bool. -**Where we stopped:** Quick task 14 complete. All 28 queue tests pass, go vet clean. +**What happened:** Quick task 15 — fixed audio glitches and skips by adding a BufferedStreamer with goroutine read-ahead between decoder/resampler and speaker output. Ring buffer provides 2s of audio runway. Speaker buffer increased from 100ms to 200ms. 5 unit tests, all 12 player tests pass. +**Where we stopped:** Quick task 15 complete. All player tests pass, go build/vet clean. **Next action:** Continue with next task --- *State initialized: 2026-02-27* -Last activity: 2026-03-05 - Fix queue/player desync after track load failure +Last activity: 2026-03-05 - Fix audio glitches with BufferedStreamer read-ahead *Last updated: 2026-03-05* diff --git a/.planning/quick/15-fix-audio-glitches-and-skips-in-decoding/15-SUMMARY.md b/.planning/quick/15-fix-audio-glitches-and-skips-in-decoding/15-SUMMARY.md new file mode 100644 index 0000000..5029bd9 --- /dev/null +++ b/.planning/quick/15-fix-audio-glitches-and-skips-in-decoding/15-SUMMARY.md @@ -0,0 +1,76 @@ +--- +phase: 15-fix-audio-glitches +plan: 01 +subsystem: player +tags: [audio, buffering, performance, glitch-fix] +dependency_graph: + requires: [] + provides: [BufferedStreamer, read-ahead-buffering] + affects: [player-pipeline, speaker-init] +tech_stack: + added: [] + patterns: [ring-buffer, goroutine-read-ahead, channel-signaling] +key_files: + created: + - backend/player/buffered_streamer.go + - backend/player/buffered_streamer_test.go + modified: + - backend/player/player.go +decisions: + - "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" +metrics: + duration: "10m 38s" + completed: "2026-03-05" + tasks: 2 + files_changed: 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.