Implement audio master clock for A/V synchronization

Priority 3 fix from PLAYER_PERFORMANCE_ISSUES.md - addresses the root
cause of A/V desync in player module.

Changes:
- Audio loop now tracks bytes written and updates master clock (audioTime)
- Audio clock calculation: bytesWritten / (sampleRate × channels × bytesPerSample)
- Video loop already syncs to audio master clock (from previous commit)
- Master clock updates happen after each audio chunk write

How it works:
- Audio is the timing master, plays at natural rate
- Video reads audio clock and adapts timing to stay in sync
- If video >3 frames behind: drop frame and resync
- If video >3 frames ahead: wait longer
- Otherwise: adjust sleep duration for gradual sync

Expected improvements:
- Rock-solid A/V sync maintained over extended playback
- No more drift between audio and video
- Adaptive recovery from temporary slowdowns

Combined with Priority 1 (larger audio buffers) and Priority 2 (FFmpeg
volume control), this completes the core A/V sync architecture.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-24 01:36:50 -05:00
parent 4a09626e28
commit a7b3452312

View File

@ -9414,6 +9414,7 @@ func (p *playSession) runAudio(offset float64) {
// Larger chunks reduce read frequency and improve performance
chunk := make([]byte, 16384)
loggedFirst := false
bytesWritten := int64(0) // Track total audio bytes for master clock
for {
select {
case <-p.stop:
@ -9434,6 +9435,13 @@ func (p *playSession) runAudio(offset float64) {
// Volume is now handled by FFmpeg, just write directly
// This eliminates per-sample processing overhead
localPlayer.Write(chunk[:n])
// Update audio master clock for A/V sync
bytesWritten += int64(n)
// Calculate elapsed audio time: bytes / (sampleRate * channels * bytesPerSample)
elapsedTime := float64(bytesWritten) / float64(sampleRate*channels*bytesPerSample)
currentAudioTime := offset + elapsedTime
p.audioTime.Store(currentAudioTime)
}
if err != nil {
if !errors.Is(err, io.EOF) {