docs(quick-15): complete audio glitch fix plan
- Add 15-SUMMARY.md with implementation details - Update STATE.md with quick task 15 completion
This commit is contained in:
+4
-3
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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
|
## Session Continuity
|
||||||
|
|
||||||
### Last Session
|
### Last Session
|
||||||
|
|
||||||
**Date:** 2026-03-05
|
**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.
|
**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 14 complete. All 28 queue tests pass, go vet clean.
|
**Where we stopped:** Quick task 15 complete. All player tests pass, go build/vet clean.
|
||||||
**Next action:** Continue with next task
|
**Next action:** Continue with next task
|
||||||
|
|
||||||
---
|
---
|
||||||
*State initialized: 2026-02-27*
|
*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*
|
*Last updated: 2026-03-05*
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user