nlve/tests/syncprobe.sh

123 lines
5.8 KiB
Bash
Raw Permalink Normal View History

Звук больше не отстаёт от картинки Рез всегда точный. Раньше начало куска притягивалось к ближайшему опорному кадру, а звук брался из непритянутой точки: по журналам сборки средняя притяжка 0.24 с, худшая 0.99 с, и на длинном куске этот сдвиг держится все его минуты. Плата за точность — доли секунды перекодирования на рез. Второй вид реза (Shift+S) стал тем же самым и убран. Разбор заголовка MP4 не учитывал список правок (elst): метки опорных кадров уезжали на задержку показа, на пробе два кадра. Сверка быстрого пути с ffprobe теперь совпадает точно, в том числе на записи с B-кадрами. Куску заказывается точное число кадров, а не только время: при копировании ffmpeg отмеряет конец по времени декодирования, и у записи с B-кадрами кусок выходил длиннее. Склейке длительность каждого куска задаётся явно. Последний пакет скопированного куска показывается позже прочих, контейнер считал его в длительность, и время картинки растягивалось — 0.05 с на кусок. Звука в кусках картинки больше нет: он не используется, а в MPEG-TS ещё и сбивает выравнивание склейки на 36 мс. Границы кусков ложатся на кадровую сетку: картинка выходит целым числом кадров, звук набирается до отсчёта, и доли кадра разводили их в стороны. Разогрев кодировщика звука компенсируется: сжатый AAC начинается со служебных отсчётов, MP4 помечает их пропускаемыми, Matroska — нет, и звук ехал на 21.3 мс позже. Величина меряется пробным сжатием. Продолжение прерванной сборки чинится заодно: готовность проверяется у каждой части, а не у куска целиком. При точном резе частей две, и вторая при продолжении терялась. В журнал сборки добавлена проверка синхронности: сколько кадров вышло и сколько просил монтаж, с вердиктом и худшей частью. Замер (tests/syncprobe.sh, исходник со вспышкой и щелчком на каждой секунде): было до 0.6 с и росло по ходу ролика, стало 0.02-0.04 с без накопления при точности измерения 0.017 с.
2026-09-16 18:28:29 +03:00
#!/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 }'