Fix clap detection silently analyzing the wrong audio track

Root cause of the reported 'timestamps off by different amounts,
sometimes early, sometimes late' bug: _extract_audio_wav() never
passed an explicit -map to ffmpeg, so on multi-audio-track files it
relied on ffmpeg's own default-stream heuristic -- which has no
knowledge of, and isn't guaranteed to agree with, whichever audio
track mpv is actually configured to play. A wrong-track clap is
essentially unrelated to the true one (not a small timestamp-math
error), which explains the inconsistent, file-dependent symptom --
and our single-audio-track synthetic fixtures never exercised this
path at all, since they only ever had one track to pick.

Fixed by threading an explicit ffmpeg stream index through
detect_claps() -> _extract_audio_wav(), sourced in main.py from mpv's
own track-list (the ff-index of whichever audio track has
selected=True). Detection now always analyzes the exact track the
user is listening to, on both 'Detect All' and 'Detect Next'.

Also: _probe_audio_start_time() used to silently fall back to 0.0 if
ffprobe couldn't parse start_time as a plain float (e.g. N/A) -- same
failure class (silently wrong instead of loud). Now emits a Python
warning instead, so a genuinely unparseable/unusual container is at
least visible rather than silently mis-corrected.

Verified two ways: (1) ruled out an initial alternate hypothesis
(video stream's own start_time mattering) empirically against the
existing single-track fixtures before ruling it out. (2) Built a new
two-audio-track fixture (sync_test_MULTITRACK.mp4: track 1 has a beep
at 3s, track 2 at 7s) and confirmed end-to-end through the real app --
switching mpv's active track from aid=1 to aid=2 correctly flips which
clap gets detected, which is exactly the mechanism this bug required.
This commit is contained in:
ThePendulum
2026-07-26 05:10:12 +02:00
parent acd63f570d
commit 76b5f827be
5 changed files with 154 additions and 11 deletions
@@ -0,0 +1,88 @@
"""Verifies clap detection analyzes whichever audio track mpv is actually
playing, not whichever one ffmpeg's own default-stream heuristic would
pick. Uses a synthetic fixture with two audio tracks with claps at very
different times (3s vs 7s) -- if detection ignored mpv's active track
selection, switching mpv to the second track would still incorrectly
report the first track's 3s clap instead of the second track's 7s one.
This directly tests the bug hypothesis from user report: 'sometimes too
early, sometimes too late, by different amounts' -- exactly what you'd
see if detection sometimes analyzed the wrong audio track on
multi-audio-track files, since a wrong-track clap position is essentially
unrelated to the true one (not a small timestamp-math error).
"""
import sys
import time
from pathlib import Path
from PyQt6.QtWidgets import QApplication
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import main as app_main
ROOT = Path(__file__).resolve().parent.parent
CLIP = str(ROOT / "test_media" / "sync_test_MULTITRACK.mp4")
def wait_for(predicate, qapp, timeout=20):
deadline = time.time() + timeout
while not predicate() and time.time() < deadline:
qapp.processEvents()
time.sleep(0.05)
return predicate()
def main():
qapp = QApplication(sys.argv)
win = app_main.MainWindow()
win.show()
qapp.processEvents()
win._load(CLIP)
qapp.processEvents()
time.sleep(0.3)
qapp.processEvents()
mpv = win.source_panel.player.mpv
print(f"default aid: {mpv.aid}")
default_index = win._current_audio_stream_index()
print(f"resolved ff-index for default track: {default_index}")
assert default_index == 1, f"expected default track's ff-index to be 1, got {default_index}"
win.detect_all_button.click()
qapp.processEvents()
ok = wait_for(lambda: win.detect_all_button.isEnabled(), qapp)
assert ok, "detection did not finish in time"
markers = win.source_panel.marker_strip.markers()
print(f"markers with default track selected: {markers}")
assert any(abs(m - 3.0) < 0.5 for m in markers), f"expected a marker near 3.0s (default track's clap): {markers}"
assert not any(abs(m - 7.0) < 0.5 for m in markers), f"should not see the OTHER track's 7.0s clap yet: {markers}"
# Now switch mpv to the second audio track (aid=2), simulating a file
# where the user is listening to a non-default track, and rerun
# detection -- it must follow the switch, not keep reporting the first
# track's clap.
mpv.aid = 2
qapp.processEvents()
time.sleep(0.2)
switched_index = win._current_audio_stream_index()
print(f"resolved ff-index after switching to aid=2: {switched_index}")
assert switched_index == 2, f"expected ff-index 2 after switching tracks, got {switched_index}"
win.source_panel.marker_strip.clear()
win.detect_all_button.click()
qapp.processEvents()
ok = wait_for(lambda: win.detect_all_button.isEnabled(), qapp)
assert ok, "detection did not finish in time"
markers = win.source_panel.marker_strip.markers()
print(f"markers with second track selected: {markers}")
assert any(abs(m - 7.0) < 0.5 for m in markers), f"expected a marker near 7.0s (second track's clap) after switching: {markers}"
assert not any(abs(m - 3.0) < 0.5 for m in markers), f"should not see the FIRST track's 3.0s clap after switching: {markers}"
win.close()
qapp.processEvents()
print("\nPASS: clap detection follows mpv's actual selected audio track, not ffmpeg's default")
if __name__ == "__main__":
main()