Оверлей по порядку дорожек, текст в окне, имена дорожек

Миникарта была видна в окне, но не в ролике. Предпросмотр рисовал оверлей
поверх всего, не глядя на порядок дорожек, а сборка живёт по правилу
«верхний клип побеждает целиком»: фотография лежала выше оверлея и в
ролике его закрывала. Теперь окно считает так же — оверлей показывается
только там, где его не закрывает сплошной клип с дорожки выше, а закрытый
посередине разбивается на несколько окон показа.

Текст не появлялся в окне. Разметка лежит в файле с одним именем на весь
проект, а плеер сравнивал имена: имя прежнее — перечитывать нечего.
Правка доходила до окна только вместе с перезагрузкой списка кусков.
Теперь сравнивается сама разметка, и при изменении файл перечитывается.
Отдельно проверено, что цепочка lavfi-complex субтитрам не мешает.

Две дорожки Т2. Имя новой дорожке давалось по счёту дорожек своего вида
плюс единица, и после удаления средней счёт совпадал с занятым именем.
Имя подбирается свободное, а повторы из старых проектов разводятся при
чтении.

Проверки 11 и 12 в nlvetool flattest.
This commit is contained in:
4aiman 2026-08-31 12:41:26 +03:00
commit 03f3c84d8c
7 changed files with 279 additions and 50 deletions

View file

@ -89,3 +89,30 @@
было; щелчок без движения их снимает.
- Заголовок дорожки: правый щелчок открывает меню (поднять, опустить,
удалить), перетаскивание меняет порядок дорожек.
## Четвёртый заход: оверлей, текст в окне и одинаковые имена
**Миникарта была в окне, но не в ролике.** Предпросмотр рисовал оверлей
поверх всего, не глядя на порядок дорожек, а сборка живёт по правилу
«верхний клип побеждает целиком». Фотография лежала выше оверлея — в
ролике она его закрывала, в окне нет.
Теперь окно считает так же, как сборка: оверлей показывается только там,
где его не закрывает сплошной клип с дорожки выше. Закрытый посередине
оверлей разбивается на несколько окон показа. Проверка 11 в
`nlvetool flattest`.
**Текст не появлялся в окне.** Разметка лежит в файле с одним и тем же
именем на весь проект, а плеер сравнивал имена: имя прежнее — значит
перечитывать нечего. Правка текста доходила до окна только вместе с
перезагрузкой всего списка кусков.
Заодно проверено, что цепочка `lavfi-complex` субтитрам не мешает: та же
цепочка, прогнанная через mpv в файл, даёт текст на кадре (28828 белых
точек против 28758 без цепочки). Сравнивается теперь сама разметка, а не
имя файла, и при изменении она перечитывается принудительно.
**Две дорожки Т2.** Имя новой дорожке давалось по счёту дорожек своего
вида плюс единица. После удаления средней счёт совпадал с уже занятым
именем. Теперь имя подбирается свободное, а одинаковые имена из старых
проектов разводятся при чтении. Проверка 12.

View file

@ -38,6 +38,9 @@ uses
Classes, SysUtils, uAppTypes, uProject, uMediaProbe, uTransform;
type
{ Набор отрезков времени: им описывают, где оверлей виден. }
TDoubleArray = array of Double;
{ Клип, который в предпросмотре кладётся поверх основы.
Место в кадре — то же самое, по чему сборка обкладывает картинку
@ -1086,6 +1089,52 @@ begin
end;
end;
{ Вырезает из набора отрезков указанный. Отрезок посередине делит тот, в
который попал, надвое: оверлей, закрытый в середине, показывается двумя
окнами. }
procedure CutRange(var AFrom, ATo: TDoubleArray; var ACount: Integer;
const ACutFrom, ACutTo: Double);
var
I, N: Integer;
NewFrom, NewTo: array of Double;
procedure Push(const A, B: Double);
begin
if B <= A + 0.001 then
Exit;
NewFrom[N] := A;
NewTo[N] := B;
Inc(N);
end;
begin
if ACutTo <= ACutFrom then
Exit;
SetLength(NewFrom, ACount * 2 + 2);
SetLength(NewTo, ACount * 2 + 2);
N := 0;
for I := 0 to ACount - 1 do
begin
{ Не пересекаются — отрезок остаётся как был. }
if (ACutTo <= AFrom[I] + 0.001) or (ACutFrom >= ATo[I] - 0.001) then
Push(AFrom[I], ATo[I])
else
begin
Push(AFrom[I], ACutFrom);
Push(ACutTo, ATo[I]);
end;
end;
ACount := N;
SetLength(AFrom, N);
SetLength(ATo, N);
for I := 0 to N - 1 do
begin
AFrom[I] := NewFrom[I];
ATo[I] := NewTo[I];
end;
end;
{ Клип кладётся поверх, а не побеждает целиком, если он не сплошной:
с выбитым цветом или полупрозрачный. }
function IsOverlayClip(AClip: TClip): Boolean;
@ -1102,11 +1151,13 @@ function FlattenPreview(AProject: TProject;
var
Hidden: TFPList;
MissA: TGenRequests;
T, C, I, N: Integer;
Track: TTrack;
T, C, I, J, K, N: Integer;
Track, Above: TTrack;
Clip: TClip;
Item: TMediaItem;
Total: Double;
RangeA, RangeB: TDoubleArray;
RangeCount: Integer;
Ov: TPreviewOverlay;
{ Занято ли это время другим оверлеем. }
@ -1163,29 +1214,66 @@ begin
Hidden.Add(Clip);
Ov := Default(TPreviewOverlay);
Ov.TlStart := Clip.TlStart;
Ov.TlEnd := Clip.TlEnd;
Ov.FilePath := Item.EffectivePath;
Ov.SrcStart := Clip.SrcIn;
Ov.Filter := OverlayFilter(Clip.Transform,
Item.Info.Width, Item.Info.Height,
AProject.Master.Width, AProject.Master.Height,
Ov.X, Ov.Y, Ov.Width, Ov.Height);
{ Порядок тот же, что при сборке: сперва вписали в кадр, потом
выбили цвет, потом приглушили. Иначе смягчение края разошлось бы
с тем, что даст готовый ролик. }
if AWithKey then
{ Оверлей виден не всюду, где лежит: сверху его может закрывать
сплошной клип, и в готовом ролике его там не будет — побеждает
верхняя дорожка целиком. Показывать в окне то, чего не будет в
ролике, нельзя: именно из-за такого расхождения человек собирает
ролик и не находит в нём миникарты.
Закрывать могут и посередине — тогда от оверлея остаётся
несколько отрезков, и каждый становится своим окном показа. }
RangeCount := 1;
SetLength(RangeA, 1);
SetLength(RangeB, 1);
RangeA[0] := Clip.TlStart;
RangeB[0] := Clip.TlEnd;
for K := 0 to T - 1 do
begin
if Clip.ColorKey.Enabled then
Ov.Filter := Ov.Filter + ',' + ColorKeyFilter(Clip.ColorKey.Color,
Clip.ColorKey.Similarity, Clip.ColorKey.Blend);
if OpacityFilter(Clip.Opacity) <> '' then
Ov.Filter := Ov.Filter + ',' + OpacityFilter(Clip.Opacity);
Above := AProject.Tracks[K];
if Above.Kind <> tkVideo then
Continue;
if not AProject.TrackAudible(Above) then
Continue;
for J := 0 to Above.Count - 1 do
{ Клип с ключом или прозрачностью сам ничего не закрывает. }
if not IsOverlayClip(Above[J]) then
CutRange(RangeA, RangeB, RangeCount,
Above[J].TlStart, Above[J].TlEnd);
end;
for J := 0 to RangeCount - 1 do
begin
if RangeB[J] <= RangeA[J] + 0.001 then
Continue;
if OverlapsTaken(RangeA[J], RangeB[J]) then
Continue;
Ov := Default(TPreviewOverlay);
Ov.TlStart := RangeA[J];
Ov.TlEnd := RangeB[J];
Ov.FilePath := Item.EffectivePath;
Ov.SrcStart := Clip.SrcIn + (RangeA[J] - Clip.TlStart);
Ov.Filter := OverlayFilter(Clip.Transform,
Item.Info.Width, Item.Info.Height,
AProject.Master.Width, AProject.Master.Height,
Ov.X, Ov.Y, Ov.Width, Ov.Height);
{ Порядок тот же, что при сборке: сперва вписали в кадр, потом
выбили цвет, потом приглушили. Иначе смягчение края разошлось
бы с тем, что даст готовый ролик. }
if AWithKey then
begin
if Clip.ColorKey.Enabled then
Ov.Filter := Ov.Filter + ',' + ColorKeyFilter(Clip.ColorKey.Color,
Clip.ColorKey.Similarity, Clip.ColorKey.Blend);
if OpacityFilter(Clip.Opacity) <> '' then
Ov.Filter := Ov.Filter + ',' + OpacityFilter(Clip.Opacity);
end;
SetLength(AOverlays, N + 1);
AOverlays[N] := Ov;
Inc(N);
end;
SetLength(AOverlays, N + 1);
AOverlays[N] := Ov;
Inc(N);
end;
end;

View file

@ -101,6 +101,8 @@ type
{ Наложение для плеера: список кусков оверлея и окна показа. }
{ Журнал текущей сборки: файл рядом с готовым роликом. }
FExportLogPath: string;
{ Разметка текста, отданная плееру в прошлый раз. }
FLastAss: string;
{ Проект открыт из командной строки: спросить про клипы на чужих
дорожках можно только когда окно уже показано. }
FCheckMisplacedOnShow: Boolean;
@ -903,7 +905,7 @@ begin
FProject.BeginChange;
if Track = nil then
Track := FProject.AddTrackOfKind('Т1', tkText);
Track := FProject.AddTrackOfKind(FProject.UniqueTrackName('Т'), tkText);
Clip := TClip.Create;
Clip.Id := FProject.NewId('c');
@ -1707,8 +1709,7 @@ begin
if ATrackIndex < 0 then
begin
FProject.AddTrack('V' +
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
FProject.AddTrack(FProject.UniqueTrackName('V'));
ATrackIndex := FProject.TrackCount - 1;
Log('Заведена дорожка ' + FProject.Tracks[ATrackIndex].Name);
end;
@ -1738,11 +1739,9 @@ begin
if Track = nil then
begin
if NeedKind = tkAudio then
Track := FProject.AddTrackOfKind('A' +
IntToStr(FProject.TrackCountOf(tkAudio) + 1), tkAudio)
Track := FProject.AddTrackOfKind(FProject.UniqueTrackName('A'), tkAudio)
else
Track := FProject.AddTrack('V' +
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
Track := FProject.AddTrack(FProject.UniqueTrackName('V'));
end;
Log(Format('Дорожка %s не для этого вида клипов — положил на %s.',
[Wanted, Track.Name]));
@ -2894,18 +2893,30 @@ end;
Пересобирать цепочку на каждый кадр нельзя — она создаётся заново и
звук щёлкает. Поэтому только при изменении. }
{ Разметка текста для предпросмотра.
Сравнивается сама разметка, а не имя файла: имя у неё одно на весь
проект, и правка текста его не меняет. Плеер же, увидев прежнее имя,
файл не перечитывал — и новая надпись в окне не появлялась вовсе, пока
не перезагрузится весь список кусков. }
procedure TMainForm.UpdateSubtitles;
var
Path: string;
{ Не Text: так называется свойство формы. }
Path, Markup: string;
begin
if (FProject = nil) or (FPlayer = nil) then
Exit;
if FProject.CacheDir = '' then
Exit;
Markup := BuildAss(FProject);
if Markup = FLastAss then
Exit;
FLastAss := Markup;
Path := IncludeTrailingPathDelimiter(FProject.CacheDir) + 'subs.ass';
if WriteAssFile(FProject, Path) then
FPlayer.SetSubtitleFile(Path)
if (Markup <> '') and WriteAssFile(FProject, Path) then
FPlayer.SetSubtitleFile(Path, True)
else
FPlayer.SetSubtitleFile('');
end;
@ -3529,8 +3540,7 @@ end;
procedure TMainForm.MenuAddTrack(Sender: TObject);
begin
FProject.BeginChange;
FProject.AddTrack('V' +
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
FProject.AddTrack(FProject.UniqueTrackName('V'));
FTimeline.Invalidate;
UpdateCaption;
end;

View file

@ -218,7 +218,10 @@ type
Держится отдельно от списка кусков: текст правится чаще, чем
монтаж, и перезагружать из-за него картинку незачем. }
procedure SetSubtitleFile(const APath: string);
{ AForce — перечитать файл, даже если имя прежнее. Нужно всегда, когда
разметка переписана: имя у неё одно на весь проект, и по имени
изменение не видно. }
procedure SetSubtitleFile(const APath: string; AForce: Boolean = False);
end;
implementation
@ -1096,9 +1099,9 @@ end;
подключаем новые, не трогая воспроизведение. При загрузке нового списка
кусков mpv забывает все внешние дорожки, поэтому там подключение
повторяется — см. SelectExternalAudio. }
procedure TMpvPlayer.SetSubtitleFile(const APath: string);
procedure TMpvPlayer.SetSubtitleFile(const APath: string; AForce: Boolean);
begin
if FSubtitlePath = APath then
if (FSubtitlePath = APath) and (not AForce) then
Exit;
FSubtitlePath := APath;
if FHandle = nil then

View file

@ -282,6 +282,14 @@ type
{ Номер новой связки. Отдельным именем, чтобы не пришлось помнить
префикс в каждом месте, где связка заводится. }
function NewGroupId: string;
{ Имя, которого ещё нет ни у одной дорожки: V1, V2, ... Считать
дорожки нужного вида и прибавлять единицу нельзя — после удаления
средней счёт совпадёт с уже занятым именем, и в окне окажутся две
дорожки Т2. }
function UniqueTrackName(const APrefix: string): string;
{ Разводит одинаковые имена дорожек, доставшиеся от старых проектов.
Имя — только подпись, но две дорожки Т2 в окне сбивают с толку. }
procedure EnsureUniqueTrackNames;
function AddTrack(const AName: string): TTrack;
function AddTrackOfKind(const AName: string; AKind: TTrackKind): TTrack;
function FindTrack(const AId: string): TTrack;
@ -717,6 +725,45 @@ begin
FModified := True;
end;
function TProject.UniqueTrackName(const APrefix: string): string;
var
N, I: Integer;
Taken: Boolean;
begin
N := 1;
repeat
Result := APrefix + IntToStr(N);
Taken := False;
for I := 0 to FTracks.Count - 1 do
if TTrack(FTracks[I]).Name = Result then
begin
Taken := True;
Break;
end;
Inc(N);
until not Taken;
end;
procedure TProject.EnsureUniqueTrackNames;
var
I, J: Integer;
Prefix: string;
begin
for I := 0 to FTracks.Count - 1 do
for J := 0 to I - 1 do
if TTrack(FTracks[J]).Name = TTrack(FTracks[I]).Name then
begin
case TTrack(FTracks[I]).Kind of
tkAudio: Prefix := 'A';
tkText: Prefix := 'Т';
else
Prefix := 'V';
end;
TTrack(FTracks[I]).Name := UniqueTrackName(Prefix);
Break;
end;
end;
function TProject.AddTrack(const AName: string): TTrack;
begin
Result := AddTrackOfKind(AName, tkVideo);
@ -1083,13 +1130,10 @@ begin
if Target = nil then
case Need of
tkAudio: Target := AddTrackOfKind('A' +
IntToStr(TrackCountOf(tkAudio) + 1), tkAudio);
tkText: Target := AddTrackOfKind('Т' +
IntToStr(TrackCountOf(tkText) + 1), tkText);
tkAudio: Target := AddTrackOfKind(UniqueTrackName('A'), tkAudio);
tkText: Target := AddTrackOfKind(UniqueTrackName('Т'), tkText);
else
Target := AddTrackOfKind('V' +
IntToStr(TrackCountOf(tkVideo) + 1), tkVideo);
Target := AddTrackOfKind(UniqueTrackName('V'), tkVideo);
end;
Track.Extract(Clip);
@ -1737,6 +1781,7 @@ begin
MigrateAudio;
FixClipKinds;
EnsureUniqueTrackNames;
Settings := Obj.Find('settings', jtObject) as TJSONObject;
if Settings <> nil then

View file

@ -2084,13 +2084,10 @@ begin
(not FTrackAddedDuringDrag) then
begin
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);
tkAudio: FProject.AddTrackOfKind(FProject.UniqueTrackName('A'), tkAudio);
tkText: FProject.AddTrackOfKind(FProject.UniqueTrackName('Т'), tkText);
else
FProject.AddTrack('V' +
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
FProject.AddTrack(FProject.UniqueTrackName('V'));
end;
FTrackAddedDuringDrag := True;
Idx := FProject.TrackCount - 1;

View file

@ -854,6 +854,65 @@ begin
end;
WriteLn;
{ --- 11. Оверлей под сплошным клипом в окне не показывается --- }
WriteLn(' 11. Оверлей закрыт сверху сплошным клипом');
Proj := MakeProject(3, Media);
try
Proj.Master := Media.Info;
{ Снизу вверх: V3 — съёмка, V2 — оверлей с ключом на весь ролик,
V1 — фотография поверх куска. Так и было в живом проекте. }
Proj.AddClip(Proj.Tracks[2], Media, 0, 20, 0);
Clip := Proj.AddClip(Proj.Tracks[1], Media, 0, 20, 0);
Key := DefaultColorKey;
Key.Enabled := True;
Clip.ColorKey := Key;
Clip := Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 5);
Clip.Kind := ckImage;
Check(FlattenPreview(Proj, Segs, AudioSegs, Overlays, Missing),
'уплощение для предпросмотра прошло');
{ Фотография лежит с 5 по 10 секунду и закрывает оверлей: показывать
его там нельзя — в ролике его там не будет. Значит, от оверлея
остаются два окна: до фотографии и после. }
Check(Length(Overlays) = 2, 'оверлей показывается двумя окнами: ' +
IntToStr(Length(Overlays)));
if Length(Overlays) = 2 then
begin
Check(Abs(Overlays[0].TlStart) < 0.001, 'первое окно с начала');
Check(Abs(Overlays[0].TlEnd - 5) < 0.001, 'и до фотографии');
Check(Abs(Overlays[1].TlStart - 10) < 0.001, 'второе после неё');
Check(Abs(Overlays[1].TlEnd - 20) < 0.001, 'и до конца');
Check(Abs(Overlays[1].SrcStart - 10) < 0.001,
'и берётся с того же места исходника');
end;
finally
Proj.Free;
end;
WriteLn;
{ --- 12. Имена дорожек не повторяются --- }
WriteLn(' 12. Имена дорожек');
Proj := MakeProject(3, Media);
try
Check(Proj.UniqueTrackName('V') = 'V4', 'следующее имя V4: ' +
Proj.UniqueTrackName('V'));
{ Убираем среднюю: счёт дорожек снова три, но имя V3 уже занято, и
считать их для имени нельзя. }
Proj.RemoveTrack(1);
Check(Proj.UniqueTrackName('V') = 'V2', 'после удаления средней — V2: ' +
Proj.UniqueTrackName('V'));
Proj.AddTrack(Proj.UniqueTrackName('V'));
Check(Proj.Tracks[2].Name = 'V2', 'новая дорожка названа V2');
{ Одинаковые имена, доставшиеся от старого проекта, разводятся. }
Proj.Tracks[2].Name := 'V1';
Proj.EnsureUniqueTrackNames;
Check(Proj.Tracks[2].Name <> 'V1', 'повтор разведён: ' + Proj.Tracks[2].Name);
finally
Proj.Free;
end;
WriteLn;
WriteLn;
if FailCount = 0 then
begin