Рез всегда точный. Раньше начало куска притягивалось к ближайшему опорному кадру, а звук брался из непритянутой точки: по журналам сборки средняя притяжка 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 с.
123 lines
5.8 KiB
Bash
Executable file
123 lines
5.8 KiB
Bash
Executable file
#!/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 }'
|