Compare commits
4 commits
3bfd8bc688
...
6cc9f5e8a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cc9f5e8a8 | |||
| d55fb1b808 | |||
| 0fa9bb6130 | |||
| 6c81fd0907 |
9 changed files with 1146 additions and 84 deletions
46
README.md
46
README.md
|
|
@ -125,6 +125,16 @@ xdg-mime query default application/x-nlve-project
|
|||
он растягивается и укорачивается. Картинку можно растянуть на любую
|
||||
длину, видео — только в пределах исходника.
|
||||
|
||||
Если тащить к краю окна, монтаж едет следом сам: иначе при большом
|
||||
увеличении кусок упирается в край, и его приходится переносить в
|
||||
несколько приёмов — отпустить, прокрутить, взять снова.
|
||||
|
||||
Файл из списка проекта встаёт **на курсор**, а не в конец дорожки. Если
|
||||
место занято, монтаж раздвигается: всё от курсора и правее едет вправо
|
||||
на длину нового клипа, на всех дорожках сразу — звук и надписи обязаны
|
||||
уехать вместе с картинкой. Строка состояния говорит, сколько клипов
|
||||
отодвинуто и на сколько.
|
||||
|
||||
Щелчок по клипу переставляет курсор воспроизведения. Именно щелчок:
|
||||
если кнопку нажали и потянули, курсор остаётся на месте, в том числе во
|
||||
время воспроизведения.
|
||||
|
|
@ -523,6 +533,18 @@ Linux потом искала по нему `.dll`.
|
|||
перекрытие, то в проекте, где клипы на разных дорожках перекрываются
|
||||
постоянно, каждое такое место пришлось бы пересобирать.
|
||||
|
||||
Наезд не остаётся незамеченным: после правки строка состояния называет
|
||||
дорожку, величину перекрытия и сами клипы. Переход заказывают мышью, а
|
||||
промахнуться легко — на общем плане одна точка экрана это секунды, — и
|
||||
раньше он появлялся сюрпризом: человек искал, почему сборка опять не
|
||||
кончается.
|
||||
|
||||
Сама сборка при этом не начинается с первым же движением. Список кусков
|
||||
пересобирается сразу, а ffmpeg запускается, когда монтаж постоял тихо
|
||||
полторы секунды (`GenerateQuietMs`) и клип никто не держит мышью. Иначе
|
||||
переход считался бы заново на каждое движение руки, а посчитанное
|
||||
отправлялось бы в корзину.
|
||||
|
||||
У клипа есть плавное появление и уход. Для картинки это затемнение, для
|
||||
звука — нарастание громкости.
|
||||
|
||||
|
|
@ -988,7 +1010,7 @@ Wine с новой библиотекой не грузилось ничего.
|
|||
уход выглядит как накапливающийся рассинхрон, которого нет. На это уже
|
||||
попались один раз.
|
||||
|
||||
Ещё четыре проверки гоняют саму программу на невидимом экране
|
||||
Ещё семь проверок гоняют саму программу на невидимом экране
|
||||
(`Xvfb :99`). Рабочий экран они не занимают:
|
||||
|
||||
```
|
||||
|
|
@ -996,18 +1018,32 @@ Wine с новой библиотекой не грузилось ничего.
|
|||
./tests/zoomanchor.py <окно> масштаб берётся от точки под мышкой
|
||||
./tests/genrestart.sh [<путь к nlve>] сборка вставки не сбрасывается зря
|
||||
./tests/fadegrab.sh [<путь к nlve>] ручку затухания видно и во второй раз
|
||||
./tests/insertat.sh [<путь к nlve>] файл из списка встаёт на курсор
|
||||
./tests/dragscroll.sh [<путь к nlve>] монтаж едет следом за клипом у кромки
|
||||
./tests/genquiet.sh [<путь к nlve>] тяжёлая сборка ждёт конца правки
|
||||
```
|
||||
|
||||
Двум последним нужен монтаж в `test/поток/`: `длинный-фейд.nlve` — один
|
||||
Последним пяти нужен монтаж в `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 с.
|
||||
|
||||
## Структура
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,11 @@ type
|
|||
её забыли или сохранение не дошло до файла. }
|
||||
FAutoSaveTimer: TTimer;
|
||||
FPreviewDirty: Boolean;
|
||||
{ Когда монтаж правили в последний раз. От этой отметки отсчитывается
|
||||
тишина, после которой можно браться за сборку наложений. }
|
||||
FLastEditTick: QWord;
|
||||
{ Сборка уже отложена — чтобы не повторять сообщение на каждый тик. }
|
||||
FGenDeferred: Boolean;
|
||||
FUpdatingFromPlayer: Boolean;
|
||||
FLastLoadedEdl: string;
|
||||
FLastLoadedAudioEdl: string;
|
||||
|
|
@ -342,6 +347,7 @@ type
|
|||
{ --- предпросмотр --- }
|
||||
procedure PrewarmInserts;
|
||||
procedure SchedulePreview;
|
||||
function GenerateQuietLeft: Integer;
|
||||
procedure PreviewTimerTick(Sender: TObject);
|
||||
{ Есть ли на монтаже файл другого соотношения сторон. Только ради
|
||||
таких файлов и нужен фильтр в предпросмотре. }
|
||||
|
|
@ -361,6 +367,7 @@ type
|
|||
{ --- обработчики компонентов --- }
|
||||
procedure TimelinePlayheadChange(Sender: TObject);
|
||||
procedure TimelineStructureChange(Sender: TObject);
|
||||
procedure ReportOverlaps;
|
||||
procedure TimelineSelectionChange(Sender: TObject);
|
||||
procedure PlayerLog(Sender: TObject; const APrefix, ALevel, AText: string);
|
||||
procedure PlayerTimePos(Sender: TObject);
|
||||
|
|
@ -449,6 +456,17 @@ const
|
|||
предпросмотр играет старую расстановку, а на монтаже уже новая. }
|
||||
PreviewDebounceMs = 120;
|
||||
|
||||
{ Сколько монтаж должен постоять без правок, прежде чем начать считать
|
||||
наложения и переходы.
|
||||
|
||||
Список кусков пересобирается сразу — предпросмотр обязан показывать
|
||||
то, что на монтаже. А вот сборка наложения силами ffmpeg занимает
|
||||
минуты, и начинать её, пока человек подбирает место, незачем: он
|
||||
двинет клип ещё раз, и вся работа уйдёт в корзину. Полторы секунды —
|
||||
это пауза между двумя правками одного движения, но меньше, чем пауза
|
||||
человека, который уже решил. }
|
||||
GenerateQuietMs = 1500;
|
||||
|
||||
{ Сколько сообщений о позиции ждать прибытия после перемотки.
|
||||
Сообщения приходят на каждый кадр, так что это доли секунды. }
|
||||
MaxSeekWaitTicks = 90;
|
||||
|
|
@ -1913,6 +1931,7 @@ var
|
|||
Track: TTrack;
|
||||
Mismatches: TMismatches;
|
||||
Duration, PlaceAt: Double;
|
||||
Moved: Integer;
|
||||
NormPath, Wanted: string;
|
||||
NeedKind: TTrackKind;
|
||||
I: Integer;
|
||||
|
|
@ -2031,18 +2050,39 @@ begin
|
|||
if Duration <= 0 then
|
||||
Duration := DefaultImageDuration;
|
||||
|
||||
{ Ставим туда, куда попросили. Если там занято — в конец дорожки: молча
|
||||
накладывать клипы друг на друга нельзя, порядок перекрытия должен быть
|
||||
осознанным. }
|
||||
PlaceAt := Max(0, ATime);
|
||||
{ Длина кладётся на кадровую сетку, и округляется вниз.
|
||||
|
||||
Раздвижка освобождает целое число кадров, и если длина клипа чуть
|
||||
больше освобождённого, хвост заезжает на соседа на долю кадра — а
|
||||
наезд на одной дорожке это переход. Вниз, а не к ближайшему: просить
|
||||
у файла больше, чем в нём есть, незачем. }
|
||||
if (FProject.Master.FpsNum > 0) and (FProject.Master.FpsDen > 0) then
|
||||
Duration := Int(Duration * FProject.Master.FpsNum /
|
||||
FProject.Master.FpsDen) * FProject.Master.FpsDen /
|
||||
FProject.Master.FpsNum;
|
||||
|
||||
{ Ставим туда, куда попросили.
|
||||
|
||||
Если там занято — раздвигаем монтаж: всё от этой точки и правее едет
|
||||
вправо на длину нового клипа, на всех дорожках сразу. Раньше клип в
|
||||
таком случае падал в конец дорожки, и это был не редкий случай, а
|
||||
обычный: клип длиной в целый файл почти всегда упирается хвостом в
|
||||
то, что правее, даже когда начало легло в свободное место.
|
||||
|
||||
Молча наложить клипы друг на друга по-прежнему нельзя: наезд на
|
||||
одной дорожке — это переход, и заказывать его надо осознанно. }
|
||||
{ Точка вставки — ровно на кадровой сетке. Курсор стоит где угодно, а
|
||||
рез внутри раздвижки округляет время к ближайшему кадру. Положить
|
||||
клип на неокруглённое время значит разойтись с собственным резом на
|
||||
долю кадра: на монтаже это пять миллисекунд наезда, а наезд на одной
|
||||
дорожке — заказ на переход, которого никто не просил. }
|
||||
PlaceAt := FTimeline.QuantizeTime(Max(0, ATime));
|
||||
FProject.BeginChange;
|
||||
Moved := 0;
|
||||
if (Track.ClipAt(PlaceAt) <> nil) or
|
||||
(Track.ClipAt(PlaceAt + Duration - 0.001) <> nil) then
|
||||
begin
|
||||
PlaceAt := Track.EndTime;
|
||||
Log('На дорожке ' + Track.Name + ' в этом месте занято — поставил в конец.');
|
||||
end;
|
||||
Moved := FTimeline.MakeRoomAt(PlaceAt, Duration, False);
|
||||
|
||||
FProject.BeginChange;
|
||||
{ Парой: клип картинки и его звук на соответствующей звуковой дорожке.
|
||||
Связка держит их вместе, пока её не развяжут по Ctrl+G. }
|
||||
FProject.AddClipPair(Track, Item, 0, Duration, PlaceAt);
|
||||
|
|
@ -2050,7 +2090,17 @@ begin
|
|||
SchedulePreview;
|
||||
UpdateCaption;
|
||||
UpdateTimeLabel;
|
||||
SetStatus('Добавлен клип на дорожку ' + Track.Name);
|
||||
if Moved > 0 then
|
||||
begin
|
||||
Log(Format('Место было занято — раздвинул монтаж на %s с, отодвинуто ' +
|
||||
'%d %s.', [SecondsToStr(Duration), Moved,
|
||||
Plural(Moved, 'клип', 'клипа', 'клипов')]));
|
||||
SetStatus(Format('Клип вставлен на дорожку %s, монтаж раздвинут на %s с ' +
|
||||
'(%d %s)', [Track.Name, SecondsToStr(Duration), Moved,
|
||||
Plural(Moved, 'клип', 'клипа', 'клипов')]), True);
|
||||
end
|
||||
else
|
||||
SetStatus('Добавлен клип на дорожку ' + Track.Name, True);
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
|
|
@ -2102,13 +2152,16 @@ end;
|
|||
procedure TMainForm.SchedulePreview;
|
||||
begin
|
||||
FPreviewDirty := True;
|
||||
FLastEditTick := GetTickCount64;
|
||||
FPreviewTimer.Enabled := False;
|
||||
FPreviewTimer.Interval := PreviewDebounceMs;
|
||||
FPreviewTimer.Enabled := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PreviewTimerTick(Sender: TObject);
|
||||
begin
|
||||
FPreviewTimer.Enabled := False;
|
||||
FPreviewTimer.Interval := PreviewDebounceMs;
|
||||
if FPreviewDirty then
|
||||
begin
|
||||
FPreviewDirty := False;
|
||||
|
|
@ -2116,6 +2169,24 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Сколько ещё миллисекунд ждать, прежде чем браться за сборку наложений.
|
||||
|
||||
Ноль означает «можно начинать». Пока клип держат мышью, ответ всегда
|
||||
«ещё подождать»: расстановка в этот момент меняется каждым движением
|
||||
руки, и собранный переход устареет раньше, чем досчитается. }
|
||||
function TMainForm.GenerateQuietLeft: Integer;
|
||||
var
|
||||
Passed: QWord;
|
||||
begin
|
||||
if FTimeline.Dragging then
|
||||
Exit(GenerateQuietMs);
|
||||
Passed := GetTickCount64 - FLastEditTick;
|
||||
if Passed >= GenerateQuietMs then
|
||||
Result := 0
|
||||
else
|
||||
Result := GenerateQuietMs - Integer(Passed);
|
||||
end;
|
||||
|
||||
function TMainForm.PreviewNeedsFitting: Boolean;
|
||||
var
|
||||
T, C: Integer;
|
||||
|
|
@ -2175,7 +2246,7 @@ var
|
|||
Overlays: TPreviewOverlays;
|
||||
GenMaster: TMediaInfo;
|
||||
Missing, TrackMissing: TGenRequests;
|
||||
I, J: Integer;
|
||||
I, J, Wait: Integer;
|
||||
TotalLen: Double;
|
||||
begin
|
||||
if (FPlayer = nil) or (not FPlayer.Initialized) then
|
||||
|
|
@ -2260,6 +2331,24 @@ begin
|
|||
|
||||
if Length(Missing) > 0 then
|
||||
begin
|
||||
{ Сборка наложений ждёт конца правки. Список кусков уже пересобран,
|
||||
монтаж показывает новую расстановку — а минуты работы ffmpeg
|
||||
начинаются только когда стало тихо. }
|
||||
Wait := GenerateQuietLeft;
|
||||
if Wait > 0 then
|
||||
begin
|
||||
if not FGenDeferred then
|
||||
begin
|
||||
FGenDeferred := True;
|
||||
SetStatus('Наложения соберу, когда правка закончится.');
|
||||
end;
|
||||
FPreviewDirty := True;
|
||||
FPreviewTimer.Interval := Wait;
|
||||
FPreviewTimer.Enabled := True;
|
||||
Exit;
|
||||
end;
|
||||
FGenDeferred := False;
|
||||
|
||||
FGenerateBusy := True;
|
||||
SetStatus('Готовлю вставки для предпросмотра...');
|
||||
{ Вставки — картинки и связки — попадают в ролик наравне со съёмкой,
|
||||
|
|
@ -3731,6 +3820,92 @@ begin
|
|||
SchedulePreview;
|
||||
UpdateCaption;
|
||||
UpdateTimeLabel;
|
||||
ReportOverlaps;
|
||||
end;
|
||||
|
||||
{ Говорит в строке состояния, что клипы наехали друг на друга.
|
||||
|
||||
Наезд двух клипов одной дорожки — это заказ на переход, то есть минуты
|
||||
работы ffmpeg и другая картинка на стыке. Заказывают его мышью, и
|
||||
промахнуться легко: при общем плане один пиксель — это секунды. Пока об
|
||||
этом не говорили вслух, переход появлялся как сюрприз, а человек искал,
|
||||
почему сборка опять не кончается. }
|
||||
procedure TMainForm.ReportOverlaps;
|
||||
var
|
||||
T, C, Count: Integer;
|
||||
Track: TTrack;
|
||||
A, B: TClip;
|
||||
Over, Worst: Double;
|
||||
Where, First, What: string;
|
||||
WorstKind: TTrackKind;
|
||||
|
||||
{ Имя клипа для строки состояния. Длинное имя файла режется с головы:
|
||||
у съёмки с камеры различается хвост, а строка состояния узкая, и
|
||||
целое имя выталкивает за край окна главное — на сколько секунд
|
||||
наехали. }
|
||||
function NameOf(AClip: TClip): string;
|
||||
var
|
||||
Item: TMediaItem;
|
||||
begin
|
||||
if (AClip <> nil) and (AClip.Kind = ckText) then
|
||||
Exit('надпись');
|
||||
Item := nil;
|
||||
if AClip <> nil then
|
||||
Item := FProject.FindMedia(AClip.MediaId);
|
||||
if Item = nil then
|
||||
Exit('клип');
|
||||
Result := ExtractFileName(Item.Path);
|
||||
if Length(Result) > 22 then
|
||||
Result := '…' + Copy(Result, Length(Result) - 21, 22);
|
||||
end;
|
||||
|
||||
begin
|
||||
Count := 0;
|
||||
Worst := 0;
|
||||
First := '';
|
||||
Where := '';
|
||||
WorstKind := tkVideo;
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
begin
|
||||
Track := FProject.Tracks[T];
|
||||
for C := 0 to Track.Count - 2 do
|
||||
begin
|
||||
A := Track[C];
|
||||
B := Track[C + 1];
|
||||
{ Полкадра — не наезд, а округление: кадровая сетка не бьётся
|
||||
нацело, и доли кадра набегают сами. }
|
||||
Over := A.TlEnd - B.TlStart;
|
||||
if Over <= 0.001 then
|
||||
Continue;
|
||||
Inc(Count);
|
||||
if Over > Worst then
|
||||
begin
|
||||
Worst := Over;
|
||||
First := NameOf(A) + ' и ' + NameOf(B);
|
||||
Where := Track.Name;
|
||||
WorstKind := Track.Kind;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Count = 0 then
|
||||
Exit;
|
||||
|
||||
{ Наезд на звуковой дорожке переходом не становится: там два клипа
|
||||
просто звучат вместе. Обещать переход значило бы врать. }
|
||||
if WorstKind = tkAudio then
|
||||
What := 'звук здесь смешается'
|
||||
else
|
||||
What := 'здесь соберётся переход';
|
||||
|
||||
{ Сначала величина и место, потом имена: строка обрывается по ширине
|
||||
окна, и обрываться должно наименее важное. }
|
||||
if Count = 1 then
|
||||
SetStatus(Format('Наезд на %s: %s с, %s (%s)',
|
||||
[Where, SecondsToStr(Worst), What, First]), True)
|
||||
else
|
||||
SetStatus(Format('Наездов: %d, самый большой на %s: %s с, %s (%s)',
|
||||
[Count, Where, SecondsToStr(Worst), What, First]), True);
|
||||
end;
|
||||
|
||||
procedure TMainForm.TimelineSelectionChange(Sender: TObject);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ interface
|
|||
|
||||
uses
|
||||
Classes, SysUtils, Controls, Graphics, LCLType, LCLIntf, Forms, Math,
|
||||
uAppTypes, uProject, uMediaProbe, uKeyframeIndex, uWaveform;
|
||||
ExtCtrls, uAppTypes, uProject, uMediaProbe, uKeyframeIndex, uWaveform;
|
||||
|
||||
type
|
||||
{ Что сейчас делает мышь. }
|
||||
|
|
@ -170,6 +170,16 @@ type
|
|||
FOnTrackRename: TTrackRenameEvent;
|
||||
{ Дорожка, которую тащат за заголовок. }
|
||||
FDragTrackIndex: Integer;
|
||||
{ Прокрутка следом за указателем, пока тащат клип или край у самой
|
||||
кромки окна. Таймер нужен потому, что рука у края останавливается:
|
||||
без него прокрутка шла бы только пока мышь движется. }
|
||||
FEdgeTimer: TTimer;
|
||||
FEdgeX: Integer;
|
||||
FEdgeY: Integer;
|
||||
{ Клавиши, зажатые при последнем движении мыши. Тик повторяет
|
||||
движение за человека, и Shift при этом обязан остаться зажатым:
|
||||
он отменяет прилипание, и без него край прыгал бы между тиками. }
|
||||
FEdgeShift: TShiftState;
|
||||
FDragTrackFrom: Integer;
|
||||
FOnMediaDrop: TMediaDropEvent;
|
||||
FOnClipTransform: TClipEvent;
|
||||
|
|
@ -244,13 +254,13 @@ type
|
|||
procedure DrawVolume(ACanvas: TCanvas; AClip: TClip; ATop: Integer;
|
||||
const ABody: TRect);
|
||||
|
||||
function GetDragging: Boolean;
|
||||
procedure SetWaveGain(const AValue: Double);
|
||||
procedure SetPlayhead(const AValue: Double);
|
||||
procedure SetPixelsPerSecond(const AValue: Double);
|
||||
procedure ClampScroll;
|
||||
procedure ClampScrollY;
|
||||
function TracksBottom: Integer;
|
||||
function SnapTime(const ATime: Double; AExclude: TClip): Double;
|
||||
function MediaDurationOf(AClip: TClip): Double;
|
||||
procedure StructureChanged;
|
||||
{ Помечает край клипа точным, если он не попал на опорный кадр.
|
||||
|
|
@ -260,6 +270,17 @@ type
|
|||
сделать не то, о чём просили. }
|
||||
procedure UpdateEdgePrecision(AClip: TClip);
|
||||
{ Составляет список клипов правее перетаскиваемого. }
|
||||
{ Перетаскивание клипа в указанную точку. Вынесено из обработчика
|
||||
движения мыши: то же самое повторяет таймер прокрутки, пока
|
||||
указатель стоит у края. }
|
||||
procedure DragClipTo(const AX, AY: Integer);
|
||||
{ Прокрутка, если указатель у кромки. Возвращает True, если
|
||||
прокрутили. }
|
||||
function ScrollByEdge(const AX: Integer): Boolean;
|
||||
procedure EdgeTimerTick(Sender: TObject);
|
||||
{ Включает и выключает слежение за кромкой. }
|
||||
procedure SetEdgeScroll(AOn: Boolean; const AX: Integer = 0;
|
||||
const AY: Integer = 0; const AShift: TShiftState = []);
|
||||
procedure CollectRippleClips(ATrackIndex: Integer; AExclude: TClip;
|
||||
const AFromTime: Double);
|
||||
procedure ApplyRipple(const ADelta: Double);
|
||||
|
|
@ -280,6 +301,9 @@ type
|
|||
procedure SetSelectionMode(AValue: TSelectionMode);
|
||||
{ Рез в указанном месте. Возвращает, сколько клипов разрезано. }
|
||||
function SplitAt(const ATime: Double; APrecise: Boolean): Integer;
|
||||
{ Рез без снимка для отмены и без сообщения о правке: для тех, кто
|
||||
режет внутри более крупного действия. }
|
||||
function SplitAtCore(const ATime: Double; APrecise: Boolean): Integer;
|
||||
protected
|
||||
procedure Paint; override;
|
||||
procedure DragOver(Source: TObject; X, Y: Integer; State: TDragState;
|
||||
|
|
@ -332,6 +356,28 @@ type
|
|||
притянется к опорному кадру, и точка съедет на величину до
|
||||
расстояния между опорными кадрами. }
|
||||
function SplitAtPlayhead(APrecise: Boolean): Integer;
|
||||
{ Раздвигает монтаж: освобождает ALength секунд начиная с ATime.
|
||||
|
||||
Нужна вставке клипа в место курсора. Клип, накрывающий точку
|
||||
вставки, разрезается — иначе перед ним осталась бы дыра. Всё, что
|
||||
начинается с этой точки и правее, едет вправо, на всех дорожках
|
||||
сразу: звук и надписи обязаны уехать вместе с картинкой, иначе
|
||||
синхрон рассыплется.
|
||||
|
||||
ASnapshot — делать ли снимок для отмены. False означает, что
|
||||
вызывающий уже сделал свой и раздвижка — часть его действия.
|
||||
|
||||
Возвращает, сколько клипов отодвинуто. }
|
||||
function MakeRoomAt(const ATime, ALength: Double;
|
||||
ASnapshot: Boolean = True): Integer;
|
||||
{ Куда притянется время ATime: к нулю, к курсору или к краю соседнего
|
||||
клипа, если они ближе SnapPixels.
|
||||
|
||||
AExclude — клип, который сейчас двигают: сам к себе он не липнет.
|
||||
ALength — его длина. Задана — кандидатами становятся и положения,
|
||||
при которых на границу садится хвост клипа, а не голова. }
|
||||
function SnapTime(const ATime: Double; AExclude: TClip;
|
||||
const ALength: Double = 0): Double;
|
||||
procedure DeleteSelected;
|
||||
{ Связать выделенные клипы или развязать, если они уже связаны.
|
||||
|
||||
|
|
@ -390,6 +436,11 @@ type
|
|||
для строки состояния: дыра могла найтись не на той дорожке, где
|
||||
стоял указатель, и молча двигать чужую дорожку нельзя. }
|
||||
property LastGapTrack: string read FLastGapTrack;
|
||||
{ Правка идёт прямо сейчас: клип держат мышью, тянут за край или за
|
||||
ручку. Окно по этому признаку откладывает тяжёлую сборку — считать
|
||||
наложение, пока расстановка меняется каждым движением руки, значит
|
||||
считать впустую. }
|
||||
property Dragging: Boolean read GetDragging;
|
||||
{ Округление к кадровой сетке проекта: между кадрами времени нет. }
|
||||
function QuantizeTime(const ATime: Double): Double;
|
||||
|
||||
|
|
@ -535,6 +586,12 @@ begin
|
|||
FPlayheadSnap := True;
|
||||
FDragTrackIndex := -1;
|
||||
FDragTrackFrom := -1;
|
||||
|
||||
FEdgeTimer := TTimer.Create(Self);
|
||||
FEdgeTimer.Enabled := False;
|
||||
{ Шаг мельче кадра: прокрутка должна течь, а не прыгать. }
|
||||
FEdgeTimer.Interval := 30;
|
||||
FEdgeTimer.OnTimer := @EdgeTimerTick;
|
||||
end;
|
||||
|
||||
destructor TTimelineControl.Destroy;
|
||||
|
|
@ -1759,6 +1816,14 @@ begin
|
|||
Invalidate;
|
||||
end;
|
||||
|
||||
{ Прокрутка и протяжка курсора правкой не считаются: монтаж при них не
|
||||
меняется, и откладывать из-за них нечего. }
|
||||
function TTimelineControl.GetDragging: Boolean;
|
||||
begin
|
||||
Result := FAction in [taDragClip, taTrimIn, taTrimOut, taVolume,
|
||||
taFadeIn, taFadeOut, taDragTrack];
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.ClampScroll;
|
||||
var
|
||||
MaxTime: Double;
|
||||
|
|
@ -1913,7 +1978,8 @@ begin
|
|||
Result := Round(ATime / Step) * Step;
|
||||
end;
|
||||
|
||||
function TTimelineControl.SnapTime(const ATime: Double; AExclude: TClip): Double;
|
||||
function TTimelineControl.SnapTime(const ATime: Double; AExclude: TClip;
|
||||
const ALength: Double): Double;
|
||||
var
|
||||
T, C: Integer;
|
||||
Track: TTrack;
|
||||
|
|
@ -1930,6 +1996,19 @@ var
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Граница притягивает клип обоими концами. Передвигаемое — начало, но
|
||||
встать вплотную человек хочет чаще хвостом: сдвинуть кусок влево,
|
||||
чтобы он кончился ровно там, где начинается следующий. Кандидат на
|
||||
такое положение — «граница минус длина клипа». Без него клип встаёт
|
||||
началом в дыру, а хвостом наезжает на соседа, и наезд молча
|
||||
превращается в переход. }
|
||||
procedure ConsiderEdge(const ACandidate: Double);
|
||||
begin
|
||||
Consider(ACandidate);
|
||||
if ALength > 0 then
|
||||
Consider(ACandidate - ALength);
|
||||
end;
|
||||
|
||||
begin
|
||||
Result := ATime;
|
||||
if FProject = nil then
|
||||
|
|
@ -1945,7 +2024,7 @@ begin
|
|||
Best := ATime;
|
||||
|
||||
Consider(0);
|
||||
Consider(FPlayhead);
|
||||
ConsiderEdge(FPlayhead);
|
||||
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
begin
|
||||
|
|
@ -1960,8 +2039,8 @@ begin
|
|||
клип цепляться за собственное следствие. }
|
||||
if IsRippling(Clip) then
|
||||
Continue;
|
||||
Consider(Clip.TlStart);
|
||||
Consider(Clip.TlEnd);
|
||||
ConsiderEdge(Clip.TlStart);
|
||||
ConsiderEdge(Clip.TlEnd);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
|
@ -2366,6 +2445,11 @@ begin
|
|||
Exit;
|
||||
end;
|
||||
|
||||
{ Пока тащат клип или край, следим за кромкой окна: у края монтаж
|
||||
едет следом сам, даже если рука остановилась. }
|
||||
SetEdgeScroll(FAction in [taDragClip, taTrimIn, taTrimOut, taRubberBand],
|
||||
X, Y, Shift);
|
||||
|
||||
case FAction of
|
||||
taPan:
|
||||
begin
|
||||
|
|
@ -2454,59 +2538,7 @@ begin
|
|||
end;
|
||||
|
||||
taDragClip:
|
||||
if FDragClip <> nil then
|
||||
begin
|
||||
NewStart := QuantizeTime(SnapTime(XToTime(X) - FDragGrabOffset,
|
||||
FDragClip));
|
||||
FDragClip.TlStart := Max(0, NewStart);
|
||||
{ Связка едет целиком: звук должен остаться под своей картинкой. }
|
||||
ApplyGroupMove(FDragClip.TlStart - FDragOrigStart);
|
||||
{ Всё, что правее, едет на ту же величину. }
|
||||
if Length(FRippleClips) > 0 then
|
||||
ApplyRipple(FDragClip.TlStart - FDragOrigStart);
|
||||
|
||||
{ Утащили ниже последней дорожки — заводим новую, как в Vegas.
|
||||
За одно перетаскивание не больше одной: иначе движение мыши
|
||||
вниз наплодило бы их десяток. Вид новой дорожки — по клипу:
|
||||
картинку на звуковую дорожку класть незачем. }
|
||||
if (Idx < 0) and (FProject <> nil) and (Y > TracksBottom) and
|
||||
(not FTrackAddedDuringDrag) then
|
||||
begin
|
||||
case TProject.TrackKindFor(FDragClip.Kind) of
|
||||
tkAudio: FProject.AddTrackOfKind(FProject.UniqueTrackName('A'), tkAudio);
|
||||
tkText: FProject.AddTrackOfKind(FProject.UniqueTrackName('Т'), tkText);
|
||||
else
|
||||
FProject.AddTrack(FProject.UniqueTrackName('V'));
|
||||
end;
|
||||
FTrackAddedDuringDrag := True;
|
||||
Idx := FProject.TrackCount - 1;
|
||||
if Assigned(FOnTrackAdded) then
|
||||
FOnTrackAdded(Self);
|
||||
end;
|
||||
|
||||
{ Перенос на другую дорожку: клип следует за указателем по
|
||||
вертикали. Именно перенос, а не копирование.
|
||||
|
||||
На дорожку чужого вида клип не пускаем. Уплощение обходит
|
||||
дорожки по видам, и клип, попавший не на свою, пропадает и из
|
||||
сборки, и из предпросмотра — молча: на монтаже он нарисован, а
|
||||
в ролике его нет. Проверено на живом проекте, где фотография
|
||||
оказалась на текстовой дорожке. }
|
||||
if (Idx >= 0) and (FProject <> nil) then
|
||||
begin
|
||||
TargetTrack := FProject.Tracks[Idx];
|
||||
if (TargetTrack <> FDragTrack) and
|
||||
(TargetTrack.Kind = TProject.TrackKindFor(FDragClip.Kind)) then
|
||||
begin
|
||||
FDragTrack.Extract(FDragClip);
|
||||
TargetTrack.Add(FDragClip);
|
||||
FDragTrack := TargetTrack;
|
||||
end;
|
||||
end;
|
||||
|
||||
FDragTrack.Sort;
|
||||
Invalidate;
|
||||
end;
|
||||
DragClipTo(X, Y);
|
||||
|
||||
taTrimIn:
|
||||
if FDragClip <> nil then
|
||||
|
|
@ -2689,6 +2721,146 @@ begin
|
|||
Max(0, QuantizeTime(FRippleOrigStarts[I] + ADelta));
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.DragClipTo(const AX, AY: Integer);
|
||||
var
|
||||
NewStart: Double;
|
||||
Idx: Integer;
|
||||
TargetTrack: TTrack;
|
||||
X, Y: Integer;
|
||||
begin
|
||||
X := AX;
|
||||
Y := AY;
|
||||
Idx := TrackIndexAt(Y);
|
||||
if FDragClip <> nil then
|
||||
begin
|
||||
NewStart := QuantizeTime(SnapTime(XToTime(X) - FDragGrabOffset,
|
||||
FDragClip, FDragClip.Duration));
|
||||
FDragClip.TlStart := Max(0, NewStart);
|
||||
{ Связка едет целиком: звук должен остаться под своей картинкой. }
|
||||
ApplyGroupMove(FDragClip.TlStart - FDragOrigStart);
|
||||
{ Всё, что правее, едет на ту же величину. }
|
||||
if Length(FRippleClips) > 0 then
|
||||
ApplyRipple(FDragClip.TlStart - FDragOrigStart);
|
||||
|
||||
{ Утащили ниже последней дорожки — заводим новую, как в Vegas.
|
||||
За одно перетаскивание не больше одной: иначе движение мыши
|
||||
вниз наплодило бы их десяток. Вид новой дорожки — по клипу:
|
||||
картинку на звуковую дорожку класть незачем. }
|
||||
if (Idx < 0) and (FProject <> nil) and (Y > TracksBottom) and
|
||||
(not FTrackAddedDuringDrag) then
|
||||
begin
|
||||
case TProject.TrackKindFor(FDragClip.Kind) of
|
||||
tkAudio: FProject.AddTrackOfKind(FProject.UniqueTrackName('A'), tkAudio);
|
||||
tkText: FProject.AddTrackOfKind(FProject.UniqueTrackName('Т'), tkText);
|
||||
else
|
||||
FProject.AddTrack(FProject.UniqueTrackName('V'));
|
||||
end;
|
||||
FTrackAddedDuringDrag := True;
|
||||
Idx := FProject.TrackCount - 1;
|
||||
if Assigned(FOnTrackAdded) then
|
||||
FOnTrackAdded(Self);
|
||||
end;
|
||||
|
||||
{ Перенос на другую дорожку: клип следует за указателем по
|
||||
вертикали. Именно перенос, а не копирование.
|
||||
|
||||
На дорожку чужого вида клип не пускаем. Уплощение обходит
|
||||
дорожки по видам, и клип, попавший не на свою, пропадает и из
|
||||
сборки, и из предпросмотра — молча: на монтаже он нарисован, а
|
||||
в ролике его нет. Проверено на живом проекте, где фотография
|
||||
оказалась на текстовой дорожке. }
|
||||
if (Idx >= 0) and (FProject <> nil) then
|
||||
begin
|
||||
TargetTrack := FProject.Tracks[Idx];
|
||||
if (TargetTrack <> FDragTrack) and
|
||||
(TargetTrack.Kind = TProject.TrackKindFor(FDragClip.Kind)) then
|
||||
begin
|
||||
FDragTrack.Extract(FDragClip);
|
||||
TargetTrack.Add(FDragClip);
|
||||
FDragTrack := TargetTrack;
|
||||
end;
|
||||
end;
|
||||
|
||||
FDragTrack.Sort;
|
||||
Invalidate;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Прокрутка за кромкой окна.
|
||||
|
||||
Тянут клип к правому краю — монтаж едет следом, иначе при большом
|
||||
увеличении кусок не вытащить дальше того, что видно. Скорость зависит
|
||||
от того, насколько глубоко указатель в кромке: у самого края быстро, на
|
||||
её границе еле-еле. Так прокруткой можно управлять, не отпуская клип.
|
||||
|
||||
Вне кромки не делается ничего: пока человек возит клип посередине
|
||||
окна, монтаж обязан стоять. }
|
||||
function TTimelineControl.ScrollByEdge(const AX: Integer): Boolean;
|
||||
const
|
||||
{ Ширина чувствительной полосы и предел скорости. }
|
||||
EdgeMargin = 48;
|
||||
MaxStepPixels = 24;
|
||||
var
|
||||
Depth, Step, Before: Double;
|
||||
begin
|
||||
Result := False;
|
||||
if (FProject = nil) or (FPixelsPerSecond <= 0) then
|
||||
Exit;
|
||||
|
||||
Depth := 0;
|
||||
if AX < FHeaderWidth + EdgeMargin then
|
||||
Depth := -(FHeaderWidth + EdgeMargin - AX)
|
||||
else if AX > ClientWidth - EdgeMargin then
|
||||
Depth := AX - (ClientWidth - EdgeMargin);
|
||||
if Depth = 0 then
|
||||
Exit;
|
||||
|
||||
{ Глубже кромки скорость не растёт: указатель легко уходит за окно на
|
||||
сотни точек, и без предела монтаж улетал бы рывком. }
|
||||
Step := Max(-EdgeMargin, Min(EdgeMargin, Depth)) / EdgeMargin;
|
||||
Before := FScrollOffset;
|
||||
FScrollOffset := FScrollOffset + Step * MaxStepPixels / FPixelsPerSecond;
|
||||
ClampScroll;
|
||||
Result := Abs(FScrollOffset - Before) > 1E-9;
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.EdgeTimerTick(Sender: TObject);
|
||||
begin
|
||||
if not (FAction in [taDragClip, taTrimIn, taTrimOut, taRubberBand]) then
|
||||
begin
|
||||
SetEdgeScroll(False);
|
||||
Exit;
|
||||
end;
|
||||
if not ScrollByEdge(FEdgeX) then
|
||||
Exit;
|
||||
|
||||
{ Монтаж уехал, а указатель стоит — значит под ним теперь другое время,
|
||||
и клип должен последовать за ним. }
|
||||
case FAction of
|
||||
taDragClip: DragClipTo(FEdgeX, FEdgeY);
|
||||
taRubberBand:
|
||||
begin
|
||||
FBandToTime := XToTime(Max(FHeaderWidth, Min(ClientWidth, FEdgeX)));
|
||||
Invalidate;
|
||||
end;
|
||||
else
|
||||
{ У краёв клипа своё движение, но пересчитывается оно там же, где и
|
||||
от мыши: повторяем событие с теми же координатами и теми же
|
||||
зажатыми клавишами. }
|
||||
MouseMove(FEdgeShift, FEdgeX, FEdgeY);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.SetEdgeScroll(AOn: Boolean; const AX: Integer;
|
||||
const AY: Integer; const AShift: TShiftState);
|
||||
begin
|
||||
FEdgeX := AX;
|
||||
FEdgeY := AY;
|
||||
FEdgeShift := AShift;
|
||||
if FEdgeTimer.Enabled <> AOn then
|
||||
FEdgeTimer.Enabled := AOn;
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.MouseUp(Button: TMouseButton; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var
|
||||
|
|
@ -2769,6 +2941,8 @@ begin
|
|||
Exit;
|
||||
end;
|
||||
|
||||
SetEdgeScroll(False);
|
||||
|
||||
WasEditing := FAction in [taDragClip, taTrimIn, taTrimOut, taVolume,
|
||||
taFadeIn, taFadeOut];
|
||||
WasTrimIn := FAction = taTrimIn;
|
||||
|
|
@ -2869,6 +3043,81 @@ end;
|
|||
больше полкадра. }
|
||||
function TTimelineControl.SplitAt(const ATime: Double;
|
||||
APrecise: Boolean): Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
if FProject = nil then
|
||||
Exit;
|
||||
|
||||
FProject.BeginChange;
|
||||
Result := SplitAtCore(ATime, APrecise);
|
||||
if Result > 0 then
|
||||
StructureChanged
|
||||
else
|
||||
{ Ничего не разрезали — снимок отмены не нужен, откатываем его. }
|
||||
FProject.Undo;
|
||||
end;
|
||||
|
||||
function TTimelineControl.MakeRoomAt(const ATime, ALength: Double;
|
||||
ASnapshot: Boolean): Integer;
|
||||
var
|
||||
T, C: Integer;
|
||||
Track: TTrack;
|
||||
Clip: TClip;
|
||||
At, Half, Shift: Double;
|
||||
begin
|
||||
Result := 0;
|
||||
if (FProject = nil) or (ALength <= 0) then
|
||||
Exit;
|
||||
|
||||
At := QuantizeTime(ATime);
|
||||
{ Сдвиг округляется один раз, и дальше складывается с началами как
|
||||
есть. Округляй мы каждое начало по отдельности — клипы, стоявшие
|
||||
встык, разъехались бы на доли кадра: каждое слагаемое село бы на свой
|
||||
ближайший кадр. Проверено числами: встык стоявшие 20.000 и 30.000
|
||||
превращались в 20.003 и 25.008, и между ними появлялась дыра. }
|
||||
Shift := QuantizeTime(ALength);
|
||||
if Shift <= 0 then
|
||||
Exit;
|
||||
if ASnapshot then
|
||||
FProject.BeginChange;
|
||||
|
||||
{ Сперва рез, потом сдвиг. В обратном порядке клип, накрывающий точку
|
||||
вставки, уехал бы целиком, и перед ним осталась бы дыра в его
|
||||
собственную длину. }
|
||||
SplitAtCore(At, False);
|
||||
|
||||
{ Полкадра допуска: клип, начинающийся ровно в точке вставки, двигать
|
||||
надо. Сравнение с точностью до доли кадра отнесло бы его то к одной
|
||||
стороне, то к другой — в зависимости от округления. }
|
||||
Half := 0.001;
|
||||
if (FProject.Master.FpsNum > 0) and (FProject.Master.FpsDen > 0) then
|
||||
Half := FProject.Master.FpsDen / FProject.Master.FpsNum / 2;
|
||||
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
begin
|
||||
Track := FProject.Tracks[T];
|
||||
for C := 0 to Track.Count - 1 do
|
||||
begin
|
||||
Clip := Track[C];
|
||||
if Clip.TlStart < At - Half then
|
||||
Continue;
|
||||
Clip.TlStart := Clip.TlStart + Shift;
|
||||
Inc(Result);
|
||||
end;
|
||||
Track.Sort;
|
||||
end;
|
||||
|
||||
if Result > 0 then
|
||||
FProject.Modified := True;
|
||||
end;
|
||||
|
||||
{ Сам рез, без снимка для отмены и без сообщения о правке.
|
||||
|
||||
Отдельно от SplitAt он нужен раздвижке монтажа: она режет, двигает и
|
||||
кладёт клип — и всё это одно действие, с одним снимком и одним
|
||||
сообщением в конце. }
|
||||
function TTimelineControl.SplitAtCore(const ATime: Double;
|
||||
APrecise: Boolean): Integer;
|
||||
var
|
||||
T: Integer;
|
||||
Track: TTrack;
|
||||
|
|
@ -2884,7 +3133,6 @@ begin
|
|||
Exit;
|
||||
|
||||
At := QuantizeTime(ATime);
|
||||
FProject.BeginChange;
|
||||
NewGroups := TStringList.Create;
|
||||
try
|
||||
|
||||
|
|
@ -2960,12 +3208,6 @@ begin
|
|||
finally
|
||||
NewGroups.Free;
|
||||
end;
|
||||
|
||||
if Result > 0 then
|
||||
StructureChanged
|
||||
else
|
||||
{ Ничего не разрезали — снимок отмены не нужен, откатываем его. }
|
||||
FProject.Undo;
|
||||
end;
|
||||
|
||||
{ Режим выделения: рамкой по клипам или промежутком по времени.
|
||||
|
|
|
|||
143
tests/dragscroll.sh
Executable file
143
tests/dragscroll.sh
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
#!/bin/sh
|
||||
# Монтаж едет следом, пока тащишь клип у кромки окна.
|
||||
#
|
||||
# При большом увеличении видно секунды, а двигать кусок надо на минуты.
|
||||
# Если монтаж не прокручивается, клип упирается в край окна и дальше не
|
||||
# идёт: приходится отпускать, прокручивать, брать снова — и так десятки
|
||||
# раз.
|
||||
#
|
||||
# Проверяется так: монтаж из одного клипа, сильное увеличение, клип
|
||||
# тащится к правому краю и там удерживается на месте. Рука стоит, значит
|
||||
# двигать может только прокрутка. Результат читается из сохранённого
|
||||
# проекта, а не с картинки.
|
||||
#
|
||||
# Запуск (экран :99 поднят):
|
||||
# tests/dragscroll.sh [<путь к nlve>]
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
BIN=${1:-$PWD/nlve}
|
||||
DISP=${NLVE_DISPLAY:-:99}
|
||||
SRC=$PWD/test/поток/пара.nlve
|
||||
PROJ=$PWD/test/поток/прокрутка.nlve
|
||||
|
||||
[ -f "$SRC" ] || { echo "нет монтажа $SRC"; exit 2; }
|
||||
|
||||
# Невидимый экран поднят через xvfb-run, у него свой файл доступа, а в
|
||||
# окружении обычно лежит файл рабочего экрана. С чужим файлом xdotool не
|
||||
# видит ни одного окна, и проверка «проходит» вслепую, ничего не проверив.
|
||||
export DISPLAY=$DISP
|
||||
reachable() { timeout 5 xdotool getdisplaygeometry >/dev/null 2>&1; }
|
||||
if ! reachable; then
|
||||
for p in $(pgrep -x Xvfb); do
|
||||
A=$(tr '\0' '\n' < /proc/$p/cmdline | grep -A1 -x -- -auth | tail -1)
|
||||
[ -f "$A" ] || continue
|
||||
XAUTHORITY=$A; export XAUTHORITY
|
||||
reachable && break
|
||||
done
|
||||
fi
|
||||
reachable ||
|
||||
{ echo "нет доступа к экрану $DISP: подними его (xvfb-run) и повтори"; exit 2; }
|
||||
|
||||
# Монтаж из одного клипа: второй убран, чтобы перетаскивание ни на кого
|
||||
# не наезжало и не заказывало переход.
|
||||
python3 - "$SRC" "$PROJ" <<'PY'
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
keep = None
|
||||
for t in d['tracks']:
|
||||
for c in t['clips']:
|
||||
if c['kind'] == 'video' and c['tl_start'] < 0.001:
|
||||
keep = c.get('group')
|
||||
for t in d['tracks']:
|
||||
t['clips'] = [c for c in t['clips'] if c.get('group') == keep]
|
||||
json.dump(d, open(sys.argv[2], 'w'), ensure_ascii=False)
|
||||
PY
|
||||
|
||||
for p in $(pgrep -x nlve); do
|
||||
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
|
||||
done
|
||||
sleep 1
|
||||
rm -rf test/поток/профиль-прокрутка
|
||||
mkdir -p test/поток/профиль-прокрутка
|
||||
rm -f "$PROJ.autosave"
|
||||
SP=$PWD/test/поток/профиль-прокрутка
|
||||
XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP DISPLAY=$DISP \
|
||||
setsid "$BIN" "$PROJ" >/dev/null 2>&1 < /dev/null &
|
||||
sleep 14
|
||||
|
||||
X="timeout 8 xdotool"
|
||||
# Окно ищем по имени монтажа и проверяем, что оно на экране. По одному
|
||||
# «nlve» находятся и обломки прошлых запусков: они невидимы, нажатия в
|
||||
# них уходят в никуда, и проверка молча показывает «ничего не двинулось».
|
||||
WID=
|
||||
for w in $($X search --name "прокрутка.nlve" 2>/dev/null); do
|
||||
xwininfo -id "$w" 2>/dev/null | grep -q IsViewable && { WID=$w; break; }
|
||||
done
|
||||
[ -n "$WID" ] || { echo "живое окно редактора не найдено"; exit 2; }
|
||||
# Экран невидимый и узкий: окно должно влезть целиком, иначе правый край
|
||||
# окажется за границей экрана и указатель до кромки не доедет.
|
||||
$X windowmove "$WID" 0 0 windowsize "$WID" 1270 990
|
||||
sleep 2
|
||||
|
||||
start() {
|
||||
$X key ctrl+s
|
||||
sleep 2
|
||||
python3 -c "
|
||||
import json
|
||||
d = json.load(open('$PROJ'))
|
||||
for t in d['tracks']:
|
||||
for c in t['clips']:
|
||||
if c['kind'] == 'video':
|
||||
print('%.3f' % c['tl_start']); raise SystemExit
|
||||
print('0')
|
||||
"
|
||||
}
|
||||
|
||||
echo " начало клипа до правок: $(start) с"
|
||||
|
||||
ROW=${NLVE_ROW:-505} # высота видеодорожки в окне
|
||||
GRAB=${NLVE_GRAB:-650} # точка захвата внутри клипа (клип начинается с 588)
|
||||
EDGE=1255 # кромка у правого края окна
|
||||
|
||||
# Увеличение: шесть щелчков колесом с Ctrl, опора в точке захвата, чтобы
|
||||
# клип из-под указателя не уехал.
|
||||
I=0
|
||||
while [ $I -lt 6 ]; do
|
||||
$X mousemove $GRAB $ROW
|
||||
$X keydown ctrl click 4 keyup ctrl
|
||||
sleep 0.3
|
||||
I=$((I + 1))
|
||||
done
|
||||
sleep 1
|
||||
|
||||
# Берём клип и тащим к правому краю, где и держим: рука стоит, едет
|
||||
# только прокрутка.
|
||||
$X mousemove $GRAB $ROW mousedown 1
|
||||
sleep 0.3
|
||||
$X mousemove 800 $ROW
|
||||
sleep 0.3
|
||||
$X mousemove $EDGE $ROW
|
||||
sleep 6
|
||||
$X mouseup 1
|
||||
sleep 2
|
||||
|
||||
MOVED=$(start)
|
||||
echo " начало клипа после перетаскивания с удержанием: $MOVED с"
|
||||
|
||||
for p in $(pgrep -x nlve); do
|
||||
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
|
||||
done
|
||||
rm -f "$PROJ.autosave"
|
||||
|
||||
python3 -c "
|
||||
moved = $MOVED
|
||||
# После шести щелчков колесом в окне видно около полутора секунд монтажа.
|
||||
# Указатель доезжает до кромки и встаёт, поэтому без прокрутки клип
|
||||
# уезжает на эти полторы секунды и больше никуда. С прокруткой за шесть
|
||||
# секунд удержания набегает втрое больше. Порог стоит между измеренными
|
||||
# величинами: 1.1 с на сборке без прокрутки, 3+ с с прокруткой.
|
||||
if moved < 2.5:
|
||||
print(' ПЛОХО: клип уехал всего на %.3f с — монтаж не прокручивался' % moved)
|
||||
raise SystemExit(1)
|
||||
print(' ok: клип уехал на %.3f с, дальше видимой части' % moved)
|
||||
"
|
||||
117
tests/genquiet.sh
Executable file
117
tests/genquiet.sh
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
#!/bin/sh
|
||||
# Тяжёлая сборка начинается не сразу, а когда правка закончилась.
|
||||
#
|
||||
# Наезд двух клипов на одной дорожке — это заказ на переход, и считается
|
||||
# он минутами. Пока человек подбирает место, заказ меняется каждым
|
||||
# движением руки, и начатая работа уходит в корзину. Поэтому список
|
||||
# кусков пересобирается сразу (монтаж обязан показывать правду), а вот
|
||||
# ffmpeg запускается, когда монтаж постоял тихо.
|
||||
#
|
||||
# Проверяется так: клип тащится на соседа, создавая наезд. С момента,
|
||||
# когда кнопку отпустили, считается время до первой команды ffmpeg в
|
||||
# журнале. Оно обязано быть близко к заданной тишине, но работа всё-таки
|
||||
# обязана начаться — отложить не значит забыть.
|
||||
#
|
||||
# Журнал только читается: усечь его значит снести запись чужой работы.
|
||||
#
|
||||
# Запуск (экран :99 поднят):
|
||||
# tests/genquiet.sh [<путь к nlve>]
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
BIN=${1:-$PWD/nlve}
|
||||
DISP=${NLVE_DISPLAY:-:99}
|
||||
SRC=$PWD/test/поток/пара.nlve
|
||||
PROJ=$PWD/test/поток/тишина.nlve
|
||||
LOG=$(dirname "$BIN")/nlve.log
|
||||
|
||||
[ -f "$SRC" ] || { echo "нет монтажа $SRC"; exit 2; }
|
||||
|
||||
export DISPLAY=$DISP
|
||||
reachable() { timeout 5 xdotool getdisplaygeometry >/dev/null 2>&1; }
|
||||
if ! reachable; then
|
||||
for p in $(pgrep -x Xvfb); do
|
||||
A=$(tr '\0' '\n' < /proc/$p/cmdline | grep -A1 -x -- -auth | tail -1)
|
||||
[ -f "$A" ] || continue
|
||||
XAUTHORITY=$A; export XAUTHORITY
|
||||
reachable && break
|
||||
done
|
||||
fi
|
||||
reachable ||
|
||||
{ echo "нет доступа к экрану $DISP: подними его (xvfb-run) и повтори"; exit 2; }
|
||||
|
||||
cp "$SRC" "$PROJ"
|
||||
for p in $(pgrep -x nlve); do
|
||||
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
|
||||
done
|
||||
sleep 1
|
||||
SP=$PWD/test/поток/профиль-тишина
|
||||
rm -rf "$SP"; mkdir -p "$SP"
|
||||
rm -rf "$PROJ-cache"
|
||||
rm -f "$PROJ.autosave"
|
||||
XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP \
|
||||
setsid "$BIN" "$PROJ" >/dev/null 2>&1 < /dev/null &
|
||||
sleep 14
|
||||
|
||||
X="timeout 8 xdotool"
|
||||
WID=
|
||||
for w in $($X search --name "тишина.nlve" 2>/dev/null); do
|
||||
xwininfo -id "$w" 2>/dev/null | grep -q IsViewable && { WID=$w; break; }
|
||||
done
|
||||
[ -n "$WID" ] || { echo "живое окно редактора не найдено"; exit 2; }
|
||||
$X windowmove "$WID" 0 0 windowsize "$WID" 1270 990
|
||||
sleep 3
|
||||
|
||||
ROW=${NLVE_ROW:-505}
|
||||
# Сколько команд ffmpeg в журнале сейчас. Считаем строки, а не чистим
|
||||
# файл: журнал общий, и усечение уносит чужую работу.
|
||||
runs() { grep -c '^> .*ffmpeg' "$LOG" 2>/dev/null || echo 0; }
|
||||
|
||||
BEFORE=$(runs)
|
||||
echo " команд в журнале до правки: $BEFORE"
|
||||
|
||||
# Тащим первый клип вправо на соседа: наезд примерно в секунду.
|
||||
$X mousemove 650 $ROW mousedown 1
|
||||
sleep 0.3
|
||||
$X mousemove 670 $ROW
|
||||
sleep 0.3
|
||||
$X mousemove 683 $ROW
|
||||
sleep 0.3
|
||||
$X mouseup 1
|
||||
|
||||
# С этой точки отсчёт. Опрашиваем журнал почаще, чтобы поймать момент
|
||||
# запуска, а не ждать наугад.
|
||||
T0=$(date +%s.%N)
|
||||
WAITED=0
|
||||
while [ "$WAITED" -lt 60 ]; do
|
||||
N=$(runs)
|
||||
[ "$N" -gt "$BEFORE" ] && break
|
||||
sleep 0.2
|
||||
WAITED=$((WAITED + 1))
|
||||
done
|
||||
T1=$(date +%s.%N)
|
||||
AFTER=$(runs)
|
||||
|
||||
for p in $(pgrep -x nlve); do
|
||||
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
|
||||
done
|
||||
pkill -f "тишина.nlve-cache" 2>/dev/null || true
|
||||
rm -f "$PROJ.autosave"
|
||||
|
||||
python3 -c "
|
||||
задержка = $T1 - $T0
|
||||
было, стало = $BEFORE, $AFTER
|
||||
print(' команд после правки: %d, пауза до запуска: %.2f с' % (стало, задержка))
|
||||
if стало <= было:
|
||||
print(' ПЛОХО: сборка не началась вовсе — отложить не значит забыть')
|
||||
raise SystemExit(1)
|
||||
# Задано полторы секунды тишины. Нижняя граница с запасом на разброс
|
||||
# опроса, верхняя — чтобы отложенное не превратилось в забытое.
|
||||
if задержка < 1.2:
|
||||
print(' ПЛОХО: сборка началась через %.2f с — правку не дождались'
|
||||
% задержка)
|
||||
raise SystemExit(1)
|
||||
if задержка > 6:
|
||||
print(' ПЛОХО: сборка началась только через %.2f с' % задержка)
|
||||
raise SystemExit(1)
|
||||
print(' ok: сборка дождалась конца правки и всё-таки началась')
|
||||
"
|
||||
|
|
@ -32,8 +32,32 @@ LOG=$(dirname "$BIN")/nlve.log
|
|||
|
||||
[ -f "$PROJ" ] || { echo "нет проекта для проверки: $PROJ"; exit 2; }
|
||||
|
||||
# Проверка настоящая: ffmpeg действительно сводит кусок из настоящего
|
||||
# файла. Нет съёмки — нет и работы, которую можно прервать или не
|
||||
# прерывать, и все случаи «падают» не из-за кода. Говорим об этом прямо.
|
||||
python3 - "$PROJ" <<'PY' || exit 2
|
||||
import json, os, sys
|
||||
нет = [m['path'] for m in json.load(open(sys.argv[1])).get('pool', [])
|
||||
if not os.path.exists(m['path'])]
|
||||
if нет:
|
||||
print('нет исходников, проверять нечего. Не найдено файлов: %d, первый:'
|
||||
% len(нет))
|
||||
print(' ' + нет[0])
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
|
||||
# Закрываем только свои окна — те, что живут на экране проверки. Простой
|
||||
# pkill -x nlve уносит и редактор пользователя на рабочем экране вместе с
|
||||
# несохранённой работой.
|
||||
killmine() {
|
||||
for p in $(pgrep -x nlve); do
|
||||
tr '\0' '\n' < /proc/$p/environ 2>/dev/null |
|
||||
grep -qx "DISPLAY=$DISP" && kill $p
|
||||
done
|
||||
}
|
||||
|
||||
start() {
|
||||
pkill -x nlve 2>/dev/null || true
|
||||
killmine
|
||||
# Закрытая программа не уносит с собой свой ffmpeg: дочерний процесс
|
||||
# остаётся и продолжает писать в кэш. Убираем только его — по пути
|
||||
# этого самого проекта, чтобы не задеть чужую работу.
|
||||
|
|
@ -117,7 +141,7 @@ else
|
|||
BAD=$((BAD + 1))
|
||||
fi
|
||||
|
||||
pkill -x nlve 2>/dev/null || true
|
||||
killmine
|
||||
pkill -f "длинный-фейд.nlve-cache" 2>/dev/null || true
|
||||
rm -f "$PROJ.autosave"
|
||||
|
||||
|
|
|
|||
158
tests/insertat.sh
Executable file
158
tests/insertat.sh
Executable file
|
|
@ -0,0 +1,158 @@
|
|||
#!/bin/sh
|
||||
# Файл из списка встаёт на курсор, а не в конец дорожки.
|
||||
#
|
||||
# Раньше клип падал в самый конец, стоило месту под ним оказаться
|
||||
# занятым, — а оно занято почти всегда: клип длиной в целый файл упирается
|
||||
# хвостом в то, что правее, даже когда начало легло в свободное место.
|
||||
# Потом человек тащил его на нужное время и получал наезд, то есть заказ
|
||||
# на сборку перехода.
|
||||
#
|
||||
# Проверяется так: курсор ставится посреди первого клипа, файл из списка
|
||||
# добавляется на ту же дорожку. Клип обязан встать на курсор, монтаж —
|
||||
# раздвинуться ровно на его длину, наездов и дыр появиться не должно.
|
||||
# Всё читается из сохранённого проекта, а не с картинки.
|
||||
#
|
||||
# Запуск (экран :99 поднят):
|
||||
# tests/insertat.sh [<путь к nlve>]
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
BIN=${1:-$PWD/nlve}
|
||||
DISP=${NLVE_DISPLAY:-:99}
|
||||
SRC=$PWD/test/поток/пара.nlve
|
||||
PROJ=$PWD/test/поток/вставка.nlve
|
||||
|
||||
[ -f "$SRC" ] || { echo "нет монтажа $SRC"; exit 2; }
|
||||
|
||||
# Невидимый экран поднят через xvfb-run, у него свой файл доступа, а в
|
||||
# окружении обычно лежит файл рабочего экрана. С чужим файлом xdotool не
|
||||
# видит ни одного окна, и проверка «проходит» вслепую, ничего не проверив.
|
||||
export DISPLAY=$DISP
|
||||
reachable() { timeout 5 xdotool getdisplaygeometry >/dev/null 2>&1; }
|
||||
if ! reachable; then
|
||||
for p in $(pgrep -x Xvfb); do
|
||||
A=$(tr '\0' '\n' < /proc/$p/cmdline | grep -A1 -x -- -auth | tail -1)
|
||||
[ -f "$A" ] || continue
|
||||
XAUTHORITY=$A; export XAUTHORITY
|
||||
reachable && break
|
||||
done
|
||||
fi
|
||||
reachable ||
|
||||
{ echo "нет доступа к экрану $DISP: подними его (xvfb-run) и повтори"; exit 2; }
|
||||
|
||||
cp "$SRC" "$PROJ"
|
||||
for p in $(pgrep -x nlve); do
|
||||
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
|
||||
done
|
||||
sleep 1
|
||||
SP=$PWD/test/поток/профиль-вставка
|
||||
rm -rf "$SP"; mkdir -p "$SP"
|
||||
rm -f "$PROJ.autosave"
|
||||
XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP \
|
||||
setsid "$BIN" "$PROJ" >/dev/null 2>&1 < /dev/null &
|
||||
sleep 14
|
||||
|
||||
X="timeout 8 xdotool"
|
||||
# Окно ищем по имени монтажа и проверяем, что оно на экране: по одному
|
||||
# «nlve» находятся и обломки прошлых запусков, нажатия в них уходят в
|
||||
# никуда, и проверка молча показывает «ничего не произошло».
|
||||
WID=
|
||||
for w in $($X search --name "вставка.nlve" 2>/dev/null); do
|
||||
xwininfo -id "$w" 2>/dev/null | grep -q IsViewable && { WID=$w; break; }
|
||||
done
|
||||
[ -n "$WID" ] || { echo "живое окно редактора не найдено"; exit 2; }
|
||||
$X windowmove "$WID" 0 0 windowsize "$WID" 1270 990
|
||||
sleep 2
|
||||
|
||||
$X mousemove 700 466 click 1 # курсор воспроизведения — посреди первого клипа
|
||||
sleep 1
|
||||
$X mousemove 200 512 click 1 # выбрать файл в списке проекта
|
||||
sleep 1
|
||||
# Меню списка: «Добавить на дорожку» — первый пункт, в подменю первой
|
||||
# стоит V1, она же и нужна.
|
||||
$X click 3
|
||||
sleep 1.5
|
||||
$X key Down; sleep 0.5
|
||||
$X key Right; sleep 0.5
|
||||
$X key Return; sleep 2
|
||||
|
||||
$X key ctrl+s
|
||||
sleep 2
|
||||
for p in $(pgrep -x nlve); do
|
||||
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
|
||||
done
|
||||
rm -f "$PROJ.autosave"
|
||||
|
||||
python3 - "$SRC" "$PROJ" <<'PY'
|
||||
import json, sys
|
||||
|
||||
def clips(path, track):
|
||||
d = json.load(open(path))
|
||||
for t in d['tracks']:
|
||||
if t['name'] == track:
|
||||
return sorted(t['clips'], key=lambda c: c['tl_start'])
|
||||
return []
|
||||
|
||||
было = clips(sys.argv[1], 'V1')
|
||||
стало = clips(sys.argv[2], 'V1')
|
||||
звук = clips(sys.argv[2], 'A1')
|
||||
плохо = 0
|
||||
|
||||
медиа = {c['media'] for c in было}
|
||||
новые = [c for c in стало if c['media'] not in медиа]
|
||||
if len(новые) != 1:
|
||||
print(' ПЛОХО: вставленный клип не найден (новых клипов %d)' % len(новые))
|
||||
raise SystemExit(1)
|
||||
новый = новые[0]
|
||||
конец_было = max(c['tl_start'] + c['duration'] for c in было)
|
||||
print(' вставленный клип: старт %.3f с, длина %.3f с' %
|
||||
(новый['tl_start'], новый['duration']))
|
||||
|
||||
if новый['tl_start'] > конец_было - 0.001:
|
||||
print(' ПЛОХО: клип упал в конец дорожки (%.3f с), а не на курсор'
|
||||
% новый['tl_start'])
|
||||
плохо = 1
|
||||
elif новый['tl_start'] < 0.5:
|
||||
print(' ПЛОХО: клип встал в начало, курсор был посреди первого клипа')
|
||||
плохо = 1
|
||||
else:
|
||||
print(' ok: клип встал на курсор, внутри материала')
|
||||
|
||||
# Соседи отъехали ровно на длину вставки: монтаж раздвинулся, а не
|
||||
# затёрся и не разъехался.
|
||||
сдвиг = новый['duration']
|
||||
последний_было = было[-1]
|
||||
последний_стало = стало[-1]
|
||||
ожидалось = последний_было['tl_start'] + сдвиг
|
||||
if abs(последний_стало['tl_start'] - ожидалось) > 0.001:
|
||||
print(' ПЛОХО: последний клип на %.3f с, ожидалось %.3f с'
|
||||
% (последний_стало['tl_start'], ожидалось))
|
||||
плохо = 1
|
||||
else:
|
||||
print(' ok: монтаж раздвинут ровно на длину вставки (%.3f с)' % сдвиг)
|
||||
|
||||
# Наезд на одной дорожке — это заказ на переход. Его тут быть не должно.
|
||||
for имя, список in (('V1', стало), ('A1', звук)):
|
||||
for a, b in zip(список, список[1:]):
|
||||
зазор = b['tl_start'] - (a['tl_start'] + a['duration'])
|
||||
if зазор < -0.001:
|
||||
print(' ПЛОХО: наезд на %s в %.3f с (%.3f с)'
|
||||
% (имя, b['tl_start'], -зазор))
|
||||
плохо = 1
|
||||
elif зазор > 0.001:
|
||||
print(' ПЛОХО: дыра на %s в %.3f с (%.3f с)'
|
||||
% (имя, a['tl_start'] + a['duration'], зазор))
|
||||
плохо = 1
|
||||
if not плохо:
|
||||
print(' ok: ни наездов, ни дыр')
|
||||
|
||||
# Звук обязан уехать вместе с картинкой, иначе синхрон рассыплется.
|
||||
if [round(c['tl_start'], 3) for c in звук] != [round(c['tl_start'], 3) for c in стало]:
|
||||
print(' ПЛОХО: звук разошёлся с картинкой')
|
||||
print(' картинка: %s' % ['%.3f' % c['tl_start'] for c in стало])
|
||||
print(' звук: %s' % ['%.3f' % c['tl_start'] for c in звук])
|
||||
плохо = 1
|
||||
else:
|
||||
print(' ok: звук уехал вместе с картинкой')
|
||||
|
||||
raise SystemExit(плохо)
|
||||
PY
|
||||
|
|
@ -637,6 +637,166 @@ end;
|
|||
Раньше курсор оставался там, где был, а рез мог попасть на долю кадра.
|
||||
Теперь рез кладётся на кадровую сетку, а курсор встаёт ровно на место
|
||||
реза — с ним и работают дальше. }
|
||||
{ Раздвижка монтажа под вставку.
|
||||
|
||||
Клип из списка файлов встаёт туда, где стоит курсор, а не в конец
|
||||
дорожки. Место под ним освобождается: клип, накрывающий точку вставки,
|
||||
режется, и всё от этой точки правее едет вправо на длину нового — на
|
||||
всех дорожках сразу, иначе звук отстанет от картинки. }
|
||||
procedure CheckMakeRoom;
|
||||
var
|
||||
Proj: TProject;
|
||||
Info: TMediaInfo;
|
||||
Media: TMediaItem;
|
||||
Timeline: TTimelineControl;
|
||||
Moved, I: Integer;
|
||||
Gaps, Overlaps: Integer;
|
||||
Step, At, Shift: Double;
|
||||
Track: TTrack;
|
||||
begin
|
||||
WriteLn;
|
||||
WriteLn('Раздвижка монтажа под вставку');
|
||||
WriteLn;
|
||||
|
||||
Info := TestInfo;
|
||||
Info.FpsNum := 60000;
|
||||
Info.FpsDen := 1001;
|
||||
|
||||
Proj := TProject.Create;
|
||||
Timeline := TTimelineControl.Create(nil);
|
||||
try
|
||||
Proj.Clear;
|
||||
Proj.AddTrack('V1');
|
||||
Proj.AddTrackOfKind('A1', tkAudio);
|
||||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||||
Proj.Master := Info;
|
||||
{ Три клипа встык на картинке и столько же на звуке: 0..10, 10..20,
|
||||
20..30. }
|
||||
for I := 0 to 2 do
|
||||
begin
|
||||
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, I * 10);
|
||||
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, I * 10);
|
||||
end;
|
||||
Timeline.Project := Proj;
|
||||
|
||||
{ Вставляем пять секунд посреди второго клипа.
|
||||
|
||||
Ожидаемое считается по той же кадровой сетке, а не круглыми
|
||||
числами: при 60000/1001 ни 15, ни 5 на кадр не попадают, и точка
|
||||
реза с величиной сдвига садятся на ближайшие кадры. }
|
||||
Step := Info.FpsDen / Info.FpsNum;
|
||||
At := Round(15 / Step) * Step;
|
||||
Shift := Round(5 / Step) * Step;
|
||||
Moved := Timeline.MakeRoomAt(15, 5);
|
||||
CheckTrue('что-то отодвинуто: ' + IntToStr(Moved), Moved >= 4);
|
||||
CheckTrue('второй клип разошёлся надвое: клипов стало ' +
|
||||
IntToStr(Proj.Tracks[0].Count), Proj.Tracks[0].Count = 4);
|
||||
CheckTrue('звук разошёлся так же', Proj.Tracks[1].Count = 4);
|
||||
|
||||
{ Голова второго клипа осталась на месте, хвост уехал ровно на пять. }
|
||||
CheckTrue('голова на месте: ' + SecondsToStr(Proj.Tracks[0][1].TlStart),
|
||||
Abs(Proj.Tracks[0][1].TlStart - 10) < 0.001);
|
||||
CheckTrue('хвост отъехал на длину вставки: ' +
|
||||
SecondsToStr(Proj.Tracks[0][2].TlStart),
|
||||
Abs(Proj.Tracks[0][2].TlStart - (At + Shift)) < 0.001);
|
||||
CheckTrue('третий клип отъехал на столько же: ' +
|
||||
SecondsToStr(Proj.Tracks[0][3].TlStart),
|
||||
Abs(Proj.Tracks[0][3].TlStart - (20 + Shift)) < 0.001);
|
||||
CheckTrue('звук уехал вместе с картинкой',
|
||||
Abs(Proj.Tracks[1][2].TlStart - (At + Shift)) < 0.001);
|
||||
CheckTrue('освобождено ровно столько, сколько просили: ' +
|
||||
SecondsToStr(Proj.Tracks[0][2].TlStart - Proj.Tracks[0][1].TlEnd),
|
||||
Abs((Proj.Tracks[0][2].TlStart - Proj.Tracks[0][1].TlEnd) - Shift)
|
||||
< 0.001);
|
||||
|
||||
{ В освобождённом месте никого нет, и больше нигде не появилось ни
|
||||
дыр, ни наездов. }
|
||||
CheckTrue('место освобождено', Proj.Tracks[0].ClipAt(At + 1) = nil);
|
||||
Gaps := 0;
|
||||
Overlaps := 0;
|
||||
for I := 0 to Proj.TrackCount - 1 do
|
||||
begin
|
||||
Track := Proj.Tracks[I];
|
||||
for Moved := 0 to Track.Count - 2 do
|
||||
if Track[Moved].TlEnd > Track[Moved + 1].TlStart + 0.001 then
|
||||
Inc(Overlaps)
|
||||
else if (Track[Moved].TlEnd < Track[Moved + 1].TlStart - 0.001) and
|
||||
(Abs(Track[Moved].TlEnd - At) > 0.001) then
|
||||
Inc(Gaps);
|
||||
end;
|
||||
CheckTrue('наездов не появилось', Overlaps = 0);
|
||||
CheckTrue('лишних дыр не появилось', Gaps = 0);
|
||||
|
||||
{ Вставка в свободное место за концом монтажа ничего не двигает. }
|
||||
Moved := Timeline.MakeRoomAt(100, 5);
|
||||
CheckTrue('за концом монтажа двигать нечего', Moved = 0);
|
||||
finally
|
||||
Timeline.Free;
|
||||
Proj.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Клип притягивается к границе и хвостом, а не только головой.
|
||||
|
||||
Подводят кусок к началу соседа слева: голова встаёт в свободное место,
|
||||
а хвост при этом накрывает соседа. Наезд на одной дорожке — это заказ
|
||||
на переход, то есть минуты сборки из-за промаха на полпикселя. }
|
||||
procedure CheckSnapTail;
|
||||
var
|
||||
Proj: TProject;
|
||||
Info: TMediaInfo;
|
||||
Media: TMediaItem;
|
||||
Timeline: TTimelineControl;
|
||||
Mover: TClip;
|
||||
Got: Double;
|
||||
begin
|
||||
WriteLn;
|
||||
WriteLn('Прилипание хвостом');
|
||||
WriteLn;
|
||||
|
||||
Info := TestInfo;
|
||||
Info.FpsNum := 60000;
|
||||
Info.FpsDen := 1001;
|
||||
|
||||
Proj := TProject.Create;
|
||||
Timeline := TTimelineControl.Create(nil);
|
||||
try
|
||||
Proj.Clear;
|
||||
Proj.AddTrack('V1');
|
||||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||||
Proj.Master := Info;
|
||||
{ Сосед занимает 20..30, двигаемый длиной 4 стоит пока в сторонке. }
|
||||
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 20);
|
||||
Mover := Proj.AddClip(Proj.Tracks[0], Media, 0, 4, 50);
|
||||
Timeline.Project := Proj;
|
||||
Timeline.PixelsPerSecond := 10; { допуск прилипания — 1.2 с }
|
||||
|
||||
{ Голова метит в 15.7, хвост при этом пришёлся бы на 19.7 — почти
|
||||
встык с соседом. Правильный ответ: 16, тогда хвост ровно на 20. }
|
||||
Got := Timeline.SnapTime(15.7, Mover, Mover.Duration);
|
||||
CheckTrue('хвост сел на начало соседа: ' + SecondsToStr(Got),
|
||||
Abs(Got - 16) < 0.001);
|
||||
|
||||
{ Голова у границы по-прежнему липнет головой: там ближе она. }
|
||||
Got := Timeline.SnapTime(19.8, Mover, Mover.Duration);
|
||||
CheckTrue('голова села на начало соседа: ' + SecondsToStr(Got),
|
||||
Abs(Got - 20) < 0.001);
|
||||
|
||||
{ Без длины поведение прежнее: хвост не рассматривается. }
|
||||
Got := Timeline.SnapTime(15.7, Mover);
|
||||
CheckTrue('без длины притяжения хвостом нет: ' + SecondsToStr(Got),
|
||||
Abs(Got - 15.7) < 0.001);
|
||||
|
||||
{ Хвост липнет и к концу соседа: клип встаёт вплотную справа. }
|
||||
Got := Timeline.SnapTime(25.8, Mover, Mover.Duration);
|
||||
CheckTrue('хвост сел на конец соседа: ' + SecondsToStr(Got),
|
||||
Abs(Got - 26) < 0.001);
|
||||
finally
|
||||
Timeline.Free;
|
||||
Proj.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CheckSplitAndPlayhead;
|
||||
var
|
||||
Proj: TProject;
|
||||
|
|
@ -994,6 +1154,8 @@ begin
|
|||
CheckRubberBand;
|
||||
CheckFrameGridRepair;
|
||||
CheckSplitAndPlayhead;
|
||||
CheckMakeRoom;
|
||||
CheckSnapTail;
|
||||
CheckSplitAllTracks;
|
||||
CheckGapTolerance;
|
||||
CheckSourceRect;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ from PIL import Image
|
|||
|
||||
DISP = os.environ.get('ЭКРАН', ':99')
|
||||
ENV = {'DISPLAY': DISP, 'PATH': '/usr/bin:/bin'}
|
||||
# Невидимый экран, поднятый через xvfb-run, пускает только по своему файлу
|
||||
# доступа. Без него ни xdotool, ни import не видят ни одного окна, и
|
||||
# проверка падает на первом же движении мыши.
|
||||
if os.environ.get('XAUTHORITY'):
|
||||
ENV['XAUTHORITY'] = os.environ['XAUTHORITY']
|
||||
# Один щелчок колеса. Должен совпадать с ZoomStep в uTimelineCtrl.
|
||||
STEP = 1.25
|
||||
# Полоса окна, по которой всё и меряется: линейка и первая дорожка.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue