Клип, не заполняющий кадр, стал уходить плееру отдельной дорожкой, и весь показ пошёл через цепочку фильтров. Цепочка работает всё время показа и считается процессором: на кадре 2688x1512 видео начало отставать от звука. Теперь плееру достаются только прозрачные клипы и клипы с ключом — ради них это оправдано, их участки бывают длинными, и пересобирать минуты материала дорого. Клип, который просто не заполняет кадр, сводится заранее вместе с тем, что под ним: предпросмотр играет обычный файл и показывает ровно то, что даст сборка. Попутно, из разбора того же случая: — заполнитель до первого наложения нарезается по длине файла. Одной записью mpv её обрезает («entry 0 uses 7082.687000 seconds, but file has only 15.021000»), дорожка оказывается короче монтажа и не совмещается с ним по времени; — появление заготовки вызывает перезагрузку списка: строка списка при этом не меняется, и без признака готовности плеер так и оставался без дорожки наложения; — в журнал пишется, сколько окон наложения и готовы ли файлы, а также собранная цепочка показа и неудача video-add. Без этого «картинки нет» нечем объяснить. В заголовке окна теперь номер сборки: nlve 42 — фильм.nlve. Он растёт на единицу при каждой сборке. По нему сразу видно, та ли это сборка, где правка уже есть, — сегодня время ушло на то, что проверяли старым двоичным файлом. Счётчик в build.number, в репозиторий не входит. Проверено на монтаже с картинкой на 300-й секунде: в окне она лежит поверх видео, цепочки показа нет.
1183 lines
99 KiB
Markdown
1183 lines
99 KiB
Markdown
# 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 |
|
||
| Удалить выделенное | Delete |
|
||
| Выделить всё | Ctrl+A |
|
||
| Выделить областью | протяжка по пустому месту дорожек |
|
||
| Режим выделения: рамкой или по времени | D |
|
||
| Схлопнуть дыру под курсором | Ctrl+Shift+R |
|
||
| Отменить, повторить | Ctrl+Z, Ctrl+Y |
|
||
| Масштаб таймлайна, от точки под мышкой | Ctrl + колесо мыши |
|
||
| Прокрутка по дорожкам | колесо мыши |
|
||
| Прокрутка по времени | Shift + колесо, средняя кнопка |
|
||
| Прокрутка журнала вбок | Shift + колесо над журналом |
|
||
| Растянуть форму звука по высоте | Shift + стрелки вверх и вниз |
|
||
| Выключить дорожку / оставить только её | кнопки М и С в заголовке |
|
||
| Громкость клипа | тянуть ручку на клипе вверх-вниз |
|
||
| Автосхлопывание вкл/выкл | Ctrl+K |
|
||
| Копировать и вставить клипы | Ctrl+C, Ctrl+V |
|
||
| Скопировать и применить оформление | Ctrl+Shift+C, Ctrl+Shift+V |
|
||
| Связать или развязать клипы | Ctrl+G |
|
||
| Отменить прилипание на время | Shift при перетаскивании |
|
||
| Вписать проект целиком | Ctrl+0 |
|
||
| Новый, открыть, сохранить проект | Ctrl+N, Ctrl+O, Ctrl+S |
|
||
| Добавить файлы | Ctrl+I |
|
||
| Собрать ролик | Ctrl+E |
|
||
|
||
Файлы попадают в проект через меню «Файл» или перетаскиванием прямо в окно
|
||
программы. Место броска решает, что произойдёт: бросили над монтажом — файл
|
||
и добавится в проект, и ляжет на дорожку под указателем; в любом другом
|
||
месте — только добавится в проект.
|
||
|
||
Файл из библиотеки кладётся на монтаж перетаскиванием либо через
|
||
контекстное меню списка. В нём: **добавить на дорожку** (подменю со
|
||
списком дорожек), **кадрирование и поворот**, **убрать со всех дорожек**
|
||
(файл остаётся в проекте), **удалить из проекта**. Дыры при этом намеренно не
|
||
схлопываются: убрать клип и переставить всё остальное — два разных
|
||
намерения.
|
||
|
||
Если бросить клип ниже всех дорожек, заведётся новая.
|
||
|
||
В заголовке каждой дорожки слева — её название, число клипов и две кнопки:
|
||
**М** выключает дорожку целиком (она не идёт ни в предпросмотр, ни в сборку),
|
||
**С** оставляет только её, заглушив остальные. Щелчок по заголовку делает
|
||
дорожку выбранной — на неё кладёт файл кнопка.
|
||
|
||
Клип переносится перетаскиванием, в том числе на другую дорожку и на
|
||
пустое место под последней — тогда дорожка тоже добавится. За края клипа
|
||
он растягивается и укорачивается. Картинку можно растянуть на любую
|
||
длину, видео — только в пределах исходника.
|
||
|
||
Если тащить к краю окна, монтаж едет следом сам: иначе при большом
|
||
увеличении кусок упирается в край, и его приходится переносить в
|
||
несколько приёмов — отпустить, прокрутить, взять снова.
|
||
|
||
Файл из списка проекта встаёт **на курсор**, а не в конец дорожки. Если
|
||
место занято, монтаж раздвигается: всё от курсора и правее едет вправо
|
||
на длину нового клипа, на всех дорожках сразу — звук и надписи обязаны
|
||
уехать вместе с картинкой. Строка состояния говорит, сколько клипов
|
||
отодвинуто и на сколько.
|
||
|
||
Щелчок по клипу переставляет курсор воспроизведения. Именно щелчок:
|
||
если кнопку нажали и потянули, курсор остаётся на месте, в том числе во
|
||
время воспроизведения.
|
||
|
||
**Прилипание клипов** (кнопка с магнитом). Края клипов притягиваются
|
||
друг к другу и к курсору. Shift во время перетаскивания меняет решение на
|
||
противоположное: при включённом прилипании временно отключает, при
|
||
выключенном — временно включает. Так одна клавиша полезна в обоих
|
||
режимах, и не нужно помнить, какой сейчас.
|
||
|
||
**Прилипание курсора** (кнопка с клеем). Курсор, поставленный щелчком,
|
||
притягивается к границам клипов на любой дорожке. Без этого в стык не
|
||
попасть: на общем плане одна точка экрана — это секунды материала, а
|
||
ставят курсор чаще всего именно на стык. Shift при щелчке работает так
|
||
же — меняет решение на противоположное.
|
||
|
||
Переключателя два, а не один, потому что это разные привычки: клипы
|
||
липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без
|
||
другого.
|
||
|
||
**Курсор посередине** (кнопка с целью). Включено — при
|
||
воспроизведении монтаж едет под курсором, и всегда видно, что будет
|
||
дальше. Выключено — окно не двигается вовсе, даже когда курсор ушёл за
|
||
край: отмотал Shift с колесом — там и останешься. Раньше в выключенном
|
||
состоянии окно всё равно подтягивалось к курсору, стоило тому выйти из
|
||
видимой части, и прокрутка отскакивала назад со второго поворота колеса.
|
||
|
||
**Ключ и прозрачность в предпросмотре** (меню «Вид»). Включено — окно
|
||
показывает то же, что даст сборка: выбитый цвет и приглушение считаются
|
||
на лету. Выключено — оверлей виден целиком, со своим фоном: так понятно,
|
||
куда он встал и где его края.
|
||
|
||
Считается это по площади самого оверлея, а не всего кадра. Замер на
|
||
2688x1512 (600 кадров, десять секунд материала): основа 2.07 с, с
|
||
наложением 2.43 с, с ключом и прозрачностью 2.66 с. То есть наложение
|
||
стоит 0.36 с, ключ добавляет ещё 0.23 с — вчетверо быстрее реального
|
||
времени. На слабой машине это может быть заметнее, потому выключатель и
|
||
есть.
|
||
|
||
**Отмеченный участок.** У линейки два этажа. Верхний, в двенадцать точек,
|
||
— про отметки: протяжка по нему отмечает участок, даже если отметок не
|
||
было, а щелчок без движения их снимает. По краям участка стоят жёлтые
|
||
уголки, за них тянут. Нижний этаж — про курсор, как и раньше.
|
||
|
||
Клавиши тоже остались: `I` ставит начало по курсору, `O` — конец; оба
|
||
пункта есть в меню «Правка», там же снятие отметок. Пока он отмечен, сборка
|
||
собирает только его — это единственный способ проверить правку на длинном
|
||
монтаже, не тратя на каждую проверку столько же времени, сколько идёт сам
|
||
ролик. Отметки живут в файле проекта: участок относится к монтажу, а не к
|
||
привычкам человека.
|
||
|
||
**Дорожки.** Слева в заголовке две стрелки: поднять и опустить. У верхней
|
||
дорожки некуда вверх, у нижней вниз — такая стрелка нарисована тускло и
|
||
не нажимается. Заголовок можно и просто потащить мышью. Двойной щелчок по
|
||
названию переименовывает дорожку: на монтаже из десятка дорожек «карта»,
|
||
«звук мотора» и «титры» читаются несравнимо лучше, чем V1, A2 и Т1.
|
||
|
||
Правый щелчок по заголовку открывает меню: поднять, опустить,
|
||
переименовать, удалить. Удаление дорожки с клипами спрашивает
|
||
подтверждение и называет число клипов.
|
||
|
||
**Копирование клипов.** Ctrl+C копирует выделенные клипы, Ctrl+V кладёт
|
||
копии по курсору. Отбивку или текстовый блок так размножить проще, чем
|
||
заводить заново и настраивать оформление с нуля.
|
||
|
||
Связанные клипы копируются с парой: у картинки со звуком иначе уехала бы
|
||
половина. Связка у копий своя — копия и подлинник не ездят вместе.
|
||
Взаимное расположение сохраняется: несколько клипов встают тем же узором,
|
||
сдвинутым к курсору.
|
||
|
||
Дорожка выбирается сама: сначала та, откуда копировали, если это время на
|
||
ней свободно; потом любая подходящая по виду; в крайнем случае заводится
|
||
новая. Класть копию поверх соседа нельзя — на одной дорожке это означает
|
||
переход, а его никто не заказывал.
|
||
|
||
**Копирование свойств клипа.** Ctrl+Shift+C берёт с выделенного клипа всё
|
||
оформление — кадрирование, поворот, увеличение, смещение, прозрачность,
|
||
цветовой ключ, затухания, громкость, — Ctrl+Shift+V применяет их ко всем
|
||
выделенным. Вид клипа и его текст при этом не переносятся: свойства
|
||
копируют между разными клипами, и превращать картинку в надпись незачем.
|
||
|
||
**Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена —
|
||
автосхлопывание включено, и тогда при перетаскивании клипа всё, что
|
||
начинается не раньше него, едет следом. Стрелка рядом открывает выбор
|
||
режима: только текущая дорожка или все. Ctrl+K включает и выключает.
|
||
|
||
Режим действует и при выключенном автосхлопывании — по нему работает
|
||
Ctrl+Shift+R, который схлопывает дыру под курсором. То есть выключенная
|
||
кнопка означает лишь «не двигать ничего само собой при перетаскивании».
|
||
|
||
Во время воспроизведения курсор держится посередине окна, а монтаж едет
|
||
под ним. При нажатии на пуск, если курсор оказался за краем видимой
|
||
части, вид подтягивается к нему — масштаб при этом не меняется.
|
||
|
||
На каждом клипе проходит линия громкости с ручкой посередине видимой его
|
||
части: тянешь ручку вверх и вниз — меняется громкость, от тишины до
|
||
двойной. Ручка стоит посередине именно видимой части, а не всего клипа:
|
||
до середины часового клипа, уехавшей за край экрана, мышью не дотянуться.
|
||
|
||
**Затухания тянутся за ручки в верхних углах клипа.** Взять их можно и
|
||
промахнувшись: зона захвата шире нарисованного квадратика и уходит внутрь
|
||
клипа. Пока затухание нулевое, ручка стоит в самом углу и целиться есть
|
||
куда; стоит её сдвинуть — она висит посреди клипа сама по себе, и попасть
|
||
в девять точек почти нельзя. Над зоной захвата меняется курсор — по нему
|
||
и видно, где взять (`tests/fadegrab.sh`).
|
||
|
||
Громкость, отличная от обычной, — единственное место, где звук
|
||
перекодируется: изменить громкость, не тронув сам звук, нельзя. Видео при
|
||
этом по-прежнему копируется. Проверено: клип с громкостью 30% дал в ролике
|
||
уровень на 10.5 дБ ниже исходного при ожидаемых 10.46 дБ.
|
||
|
||
Полоски по краям клипа показывают тип реза: серая — притянут к опорному
|
||
кадру, голубая — точный, стык будет перекодирован при сборке. Если край
|
||
подвинуть руками и он не попадёт на опорный кадр, рез сам становится
|
||
точным: раз человек выбрал конкретный кадр, притягивать его к соседнему
|
||
неправильно.
|
||
|
||
В списке файлов слева показывается кадр из каждого файла, его параметры
|
||
и длительность. Кадры делаются в фоне и запоминаются в кэше проекта.
|
||
|
||
На клипах видна форма звука. Считается она по заранее разобранным пикам —
|
||
сто значений в секунду, каждое есть наибольший модуль отсчёта за свои
|
||
десять миллисекунд. Рисовать по настоящим отсчётам нельзя: в секунде их
|
||
48 тысяч, а в клипе на экране сотни пикселей.
|
||
|
||
Разбор идёт в фоне и запоминается в кэше проекта. Для записи на 892 с это
|
||
около 13 секунд — столько занимает чтение шести гигабайт с диска, и
|
||
быстрее не выйдет; при следующем открытии проекта пики берутся готовыми.
|
||
|
||
**Shift со стрелками вверх и вниз растягивает форму по высоте.** Звук
|
||
бывает тихим, и угадывать за человека, во сколько раз растянуть картинку,
|
||
неправильно. Усиление меняет только рисунок, самого звука не касается, и
|
||
запоминается между запусками.
|
||
|
||
## Сохранение и автосохранение
|
||
|
||
**Сохранение видно.** После Ctrl+S внизу появляется «Проект сохранён:
|
||
имя, время». Раньше об этом говорила только строчка в журнале, а журнал
|
||
по умолчанию закрыт: нажал — и непонятно, дошло до файла или нет.
|
||
|
||
**Запись не оставляет огрызков.** Проект пишется во временный файл рядом
|
||
и подставляется на место настоящего, только когда записан целиком.
|
||
Прежний файл при этом становится запасным — `имя.nlve.bak`. Падение или
|
||
кончившееся место посреди записи больше не оставляют половину проекта.
|
||
|
||
**Копия пишется сама.** Пока в проекте есть несохранённое, раз в полминуты
|
||
рядом появляется `имя.nlve.autosave`. Это не сохранение: файл проекта не
|
||
трогается, признак «изменён» остаётся.
|
||
|
||
При открытии проекта программа смотрит, нет ли рядом копии новее его
|
||
самого. Есть — значит прошлая работа до файла не дошла, и программа
|
||
показывает оба времени и предлагает открыть копию. Отказ копию не
|
||
удаляет: она лежит, пока проект не сохранят заново. После сохранения
|
||
копия не нужна и убирается.
|
||
|
||
## Кнопки внизу
|
||
|
||
Переключатели в нижней полосе — прилипание, прилипание курсора, курсор
|
||
посередине, автосхлопывание с выбором режима, режим выделения, журнал.
|
||
|
||
**Подсказка идёт в строку состояния, а не во всплывающее окно.**
|
||
Всплывающее окно появлялось прямо поверх кнопки, о которой
|
||
рассказывало, и перехватывало щелчок: нажать кнопку было нельзя, пока
|
||
подсказка не уйдёт. Теперь наведение мыши пишет ту же фразу в строку
|
||
состояния справа от кнопок — она ничего не закрывает. Щелчок по кнопке
|
||
подсказку тут же переписывает: «Журнал виден» становится «Журнал
|
||
спрятан».
|
||
|
||
Строка состояния занимает всё свободное место справа и ужимается только
|
||
на время, пока рядом показан ход работы.
|
||
|
||
**Каждый щелчок считается.** Раньше второй щелчок подряд оконная
|
||
библиотека отдавала как двойной, и до кнопки он не доходил: нажимаешь
|
||
журнал три раза быстро — срабатывает один. Теперь двойных щелчков
|
||
кнопкам не приходит вовсе, и любая очередь щелчков отрабатывается вся
|
||
(`tests/lfmcheck`, раздел «Кнопки нижней панели»; `tests/buttonclicks.sh`
|
||
проверяет то же самое мышью на невидимом экране).
|
||
|
||
## Два режима выделения
|
||
|
||
Кнопка внизу (справа от схлопывания) и клавиша `D`. Режим показан
|
||
значком: рамка — выделение клипов, промежуток со стрелкой — выделение по
|
||
времени.
|
||
|
||
**Рамкой по клипам.** Протяжка по пустому месту дорожек выделяет всё, что
|
||
рамка задела. Обычное выделение: выделил, нажал Delete.
|
||
|
||
**По времени.** Протяжка отмечает промежуток от и до — по всем дорожкам
|
||
сразу, сколько бы их ни было. Промежуток виден пунктиром и после того,
|
||
как кнопку отпустили: по нему режет `S`.
|
||
|
||
Зачем второй режим: вырезать из фильма кусок целиком. Отметил промежуток,
|
||
нажал `S` — все дорожки разрезаны в двух местах, по краям промежутка, и
|
||
выделенной осталась ровно середина. Дальше Delete, и при включённом
|
||
автосхлопывании дыра закрывается сама.
|
||
|
||
Края промежутка кладутся на кадровую сетку: между кадрами времени нет.
|
||
|
||
**Курсор идёт за резом.** После `S` он встаёт ровно на место реза (в
|
||
режиме по времени — на начало промежутка), после удаления куска и
|
||
схлопывания — на место стыка. Дальше с этим местом и работают: следующий
|
||
рез, схлопывание дыры, просмотр перехода. Раньше курсор оставался там, где
|
||
был, и в место реза приходилось попадать мышью заново.
|
||
|
||
## Починка проекта
|
||
|
||
Меню «Проект» → **Проверить и починить проект...**. Показывает, что нашла,
|
||
и чинит только по согласию; отменяется через Ctrl+Z.
|
||
|
||
Что ищется:
|
||
|
||
- **клипы мимо кадровой сетки.** Между кадрами времени нет, а в старых
|
||
проектах времена записаны как попало: 2.457599 с при 59.94 к/с — это
|
||
147.31 кадра. Картинка всё равно выйдет целым числом кадров, звук
|
||
наберётся с точностью до отсчёта, и предпросмотр со сборкой не смогут
|
||
совпасть в точности. Починка двигает клип не больше чем на полкадра;
|
||
на проекте из 82 клипов общая длина изменилась на 0.028 с, то есть на
|
||
полтора кадра за два часа;
|
||
- **клипы на дорожках чужого вида.** Такой клип нарисован на монтаже, но
|
||
в готовый ролик не попадает. То же самое предлагается при открытии
|
||
проекта, здесь — по требованию;
|
||
- **дорожки с повторяющимися именами.**
|
||
|
||
Для синхронности звука с картинкой чинить проект не нужно: рез правится
|
||
при сборке, и старый проект соберётся сходящимся. Починка — про то, чтобы
|
||
монтаж не хранил невозможных времён.
|
||
|
||
## Файл не совпадает с проектом
|
||
|
||
Проект имеет одно разрешение и один набор кодеков на всё — иначе куски не
|
||
склеить копированием. Файл с другими параметрами можно добавить, но его
|
||
куски скопировать не выйдет.
|
||
|
||
Пересобираются **только они**. Остальной материал по-прежнему копируется
|
||
без потерь и почти мгновенно. Ролик целиком не перекодируется никогда.
|
||
|
||
Как именно кадр ложится в кадр проекта, решает окно «Кадрирование и
|
||
поворот»: вписать целиком с полями, заполнить кадр с обрезкой или
|
||
растянуть, плюс поворот на 90, отражение, увеличение и смещение.
|
||
|
||
Слева в окне — кадр источника с рамкой: её тянут мышью за середину и за
|
||
правый нижний угол, и она держит пропорции кадра проекта. Справа —
|
||
настоящий результат, пропущенный через ту же цепочку фильтров, что пойдёт
|
||
в дело.
|
||
|
||
**Настройка принадлежит клипу**, а открывается кнопкой в правом нижнем
|
||
видимом углу самого клипа. Именно видимом: у клипа длиной в час настоящий
|
||
угол почти всегда за краем экрана. Два куска одного файла можно
|
||
кадрировать по-разному.
|
||
|
||
У файла в библиотеке хранится такая же настройка, но она лишь заготовка:
|
||
её берёт новый клип при добавлении. Правится она из контекстного меню
|
||
библиотеки.
|
||
|
||
По умолчанию кадр вписывается целиком. Растяжение надо выбрать руками:
|
||
раньше оно применялось молча, и вертикальное видео размазывалось по ширине.
|
||
|
||
«Приведение к параметрам проекта» делает ту же работу один раз при
|
||
добавлении: файл перекодируется целиком, зато каждая последующая сборка
|
||
остаётся быстрой.
|
||
|
||
Что и почему происходит с каждым файлом, видно в окне «Проект → Сведения
|
||
о проекте»: параметры проекта, параметры каждого файла, выбранное
|
||
вписывание и вердикт — копируется, пересобирается или из него делается
|
||
клип. Там же кнопка полного ответа ffprobe.
|
||
|
||
### Что показывает предпросмотр
|
||
|
||
Предпросмотр показывает **кадр проекта**, а не кадр исходника. Файл другого
|
||
соотношения сторон виден с полями по краям — ровно так, как он попадёт в
|
||
готовый ролик. Проверено сравнением: кадр предпросмотра и кадр сборки
|
||
совпадают бит в бит.
|
||
|
||
Само по себе это не бесплатно, поэтому включается только когда есть что
|
||
вписывать. Разное разрешение при одинаковом соотношении сторон (скажем,
|
||
3840x2160 в проекте 2688x1512) ничего не требует: mpv сам впишет такой
|
||
файл в панель, и картинка совпадёт до пикселя.
|
||
|
||
Когда вписывание нужно, декодирование переключается на вариант с
|
||
копированием кадров в обычную память — цепочка фильтров не умеет работать
|
||
с кадрами, лежащими в памяти видеокарты. Аппаратное декодирование при этом
|
||
сохраняется, видно в журнале: `nvdec` без вписывания, `nvdec-copy` с ним.
|
||
Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра,
|
||
230 с настоящим масштабированием — запас четырёхкратный.
|
||
|
||
**Кадрирование клипа окно показывает.** Цепочка фильтров у mpv одна на всю
|
||
склейку, а кадрирование у каждого клипа своё, и включать его по времени в
|
||
цепочке нечем: из фильтров ffmpeg по времени переключается только
|
||
наложение. Зато у вывода есть своя обрезка, и делает её видеокарта даром.
|
||
Курсор зашёл в клип со своим кадрированием — обрезка выставляется, вышел —
|
||
снимается. Ни лишней цепочки, ни копирования кадров из памяти видеокарты.
|
||
|
||
Рамку считает один и тот же код, что рисует её в окне «Кадрирование и
|
||
поворот», — иначе окно обещало бы одно, а предпросмотр показывал другое
|
||
(`TransformSourceRect` в `uTransform`, проверка в `tests/lfmcheck`).
|
||
|
||
Чего это не покрывает: отражение (свойства для него у вывода нет),
|
||
растяжение (там кадр жмётся неравномерно, рамкой не описать) и участки с
|
||
наложением — там кадр уже сведён цепочкой, и обрезка резала бы сведённое
|
||
целиком, вместе с нижним слоем. Для таких клипов точную картинку
|
||
по-прежнему показывает окно «Кадрирование и поворот».
|
||
|
||
## Цвета окна
|
||
|
||
Все цвета заданы в `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`. Громкость клипов подставляется в ту
|
||
же цепочку — иначе предпросмотр врал бы про баланс. Проверено: в
|
||
предпросмотре и в готовом ролике уровни совпадают до десятой доли
|
||
децибела.
|
||
|
||
Два клипа **на одной дорожке**, наползшие друг на друга, дают переход:
|
||
один плавно перетекает в другой. На разных дорожках перекрытие означает
|
||
не переход, а наложение — там по-прежнему побеждает верхний, если он
|
||
сплошной.
|
||
|
||
Сплошным клип перестаёт быть по трём причинам: выбит цвет, задана
|
||
прозрачность или **он не заполняет кадр**. Последнее — то же самое,
|
||
сказанное иначе: уменьшенный или сдвинутый клип занимает часть кадра, а
|
||
вокруг остаётся пустое место, которое сборка обкладывает чёрными полями.
|
||
Поле непрозрачно, и нижнюю дорожку закрывает не сам клип, а его поля —
|
||
поэтому такой клип кладётся поверх нижнего, а не вместо него. Уменьшают
|
||
клип ровно затем, чтобы увидеть под ним другое. Класть не на что —
|
||
остаются поля.
|
||
|
||
Отсюда же берётся картинка в углу кадра и два видео рядом: достаточно
|
||
уменьшить и сдвинуть клипы на разных дорожках.
|
||
|
||
В месте наезда под указателем два клипа. Первый щелчок берёт верхний,
|
||
повторный по тому же месту переводит выделение на следующий, после
|
||
последнего снова на верхний; строка состояния говорит, сколько их там.
|
||
Нажатие для перетаскивания берёт именно выделенный — иначе нижний клип
|
||
нельзя было бы ни выделить, ни потащить, не убрав сперва верхний.
|
||
|
||
Различие не косметическое. Если бы кроссфейдом считалось любое
|
||
перекрытие, то в проекте, где клипы на разных дорожках перекрываются
|
||
постоянно, каждое такое место пришлось бы пересобирать.
|
||
|
||
Наезд не остаётся незамеченным: после правки строка состояния называет
|
||
дорожку, величину перекрытия и сами клипы. Переход заказывают мышью, а
|
||
промахнуться легко — на общем плане одна точка экрана это секунды, — и
|
||
раньше он появлялся сюрпризом: человек искал, почему сборка опять не
|
||
кончается. Меряется наезд той же мерой, что и в сборке: короче половины
|
||
кадра — не наезд, и говорить о нём незачем.
|
||
|
||
Косой крест наезда и это сообщение берут соседей из списка клипов
|
||
дорожки, а он отсортирован по времени. Держать этот порядок обязан
|
||
каждый, кто клипы двигает: перетаскивание ведёт за собой связку на
|
||
звуковой дорожке и соседей при схлопывании, и все их дорожки
|
||
пересортировываются (`SortMovedTracks`). Пока сортировалась только та
|
||
дорожка, с которой тащили, монтаж рисовал крест через целый клип там,
|
||
где ничего не накладывалось, и обещал переход, которого никто не
|
||
заказывал.
|
||
|
||
Сама сборка при этом не начинается с первым же движением. Список кусков
|
||
пересобирается сразу, а ffmpeg запускается, когда монтаж постоял тихо
|
||
полторы секунды (`GenerateQuietMs`) и клип никто не держит мышью. Иначе
|
||
переход считался бы заново на каждое движение руки, а посчитанное
|
||
отправлялось бы в корзину.
|
||
|
||
У клипа есть плавное появление и уход. Для картинки это затемнение, для
|
||
звука — нарастание громкости.
|
||
|
||
**Всё это стоит перекодирования** — но только своего участка. Перед
|
||
сборкой программа сообщает, сколько секунд из скольких придётся
|
||
пересобрать, и даёт отказаться.
|
||
|
||
Звук при наложении сводится прямо в отсчётах, без фильтров ffmpeg: доля
|
||
каждого слоя считается на каждый отсчёт, и длина результата получается
|
||
ровно такой, какая нужна.
|
||
|
||
## Текст режет кусок, а не пересобирает его целиком
|
||
|
||
Вжигание надписи не делает весь кусок пересобираемым. Кусок режется на
|
||
части: до надписи, под надписью и после. Пересобирается только середина,
|
||
остальное копируется. Надпись в тридцать секунд внутри сорокаминутного
|
||
куска раньше стоила сорока минут пересборки — половины ролика.
|
||
|
||
Часть, которая начинается не с начала куска, помечается точным резом:
|
||
копия пошла бы от ближайшего опорного кадра и сдвинула бы картинку.
|
||
Сборка перекодирует огрызок до опорного кадра, а дальше снова копирует —
|
||
доли секунды вместо минут.
|
||
|
||
Обрезки короче секунды не копируются, а дожигаются вместе с надписью:
|
||
возни с опорными кадрами там больше, чем выгоды.
|
||
|
||
## Качество пересобираемых кусков
|
||
|
||
Куски, которые нельзя скопировать, целятся в наибольший битрейт среди
|
||
файлов проекта. Без этого кодировщик берёт своё значение по умолчанию, и
|
||
оно скромное: на съёмке 2688x1512 с 56 Мбит/с NVENC выдавал 5.9 Мбит/с —
|
||
пересобранный кусок бросался в глаза среди скопированных соседей.
|
||
|
||
Кроме среднего задаётся потолок в полтора раза выше и буфер вдвое: без
|
||
них кодировщик разгоняется на сложных кадрах ровно настолько, насколько
|
||
позволит средний, и мелкие детали всё равно теряются.
|
||
|
||
Берётся именно наибольший, а не средний: пересобранный кусок стоит рядом
|
||
со скопированными, и если он окажется хуже их, разница видна именно на
|
||
нём. Лишнее место дешевле.
|
||
|
||
## Место на диске
|
||
|
||
Куски весят примерно столько же, сколько готовый ролик, а на длинном
|
||
материале и больше: на часовой съёмке набралось 110 ГБ. Поэтому:
|
||
|
||
- **картинка не собирается в промежуточный файл.** Звук собирается
|
||
раньше, и склейка сразу кладёт всё вместе в конечный файл. Раньше
|
||
получалось два полных ролика на диске одновременно — 91 ГБ в кэше и 91
|
||
ГБ рядом — и лишний проход копирования ради одной звуковой дорожки;
|
||
- **после удачной сборки куски удаляются,** и в журнале сказано, сколько
|
||
места освободилось. После неудачной — остаются: с них продолжают.
|
||
|
||
Оставить их можно настройкой `keep_parts` в файле настроек: пригодится,
|
||
когда разбираешься, из чего собрался ролик.
|
||
|
||
## Дыры и вставки
|
||
|
||
Промежуток короче половины кадра проекта дырой не считается: между
|
||
клипами, сдвинутыми встык мышью, остаются микросекунды, и заказывать под
|
||
них чёрную вставку не нужно. Мышь при этом кладёт клипы на кадровую
|
||
сетку — между кадрами времени не существует.
|
||
|
||
Готовые вставки делаются двух длин, 15 и 60 секунд. Дыра длиннее минуты
|
||
набирается повторами одного и того же минутного файла: и список кусков
|
||
предпросмотра, и список склейки повторы принимают. Раньше лестница длин
|
||
доходила до восьми часов, и дыра в сорок минут требовала двухчасового
|
||
чёрного файла — программа выглядела зависшей.
|
||
|
||
Чёрное кодируется отдельным скромным потоком в мегабит: целиться в
|
||
битрейт съёмки там нечему.
|
||
|
||
Если правка монтажа сделала идущую генерацию ненужной, она прекращается:
|
||
программа проверяет, нужна ли ещё та вставка, что делается сейчас.
|
||
|
||
## Прерванная сборка
|
||
|
||
Если сборка не дошла до конца — упала, была отменена или программу
|
||
закрыли, — при следующей сборке в тот же файл программа предложит
|
||
продолжить.
|
||
|
||
**При сборке потоком** продолжают с самого недописанного файла: он и есть
|
||
уже собранная часть фильма, и в нём видно, сколько целых кадров успело
|
||
записаться. Программа отступает назад до границы последнего целого куска,
|
||
переносит эту часть в новый файл и досылает остаток. Собранное таким
|
||
образом совпадает с собранным за один раз попиксельно.
|
||
|
||
Дописать в конец существующего MKV нельзя — Matroska этого не даёт, —
|
||
поэтому продолжение всегда пишется в новый файл, и на диске в этот момент
|
||
лежат оба. Места нужно вдвое больше готового ролика.
|
||
|
||
**MP4 продолжать нельзя вовсе:** оглавление пишется в конец, и оборванный
|
||
файл не открывается — `moov atom not found`. Программа говорит об этом
|
||
прямо и собирает заново. Если сборка длинная, лучше собрать в MKV, а потом
|
||
переупаковать в MP4 окном ремукса: это копирование потоков, без
|
||
перекодирования.
|
||
|
||
Готовый файл до последнего цел: сборка идёт в файл рядом и занимает место
|
||
готового, только когда дописана. Сорвавшаяся попытка не отбирает то, что
|
||
уже было собрано, — из двух недописанных остаётся тот, где кадров больше.
|
||
|
||
**При сборке через кэш кусков** продолжают иначе: каждый доведённый до
|
||
конца кусок помечается в списке рядом с самими кусками (`готово.txt`).
|
||
Проверяется не только наличие файла — сборку могли прервать посреди
|
||
записи; куском считается только тот, который успел попасть в список.
|
||
Первая строка списка — отпечаток плана: пути, места и длины всех кусков.
|
||
Не совпал — значит монтаж с тех пор менялся, и готовые куски не подойдут;
|
||
программа скажет об этом и соберёт заново.
|
||
|
||
## Сколько осталось
|
||
|
||
Окно сборки показывает, сколько прошло и сколько примерно осталось.
|
||
Оценка простая: если четверть пути заняла минуту, весь путь займёт
|
||
четыре. На деле скорость меняется — копируемые куски идут мгновенно,
|
||
пересобираемые медленно, — поэтому число называется «примерно» и
|
||
пересчитывается на каждом шаге. Даже такая оценка отвечает на главный
|
||
вопрос: успею ли я сходить за чаем.
|
||
|
||
**Полоса и число — одно и то же число.** Показатель работы внизу главного
|
||
окна подписан процентами, и это ровно та доля, которую он нарисовал.
|
||
Раньше полоса ползла по догадке, а подпись показывала замер: человек
|
||
видел полосу, заполненную на девять десятых, рядом с нулём процентов, и
|
||
понять по такой паре было нельзя ничего.
|
||
|
||
**Доля берётся у ffmpeg, а не угадывается.** Сборка участка с наложением
|
||
или затуханием может идти минуту и дольше, и всё это время «кусок 1 из 1»
|
||
не меняется. Поэтому ffmpeg спрашивают, докуда он дошёл (`-progress`), и
|
||
доля считается по его ответу. Пока он не ответил ни разу, полоса трогается
|
||
с места на пару процентов и там стоит: пустая полоса и есть правда о
|
||
только что начатой работе.
|
||
|
||
**Обе полосы рисует один и тот же код.** Полоса в окне сборки раньше была
|
||
системной (`TProgressBar`), и под gtk2 она заполнялась ступенями — восемь
|
||
кусков на всю ширину. Теперь и там, и внизу главного окна стоит своя
|
||
`TThemedProgress`, которая заполняется сплошь.
|
||
|
||
## Как собирается ролик
|
||
|
||
1. Монтаж уплощается в последовательность непересекающихся кусков.
|
||
Перекрытия разрешаются в пользу верхней дорожки, дыры превращаются в
|
||
чёрные вставки.
|
||
2. Картинки и дыры материализуются в клипы в параметрах проекта. Иначе их
|
||
нечем показать: подсунуть mpv картинку или генератор прямо в списке
|
||
кусков нельзя, он на этом зависает. Заготовки делаются готовых длин
|
||
(15 с, 60 с, 5 мин и так далее), а нужный кусок из них вырезается —
|
||
тогда любое движение клипа не требует новой генерации.
|
||
|
||
Своё кадрирование картинки — увеличение, смещение, поворот — запекается
|
||
прямо в эту заготовку, той же цепочкой фильтров, что показывает окно
|
||
«Кадрирование и поворот». Поэтому в кэше лежит столько заготовок,
|
||
сколько разных кадрирований у этой картинки, и предпросмотр с роликом
|
||
показывают одно и то же.
|
||
|
||
Заготовка нужна не всегда: если картинка не заполняет кадр, а под ней
|
||
что-то лежит, участок собирается наложением, и чёрным полям взяться
|
||
неоткуда. Заготовка с полями остаётся для случая, когда класть не на
|
||
что.
|
||
|
||
Предпросмотр идёт тем же путём: участок с наложением сводится заранее
|
||
и играется обычным файлом. Силами плеера рисуются только прозрачные
|
||
клипы и клипы с ключом — их участки бывают длинными, и пересобирать
|
||
минуты материала ради них дорого. Цепочка плеера работает всё время
|
||
показа и считается процессором: на кадре 2688x1512 видео начинает
|
||
отставать от звука, поэтому ради клипа, который просто не заполняет
|
||
кадр, её не включают.
|
||
|
||
Картинка, которую всё же рисует плеер, отдаётся ему заготовкой без
|
||
полей, а не самим файлом: своих декодеров ему хватает не на всё
|
||
(настоящий webp из сети дал «webp: invalid TIFF header in Exif data»),
|
||
да и у неподвижного кадра нет длины в списке кусков.
|
||
3. Границы кусков кладутся на кадровую сетку проекта: между кадрами
|
||
времени нет, а звук набирается с точностью до отсчёта, и доли кадра
|
||
разводят их в стороны.
|
||
4. Рез всегда точный: огрызок от точки реза до ближайшего опорного кадра
|
||
перекодируется, остальное копируется. Копировать можно только с
|
||
опорного кадра, поэтому «резать где угодно и ничего не перекодировать»
|
||
не бывает — но платить приходится долями секунды на рез.
|
||
5. Каждому куску заказывается точное число кадров (`-frames:v`), а не
|
||
только время: при копировании ffmpeg отмеряет конец по времени
|
||
декодирования, и у записи с B-кадрами кусок выходит длиннее
|
||
запрошенного.
|
||
6. Куски идут в готовый файл потоком. Каждый режется своим ffmpeg прямо
|
||
в MPEG-TS на стандартный вывод, все потоки подряд переливаются в один
|
||
принимающий ffmpeg, а тот пишет готовый файл. Место куску в общем
|
||
потоке задаёт `-output_ts_offset`, считаемое в кадрах от начала. На
|
||
диск не ложится ничего, кроме результата и звуковой дорожки.
|
||
7. Результат сверяется дважды: сумма кадров кусков обязана совпасть с
|
||
числом пакетов в готовом файле, а число кадров — с тем, что просил
|
||
монтаж. Второе и есть проверка синхронности звука с картинкой; вердикт
|
||
пишется в журнал сборки.
|
||
|
||
Звук собирается раньше картинки: приёмник берёт его вторым входом. В
|
||
кусках картинки звука нет — звуковая дорожка собирается отдельно и точнее,
|
||
а лишний поток в куске MPEG-TS ещё и сбивает тайминг склейки: замерено
|
||
36 мс на стык.
|
||
|
||
**Через кэш кусков** собирается то, что потоком не выходит: когда кодек
|
||
проекта не переносится в MPEG-TS или когда весь ролик пересобирается под
|
||
площадку одним кодировщиком. Тогда каждый кусок пишется в отдельный файл,
|
||
а склейка идёт concat-демультиплексором с явно указанной длительностью
|
||
каждого. Этот путь и был единственным раньше; оба дают побитово один и тот
|
||
же ролик — сверено на материале с дрона, PSNR бесконечен на каждом кадре.
|
||
Разница в цене: на отрывке в 17.5 с кэш весил 152 МБ против 421 КБ, а
|
||
времени ушло 17.5 с против 14.2 с.
|
||
|
||
Напрашивающийся способ — один проход с директивами `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. Резы теперь всегда точные, так что на деле это почти всегда
|
||
MPEG-TS.
|
||
|
||
**Лишний кадр на стыке точного реза.** При копировании `-to` работает как
|
||
«строго меньше», при перекодировании — как «меньше либо равно». Кадр на
|
||
границе попадал в ролик дважды: 601 вместо 600. Лечится отступом на
|
||
полкадра от границы.
|
||
|
||
**B-кадры во вставках.** Двунаправленные кадры от NVENC при переносе в
|
||
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 с.
|
||
|
||
**Вставка под каждую длину дыры.** Сначала под каждую дыру делался чёрный
|
||
клип ровно её длины. Любое движение клипа меняло длину дыры, требовало
|
||
новой генерации, и до её готовности предпросмотр играл прежнюю расстановку.
|
||
Выглядело так, будто выключение дорожки или удаление клипа не действуют.
|
||
Лечится заготовками готовых длин, из которых вырезается нужный кусок.
|
||
|
||
**Первая загрузка начинала воспроизведение.** У незагруженного 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. Записанный по привычке код даёт цвет с переставленными
|
||
красным и синим.
|
||
|
||
**Последняя строка блока о ходе работы обнуляет место.** ffmpeg шлёт ход
|
||
блоками, и заканчивается блок строкой `progress=continue`, в которой
|
||
места нет — она сообщает только, что работа продолжается. Разбор честно
|
||
оставляет переданное число как было, поэтому передавать туда надо уже
|
||
известное место, а не ноль. С нулём доля обнуляется в конце каждого
|
||
блока: показатель стоит на месте и дёргается вверх лишь тогда, когда
|
||
опрос попадёт между строкой со временем и этой — «висит на пяти
|
||
процентах, потом скачет до семидесяти девяти». Сам ffmpeg при этом
|
||
отдаёт ход безупречно ровно: блок каждые полсекунды, замерено.
|
||
|
||
**Разбор HEVC на процессоре — самое дорогое в сведении слоёв.** Не
|
||
фильтры, как кажется: на десяти секундах 2688x1512 цепочка со сведением
|
||
стоит 13.5 с, а без всяких фильтров — 12.7 с. Всё время уходит на разбор
|
||
исходника. Ключ `-hwaccel cuda` перед входом убирает его почти целиком:
|
||
затухание на семьдесят секунд собирается 18.3 с вместо 75.9 с, картинка
|
||
бит в бит та же. Просить его стоит только там, где кодирует та же карта
|
||
(выбран NVENC): иначе на машине без неё пришлось бы ловить отказ.
|
||
|
||
**Список недостающего — не список нужного.** Недостающее у нас — это то,
|
||
чего нет на диске. Но у куска, который делается прямо сейчас, файл уже
|
||
есть: ffmpeg создаёт его в первую же секунду и пишет до конца. Проверка
|
||
«нужна ли ещё эта работа» спрашивала по списку недостающего — и отвечала
|
||
«не нужна» про то, что делается. Работа отменялась в разгаре и начиналась
|
||
заново. На коротком затухании это проходило незаметно, на затухании в
|
||
семьдесят секунд сборка не заканчивалась никогда: любая правка сбрасывала
|
||
её. Теперь уплощение отдельно отдаёт список нужного, независимо от того,
|
||
лежит файл на диске или ещё пишется (`tests/genrestart.sh`).
|
||
|
||
**MousePos у колеса мыши — не экранный.** По описанию `DoMouseWheel`
|
||
получает точку в координатах экрана, но под gtk2 в ней уже клиентские:
|
||
`ScreenToClient` переводит их второй раз, и точка уезжает влево ровно на
|
||
левый край элемента. У масштаба таймлайна это выглядело так, будто он
|
||
берётся не от мыши: замерено на невидимом экране — мышь в 700, 1000 и
|
||
1300, опора в 260, 560 и 860, всегда на 440 левее. Лечится обращением к
|
||
`Mouse.CursorPos`: он экранный в любой системе, и перевод остаётся один.
|
||
|
||
**Имена, занятые предками.** Локальная переменная перекрывает одноимённое
|
||
свойство класса: `Text` и `Position` заняты у `TControl`, `Finished` — у
|
||
`TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как
|
||
«Duplicate identifier», но причину не объясняет.
|
||
|
||
**Стык кусков в потоке выглядит как порча.** Каждый кусок — это
|
||
отдельный поток MPEG-TS со своим счётчиком непрерывности, и на стыке
|
||
приёмник пишет «Packet corrupt», по паре строк на кусок. Данные при этом
|
||
целы, но журнал сборки забивается жалобами, из-за которых не найти
|
||
настоящую ошибку. Лечится ключом `-mpegts_flags +initial_discontinuity`
|
||
у отправителя: он помечает начало куска как разрыв, жалобы исчезают, а
|
||
результат тот же — сверено по кадрам и по md5.
|
||
|
||
**Приёмник двигает весь ролик.** `-avoid_negative_ts make_zero` у
|
||
приёмника сдвигает поток так, чтобы самая ранняя метка декодирования
|
||
стала нулём, — а у первого куска она уходит в минус на задержку
|
||
кодировщика, и весь ролик встаёт на 21 мс позже. Само по себе это
|
||
безобидно, но продолжение прерванной сборки несёт этот сдвиг в
|
||
перенесённой части, и куски за ней, размещённые по счёту кадров от нуля,
|
||
налезают на неё на кадр. Поэтому при переносе сдвиг снимается
|
||
отрицательным `-output_ts_offset`, а приёмник накладывает его заново.
|
||
|
||
**Сдвиг к нулю отменяет сдвиг по времени.** При сборке потоком место
|
||
куску задаёт `-output_ts_offset`, но `-avoid_negative_ts make_zero`
|
||
считается уже после него и двигает кусок обратно в ноль — ключ просто
|
||
перестаёт действовать. Замерено: все шесть кусков встали в начало,
|
||
картинка разошлась с эталоном (PSNR 18 дБ) при верном числе кадров и
|
||
тех же пакетах. В потоке сдвиг к нулю выключен; отрицательные метки в
|
||
самом начале подбирает приёмник, у него make_zero свой.
|
||
|
||
**`-fflags +igndts` сминает кадры на стыках.** С ним ffmpeg выбрасывает
|
||
пришедшие метки декодирования и считает свои, а материал с B-кадрами
|
||
хранится не в том порядке, в каком показывается. На стыке появлялось по
|
||
три кадра с одной меткой, картинка расходилась с эталоном. Без этого
|
||
ключа метки идут шаг в шаг, и ни одной жалобы на непоследовательность.
|
||
|
||
**Двойной щелчок съедает второй щелчок.** Пока у элемента есть
|
||
`csDoubleClicks`, второй щелчок подряд приходит не как `Click`, а как
|
||
`DblClick`, и обработчик кнопки его не видит: человек щёлкает три раза,
|
||
срабатывает один. Лечится двумя строками в конструкторе:
|
||
`ControlStyle := ControlStyle - [csDoubleClicks]` и `DblClick`,
|
||
переопределённый в `Click`, — на случай, если двойной всё же дойдёт.
|
||
|
||
**Всплывающая подсказка перехватывает щелчок.** Окно подсказки в gtk2
|
||
встаёт поверх кнопки, о которой рассказывает, и щелчок попадает в него,
|
||
а не в кнопку. У кнопок в тесной полосе `ShowHint` выключен, а текст
|
||
подсказки идёт в строку состояния (`UseStatusHints` в `uMainForm`).
|
||
|
||
## Консольный инструмент проверок
|
||
|
||
Часть вещей проверяется числами, а не глазами. Сборка: `./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 <проект> <выход>
|
||
./tests/nlvetool syncproj <проект> <видео> монтаж для замера синхронности
|
||
```
|
||
|
||
Команды возвращают ненулевой код при расхождении, поэтому годятся для
|
||
автоматической проверки.
|
||
|
||
Отдельно — замер синхронности звука и картинки:
|
||
|
||
```
|
||
./tests/syncprobe.sh весь прогон: образец, сборка, замер
|
||
./tests/syncprobe.sh measure ФАЙЛ замер готового файла
|
||
```
|
||
|
||
Как он устроен. Делается исходник, где ровно на каждой секунде в кадре
|
||
белая вспышка, а в звуке щелчок. Из него собирается монтаж, у которого
|
||
начала кусков заведомо не попадают на опорные кадры. В готовом ролике
|
||
ищутся времена вспышек и щелчков: если они разошлись — звук уехал от
|
||
картинки, и видно, на сколько и с какого куска.
|
||
|
||
Времена щелчков считаются по самим отсчётам (`tests/clicktimes.py`), а не
|
||
оконным анализом: окно двигается кратно своему размеру, и его собственный
|
||
уход выглядит как накапливающийся рассинхрон, которого нет. На это уже
|
||
попались один раз.
|
||
|
||
Ещё десять проверок гоняют саму программу на невидимом экране
|
||
(`Xvfb :99`). Рабочий экран они не занимают:
|
||
|
||
```
|
||
./tests/buttonclicks.sh <окно> <x> <y> каждый щелчок по кнопке считается
|
||
./tests/zoomanchor.py <окно> масштаб берётся от точки под мышкой
|
||
./tests/genrestart.sh [<путь к nlve>] сборка вставки не сбрасывается зря
|
||
./tests/fadegrab.sh [<путь к nlve>] ручку затухания видно и во второй раз
|
||
./tests/insertat.sh [<путь к nlve>] файл из списка встаёт на курсор
|
||
./tests/dragscroll.sh [<путь к nlve>] монтаж едет следом за клипом у кромки
|
||
./tests/genquiet.sh [<путь к nlve>] тяжёлая сборка ждёт конца правки
|
||
./tests/dropgap.sh [<путь к nlve>] клип садится в дыру без наезда
|
||
./tests/dragorder.sh [<путь к nlve>] связка переехала — порядок цел
|
||
./tests/pickcycle.sh [<путь к nlve>] щелчок перебирает наехавшие клипы
|
||
```
|
||
|
||
Отдельно от них `tests/imagecrop.sh` — сквозная, без окна: делает
|
||
картинку, кладёт её клипом с уменьшением и смещением, собирает секунду
|
||
ролика и смотрит на точки готового кадра. Случая два: класть не на что —
|
||
вокруг картинки чёрные поля; под картинкой видео — вокруг неё видно его,
|
||
а не поля. Ей нужны `tests/nlvetool` (`./build.sh test`), ffmpeg и python
|
||
с PIL.
|
||
|
||
Последним восьми нужен монтаж в `test/поток/`: `длинный-фейд.nlve` — один
|
||
клип с затуханием секунд на семьдесят (такой кусок сводится около
|
||
полутора минут, и всё это время человек продолжает работать),
|
||
`пара.nlve` — два клипа без затуханий. Путь к первому задаётся
|
||
переменной `NLVE_FADE_PROJECT`. В репозиторий они не входят, потому что
|
||
ссылаются на съёмку, которой здесь нет. `genrestart.sh` съёмку
|
||
действительно открывает и, не найдя её, честно говорит об этом и
|
||
отказывается считать себя пройденной; остальным хватает самого монтажа.
|
||
|
||
Экран поднимается через `xvfb-run`, и у него свой файл доступа. Проверки
|
||
находят его сами по строке запуска `Xvfb`, так что отдельно задавать
|
||
`XAUTHORITY` не нужно; чужой файл из окружения они не берут — с ним
|
||
xdotool не видит ни одного окна и проверка «проходит», ничего не
|
||
проверив. `zoomanchor.py` целится мышью в точку 1300, поэтому ему нужен
|
||
экран шире: `Xvfb :98 -screen 0 1600x1000x24` и `ЭКРАН=:98`.
|
||
|
||
Каждую из них стоит прогонять и на прежней сборке: проверка, которая
|
||
проходит везде, не проверяет ничего. Все десять на сборке до своей правки
|
||
падают. Замерено: клип у кромки уезжает на 0.7 с против 4.2 с, файл из
|
||
списка падает в конец дорожки вместо курсора, сборка начинается через
|
||
0.2 с вместо 1.6 с, клип в дыру садится с наездом 0.007383 с, дорожка
|
||
картинки после переноса связки остаётся в порядке 0, 14, 7.991 — не по
|
||
времени, второй щелчок по наезду тащит верхний клип вместо нижнего, а
|
||
картинка с уменьшением 50% выходит в ролик во весь кадр.
|
||
|
||
## Номер сборки
|
||
|
||
В заголовке окна стоит номер: `nlve 42 — фильм.nlve`. Он растёт на
|
||
единицу при каждой сборке редактора (`build.sh`), и по нему сразу видно,
|
||
та ли это сборка, где нужная правка уже есть. Без него проверяют старым
|
||
двоичным файлом, видят прежнее поведение, и время уходит на выяснение,
|
||
у кого что не работает.
|
||
|
||
Счётчик лежит в `build.number`, из него создаётся `src/uBuildInfo.inc`.
|
||
Оба файла в репозиторий не входят: номер про эту машину, а не про
|
||
историю проекта.
|
||
|
||
## Структура
|
||
|
||
| Файл | Что это |
|
||
|---|---|
|
||
| `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` | Предупреждение о несовпадении параметров |
|
||
| `src/uTextForm.pas` | Надпись на клипе: текст и оформление |
|
||
| `src/uTextMeasure.pas` | Точный замер ширины текста для подбора размера |
|
||
| `src/uSubtitles.pas` | Текст поверх картинки: разметка для libass |
|
||
| `src/uColorKeyForm.pas` | Выбор цвета, который станет прозрачным |
|
||
| `src/uEncodeForm.pas` | Настройки сборки с перекодированием |
|
||
| `src/uExportForm.pas` | Куда собирать, в каком контейнере, что с текстом |
|
||
| `src/uAboutForm.pas` | О программе: возможности и чем она обязана чужому |
|
||
| `src/uArticleView.pas` | Страница текста со ссылками |
|
||
| `src/uFfmpegJob.pas` | Один запуск ffmpeg в потоке, с ходом работы и отменой |
|
||
| `src/uIconData.pas` | Значки, вшитые в программу |
|
||
| `build.sh` | Сборка целями |
|
||
| `tests/` | Консольный инструмент проверок |
|
||
| `vendor/mpv-headers/` | Заголовки mpv 0.37.0 — эталон для биндингов |
|
||
|
||
## Чего пока нет
|
||
|
||
**Раздельные звуковые дорожки.** Звук идёт с того же клипа, что и картинка.
|
||
Громкость у клипа своя, но смешивать звук с нескольких дорожек нельзя:
|
||
это потребовало бы второго прохода по всему материалу. Модель данных к
|
||
разделению готова, само разделение не сделано.
|
||
|
||
**Осциллограмма звука на клипах.**
|
||
|
||
**Переходы, эффекты, изменение скорости.** Всё это требует перекодирования и
|
||
в замысел не входит.
|