Звук больше не отстаёт от картинки

Рез всегда точный. Раньше начало куска притягивалось к ближайшему
опорному кадру, а звук брался из непритянутой точки: по журналам сборки
средняя притяжка 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 с.
This commit is contained in:
4aiman 2026-09-16 18:28:29 +03:00
commit 019d016db9
9 changed files with 849 additions and 158 deletions

View file

@ -78,7 +78,6 @@ xdg-mime query default application/x-nlve-project
| В начало / конец монтажа | Home / End |
| Показать, где курсор (окно едет к нему) | \ |
| Разрезать по курсору (все дорожки) | S |
| Разрезать точно (перекодирует стык) | Shift+S |
| Удалить выделенное | Delete |
| Выделить всё | Ctrl+A |
| Выделить областью | протяжка по пустому месту дорожек |
@ -542,13 +541,26 @@ Linux потом искала по нему `.dll`.
кусков нельзя, он на этом зависает. Заготовки делаются готовых длин
(15 с, 60 с, 5 мин и так далее), а нужный кусок из них вырезается —
тогда любое движение клипа не требует новой генерации.
3. Начало каждого куска притягивается к опорному кадру, потому что копировать
можно только с него. Конец притягивать не нужно — при копировании он и так
получается кадроточным.
4. При точном резе огрызок от точки реза до следующего опорного кадра
перекодируется, остальное копируется.
5. Куски склеиваются concat-демультиплексором, результат сверяется: сумма
пакетов кусков обязана совпасть с числом пакетов в готовом файле.
3. Границы кусков кладутся на кадровую сетку проекта: между кадрами
времени нет, а звук набирается с точностью до отсчёта, и доли кадра
разводят их в стороны.
4. Рез всегда точный: огрызок от точки реза до ближайшего опорного кадра
перекодируется, остальное копируется. Копировать можно только с
опорного кадра, поэтому «резать где угодно и ничего не перекодировать»
не бывает — но платить приходится долями секунды на рез.
5. Каждому куску заказывается точное число кадров (`-frames:v`), а не
только время: при копировании ffmpeg отмеряет конец по времени
декодирования, и у записи с B-кадрами кусок выходит длиннее
запрошенного.
6. Куски склеиваются concat-демультиплексором с явно указанной
длительностью каждого. Результат сверяется дважды: сумма пакетов кусков
обязана совпасть с числом пакетов в готовом файле, а число кадров — с
тем, что просил монтаж. Второе и есть проверка синхронности звука с
картинкой; вердикт пишется в журнал сборки.
Звук в кусках картинки не хранится: звуковая дорожка собирается отдельно и
точнее, а лишний поток в куске MPEG-TS ещё и сбивает тайминг склейки —
замерено 36 мс на стык.
Напрашивающийся способ — один проход с директивами `inpoint` и `outpoint`,
без временных файлов — проверен и отвергнут: он давал 574 кадра вместо 600
@ -579,7 +591,8 @@ Linux потом искала по нему `.dll`.
числе кадров. Лечится склейкой через промежуточный MPEG-TS, где параметры
повторяются перед каждым опорным кадром — стало 0 ошибок. Способ выбирается
автоматически: один источник и никаких точных резов — прямая склейка,
иначе MPEG-TS.
иначе MPEG-TS. Резы теперь всегда точные, так что на деле это почти всегда
MPEG-TS.
**Лишний кадр на стыке точного реза.** При копировании `-to` работает как
«строго меньше», при перекодировании — как «меньше либо равно». Кадр на
@ -591,6 +604,33 @@ Matroska давали метки декодирования, идущие наз
меток на шестьсот кадров. Коротким вставкам B-кадры не нужны, поэтому они
запрещены (`-bf 0`).
**Звук отставал от картинки — четыре причины сразу.** Разбор занял день, и
каждая находка мерялась, а не угадывалась (`tests/syncprobe.sh`: исходник
со вспышкой и щелчком на каждой секунде, монтаж с резами не на опорных
кадрах, поиск обоих следов в готовом ролике).
1. *Притянутый рез.* Начало куска картинки сдвигалось на ближайший опорный
кадр, а звук брался из непритянутой точки: в журналах пользователя
средняя притяжка 0.24 с, худшая 0.99 с. Лечится точным резом всегда.
2. *Список правок MP4.* Разбор заголовка не учитывал блок `elst`, и метки
опорных кадров уезжали на задержку показа — на пробе два кадра. Рез
просил перекодировать до метки, которая опорным кадром не была.
3. *Конец куска при копировании.* `-to` отмеряет по времени декодирования,
и у записи с B-кадрами кусок выходил длиннее запрошенного. Лечится
явным числом кадров (`-frames:v`).
4. *Длительность куска в склейке.* Последний пакет скопированного куска
показывается позже всех прочих, контейнер считает его в длительность, и
склейка растягивала время — 0.05 с на кусок. Лечится директивой
`duration` в списке склейки. Туда же: звук внутри кусков картинки сбивал
выравнивание на 36 мс, теперь его там нет.
Плюс «разогрев» кодировщика звука: сжатый AAC начинается со служебных
отсчётов, MP4 помечает их пропускаемыми, Matroska такой пометки не хранит —
звук ехал на 21.3 мс позже. Величина меряется пробным сжатием и вычитается.
Итог замера на образце: было до 0.6 с и росло по ходу ролика, стало 0.02-0.04 с
без накопления, при точности самого измерения 0.017 с.
**Вставка под каждую длину дыры.** Сначала под каждую дыру делался чёрный
клип ровно её длины. Любое движение клипа меняло длину дыры, требовало
новой генерации, и до её готовности предпросмотр играл прежнюю расстановку.
@ -703,11 +743,30 @@ Wine с новой библиотекой не грузилось ничего.
./tests/nlvetool build <проект> <видео> [<картинка>]
./tests/nlvetool buildcut <проект> <видео>
./tests/nlvetool render <проект> <выход>
./tests/nlvetool syncproj <проект> <видео> монтаж для замера синхронности
```
Команды возвращают ненулевой код при расхождении, поэтому годятся для
автоматической проверки.
Отдельно — замер синхронности звука и картинки:
```
./tests/syncprobe.sh весь прогон: образец, сборка, замер
./tests/syncprobe.sh measure ФАЙЛ замер готового файла
```
Как он устроен. Делается исходник, где ровно на каждой секунде в кадре
белая вспышка, а в звуке щелчок. Из него собирается монтаж, у которого
начала кусков заведомо не попадают на опорные кадры. В готовом ролике
ищутся времена вспышек и щелчков: если они разошлись — звук уехал от
картинки, и видно, на сколько и с какого куска.
Времена щелчков считаются по самим отсчётам (`tests/clicktimes.py`), а не
оконным анализом: окно двигается кратно своему размеру, и его собственный
уход выглядит как накапливающийся рассинхрон, которого нет. На это уже
попались один раз.
## Структура
| Файл | Что это |