diff --git a/toolchain/tv/tv-sync.sh b/toolchain/tv/tv-sync.sh new file mode 100755 index 0000000000..35e6dea05c --- /dev/null +++ b/toolchain/tv/tv-sync.sh @@ -0,0 +1,102 @@ +#!/bin/sh +# tv-sync.sh — read, set, or measure a TV station's audio delay, fast. +# +# The A/V offset is one number (AV_OFFSET seconds, itsoffset on the pulse +# input; positive = delay audio, i.e. push early audio later). Tuning it +# by hand meant ssh + sed + restart every time; this is one line. +# +# tv-sync.sh show current offset (+ live encoder arg) +# tv-sync.sh set offset to N milliseconds, restart +# tv-sync.sh + nudge by N ms (e.g. +20, -15) +# tv-sync.sh measure rig-side A/B card measurement (no Chrome, +# no YouTube) — prints the true baseline +# +# station = wgtv | actv host fixed to jasellite (the rig box) +# +# Setting restarts the one station (a few-second stream blip); measure +# swaps in the test card for ~70s then restores. YouTube adds its own +# viewer-side skew on top of the rig number, so the ear stays the final +# judge — but this makes each guess cost one command, not five. +set -eu +HOST=jasellite +UNIT="${1:?usage: tv-sync.sh [ms|+ms|measure]}" +ENV="$UNIT/$UNIT.env" + +# ── read ────────────────────────────────────────────────────────────── +if [ $# -lt 2 ]; then + ssh "$HOST" "sh -c ' + printf \"env: \"; grep \"^AV_OFFSET=\" $ENV | tail -1 + printf \"live: \"; ps -eo args | grep -o -- \"itsoffset [0-9.]*\" | sort -u | tail -1'" + exit 0 +fi + +ARG="$2" + +# ── measure (rig-side A/B card) ─────────────────────────────────────── +if [ "$ARG" = "measure" ]; then + SIZE=$(ssh "$HOST" "grep '^SIZE=' $UNIT/$UNIT.env | tail -1 | cut -d= -f2") + DISP=$(ssh "$HOST" "grep '^DISPLAY_NUM=' $UNIT/$UNIT.env | tail -1 | cut -d= -f2") + SINK=$(ssh "$HOST" "grep '^SINK=' $UNIT/$UNIT.env | tail -1 | cut -d= -f2") + [ -n "$SIZE" ] || SIZE=1280x720 + [ -n "$DISP" ] || DISP=:99 + [ -n "$SINK" ] || SINK=tvsink + echo "▸ measuring $UNIT ($SIZE $DISP $SINK) via the sync card — ~70s, station restores after" + ssh "$HOST" "sh -c ' + systemctl --user stop $UNIT-watchdog.timer $UNIT; sleep 2 + grep -v \"^URL=\" $ENV > /tmp/tvsync.env + echo URL=file:///home/jas/wgtv/sync-test.html >> /tmp/tvsync.env + echo UNMUTE_CLICK=1 >> /tmp/tvsync.env + nohup sh $UNIT/rig.sh /tmp/tvsync.env > $UNIT/sync-rig.log 2>&1 & + sleep 25 + ffmpeg -v error -thread_queue_size 1024 -use_wallclock_as_timestamps 1 \ + -f x11grab -framerate 30 -video_size $SIZE -i $DISP \ + -thread_queue_size 1024 -use_wallclock_as_timestamps 1 \ + -f pulse -fragment_size 1024 -i ${SINK}.monitor \ + -af aresample=async=1 -fps_mode cfr -t 30 \ + -c:v libx264 -preset veryfast -pix_fmt yuv420p -c:a aac -y /tmp/tvsync.mp4 + pkill -f \"user-data-dir=/home/jas/.tv-rig-chrom[e] \" 2>/dev/null || true + pkill -f \"Xvfb ${DISP%%:*}:${DISP##*:}\" 2>/dev/null || true + pkill -f -- \"-i ${DISP%%x*} \" 2>/dev/null || true + sleep 2 + systemctl --user start $UNIT $UNIT-watchdog.timer + echo ===FLASH=== + ffmpeg -i /tmp/tvsync.mp4 -vf blackdetect=d=0.15:pix_th=0.10 -an -f null - 2>&1 | grep -o \"black_end:[0-9.]*\" | cut -d: -f2 | tr \"\n\" \" \" + echo; echo ===BEEP=== + ffmpeg -i /tmp/tvsync.mp4 -af silencedetect=n=-25dB:d=0.25 -vn -f null - 2>&1 | grep -o \"silence_end: [0-9.]*\" | awk \"{print \\\$2}\" | tr \"\n\" \" \" + echo'" 2>/dev/null | FL_BP_READER=1 python3 -c ' +import sys +fl=bp=None +for ln in sys.stdin: + ln=ln.strip() + if ln=="===FLASH===": mode="f"; continue + if ln=="===BEEP===": mode="b"; continue + vals=[float(x) for x in ln.split()] if ln and ln[0].isdigit() else None + if vals and mode=="f": fl=vals + if vals and mode=="b": bp=vals +if not fl or not bp: + print("✗ measurement failed (no card events captured)"); sys.exit(1) +o=sorted((b-min(fl,key=lambda f:abs(f-b)))*1000 for b in bp if abs(b-min(fl,key=lambda f:abs(f-b)))<0.5) +m=o[len(o)//2] +print(f"rig A/V offset: {m:+.0f} ms ({\"audio LATE\" if m>0 else \"audio EARLY\"}); spread {o[0]:+.0f}…{o[-1]:+.0f}") +print(f"suggested: tv-sync.sh {\"'\"$UNIT\"'\"} {int(round(-m))} (delay audio to cancel it)") +' + exit 0 +fi + +# ── set / nudge ─────────────────────────────────────────────────────── +case "$ARG" in + +*|-*) # nudge + CUR=$(ssh "$HOST" "grep '^AV_OFFSET=' $ENV | tail -1 | cut -d= -f2" || echo 0) + [ -n "$CUR" ] || CUR=0 + NEW=$(awk "BEGIN{printf \"%.3f\", $CUR + ($ARG)/1000}") + ;; + *) NEW=$(awk "BEGIN{printf \"%.3f\", $ARG/1000}") ;; +esac + +echo "▸ $UNIT AV_OFFSET → ${NEW}s (audio delayed ${NEW}s) — restarting" +ssh "$HOST" "sh -c ' + grep -q \"^AV_OFFSET=\" $ENV && sed -i \"s/^AV_OFFSET=.*/AV_OFFSET=$NEW/\" $ENV || echo AV_OFFSET=$NEW >> $ENV + systemctl --user restart $UNIT + sleep 24 + systemctl --user is-active $UNIT + ps -eo args | grep -o -- \"itsoffset $NEW\" | head -1'"