Клип не попадает на дорожку чужого вида

В живом проекте фотография оказалась на текстовой дорожке, а надпись — на
дорожке картинки. Уплощение обходит дорожки по видам, и такой клип не
видит ни сборка, ни предпросмотр: на монтаже он нарисован, в готовом
ролике его нет, и ни одной жалобы по дороге. Именно это и было причиной
пропавших фотографии и текста, а не ошибка со стоп-кадром.

Теперь перетаскивание на дорожку чужого вида не проходит, а файл из
списка, брошенный на такую дорожку, ложится на подходящую — с записью в
журнал, куда именно. Новая дорожка, заведённая перетаскиванием вниз,
заводится того вида, какой нужен клипу.

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

Проверка 10 в nlvetool flattest: картинка на текстовой дорожке в
уплощение не попадает, после переноса попадает и остаётся на своём месте.
This commit is contained in:
4aiman 2026-08-31 11:12:05 +03:00
commit 07aee86631
5 changed files with 261 additions and 6 deletions

View file

@ -121,6 +121,12 @@ NVENC, если он есть). Это разрозненно и не даёт
## Мелочи, до которых не дошли руки
**Вид дорожки не виден в её заголовке.** Дорожки картинки, звука и текста
подписаны V1, A1, Т1, и по букве вид угадывается, но не более того.
Клип, попавший на дорожку чужого вида, теперь не принимается и не
теряется, но само различие стоило бы показать явно — значком или цветом
полосы заголовка.
**Значок файла программы под Windows.** Значок окна задаётся из
`uIconData`, а значок самого `.exe` в проводнике берётся из ресурса при
сборке — там он до сих пор стандартный.

View file

@ -101,6 +101,9 @@ type
{ Наложение для плеера: список кусков оверлея и окна показа. }
{ Журнал текущей сборки: файл рядом с готовым роликом. }
FExportLogPath: string;
{ Проект открыт из командной строки: спросить про клипы на чужих
дорожках можно только когда окно уже показано. }
FCheckMisplacedOnShow: Boolean;
FPreviewOverlayUrl: string;
FPreviewOverlayWindows: TOverlayWindows;
{ Списки кусков звуковых дорожек для плеера. }
@ -200,6 +203,7 @@ type
{ Переписывает файл субтитров и отдаёт его плееру. }
procedure UpdateSubtitles;
procedure UpdateRippleButton;
procedure CheckMisplacedClips;
procedure UpdateSnapButton;
procedure UpdateGlueButton;
procedure UpdateCenterButton;
@ -1159,6 +1163,7 @@ begin
UpdateRippleButton;
UpdateCaption;
SchedulePreview;
FCheckMisplacedOnShow := True;
end
else
Log('Не удалось открыть проект: ' + Err);
@ -1175,6 +1180,14 @@ begin
if lstPool.Items.Count > 0 then
lstPool.ItemIndex := 0;
end;
{ Спрашивать про клипы на чужих дорожках можно только теперь: окно
показано, и вопрос не окажется за кадром. }
if FCheckMisplacedOnShow then
begin
FCheckMisplacedOnShow := False;
CheckMisplacedClips;
end;
end;
{ Проверка не по одному месту, а разом: человеку нужен ответ «чего не
@ -1666,7 +1679,9 @@ var
Track: TTrack;
Mismatches: TMismatches;
Duration, PlaceAt: Double;
NormPath: string;
NormPath, Wanted: string;
NeedKind: TTrackKind;
I: Integer;
begin
Result := False;
if (APoolIndex < 0) or (APoolIndex >= FProject.PoolCount) then
@ -1687,6 +1702,38 @@ begin
ATrackIndex := FProject.TrackCount - 1;
Track := FProject.Tracks[ATrackIndex];
{ Дорожка чужого вида не годится: клип на ней не увидит ни сборка, ни
предпросмотр — уплощение обходит дорожки по видам. Поэтому не кладём
туда молча, а находим подходящую и говорим, куда положили. }
{ Файл без картинки — только звук; всё остальное идёт на дорожку
картинки, а парный звук программа положит сама. }
if Item.Info.HasVideo or (Item.Info.Kind = mkImage) then
NeedKind := tkVideo
else
NeedKind := tkAudio;
if Track.Kind <> NeedKind then
begin
Wanted := Track.Name;
Track := nil;
for I := 0 to FProject.TrackCount - 1 do
if FProject.Tracks[I].Kind = NeedKind then
begin
Track := FProject.Tracks[I];
Break;
end;
if Track = nil then
begin
if NeedKind = tkAudio then
Track := FProject.AddTrackOfKind('A' +
IntToStr(FProject.TrackCountOf(tkAudio) + 1), tkAudio)
else
Track := FProject.AddTrack('V' +
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
end;
Log(Format('Дорожка %s не для этого вида клипов — положил на %s.',
[Wanted, Track.Name]));
end;
{ Предупреждаем именно здесь, при попадании на монтаж: пока файл просто
лежит в списке, он ни с чем не склеивается. }
Mismatches := CompareToMaster(FProject.Master, Item.Info);
@ -2202,12 +2249,59 @@ begin
FSettings.NoteRecentProject(FProject.FileName);
RefreshRecentMenu;
Log('Открыт проект: ' + FProject.FileName);
CheckMisplacedClips;
end;
{ --- Схлопывание --- }
{ Кнопка показывает и состояние, и режим: утоплена — автосхлопывание
включено, надпись говорит, на какие дорожки оно действует. }
{ Клипы на дорожках чужого вида: находим и предлагаем перенести.
Такой клип нарисован на монтаже, но в ролик не попадает: уплощение
обходит дорожки по видам и клип с чужой дорожки не видит. Пропажа тихая
— сборка проходит без единой жалобы, а в готовом файле фотографии или
надписи нет. Проверено на живом проекте, где фотография оказалась на
текстовой дорожке, а надпись — на дорожке картинки.
Молча переносить нельзя: человек мог положить клип туда осознанно, и
внезапно переехавший клип — сюрприз хуже пропажи. Поэтому спрашиваем. }
procedure TMainForm.CheckMisplacedClips;
var
N: Integer;
begin
N := FProject.MisplacedClipCount;
if N = 0 then
Exit;
Log(Format('ВНИМАНИЕ: %d %s на дорожках чужого вида — в сборку они не попадут.',
[N, Plural(N, 'клип', 'клипа', 'клипов')]));
if ThemedMessageDlg('nlve',
Format('В проекте %d %s лежит на дорожках не своего вида: ' +
'картинка на звуковой или текстовой, надпись на дорожке картинки.' +
LineEnding + LineEnding +
'Такой клип виден на монтаже, но в готовый ролик не попадает — ' +
'ни в сборку, ни в предпросмотр.' + LineEnding + LineEnding +
'Перенести их на подходящие дорожки? Время клипов не изменится.',
[N, Plural(N, 'клип', 'клипа', 'клипов')]),
mtWarning, [mbYes, mbNo], 0) <> mrYes then
begin
SetStatus(Format('%d %s не попадёт в сборку: они на дорожках чужого вида.',
[N, Plural(N, 'клип', 'клипа', 'клипов')]));
Exit;
end;
FProject.BeginChange;
N := FProject.FixMisplacedClips;
FTimeline.Invalidate;
FLastLoadedEdl := '';
SchedulePreview;
UpdateCaption;
Log(Format('Перенесено на подходящие дорожки: %d.', [N]));
SetStatus(Format('Перенесено клипов: %d.', [N]));
end;
procedure TMainForm.UpdateRippleButton;
var
ModeText: string;

View file

@ -380,6 +380,22 @@ type
если он окажется хуже их, разница бросится в глаза именно на нём.
Лучше потратить место, чем показать шакала посреди чистой съёмки. }
function MaxVideoBitrate: Int64;
{ Какая дорожка нужна клипу этого вида. Правило одно на всю
программу: картинке — дорожка картинки, звуку — звуковая, надписи —
текстовая. }
class function TrackKindFor(AClipKind: TClipKind): TTrackKind;
{ Сколько клипов лежит на дорожках не своего вида.
Такой клип не попадает никуда: уплощение обходит дорожки по видам,
и клип с чужой дорожки не видит ни сборка, ни предпросмотр. Пропажа
при этом тихая — на монтаже он нарисован, а в ролике его нет. }
function MisplacedClipCount: Integer;
{ Переносит их на подходящие дорожки. Возвращает, сколько перенесено.
Ищется первая дорожка нужного вида, где это время свободно; если
такой нет — заводится новая. Время клипа не меняется: человек ставил
его именно туда. }
function FixMisplacedClips: Integer;
{ Автоматическое схлопывание при перетаскивании клипов. }
property RippleEnabled: Boolean read FRippleEnabled write FRippleEnabled;
property FileName: string read FFileName write FFileName;
@ -990,6 +1006,102 @@ begin
Result := FRegionEnd > FRegionStart + 0.001;
end;
class function TProject.TrackKindFor(AClipKind: TClipKind): TTrackKind;
begin
case AClipKind of
ckAudio: Result := tkAudio;
ckText: Result := tkText;
else
Result := tkVideo;
end;
end;
function TProject.MisplacedClipCount: Integer;
var
T, C: Integer;
Track: TTrack;
begin
Result := 0;
for T := 0 to TrackCount - 1 do
begin
Track := Tracks[T];
for C := 0 to Track.Count - 1 do
if TrackKindFor(Track[C].Kind) <> Track.Kind then
Inc(Result);
end;
end;
function TProject.FixMisplacedClips: Integer;
var
T, C, K: Integer;
Track, Target: TTrack;
Clip: TClip;
Need: TTrackKind;
Moved: TFPList;
{ Свободна ли дорожка в это время. }
function Fits(ATrack: TTrack; AClip: TClip): Boolean;
var
I: Integer;
begin
Result := True;
for I := 0 to ATrack.Count - 1 do
if (AClip.TlStart < ATrack[I].TlEnd - 0.001) and
(AClip.TlEnd > ATrack[I].TlStart + 0.001) then
Exit(False);
end;
begin
Result := 0;
Moved := TFPList.Create;
try
{ Сперва собираем список: переносить прямо во время обхода нельзя —
состав дорожек меняется под ногами. }
for T := 0 to TrackCount - 1 do
begin
Track := Tracks[T];
for C := 0 to Track.Count - 1 do
if TrackKindFor(Track[C].Kind) <> Track.Kind then
Moved.Add(Track[C]);
end;
for C := 0 to Moved.Count - 1 do
begin
Clip := TClip(Moved[C]);
Track := FindClipTrack(Clip);
if Track = nil then
Continue;
Need := TrackKindFor(Clip.Kind);
Target := nil;
for K := 0 to TrackCount - 1 do
if (Tracks[K].Kind = Need) and Fits(Tracks[K], Clip) then
begin
Target := Tracks[K];
Break;
end;
if Target = nil then
case Need of
tkAudio: Target := AddTrackOfKind('A' +
IntToStr(TrackCountOf(tkAudio) + 1), tkAudio);
tkText: Target := AddTrackOfKind('Т' +
IntToStr(TrackCountOf(tkText) + 1), tkText);
else
Target := AddTrackOfKind('V' +
IntToStr(TrackCountOf(tkVideo) + 1), tkVideo);
end;
Track.Extract(Clip);
Target.Add(Clip);
Target.Sort;
Inc(Result);
end;
finally
Moved.Free;
end;
end;
function TProject.MaxVideoBitrate: Int64;
var
I: Integer;

View file

@ -2013,12 +2013,20 @@ begin
{ Утащили ниже последней дорожки — заводим новую, как в Vegas.
За одно перетаскивание не больше одной: иначе движение мыши
вниз наплодило бы их десяток. }
вниз наплодило бы их десяток. Вид новой дорожки — по клипу:
картинку на звуковую дорожку класть незачем. }
if (Idx < 0) and (FProject <> nil) and (Y > TracksBottom) and
(not FTrackAddedDuringDrag) then
begin
FProject.AddTrack('V' +
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
case TProject.TrackKindFor(FDragClip.Kind) of
tkAudio: FProject.AddTrackOfKind('A' +
IntToStr(FProject.TrackCountOf(tkAudio) + 1), tkAudio);
tkText: FProject.AddTrackOfKind('Т' +
IntToStr(FProject.TrackCountOf(tkText) + 1), tkText);
else
FProject.AddTrack('V' +
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
end;
FTrackAddedDuringDrag := True;
Idx := FProject.TrackCount - 1;
if Assigned(FOnTrackAdded) then
@ -2026,11 +2034,18 @@ begin
end;
{ Перенос на другую дорожку: клип следует за указателем по
вертикали. Именно перенос, а не копирование. }
вертикали. Именно перенос, а не копирование.
На дорожку чужого вида клип не пускаем. Уплощение обходит
дорожки по видам, и клип, попавший не на свою, пропадает и из
сборки, и из предпросмотра — молча: на монтаже он нарисован, а
в ролике его нет. Проверено на живом проекте, где фотография
оказалась на текстовой дорожке. }
if (Idx >= 0) and (FProject <> nil) then
begin
TargetTrack := FProject.Tracks[Idx];
if TargetTrack <> FDragTrack then
if (TargetTrack <> FDragTrack) and
(TargetTrack.Kind = TProject.TrackKindFor(FDragClip.Kind)) then
begin
FDragTrack.Extract(FDragClip);
TargetTrack.Add(FDragClip);

View file

@ -826,6 +826,34 @@ begin
end;
WriteLn;
{ --- 10. Клипы на дорожках чужого вида --- }
WriteLn(' 10. Клип на дорожке не своего вида');
Proj := MakeProject(1, Media);
try
Proj.EnsureAudioTracks;
Proj.AddTrackOfKind('Т1', tkText);
{ Картинка на текстовой дорожке — ровно то, что нашлось в живом
проекте: клип виден на монтаже, а в ролик не попадает. }
Clip := Proj.AddClip(Proj.Tracks[Proj.TrackCount - 1], Media, 0, 3, 10);
Clip.Kind := ckImage;
Check(Proj.MisplacedClipCount = 1, 'нашёлся один такой клип');
FlattenStream(Proj, tkVideo, 0, Segs, Missing);
Check(Length(Segs) = 0, 'в уплощении его нет');
Check(Proj.FixMisplacedClips = 1, 'перенесён один клип');
Check(Proj.MisplacedClipCount = 0, 'чужих клипов не осталось');
FlattenStream(Proj, tkVideo, 0, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) > 0, 'после переноса он попадает в уплощение');
Clip := Proj.Tracks[0].ClipAt(11);
Check((Clip <> nil) and (Abs(Clip.TlStart - 10) < 0.001),
'и стоит на прежнем месте');
finally
Proj.Free;
end;
WriteLn;
WriteLn;
if FailCount = 0 then
begin