#!/bin/sh # Замер расхождения звука и картинки в собранном ролике. # # Как меряется. Делается исходник с метками: ровно на каждой секунде в # кадре белая вспышка, а в звуке щелчок. Из него собирается монтаж, у # которого начала кусков заведомо не попадают на опорные кадры. В готовом # ролике ищутся времена вспышек и времена щелчков; если звук и картинка # сходятся, каждая вспышка совпадает со щелчком. # # Сравнение идёт с поправкой на сам способ измерения: щелчок в сжатом # звуке размазывается на десяток-другой миллисекунд, и эта поправка # берётся замером того же исходника. # # Запуск: tests/syncprobe.sh — весь прогон # tests/syncprobe.sh measure ФАЙЛ — только замер готового файла set -e HERE=$(cd "$(dirname "$0")" && pwd) ROOT=$(dirname "$HERE") WORK="$ROOT/test/sync" SRC="$WORK/метки.mp4" PROJ="$WORK/синхро.nlve" OUT="$WORK/синхро.mkv" # Времена вспышек: кадры, у которых средняя яркость выше порога. Берётся # первый кадр каждой вспышки — она длится несколько кадров. flashes() { ffprobe -v error -f lavfi "movie=$1,signalstats" \ -show_entries frame=pts_time:frame_tags=lavfi.signalstats.YAVG \ -of csv=p=0 2>/dev/null | awk -F, '$2>150 { if ($1 - prev > 0.5 || NR == 1) print $1; prev = $1 }' } # Времена щелчков: по самим отсчётам, без оконного анализа. Оконный # уплывает (окно двигается кратно своему размеру), и на замере это # выглядит как накапливающийся рассинхрон, которого нет. clicks() { ffmpeg -v error -i "$1" -vn -ac 1 -ar 48000 -f s16le - | python3 "$HERE/clicktimes.py" } # Сводит два списка времён и печатает расхождение по каждой метке. # # Пара ищется по близости, а не по порядку: при резе не на опорном кадре # в куске может оказаться лишняя вспышка без своего щелчка, и сравнение # «первый с первым» после такого куска поехало бы всё. compare() { awk ' FNR == NR { v[++nv] = $1; next } { a = $1; best = 1e9; for (i = 1; i <= nv; i++) { d = a - v[i]; ad = (d < 0) ? -d : d; if (ad < best) { best = ad; bd = d } } { n++ } { # Щелчок, у которого вспышки рядом нет вообще, — это край куска: # метка попала в звук, а её кадр остался за резом. Такую пару # считать нельзя, иначе итог перевирает сам себя. if (best > 0.5) { skipped++; next } } printf " метка %2d: звук %8.3f ближайшая вспышка %+7.3f с\n", n, a, -bd; paired++; s += bd; if (best > worst) { worst = best; wi = n } } END { if (paired == 0) { print " пар не нашлось"; exit 1 } printf " ИТОГ: среднее расхождение %+.3f с, худшее %.3f с (метка %d)", s / paired, worst, wi if (skipped > 0) printf ", одиноких меток %d", skipped printf "\n" }' "$1" "$2" } measure() { FILE="$1" flashes "$FILE" > "$WORK/вспышки.txt" clicks "$FILE" > "$WORK/щелчки.txt" echo " вспышек: $(wc -l < "$WORK/вспышки.txt"), щелчков: $(wc -l < "$WORK/щелчки.txt")" compare "$WORK/вспышки.txt" "$WORK/щелчки.txt" } if [ "$1" = "measure" ]; then mkdir -p "$WORK" measure "$2" exit 0 fi mkdir -p "$WORK" if [ ! -f "$SRC" ]; then echo "=== Исходник с метками ===" # Опорные кадры через две секунды: тогда рез почти всегда приходится на # середину группы, как на настоящей съёмке с камеры. ffmpeg -y -v error \ -f lavfi -i "color=c=black:s=640x360:r=60000/1001:d=60" \ -f lavfi -i "aevalsrc=sin(1000*2*PI*t)*lt(mod(t\,1)\,0.05):s=48000:c=stereo:d=60" \ -vf "drawbox=x=0:y=0:w=640:h=360:color=white:t=fill:enable='lt(mod(t\,1)\,0.02)'" \ -c:v libx264 -preset veryfast -g 120 -keyint_min 120 -sc_threshold 0 \ -pix_fmt yuv420p -c:a aac -b:a 128k "$SRC" echo "готово: $SRC" fi echo "=== Поправка на способ измерения (сам исходник) ===" measure "$SRC" echo "=== Монтаж с резами не на опорных кадрах ===" "$HERE/nlvetool" syncproj "$PROJ" "$SRC" echo "=== Сборка ===" rm -f "$OUT" "$HERE/nlvetool" render "$PROJ" "$OUT" | tail -5 echo "=== Замер готового ролика ===" measure "$OUT" echo "=== Длительности потоков готового ролика ===" ffprobe -v error -select_streams v:0 -count_packets \ -show_entries stream=r_frame_rate,nb_read_packets -of csv=p=0 "$OUT" | awk -F, '{ split($1, r, "/"); printf " картинка: %s кадров = %.3f с\n", $2, $2 * r[2] / r[1] }' ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUT" | awk -F, '{ printf " файл целиком: %.3f с\n", $2 }'