diff --git a/BACKLOG.md b/BACKLOG.md index 0c85fee..07a84cf 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -121,6 +121,12 @@ NVENC, если он есть). Это разрозненно и не даёт ## Мелочи, до которых не дошли руки +**Вид дорожки не виден в её заголовке.** Дорожки картинки, звука и текста +подписаны V1, A1, Т1, и по букве вид угадывается, но не более того. +Клип, попавший на дорожку чужого вида, теперь не принимается и не +теряется, но само различие стоило бы показать явно — значком или цветом +полосы заголовка. + **Значок файла программы под Windows.** Значок окна задаётся из `uIconData`, а значок самого `.exe` в проводнике берётся из ресурса при сборке — там он до сих пор стандартный. diff --git a/src/uMainForm.pas b/src/uMainForm.pas index a8fb45b..72b857c 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -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; diff --git a/src/uProject.pas b/src/uProject.pas index d355467..6aac70c 100644 --- a/src/uProject.pas +++ b/src/uProject.pas @@ -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; diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas index 5179e58..26aff0b 100644 --- a/src/uTimelineCtrl.pas +++ b/src/uTimelineCtrl.pas @@ -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); diff --git a/tests/nlvetool.lpr b/tests/nlvetool.lpr index 9e5f7dc..b155346 100644 --- a/tests/nlvetool.lpr +++ b/tests/nlvetool.lpr @@ -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