nlve/tests/syncprobe.sh
4aiman 019d016db9 Звук больше не отстаёт от картинки
Рез всегда точный. Раньше начало куска притягивалось к ближайшему
опорному кадру, а звук брался из непритянутой точки: по журналам сборки
средняя притяжка 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

123 lines
5.8 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 }'