nlve/README.md
4aiman 4c422ef57d Курсор прилипает к границам клипов
Курсор воспроизведения по щелчку вставал ровно туда, куда попала мышь.
В стык двух кусков так не попасть: на общем плане одна точка экрана —
это секунды материала, а ставят курсор чаще всего именно на стык.

Теперь он притягивается к границам клипов на любой дорожке и к началу
монтажа. Работает и при щелчке по клипу, и по линейке, и по пустому
месту, и при протяжке. Shift меняет решение на противоположное — как у
прилипания клипов.

Переключатель свой, со значком клея, рядом с магнитом: клипы липнут при
перетаскивании, курсор — при щелчке, это разные привычки, и одно бывает
нужно без другого. Состояние переживает перезапуск.

Расчёт открыт наружу и проверяется числами: семь проверок в lfmcheck,
от «стык рядом — притянуло» до «выключено — не притягивает».
2026-08-31 00:32:44 +03:00

585 lines
45 KiB
Markdown
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.

# nlve
Нелинейный видеоредактор без перекодирования. Куски видео вырезаются и
склеиваются копированием потока, поэтому сборка ролика занимает время
копирования файла, а не время рендера.
Композитинга нет принципиально: когда клипы перекрываются во времени,
побеждает тот, что на верхней дорожке — целиком, и картинкой, и звуком.
Именно это ограничение и делает возможной сборку копированием.
## Сборка
```
./build.sh редактор под эту систему
./build.sh windows редактор под Windows (кросс-сборка)
./build.sh tool консольный инструмент проверок
./build.sh test инструмент плюс прогон проверок
./build.sh all всё перечисленное
./build.sh run собрать и запустить
./build.sh clean убрать промежуточные файлы
```
Используется тулчейн fpcupdeluxe, а не системные fpc и Lazarus: системный
Lazarus другой версии, и собранные им модули LCL с нашими несовместимы.
Другой корень задаётся переменной: `FPCUP=/путь ./build.sh`.
То же самое вручную:
```
/home/chaiman/fpcupdeluxe/lazarus/lazbuild --primary-config-path=/home/chaiman/fpcupdeluxe/config_lazarus nlve.lpi
/home/chaiman/fpcupdeluxe/lazarus/lazbuild --primary-config-path=/home/chaiman/fpcupdeluxe/config_lazarus --cpu=x86_64 --os=win64 --ws=win32 nlve.lpi
```
Требования во время работы: `libmpv.so.2` (пакет `mpv`), `ffmpeg` и `ffprobe`
в `PATH`. Линковки с libav* нет — ffmpeg вызывается внешним процессом.
## Запуск
```
./nlve # пустой проект
./nlve <проект.nlve> # открыть проект
./nlve <видео> <картинка> ... # добавить файлы в новый проект
```
Окно перепаковки открывается из меню «Файл». Здесь раньше значился запуск
`./nlve remux <файл>...` — в коде этого нет и не было: слово `remux` не
является именем файла и молча пропускалось.
### Встраивание в рабочий стол
```
tools/install-desktop.sh # берёт ./nlve рядом с репозиторием
tools/install-desktop.sh /путь/к/nlve
```
Скрипт кладёт пункт меню, значки и описание типа файла — всё в домашний
каталог, без прав администратора. После этого `*.nlve` перестают быть
просто текстом: у них своё имя типа (`application/x-nlve-project`), свой
значок и открытие двойным щелчком. Проверяется так:
```
xdg-mime query filetype проект.nlve # application/x-nlve-project
xdg-mime query default application/x-nlve-project
```
Внутри проект — обычный JSON, и тип объявлен наследником
`application/json`. Наследником простого текста делать не стали: тогда
файл снова открывался бы редактором по умолчанию.
Файловый менеджер может показать новый значок не сразу — ему нужно
перечитать базу.
## Управление
| Действие | Клавиши |
|---|---|
| Пуск и пауза | Пробел |
| В начало / конец монтажа | Home / End |
| Разрезать по курсору | S |
| Разрезать точно (перекодирует стык) | Shift+S |
| Удалить выделенное | Delete |
| Выделить всё | Ctrl+A |
| Схлопнуть дыру под курсором | Ctrl+Shift+R |
| Отменить, повторить | Ctrl+Z, Ctrl+Y |
| Масштаб таймлайна | Ctrl + колесо мыши |
| Прокрутка по дорожкам | колесо мыши |
| Прокрутка по времени | Shift + колесо, средняя кнопка |
| Прокрутка журнала вбок | Shift + колесо над журналом |
| Растянуть форму звука по высоте | Shift + стрелки вверх и вниз |
| Выключить дорожку / оставить только её | кнопки М и С в заголовке |
| Громкость клипа | тянуть ручку на клипе вверх-вниз |
| Автосхлопывание вкл/выкл | Ctrl+K |
| Связать или развязать клипы | Ctrl+G |
| Отменить прилипание на время | Shift при перетаскивании |
| Вписать проект целиком | Ctrl+0 |
| Новый, открыть, сохранить проект | Ctrl+N, Ctrl+O, Ctrl+S |
| Добавить файлы | Ctrl+I |
| Собрать ролик | Ctrl+E |
Файлы попадают в проект через меню «Файл» или перетаскиванием прямо в окно
программы. Место броска решает, что произойдёт: бросили над монтажом — файл
и добавится в проект, и ляжет на дорожку под указателем; в любом другом
месте — только добавится в проект.
Файл из библиотеки кладётся на монтаж перетаскиванием либо через
контекстное меню списка. В нём: **добавить на дорожку** (подменю со
списком дорожек), **кадрирование и поворот**, **убрать со всех дорожек**
(файл остаётся в проекте), **удалить из проекта**. Дыры при этом намеренно не
схлопываются: убрать клип и переставить всё остальное — два разных
намерения.
Если бросить клип ниже всех дорожек, заведётся новая.
В заголовке каждой дорожки слева — её название, число клипов и две кнопки:
**М** выключает дорожку целиком (она не идёт ни в предпросмотр, ни в сборку),
**С** оставляет только её, заглушив остальные. Щелчок по заголовку делает
дорожку выбранной — на неё кладёт файл кнопка.
Клип переносится перетаскиванием, в том числе на другую дорожку и на
пустое место под последней — тогда дорожка тоже добавится. За края клипа
он растягивается и укорачивается. Картинку можно растянуть на любую
длину, видео — только в пределах исходника.
Щелчок по клипу переставляет курсор воспроизведения. Именно щелчок:
если кнопку нажали и потянули, курсор остаётся на месте, в том числе во
время воспроизведения.
**Прилипание клипов** (кнопка с магнитом). Края клипов притягиваются
друг к другу и к курсору. Shift во время перетаскивания меняет решение на
противоположное: при включённом прилипании временно отключает, при
выключенном — временно включает. Так одна клавиша полезна в обоих
режимах, и не нужно помнить, какой сейчас.
**Прилипание курсора** (кнопка с клеем). Курсор, поставленный щелчком,
притягивается к границам клипов на любой дорожке. Без этого в стык не
попасть: на общем плане одна точка экрана — это секунды материала, а
ставят курсор чаще всего именно на стык. Shift при щелчке работает так
же — меняет решение на противоположное.
Переключателя два, а не один, потому что это разные привычки: клипы
липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без
другого.
**Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена —
автосхлопывание включено, и тогда при перетаскивании клипа всё, что
начинается не раньше него, едет следом. Стрелка рядом открывает выбор
режима: только текущая дорожка или все. Ctrl+K включает и выключает.
Режим действует и при выключенном автосхлопывании — по нему работает
Ctrl+Shift+R, который схлопывает дыру под курсором. То есть выключенная
кнопка означает лишь «не двигать ничего само собой при перетаскивании».
Во время воспроизведения курсор держится посередине окна, а монтаж едет
под ним. При нажатии на пуск, если курсор оказался за краем видимой
части, вид подтягивается к нему — масштаб при этом не меняется.
На каждом клипе проходит линия громкости с ручкой посередине видимой его
части: тянешь ручку вверх и вниз — меняется громкость, от тишины до
двойной. Ручка стоит посередине именно видимой части, а не всего клипа:
до середины часового клипа, уехавшей за край экрана, мышью не дотянуться.
Громкость, отличная от обычной, — единственное место, где звук
перекодируется: изменить громкость, не тронув сам звук, нельзя. Видео при
этом по-прежнему копируется. Проверено: клип с громкостью 30% дал в ролике
уровень на 10.5 дБ ниже исходного при ожидаемых 10.46 дБ.
Полоски по краям клипа показывают тип реза: серая — притянут к опорному
кадру, голубая — точный, стык будет перекодирован при сборке. Если край
подвинуть руками и он не попадёт на опорный кадр, рез сам становится
точным: раз человек выбрал конкретный кадр, притягивать его к соседнему
неправильно.
В списке файлов слева показывается кадр из каждого файла, его параметры
и длительность. Кадры делаются в фоне и запоминаются в кэше проекта.
На клипах видна форма звука. Считается она по заранее разобранным пикам —
сто значений в секунду, каждое есть наибольший модуль отсчёта за свои
десять миллисекунд. Рисовать по настоящим отсчётам нельзя: в секунде их
48 тысяч, а в клипе на экране сотни пикселей.
Разбор идёт в фоне и запоминается в кэше проекта. Для записи на 892 с это
около 13 секунд — столько занимает чтение шести гигабайт с диска, и
быстрее не выйдет; при следующем открытии проекта пики берутся готовыми.
**Shift со стрелками вверх и вниз растягивает форму по высоте.** Звук
бывает тихим, и угадывать за человека, во сколько раз растянуть картинку,
неправильно. Усиление меняет только рисунок, самого звука не касается, и
запоминается между запусками.
## Файл не совпадает с проектом
Проект имеет одно разрешение и один набор кодеков на всё — иначе куски не
склеить копированием. Файл с другими параметрами можно добавить, но его
куски скопировать не выйдет.
Пересобираются **только они**. Остальной материал по-прежнему копируется
без потерь и почти мгновенно. Ролик целиком не перекодируется никогда.
Как именно кадр ложится в кадр проекта, решает окно «Кадрирование и
поворот»: вписать целиком с полями, заполнить кадр с обрезкой или
растянуть, плюс поворот на 90, отражение, увеличение и смещение.
Слева в окне — кадр источника с рамкой: её тянут мышью за середину и за
правый нижний угол, и она держит пропорции кадра проекта. Справа —
настоящий результат, пропущенный через ту же цепочку фильтров, что пойдёт
в дело.
**Настройка принадлежит клипу**, а открывается кнопкой в правом нижнем
видимом углу самого клипа. Именно видимом: у клипа длиной в час настоящий
угол почти всегда за краем экрана. Два куска одного файла можно
кадрировать по-разному.
У файла в библиотеке хранится такая же настройка, но она лишь заготовка:
её берёт новый клип при добавлении. Правится она из контекстного меню
библиотеки.
По умолчанию кадр вписывается целиком. Растяжение надо выбрать руками:
раньше оно применялось молча, и вертикальное видео размазывалось по ширине.
«Приведение к параметрам проекта» делает ту же работу один раз при
добавлении: файл перекодируется целиком, зато каждая последующая сборка
остаётся быстрой.
Что и почему происходит с каждым файлом, видно в окне «Проект → Сведения
о проекте»: параметры проекта, параметры каждого файла, выбранное
вписывание и вердикт — копируется, пересобирается или из него делается
клип. Там же кнопка полного ответа ffprobe.
### Что показывает предпросмотр
Предпросмотр показывает **кадр проекта**, а не кадр исходника. Файл другого
соотношения сторон виден с полями по краям — ровно так, как он попадёт в
готовый ролик. Проверено сравнением: кадр предпросмотра и кадр сборки
совпадают бит в бит.
Само по себе это не бесплатно, поэтому включается только когда есть что
вписывать. Разное разрешение при одинаковом соотношении сторон (скажем,
3840x2160 в проекте 2688x1512) ничего не требует: mpv сам впишет такой
файл в панель, и картинка совпадёт до пикселя.
Когда вписывание нужно, декодирование переключается на вариант с
копированием кадров в обычную память — цепочка фильтров не умеет работать
с кадрами, лежащими в памяти видеокарты. Аппаратное декодирование при этом
сохраняется, видно в журнале: `nvdec` без вписывания, `nvdec-copy` с ним.
Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра,
230 с настоящим масштабированием — запас четырёхкратный.
Чего предпросмотр не умеет: показать собственную настройку кадрирования
файла. Цепочка фильтров у mpv одна на всю склейку, а заполнение с обрезкой,
поворот и смещение задаются для каждого файла отдельно — выбрать между ними
общий фильтр не может, поэтому он всегда вписывает целиком. Точную картинку
для такого файла показывает окно «Кадрирование и поворот».
## Цвета окна
Все цвета заданы в `uTheme` и применяются принудительно, а не берутся у
системы. Причина простая: под Linux вид назначает тема GTK, под Windows —
тема оформления, и на светлой теме тёмный таймлайн оказывается вставленным
в белую рамку.
Что для этого пришлось сделать:
- **Свои кнопки** (`TThemedButton`). Системная кнопка под Windows свой цвет
не отдаёт: LCL рисует её через службу оформления, и та кладёт светлую
подложку поверх заданного цвета. Проверено по исходникам LCL —
`TCustomSpeedButton.PaintBackground` всегда вызывает
`ThemeServices.DrawElement`. Поведение обычной кнопки сохранено:
`Default`, `Cancel`, `ModalResult`, работа по Enter и Esc.
- **Своя полоса меню** из плоских кнопок со всплывающими списками.
Системную полосу рисует Windows, и её фон нашим цветом не закрашивается:
`SetMenuInfo` меняет подложку пунктов, но не самой полосы. Сочетания
клавиш при этом не теряются — их ищет `TMenu.IsShortCut`, вызываемый из
`TMainForm.FormShortCut`. Ценой стал переход по меню клавишей Alt: его
больше нет.
- **Своё окно сообщения** (`ThemedMessageDlg`) вместо `MessageDlg`.
- **Отрисовка пунктов меню и строк выпадающих списков** своими руками.
Чего сделать нельзя, и это надо знать:
- полосы прокрутки рисует система, и цвет им задать нечем. Единственный
способ получить их в своих цветах — не пользоваться системными вовсе.
Так и сделано: журнал (`uLogView`) и таблицы (`uThemedScroll`) обходятся
своими. Осталась системная полоса у списка файлов слева — там её нечем
выключить, не переписав сам список;
- рамка окна и её кнопки принадлежат оконному менеджеру;
- под gtk2 пункты меню своими руками не рисуются — в LCL это прямо
помечено как неподдерживаемое (`interfaces/gtk2/issues.xml`), там их
цвет назначает тема GTK. Под Windows рисуются, там это и нужно.
## Зависимости и запуск под Wine
Программе нужны `ffmpeg`, `ffprobe` и `libmpv`. Под Linux они ставятся
пакетным менеджером. Под Windows ставить нечем, поэтому файлы кладутся
рядом с программой, а меню «Файл → Зависимости...» умеет их скачать.
Список и адреса лежат в `deps-windows.json` или `deps-linux.json` рядом с
программой — имя своё для каждой системы. Общий файл приводил к тому, что
запущенная под Wine программа записывала windows-список, а сборка под
Linux потом искала по нему `.dll`.
Где имя файла сборки меняется с каждой датой, адрес не задаётся: программа
спрашивает последний выпуск у GitHub. Архивы `.7z` распаковываются внешним
`7zr.exe` — он тоже есть в списке и качается одним файлом, поэтому
замкнутого круга не возникает.
Под Wine программа узнаёт себя по наличию `wine_get_version` в `ntdll` и
настраивается сразу:
- **вывод через OpenGL**, а не Direct3D 11: последний в Wine падает при
создании контекста, ещё до первого кадра;
- **аппаратное декодирование выключено**. Дело не в бесполезных попытках:
для DXVA2 mpv создаёт устройство Direct3D 9, а Wine при его создании
сбрасывает гамму экрана в единицу — настройку всей системы. Замерено:
с `auto-safe` гамма 0.89 превращалась в 1.0 при каждом открытии
проекта, с `no` остаётся нетронутой.
На настоящей Windows обе настройки остаются обычными.
Журнал дублируется в `nlve.log` рядом с программой. Нужно ровно для
случая, когда программа падает до появления окна: иначе всё, что она о
себе рассказала, пропадает вместе с окном.
## Наложение, переходы и прозрачность
Ползунок на клипе картинки задаёт **непрозрачность**, на звуковом —
громкость. Клип с непрозрачностью меньше единицы смешивается с тем, что
под ним.
**Звуковые дорожки смешиваются.** Правило «верхний побеждает» придумано
для картинки — оно и делает возможной сборку копированием. Для звука оно
бессмысленно: без смешивания нельзя положить фоновую музыку под голос. И
стоит оно ничего, потому что звук всё равно перекодируется.
Плеер играет одну звуковую дорожку за раз, поэтому в предпросмотре смесь
собирается его же цепочкой фильтров: каждая дорожка монтажа подаётся
своим списком кусков, дальше `amix`. Громкость клипов подставляется в ту
же цепочку — иначе предпросмотр врал бы про баланс. Проверено: в
предпросмотре и в готовом ролике уровни совпадают до десятой доли
децибела.
Два клипа **на одной дорожке**, наползшие друг на друга, дают переход:
один плавно перетекает в другой. На разных дорожках перекрытие означает
не переход, а наложение — там по-прежнему побеждает верхний, если он
непрозрачен.
Различие не косметическое. Если бы кроссфейдом считалось любое
перекрытие, то в проекте, где клипы на разных дорожках перекрываются
постоянно, каждое такое место пришлось бы пересобирать.
У клипа есть плавное появление и уход. Для картинки это затемнение, для
звука — нарастание громкости.
**Всё это стоит перекодирования** — но только своего участка. Перед
сборкой программа сообщает, сколько секунд из скольких придётся
пересобрать, и даёт отказаться.
Звук при наложении сводится прямо в отсчётах, без фильтров ffmpeg: доля
каждого слоя считается на каждый отсчёт, и длина результата получается
ровно такой, какая нужна.
## Как собирается ролик
1. Монтаж уплощается в последовательность непересекающихся кусков.
Перекрытия разрешаются в пользу верхней дорожки, дыры превращаются в
чёрные вставки.
2. Картинки и дыры материализуются в клипы в параметрах проекта. Иначе их
нечем показать: подсунуть mpv картинку или генератор прямо в списке
кусков нельзя, он на этом зависает. Заготовки делаются готовых длин
(15 с, 60 с, 5 мин и так далее), а нужный кусок из них вырезается —
тогда любое движение клипа не требует новой генерации.
3. Начало каждого куска притягивается к опорному кадру, потому что копировать
можно только с него. Конец притягивать не нужно — при копировании он и так
получается кадроточным.
4. При точном резе огрызок от точки реза до следующего опорного кадра
перекодируется, остальное копируется.
5. Куски склеиваются concat-демультиплексором, результат сверяется: сумма
пакетов кусков обязана совпасть с числом пакетов в готовом файле.
Напрашивающийся способ — один проход с директивами `inpoint` и `outpoint`,
без временных файлов — проверен и отвергнут: он давал 574 кадра вместо 600
и не пересчитывал метки времени.
## Ловушки, на которые уже наступили
Все перечисленные ниже вещи найдены замерами, а не рассуждениями.
**Локаль.** libmpv отказывается создаваться, если числовая часть локали
процесса не `C`: `mpv_create` возвращает nil. В русской локали десятичный
разделитель запятая, и разбор чисел внутри mpv ломается. Лечится вызовом
`ForceCNumericLocale` до `mpv_create` (`uMpvLib`).
**Панель без окна.** Под gtk2 панель LCL не имеет собственного окна X11, и
запрос окна возвращает окно формы. Отдав такой идентификатор mpv, получаем
видео поверх всего интерфейса. Поэтому под X11 создаётся собственное
дочернее окно по границам панели (`EnsureVideoWindow` в `uMpvPlayer`).
**Относительные пути в списке склейки.** Путь с префиксом `file:` внутри
списка concat разбирается относительно текущего рабочего каталога, а не
относительно самого списка. Все пути приводятся к абсолютным сразу.
**Смешивание кусков от разных кодировщиков.** MP4 и Matroska хранят набор
параметров кодека один на дорожку. Как только рядом оказываются
скопированный кусок из камеры и созданная нами вставка, декодер применяет
параметры первого ко всем: замерено 384 ошибки декодера HEVC при верном
числе кадров. Лечится склейкой через промежуточный MPEG-TS, где параметры
повторяются перед каждым опорным кадром — стало 0 ошибок. Способ выбирается
автоматически: один источник и никаких точных резов — прямая склейка,
иначе MPEG-TS.
**Лишний кадр на стыке точного реза.** При копировании `-to` работает как
«строго меньше», при перекодировании — как «меньше либо равно». Кадр на
границе попадал в ролик дважды: 601 вместо 600. Лечится отступом на
полкадра от границы.
**B-кадры во вставках.** Двунаправленные кадры от NVENC при переносе в
Matroska давали метки декодирования, идущие назад, и двадцать совпадающих
меток на шестьсот кадров. Коротким вставкам B-кадры не нужны, поэтому они
запрещены (`-bf 0`).
**Вставка под каждую длину дыры.** Сначала под каждую дыру делался чёрный
клип ровно её длины. Любое движение клипа меняло длину дыры, требовало
новой генерации, и до её готовности предпросмотр играл прежнюю расстановку.
Выглядело так, будто выключение дорожки или удаление клипа не действуют.
Лечится заготовками готовых длин, из которых вырезается нужный кусок.
**Первая загрузка начинала воспроизведение.** У незагруженного mpv признак
паузы снят, поэтому первый же список кусков ехал сам. Лечится опцией
`pause=yes` при создании плеера.
**Молчаливое растяжение кадра.** Приведение файла к проекту задавало размер
ключом `-s`, а он растягивает кадр не считаясь с соотношением сторон.
Вертикальное видео превращалось в размазанное по ширине. Теперь размер
задаётся цепочкой фильтров, а вписывание выбирается осознанно.
**Фильтр mpv и кадры в памяти видеокарты.** Цепочка фильтров ffmpeg
работает с обычной памятью, а при аппаратном декодировании кадры остаются
в памяти видеокарты. Граф фильтров тогда просто не собирается: `Impossible
to convert between the formats supported by the filter 'mpv_src_in0'`.
Лечится переключением на `hwdec=auto-copy`, пока фильтр нужен.
Ловушка была не в этом, а в том, как я это проверял: из командной строки
mpv сам выбрал `nvdec-copy`, всё работало, и ошибки не было видно. Она
вылезла только на снимке окна работающего редактора. Проверять такое надо
там, где оно живёт, а не в удобной обстановке.
**Скрипт сборки соврал об успехе.** `PIPESTATUS` живёт ровно до следующей
команды, а в разборе результата стояло двоеточие в ветке `then` — оно и
затирало состояние конвейера. Сборка падала, скрипт докладывал «готово».
Читать `PIPESTATUS` надо сразу и в переменную.
**Модуль Windows конфликтует именами.** Он объявляет свои `TRect`, `Point`
и `RegisterClass`, а имена в Паскале нечувствительны к регистру — обычные
вызовы LCL начинают разбираться как системные, с невнятными ошибками вида
«ожидалось `)`». Подключать его целиком не нужно: `SetMenuInfo` и
`MENUINFO` объявлены в `uTheme` напрямую.
**Знак «▼» в шрифте Windows.** Вместо стрелки выходил пустой квадрат.
Треугольник рисуется сам (`TThemedButton.Arrow`).
**Раскраска до построения меню.** `ApplyDarkTheme` вызывался в начале
`FormCreate`, а меню строится в середине — и цветов не получало. Вызов
должен идти последним.
**Исключения вещественной арифметики.** Паскаль по умолчанию требует
сообщать о каждой неточности вычислений. libmpv и ffmpeg внутри считают
как им удобно и на это не рассчитывают: при создании плеера вылезало
«Invalid floating point operation», и предпросмотр не запускался. Лечится
`SetExceptionMask` после загрузки библиотеки.
**Цепочку смешивания надо снимать до загрузки.** Она ссылается на дорожки
по номерам, и оставшаяся от прошлого раза не даёт открыть новый файл
вовсе: проверено на живом mpv — после такой загрузки дорожек ноль, кадра
нет, не играет ничего. Выглядит это как чёрное окно и полная тишина, и
на плеер не похоже совсем.
Снимать её после загрузки поздно: ломается сама загрузка.
**Внешняя звуковая дорожка не подключается свойством.** Задать
`audio-files` перед загрузкой недостаточно: список дорожек после
загрузки остаётся прежним, внешний файл не подключается вовсе, и играет
звук, встроенный в саму картинку. Проверено на живом mpv через его канал
управления: после установки свойства дорожек по-прежнему две, обе не
внешние.
Из-за этого звук был слышен там, где на монтаже его нет. Попытка
защититься, отключая звук при ненайденной внешней дорожке, сделала
хуже — звука не стало нигде.
Правильный способ — команда `audio-add` с признаком `select` после
загрузки: она добавляет дорожку, выбирает её и снимает выбор с
внутренней. Проверено: дорожек становится четыре, внутренняя звуковая
помечается невыбранной.
**Аргументы loadfile.** В mpv 0.38 у команды появился четвёртый аргумент —
номер позиции в списке, и настройки файла сдвинулись на пятое место.
Прежний вызов стал ошибкой «The loadfile option must be an integer», и под
Wine с новой библиотекой не грузилось ничего. Гадать по номеру версии не
нужно: те же настройки задаются свойствами перед загрузкой, и называются
они одинаково во всех версиях.
**Модуль Windows конфликтует именами.** Он объявляет свои `TRect`,
`Point`, `DeleteFile`, `GetTempFileName` и `RegisterClass`, а имена в
Паскале нечувствительны к регистру — обычные вызовы LCL начинают
разбираться как системные, с невнятными ошибками вида «ожидалось `)`».
Подключать его целиком не нужно: `SetMenuInfo` и `MENUINFO` объявлены в
`uTheme` напрямую. За сессию эта ловушка сработала четыре раза.
**Порядок байт в TColor.** LCL хранит цвет как `$00BBGGRR`, а не как
привычный RGB. Записанный по привычке код даёт цвет с переставленными
красным и синим.
**Имена, занятые предками.** Локальная переменная перекрывает одноимённое
свойство класса: `Text` и `Position` заняты у `TControl`, `Finished` — у
`TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как
«Duplicate identifier», но причину не объясняет.
## Консольный инструмент проверок
Часть вещей проверяется числами, а не глазами. Сборка: `./build.sh tool`.
```
./tests/nlvetool kf <файл> индекс опорных кадров
./tests/nlvetool kfcheck <файл> сверка быстрого пути с ffprobe
./tests/nlvetool flattest проверка уплощения монтажа
./tests/nlvetool edl <файл> <старт> <длина> [...]
./tests/nlvetool export <файл> <выход> <старт> <длина> [...]
./tests/nlvetool export --cancel-after=<сек> ... проверка остановки
./tests/nlvetool remux <файл>... <расширение>
./tests/nlvetool build <проект> <видео> [<картинка>]
./tests/nlvetool buildcut <проект> <видео>
./tests/nlvetool render <проект> <выход>
```
Команды возвращают ненулевой код при расхождении, поэтому годятся для
автоматической проверки.
## Структура
| Файл | Что это |
|---|---|
| `src/uAppTypes.pas` | Базовые типы, форматирование чисел без учёта локали |
| `src/uSettings.pas` | Настройки между запусками (JSON) |
| `src/uProcRunner.pas` | Запуск ffmpeg и ffprobe, разбор вывода, остановка |
| `src/uFfmpegCmd.pas` | Сборка аргументов ffmpeg, разбор его прогресса |
| `src/uMediaProbe.pas` | Что за файл нам дали и чем он отличается от проекта |
| `src/uKeyframeIndex.pas` | Где опорные кадры: разбор заголовка MP4 и запасной путь |
| `src/uProject.pas` | Модель монтажа, JSON проекта, отмена действий |
| `src/uFlatten.pas` | Монтаж в плоский список кусков |
| `src/uGenerate.pas` | Клипы для картинок и дыр |
| `src/uExport.pas` | Вырезание, склейка, сверка числа кадров |
| `src/uRemuxQueue.pas` | Очередь перепаковки |
| `src/uEdl.pas` | Сборка `edl://` для предпросмотра |
| `src/uMpvLib.pas` | Объявления libmpv, динамическая загрузка |
| `src/uMpvPlayer.pas` | Плеер: встраивание в окно, воспроизведение, события |
| `src/uTimelineCtrl.pas` | Компонент монтажного стола |
| `src/uTransform.pas` | Как кадр исходника ложится в кадр проекта |
| `src/uPanCropForm.pas` | Окно кадрирования и поворота |
| `src/uProjectInfoForm.pas` | Сведения о проекте и ответ ffprobe |
| `src/uThumbs.pas` | Кадры-миниатюры для списка файлов |
| `src/uWaveform.pas` | Пики звука для формы волны на клипах |
| `src/uTheme.pas` | Цвета окна, своя кнопка, своё окно сообщения |
| `src/uLogView.pas` | Журнал: своя отрисовка, свои полосы прокрутки |
| `src/uThemedScroll.pas` | Полоса прокрутки и обвязка для таблиц |
| `src/uDeps.pas` | Что нужно программе, где искать, как ставить |
| `src/uDownload.pas` | Загрузка по сети: WinHTTP под Windows, curl под Linux |
| `src/uDepsForm.pas` | Окно «Зависимости» |
| `src/uMainForm.pas` | Главное окно |
| `src/uProgressForm.pas` | Ход долгой операции |
| `src/uRemuxForm.pas` | Окно перепаковки |
| `src/uMismatchForm.pas` | Предупреждение о несовпадении параметров |
| `build.sh` | Сборка целями |
| `tests/` | Консольный инструмент проверок |
| `vendor/mpv-headers/` | Заголовки mpv 0.37.0 — эталон для биндингов |
## Чего пока нет
**Раздельные звуковые дорожки.** Звук идёт с того же клипа, что и картинка.
Громкость у клипа своя, но смешивать звук с нескольких дорожек нельзя:
это потребовало бы второго прохода по всему материалу. Модель данных к
разделению готова, само разделение не сделано.
**Осциллограмма звука на клипах.**
**Переходы, эффекты, изменение скорости.** Всё это требует перекодирования и
в замысел не входит.