Починка проекта: клипы на кадровую сетку, пункт в меню
Меню «Проект» → «Проверить и починить проект...». Показывает найденное и чинит только по согласию, отменяется через Ctrl+Z. Ищутся клипы мимо кадровой сетки, клипы на дорожках чужого вида и повторяющиеся имена дорожек. Первое — наследство: в старых проектах времена записаны как попало (2.457599 с при 59.94 к/с — это 147.31 кадра), и пока монтаж просит доли кадра, предпросмотр и сборка округляют их каждый по-своему. Сдвиг при починке не больше полкадра. Края отмеченного участка при сборке тоже кладутся на сетку: подрезка возвращала в крайние куски доли кадра, которые уплощение только что убрало. Проверено на копии настоящего проекта: 82 клипа, после починки мимо сетки ни одного, клипы остались встык, общая длина изменилась на 0.028 с за два часа; Ctrl+Z возвращает как было. Плюс проверка в lfmcheck, включая клип короче кадра — он не должен схлопнуться в ничто.
This commit is contained in:
parent
019d016db9
commit
37787ce96c
4 changed files with 311 additions and 0 deletions
23
README.md
23
README.md
|
|
@ -272,6 +272,29 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро
|
|||
удаляет: она лежит, пока проект не сохранят заново. После сохранения
|
||||
копия не нужна и убирается.
|
||||
|
||||
## Починка проекта
|
||||
|
||||
Меню «Проект» → **Проверить и починить проект...**. Показывает, что нашла,
|
||||
и чинит только по согласию; отменяется через Ctrl+Z.
|
||||
|
||||
Что ищется:
|
||||
|
||||
- **клипы мимо кадровой сетки.** Между кадрами времени нет, а в старых
|
||||
проектах времена записаны как попало: 2.457599 с при 59.94 к/с — это
|
||||
147.31 кадра. Картинка всё равно выйдет целым числом кадров, звук
|
||||
наберётся с точностью до отсчёта, и предпросмотр со сборкой не смогут
|
||||
совпасть в точности. Починка двигает клип не больше чем на полкадра;
|
||||
на проекте из 82 клипов общая длина изменилась на 0.028 с, то есть на
|
||||
полтора кадра за два часа;
|
||||
- **клипы на дорожках чужого вида.** Такой клип нарисован на монтаже, но
|
||||
в готовый ролик не попадает. То же самое предлагается при открытии
|
||||
проекта, здесь — по требованию;
|
||||
- **дорожки с повторяющимися именами.**
|
||||
|
||||
Для синхронности звука с картинкой чинить проект не нужно: рез правится
|
||||
при сборке, и старый проект соберётся сходящимся. Починка — про то, чтобы
|
||||
монтаж не хранил невозможных времён.
|
||||
|
||||
## Файл не совпадает с проектом
|
||||
|
||||
Проект имеет одно разрешение и один набор кодеков на всё — иначе куски не
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ type
|
|||
procedure PoolEditTransform(Sender: TObject);
|
||||
procedure TimelineClipTransform(Sender: TObject; AClip: TClip);
|
||||
procedure MenuProjectInfo(Sender: TObject);
|
||||
procedure MenuRepairProject(Sender: TObject);
|
||||
procedure FormFilesDropped(Sender: TObject; const FileNames: array of string);
|
||||
function ThumbIndexFor(const APath: string): Integer;
|
||||
procedure ApplySettings;
|
||||
|
|
@ -807,6 +808,7 @@ begin
|
|||
|
||||
MProject := AddTop('Проект');
|
||||
AddItem(MProject, 'Сведения о проекте...', @MenuProjectInfo);
|
||||
AddItem(MProject, 'Проверить и починить проект...', @MenuRepairProject);
|
||||
AddSep(MProject);
|
||||
AddItem(MProject, 'Добавить текст', @MenuAddText, ShortCut(VK_T, [ssCtrl]));
|
||||
AddSep(MProject);
|
||||
|
|
@ -3035,6 +3037,112 @@ begin
|
|||
ShowProjectInfo(Self, FProject);
|
||||
end;
|
||||
|
||||
{ Проверка и починка проекта.
|
||||
|
||||
Зачем. В проекте копятся вещи, которые сами по себе не видны, а в
|
||||
готовом ролике оборачиваются сюрпризом: клип на дорожке чужого вида
|
||||
(нарисован, но в сборку не попадает), одинаковые имена дорожек, времена
|
||||
мимо кадровой сетки. Последнее — наследство: доли кадра в монтаже
|
||||
означают, что предпросмотр и сборка округлят их каждый по-своему.
|
||||
|
||||
Молча ничего не правится: сначала показываем, что нашли, и спрашиваем.
|
||||
Отмена работает как у любой правки — Ctrl+Z. }
|
||||
procedure TMainForm.MenuRepairProject(Sender: TObject);
|
||||
var
|
||||
OffGrid, Misplaced, Renamed, MovedClips, SnappedClips: Integer;
|
||||
Report: string;
|
||||
Names: TStringList;
|
||||
T: Integer;
|
||||
begin
|
||||
OffGrid := FProject.OffGridClipCount;
|
||||
Misplaced := FProject.MisplacedClipCount;
|
||||
|
||||
{ Одинаковые имена дорожек: считаем до правки, иначе не о чем говорить. }
|
||||
Renamed := 0;
|
||||
Names := TStringList.Create;
|
||||
try
|
||||
Names.Sorted := True;
|
||||
Names.Duplicates := dupAccept;
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
begin
|
||||
if Names.IndexOf(FProject.Tracks[T].Name) >= 0 then
|
||||
Inc(Renamed);
|
||||
Names.Add(FProject.Tracks[T].Name);
|
||||
end;
|
||||
finally
|
||||
Names.Free;
|
||||
end;
|
||||
|
||||
if (OffGrid = 0) and (Misplaced = 0) and (Renamed = 0) then
|
||||
begin
|
||||
ThemedMessageDlg('nlve',
|
||||
'Проект в порядке: клипы стоят на кадрах, все они на дорожках ' +
|
||||
'своего вида, имена дорожек не повторяются.',
|
||||
mtInformation, [mbOK], 0);
|
||||
SetStatus('Проект проверен: чинить нечего.', True);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
Report := 'Найдено:' + LineEnding + LineEnding;
|
||||
if OffGrid > 0 then
|
||||
Report := Report + Format('— клипов мимо кадровой сетки: %d.' + LineEnding +
|
||||
'Между кадрами времени нет, и доля кадра означает, что предпросмотр ' +
|
||||
'и сборка округлят её каждый по-своему. Сдвиг при починке не больше ' +
|
||||
'полкадра.' + LineEnding + LineEnding, [OffGrid]);
|
||||
if Misplaced > 0 then
|
||||
Report := Report + Format('— клипов на дорожках чужого вида: %d.' +
|
||||
LineEnding + 'Такой клип нарисован на монтаже, но в готовый ролик не ' +
|
||||
'попадает.' + LineEnding + LineEnding, [Misplaced]);
|
||||
if Renamed > 0 then
|
||||
Report := Report + Format('— дорожек с повторяющимся именем: %d.' +
|
||||
LineEnding + LineEnding, [Renamed]);
|
||||
Report := Report + 'Починить? Отменить можно будет через Ctrl+Z.';
|
||||
|
||||
if ThemedMessageDlg('nlve', Report, mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
|
||||
Exit;
|
||||
|
||||
FProject.BeginChange;
|
||||
SnappedClips := FProject.SnapClipsToFrameGrid;
|
||||
MovedClips := FProject.FixMisplacedClips;
|
||||
FProject.EnsureUniqueTrackNames;
|
||||
|
||||
if (SnappedClips = 0) and (MovedClips = 0) and (Renamed = 0) then
|
||||
begin
|
||||
FProject.CancelChange;
|
||||
SetStatus('Чинить не пришлось.', True);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
FProject.Modified := True;
|
||||
FTimeline.Invalidate;
|
||||
RefreshPool;
|
||||
UpdateCaption;
|
||||
FLastLoadedEdl := '';
|
||||
SchedulePreview;
|
||||
|
||||
Report := '';
|
||||
if SnappedClips > 0 then
|
||||
Report := Report + Format('на кадры поставлено %d %s', [SnappedClips,
|
||||
Plural(SnappedClips, 'клип', 'клипа', 'клипов')]);
|
||||
if MovedClips > 0 then
|
||||
begin
|
||||
if Report <> '' then
|
||||
Report := Report + ', ';
|
||||
Report := Report + Format('перенесено %d %s', [MovedClips,
|
||||
Plural(MovedClips, 'клип', 'клипа', 'клипов')]);
|
||||
end;
|
||||
if Renamed > 0 then
|
||||
begin
|
||||
if Report <> '' then
|
||||
Report := Report + ', ';
|
||||
Report := Report + Format('переименовано %d %s', [Renamed,
|
||||
Plural(Renamed, 'дорожка', 'дорожки', 'дорожек')]);
|
||||
end;
|
||||
|
||||
Log('Починка проекта: ' + Report + '.');
|
||||
SetStatus('Проект починен: ' + Report + '. Отменить — Ctrl+Z.', True);
|
||||
end;
|
||||
|
||||
procedure TMainForm.PoolRemoveFromProject(Sender: TObject);
|
||||
var
|
||||
Idx: Integer;
|
||||
|
|
@ -4317,6 +4425,11 @@ begin
|
|||
end
|
||||
else
|
||||
begin
|
||||
{ Края участка — тоже на кадровую сетку. Иначе подрезка вернёт в
|
||||
крайние куски доли кадра, которые уплощение только что убрало, и
|
||||
картинка с звуком на краях участка разойдутся на полкадра. }
|
||||
RegionFrom := FrameAlign(RegionFrom, FProject.Master);
|
||||
RegionTo := FrameAlign(RegionTo, FProject.Master);
|
||||
ClipSegmentsToRange(Segments, RegionFrom, RegionTo);
|
||||
ClipSegmentsToRange(AudioSegs, RegionFrom, RegionTo);
|
||||
if Length(Segments) = 0 then
|
||||
|
|
|
|||
107
src/uProject.pas
107
src/uProject.pas
|
|
@ -422,6 +422,19 @@ type
|
|||
такой нет — заводится новая. Время клипа не меняется: человек ставил
|
||||
его именно туда. }
|
||||
function FixMisplacedClips: Integer;
|
||||
{ Сколько клипов стоит мимо кадровой сетки.
|
||||
|
||||
Доля кадра в монтаже — это не «почти точно»: картинка всё равно
|
||||
выйдет целым числом кадров, а звук набирается с точностью до
|
||||
отсчёта. Пока монтаж просит доли кадра, предпросмотр и готовый ролик
|
||||
не могут совпасть в точности. }
|
||||
function OffGridClipCount: Integer;
|
||||
{ Ставит клипы на кадровую сетку: начало на монтаже, длину и точку
|
||||
входа в исходник. Возвращает, сколько клипов подвинулось.
|
||||
|
||||
Сдвиг не превышает полкадра — на глаз и на слух незаметен, но
|
||||
монтаж перестаёт хранить невозможные времена. }
|
||||
function SnapClipsToFrameGrid: Integer;
|
||||
{ Автоматическое схлопывание при перетаскивании клипов. }
|
||||
property RippleEnabled: Boolean read FRippleEnabled write FRippleEnabled;
|
||||
property FileName: string read FFileName write FFileName;
|
||||
|
|
@ -444,6 +457,14 @@ function SameColorKey(const A, B: TColorKey): Boolean;
|
|||
центру — то, что читается на любой картинке. }
|
||||
function DefaultTextStyle: TTextStyle;
|
||||
|
||||
{ Время на кадровую сетку проекта.
|
||||
|
||||
Между кадрами времени не существует: доля кадра в монтаже означает, что
|
||||
картинка и звук округлят её каждый по-своему и разойдутся. Мера общая
|
||||
для монтажа, уплощения и подрезки по участку — чтобы все считали
|
||||
одинаково. }
|
||||
function FrameAlign(const ATime: Double; const AMaster: TMediaInfo): Double;
|
||||
|
||||
{ Шрифт по умолчанию. Разный по системам: одинаковых имён у них нет. }
|
||||
const
|
||||
DefaultTextFont =
|
||||
|
|
@ -1125,6 +1146,81 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
function TProject.OffGridClipCount: Integer;
|
||||
var
|
||||
T, C: Integer;
|
||||
Track: TTrack;
|
||||
Clip: TClip;
|
||||
|
||||
function OffGrid(const ATime: Double): Boolean;
|
||||
begin
|
||||
Result := Abs(FrameAlign(ATime, FMaster) - ATime) > 0.0005;
|
||||
end;
|
||||
|
||||
begin
|
||||
Result := 0;
|
||||
if (FMaster.FpsNum <= 0) or (FMaster.FpsDen <= 0) then
|
||||
Exit;
|
||||
for T := 0 to TrackCount - 1 do
|
||||
begin
|
||||
Track := Tracks[T];
|
||||
for C := 0 to Track.Count - 1 do
|
||||
begin
|
||||
Clip := Track[C];
|
||||
if OffGrid(Clip.TlStart) or OffGrid(Clip.Duration) or
|
||||
OffGrid(Clip.SrcIn) then
|
||||
Inc(Result);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TProject.SnapClipsToFrameGrid: Integer;
|
||||
var
|
||||
T, C: Integer;
|
||||
Track: TTrack;
|
||||
Clip: TClip;
|
||||
NewStart, NewDur, NewSrc, Step: Double;
|
||||
begin
|
||||
Result := 0;
|
||||
if (FMaster.FpsNum <= 0) or (FMaster.FpsDen <= 0) then
|
||||
Exit;
|
||||
Step := FMaster.FpsDen / FMaster.FpsNum;
|
||||
|
||||
for T := 0 to TrackCount - 1 do
|
||||
begin
|
||||
Track := Tracks[T];
|
||||
for C := 0 to Track.Count - 1 do
|
||||
begin
|
||||
Clip := Track[C];
|
||||
NewStart := FrameAlign(Clip.TlStart, FMaster);
|
||||
NewDur := FrameAlign(Clip.Duration, FMaster);
|
||||
{ Клип короче кадра оставить нельзя: он исчезнет. }
|
||||
if NewDur < Step then
|
||||
NewDur := Step;
|
||||
NewSrc := FrameAlign(Clip.SrcIn, FMaster);
|
||||
if NewSrc < 0 then
|
||||
NewSrc := 0;
|
||||
|
||||
if (Abs(NewStart - Clip.TlStart) > 0.0000001) or
|
||||
(Abs(NewDur - Clip.Duration) > 0.0000001) or
|
||||
(Abs(NewSrc - Clip.SrcIn) > 0.0000001) then
|
||||
begin
|
||||
Clip.TlStart := NewStart;
|
||||
Clip.Duration := NewDur;
|
||||
Clip.SrcIn := NewSrc;
|
||||
Inc(Result);
|
||||
end;
|
||||
end;
|
||||
{ Порядок мог измениться на доли кадра — пересортируем, иначе дальше
|
||||
всё, что полагается на порядок, будет считать неверно. }
|
||||
Track.Sort;
|
||||
end;
|
||||
|
||||
{ Отметки участка — той же мерой: по ним режется сборка. }
|
||||
FRegionStart := FrameAlign(FRegionStart, FMaster);
|
||||
FRegionEnd := FrameAlign(FRegionEnd, FMaster);
|
||||
end;
|
||||
|
||||
function TProject.FixMisplacedClips: Integer;
|
||||
var
|
||||
T, C, K: Integer;
|
||||
|
|
@ -1429,6 +1525,17 @@ end;
|
|||
{ Цветовой ключ по умолчанию: выключен, пурпурный, с небольшим запасом по
|
||||
сходству — сжатие видео размывает чистый цвет, и точное совпадение
|
||||
оставило бы кайму. }
|
||||
function FrameAlign(const ATime: Double; const AMaster: TMediaInfo): Double;
|
||||
var
|
||||
Step: Double;
|
||||
begin
|
||||
Result := ATime;
|
||||
if (AMaster.FpsNum <= 0) or (AMaster.FpsDen <= 0) then
|
||||
Exit;
|
||||
Step := AMaster.FpsDen / AMaster.FpsNum;
|
||||
Result := Round(ATime / Step) * Step;
|
||||
end;
|
||||
|
||||
function DefaultColorKey: TColorKey;
|
||||
begin
|
||||
Result.Enabled := False;
|
||||
|
|
|
|||
|
|
@ -564,6 +564,73 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Починка проекта: клипы на кадровую сетку.
|
||||
|
||||
Между кадрами времени нет, и монтаж не должен хранить доли кадра:
|
||||
картинка выйдет целым числом кадров, звук наберётся до отсчёта, и
|
||||
округление разведёт их. Правка двигает клип не больше чем на полкадра,
|
||||
но сама сетка обязана получиться ровной. }
|
||||
procedure CheckFrameGridRepair;
|
||||
var
|
||||
Proj: TProject;
|
||||
Info: TMediaInfo;
|
||||
Media: TMediaItem;
|
||||
Step: Double;
|
||||
I: Integer;
|
||||
Clip: TClip;
|
||||
OnGrid: Boolean;
|
||||
begin
|
||||
WriteLn;
|
||||
WriteLn('Починка: клипы на кадровую сетку');
|
||||
WriteLn;
|
||||
|
||||
Info := TestInfo;
|
||||
{ 59.94 к/с — та самая сетка, на которой доли кадра особенно заметны. }
|
||||
Info.FpsNum := 60000;
|
||||
Info.FpsDen := 1001;
|
||||
|
||||
Proj := TProject.Create;
|
||||
try
|
||||
Proj.Clear;
|
||||
Proj.AddTrack('V1');
|
||||
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
|
||||
Proj.Master := Info;
|
||||
Step := Info.FpsDen / Info.FpsNum;
|
||||
|
||||
{ Времена как в настоящем проекте: ни одно не попадает на кадр. }
|
||||
Proj.AddClip(Proj.Tracks[0], Media, 48.558521, 2.457599, 0);
|
||||
Proj.AddClip(Proj.Tracks[0], Media, 56.054198, 1.469934, 2.457599);
|
||||
{ И клип короче кадра: его нельзя схлопнуть в ничто. }
|
||||
Proj.AddClip(Proj.Tracks[0], Media, 62.710196, 0.004, 3.927533);
|
||||
|
||||
CheckTrue('все три клипа мимо сетки', Proj.OffGridClipCount = 3);
|
||||
CheckTrue('починка подвинула три клипа', Proj.SnapClipsToFrameGrid = 3);
|
||||
CheckTrue('мимо сетки не осталось', Proj.OffGridClipCount = 0);
|
||||
|
||||
OnGrid := True;
|
||||
for I := 0 to Proj.Tracks[0].Count - 1 do
|
||||
begin
|
||||
Clip := Proj.Tracks[0][I];
|
||||
if Abs(Clip.TlStart / Step - Round(Clip.TlStart / Step)) > 0.001 then
|
||||
OnGrid := False;
|
||||
if Abs(Clip.Duration / Step - Round(Clip.Duration / Step)) > 0.001 then
|
||||
OnGrid := False;
|
||||
if Abs(Clip.SrcIn / Step - Round(Clip.SrcIn / Step)) > 0.001 then
|
||||
OnGrid := False;
|
||||
end;
|
||||
CheckTrue('начала, длины и точки входа — целые кадры', OnGrid);
|
||||
|
||||
CheckTrue('короткий клип остался длиной в кадр',
|
||||
Abs(Proj.Tracks[0][2].Duration - Step) < 0.0001);
|
||||
CheckTrue('клипы не разъехались больше чем на полкадра',
|
||||
Abs(Proj.Tracks[0][0].TlStart - 0) < Step);
|
||||
CheckTrue('повторная починка ничего не двигает',
|
||||
Proj.SnapClipsToFrameGrid = 0);
|
||||
finally
|
||||
Proj.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
begin
|
||||
WriteLn('Свойства из файлов форм');
|
||||
WriteLn;
|
||||
|
|
@ -595,6 +662,7 @@ begin
|
|||
CheckRippleWithoutMouse;
|
||||
CheckClipCopy;
|
||||
CheckRubberBand;
|
||||
CheckFrameGridRepair;
|
||||
CheckSplitAllTracks;
|
||||
CheckGapTolerance;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue