Связки клипов и порядок дорожек
Ctrl+G связывает выделенные клипы и развязывает их, если они уже в одной связке. Связка двигается, режется и удаляется целиком: половина связки — это разъехавшийся звук, который потом не найти. Форма звука переехала на звуковые клипы: на клипе картинки ей не место, звук у него теперь свой и живёт на своей дорожке. Кнопка кадрирования, наоборот, осталась только у картинки. Новая дорожка картинки встаёт перед первой звуковой, а не в конец списка: порядок дорожек задаёт, кто побеждает при перекрытии. Найдена и исправлена настоящая ошибка: при резе новый кусок терял громкость и кадрирование — они просто не переносились на хвост.
This commit is contained in:
parent
a06ecfd720
commit
b6e215a03f
3 changed files with 221 additions and 17 deletions
|
|
@ -1081,7 +1081,8 @@ begin
|
|||
|
||||
if ATrackIndex < 0 then
|
||||
begin
|
||||
FProject.AddTrack('V' + IntToStr(FProject.TrackCount + 1));
|
||||
FProject.AddTrack('V' +
|
||||
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
|
||||
ATrackIndex := FProject.TrackCount - 1;
|
||||
Log('Заведена дорожка ' + FProject.Tracks[ATrackIndex].Name);
|
||||
end;
|
||||
|
|
@ -1167,7 +1168,9 @@ begin
|
|||
end;
|
||||
|
||||
FProject.BeginChange;
|
||||
FProject.AddClip(Track, Item, 0, Duration, PlaceAt);
|
||||
{ Парой: клип картинки и его звук на соответствующей звуковой дорожке.
|
||||
Связка держит их вместе, пока её не развяжут по Ctrl+G. }
|
||||
FProject.AddClipPair(Track, Item, 0, Duration, PlaceAt);
|
||||
FTimeline.Invalidate;
|
||||
SchedulePreview;
|
||||
UpdateCaption;
|
||||
|
|
@ -2278,7 +2281,8 @@ end;
|
|||
procedure TMainForm.MenuAddTrack(Sender: TObject);
|
||||
begin
|
||||
FProject.BeginChange;
|
||||
FProject.AddTrack('V' + IntToStr(FProject.TrackCount + 1));
|
||||
FProject.AddTrack('V' +
|
||||
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
|
||||
FTimeline.Invalidate;
|
||||
UpdateCaption;
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -188,7 +188,11 @@ type
|
|||
function FindMediaByPath(const APath: string): TMediaItem;
|
||||
procedure RemoveMedia(AItem: TMediaItem);
|
||||
|
||||
{ Номер новой связки. Отдельным именем, чтобы не пришлось помнить
|
||||
префикс в каждом месте, где связка заводится. }
|
||||
function NewGroupId: string;
|
||||
function AddTrack(const AName: string): TTrack;
|
||||
function AddTrackOfKind(const AName: string; AKind: TTrackKind): TTrack;
|
||||
function FindTrack(const AId: string): TTrack;
|
||||
procedure RemoveTrack(AIndex: Integer);
|
||||
procedure MoveTrack(AFrom, ATo: Integer);
|
||||
|
|
@ -458,8 +462,8 @@ begin
|
|||
так он не мешает смотреть на картинку. }
|
||||
AddTrack('V1');
|
||||
AddTrack('V2');
|
||||
AddTrack('A1').Kind := tkAudio;
|
||||
AddTrack('A2').Kind := tkAudio;
|
||||
AddTrackOfKind('A1', tkAudio);
|
||||
AddTrackOfKind('A2', tkAudio);
|
||||
FModified := False;
|
||||
end;
|
||||
|
||||
|
|
@ -483,6 +487,11 @@ begin
|
|||
Result := FTracks.Count;
|
||||
end;
|
||||
|
||||
function TProject.NewGroupId: string;
|
||||
begin
|
||||
Result := NewId('g');
|
||||
end;
|
||||
|
||||
function TProject.NewId(const APrefix: string): string;
|
||||
begin
|
||||
Result := APrefix + IntToStr(FNextId);
|
||||
|
|
@ -544,11 +553,37 @@ begin
|
|||
end;
|
||||
|
||||
function TProject.AddTrack(const AName: string): TTrack;
|
||||
begin
|
||||
Result := AddTrackOfKind(AName, tkVideo);
|
||||
end;
|
||||
|
||||
function TProject.AddTrackOfKind(const AName: string;
|
||||
AKind: TTrackKind): TTrack;
|
||||
var
|
||||
I, Pos: Integer;
|
||||
begin
|
||||
Result := TTrack.Create;
|
||||
Result.Id := NewId('t');
|
||||
Result.Name := AName;
|
||||
FTracks.Add(Result);
|
||||
Result.Kind := AKind;
|
||||
|
||||
{ Порядок дорожек не только украшение: он задаёт, кто побеждает при
|
||||
перекрытии. Картинка всегда выше звука, поэтому новая дорожка
|
||||
картинки встаёт перед первой звуковой, а не в самый конец. }
|
||||
if AKind = tkVideo then
|
||||
begin
|
||||
Pos := FTracks.Count;
|
||||
for I := 0 to FTracks.Count - 1 do
|
||||
if TTrack(FTracks[I]).Kind = tkAudio then
|
||||
begin
|
||||
Pos := I;
|
||||
Break;
|
||||
end;
|
||||
FTracks.Insert(Pos, Result);
|
||||
end
|
||||
else
|
||||
FTracks.Add(Result);
|
||||
|
||||
FModified := True;
|
||||
end;
|
||||
|
||||
|
|
@ -693,8 +728,7 @@ begin
|
|||
Have := TrackCountOf(tkAudio);
|
||||
for I := Have to Need - 1 do
|
||||
begin
|
||||
Track := AddTrack('A' + IntToStr(I + 1));
|
||||
Track.Kind := tkAudio;
|
||||
Track := AddTrackOfKind('A' + IntToStr(I + 1), tkAudio);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ type
|
|||
клипы, которые уже сдвинулись. }
|
||||
FRippleClips: array of TClip;
|
||||
FRippleOrigStarts: array of Double;
|
||||
FGroupClips: array of TClip;
|
||||
FGroupOrigStarts: array of Double;
|
||||
|
||||
FOnPlayheadChange: TNotifyEvent;
|
||||
FOnStructureChange: TNotifyEvent;
|
||||
|
|
@ -215,6 +217,15 @@ type
|
|||
расстояния между опорными кадрами. }
|
||||
procedure SplitAtPlayhead(APrecise: Boolean; AOnlyCurrentTrack: Boolean);
|
||||
procedure DeleteSelected;
|
||||
{ Связать выделенные клипы или развязать, если они уже связаны.
|
||||
|
||||
Связанные двигаются, режутся и удаляются вместе. По умолчанию так
|
||||
связаны картинка и её звук. }
|
||||
procedure ToggleGroupSelected;
|
||||
procedure SelectWholeGroups;
|
||||
{ Клипы одной связки с перетаскиваемым и их исходные позиции. }
|
||||
procedure CollectGroupClips(AClip: TClip);
|
||||
procedure ApplyGroupMove(const ADelta: Double);
|
||||
{ Сдвинуть клипы влево, закрыв дыру под курсором. }
|
||||
procedure RippleCloseGap;
|
||||
procedure SelectAll;
|
||||
|
|
@ -311,6 +322,7 @@ const
|
|||
ColorEdgePrecise = TColor($FFC030); // голубой: рез потребует перекодирования
|
||||
ColorWave = TColor($96C8D2); // пастельно-жёлтый: форма звука
|
||||
ColorCropButton = TColor($4A6A8A); // кнопка кадрирования на клипе
|
||||
ColorClipAudio = TColor($5A4A2A); // приглушённый синий: звуковой клип
|
||||
ColorVolumeLine = TColor($60E0E0); // жёлтый: линия громкости
|
||||
ColorVolumeHandle = TColor($30C0FF); // оранжевый: ручка громкости
|
||||
ColorHeaderActive = TColor($3A3A3A); // подсветка выбранной дорожки
|
||||
|
|
@ -659,6 +671,8 @@ begin
|
|||
ACanvas.Brush.Color := ColorClipSelected
|
||||
else if AClip.Kind = ckImage then
|
||||
ACanvas.Brush.Color := ColorClipImage
|
||||
else if AClip.Kind = ckAudio then
|
||||
ACanvas.Brush.Color := ColorClipAudio
|
||||
else
|
||||
ACanvas.Brush.Color := ColorClip;
|
||||
ACanvas.FillRect(Body);
|
||||
|
|
@ -691,9 +705,8 @@ begin
|
|||
нём рисуется. }
|
||||
DrawWaveform(ACanvas, AClip, Body);
|
||||
|
||||
{ Кнопка кадрирования — поверх всего, чтобы её не перекрывала ни форма
|
||||
звука, ни подписи. }
|
||||
if AClip.Kind <> ckBlack then
|
||||
{ Кнопка кадрирования — только у картинки: звук кадрировать нечем. }
|
||||
if AClip.Kind in [ckVideo, ckImage] then
|
||||
DrawCropButton(ACanvas, Body);
|
||||
|
||||
{ Громкость рисуется до подписей: иначе её линия перечёркивает название
|
||||
|
|
@ -738,7 +751,9 @@ var
|
|||
SrcTime: Double;
|
||||
Idx, Value: Integer;
|
||||
begin
|
||||
if (FProject = nil) or (AClip.Kind <> ckVideo) then
|
||||
{ Форма звука рисуется на звуковом клипе. На клипе картинки ей не место:
|
||||
звук у него отдельный и живёт на своей дорожке. }
|
||||
if (FProject = nil) or (AClip.Kind <> ckAudio) then
|
||||
Exit;
|
||||
Item := FProject.FindMedia(AClip.MediaId);
|
||||
if (Item = nil) or (not Item.Info.HasAudio) then
|
||||
|
|
@ -794,7 +809,7 @@ var
|
|||
L, R: Integer;
|
||||
begin
|
||||
Result := Rect(0, 0, 0, 0);
|
||||
if (AClip = nil) or (AClip.Kind = ckBlack) then
|
||||
if (AClip = nil) or (not (AClip.Kind in [ckVideo, ckImage])) then
|
||||
Exit;
|
||||
|
||||
L := TimeToX(AClip.TlStart);
|
||||
|
|
@ -1329,6 +1344,9 @@ begin
|
|||
|
||||
FDragClip := Clip;
|
||||
FDragTrack := Track;
|
||||
{ Клипы связки поедут следом. Список составляется один раз при нажатии:
|
||||
пересчитывать его на каждое движение нельзя — они уже сдвинулись. }
|
||||
CollectGroupClips(Clip);
|
||||
FDragOrigStart := Clip.TlStart;
|
||||
FDragOrigSrcIn := Clip.SrcIn;
|
||||
FDragOrigDuration := Clip.Duration;
|
||||
|
|
@ -1463,6 +1481,8 @@ begin
|
|||
begin
|
||||
NewStart := SnapTime(XToTime(X) - FDragGrabOffset, FDragClip);
|
||||
FDragClip.TlStart := Max(0, NewStart);
|
||||
{ Связка едет целиком: звук должен остаться под своей картинкой. }
|
||||
ApplyGroupMove(FDragClip.TlStart - FDragOrigStart);
|
||||
{ Всё, что правее, едет на ту же величину. }
|
||||
if Length(FRippleClips) > 0 then
|
||||
ApplyRipple(FDragClip.TlStart - FDragOrigStart);
|
||||
|
|
@ -1473,7 +1493,8 @@ begin
|
|||
if (Idx < 0) and (FProject <> nil) and (Y > TracksBottom) and
|
||||
(not FTrackAddedDuringDrag) then
|
||||
begin
|
||||
FProject.AddTrack('V' + IntToStr(FProject.TrackCount + 1));
|
||||
FProject.AddTrack('V' +
|
||||
IntToStr(FProject.TrackCountOf(tkVideo) + 1));
|
||||
FTrackAddedDuringDrag := True;
|
||||
Idx := FProject.TrackCount - 1;
|
||||
if Assigned(FOnTrackAdded) then
|
||||
|
|
@ -1592,6 +1613,39 @@ end;
|
|||
двигал нижние, а нижний не двигал верхние — те начинались раньше его
|
||||
конца и в список не попадали. От начала правило читается просто: едет
|
||||
всё, что начинается не раньше того, за что взялись. }
|
||||
procedure TTimelineControl.CollectGroupClips(AClip: TClip);
|
||||
var
|
||||
T, C, N: Integer;
|
||||
Clip: TClip;
|
||||
begin
|
||||
FGroupClips := nil;
|
||||
FGroupOrigStarts := nil;
|
||||
if (FProject = nil) or (AClip = nil) or (AClip.GroupId = '') then
|
||||
Exit;
|
||||
|
||||
N := 0;
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
for C := 0 to FProject.Tracks[T].Count - 1 do
|
||||
begin
|
||||
Clip := FProject.Tracks[T][C];
|
||||
if (Clip = AClip) or (Clip.GroupId <> AClip.GroupId) then
|
||||
Continue;
|
||||
SetLength(FGroupClips, N + 1);
|
||||
SetLength(FGroupOrigStarts, N + 1);
|
||||
FGroupClips[N] := Clip;
|
||||
FGroupOrigStarts[N] := Clip.TlStart;
|
||||
Inc(N);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.ApplyGroupMove(const ADelta: Double);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := 0 to High(FGroupClips) do
|
||||
FGroupClips[I].TlStart := Max(0, FGroupOrigStarts[I] + ADelta);
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.CollectRippleClips(ATrackIndex: Integer;
|
||||
AExclude: TClip; const AFromTime: Double);
|
||||
var
|
||||
|
|
@ -1741,6 +1795,7 @@ var
|
|||
Clip, Tail: TClip;
|
||||
AnyChange: Boolean;
|
||||
Offset: Double;
|
||||
HoverGroup: string;
|
||||
begin
|
||||
if FProject = nil then
|
||||
Exit;
|
||||
|
|
@ -1748,16 +1803,28 @@ begin
|
|||
AnyChange := False;
|
||||
FProject.BeginChange;
|
||||
|
||||
{ Связка режется целиком. Резать только картинку, оставив звук цельным,
|
||||
значит развалить пару: дальше они поедут порознь. }
|
||||
HoverGroup := '';
|
||||
if AOnlyCurrentTrack and (FHoverTrack >= 0) and
|
||||
(FHoverTrack < FProject.TrackCount) then
|
||||
begin
|
||||
Clip := FProject.Tracks[FHoverTrack].ClipAt(FPlayhead);
|
||||
if Clip <> nil then
|
||||
HoverGroup := Clip.GroupId;
|
||||
end;
|
||||
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
begin
|
||||
if AOnlyCurrentTrack and (FHoverTrack >= 0) and (T <> FHoverTrack) then
|
||||
Continue;
|
||||
|
||||
Track := FProject.Tracks[T];
|
||||
Clip := Track.ClipAt(FPlayhead);
|
||||
if Clip = nil then
|
||||
Continue;
|
||||
|
||||
if AOnlyCurrentTrack and (FHoverTrack >= 0) and (T <> FHoverTrack) then
|
||||
if (HoverGroup = '') or (Clip.GroupId <> HoverGroup) then
|
||||
Continue;
|
||||
|
||||
Offset := FPlayhead - Clip.TlStart;
|
||||
{ Рез вплотную к краю ничего не даёт, только плодит клипы нулевой длины. }
|
||||
if (Offset < 0.001) or (Offset > Clip.Duration - 0.001) then
|
||||
|
|
@ -1771,6 +1838,12 @@ begin
|
|||
Tail.TlStart := FPlayhead;
|
||||
Tail.Duration := Clip.Duration - Offset;
|
||||
Tail.EdgeOut := Clip.EdgeOut;
|
||||
{ Хвост — тот же клип, только вторая его половина. Всё, что было
|
||||
настроено, должно перейти к нему: иначе после реза половина клипа
|
||||
теряет громкость и кадрирование. }
|
||||
Tail.Volume := Clip.Volume;
|
||||
Tail.Transform := Clip.Transform;
|
||||
Tail.GroupId := Clip.GroupId;
|
||||
|
||||
{ Новый рез описывается одинаково с обеих сторон: конец первой части
|
||||
и начало второй — это одно и то же место. }
|
||||
|
|
@ -1798,6 +1871,89 @@ begin
|
|||
FProject.Undo;
|
||||
end;
|
||||
|
||||
{ Расширяет выделение на всю связку: клипы, связанные с уже выделенными,
|
||||
тоже считаются выделенными. Иначе удаление или перенос разорвали бы
|
||||
связку пополам. }
|
||||
procedure TTimelineControl.SelectWholeGroups;
|
||||
var
|
||||
T, C, T2, C2: Integer;
|
||||
Groups: TStringList;
|
||||
Clip: TClip;
|
||||
begin
|
||||
if FProject = nil then
|
||||
Exit;
|
||||
|
||||
Groups := TStringList.Create;
|
||||
try
|
||||
Groups.Sorted := True;
|
||||
Groups.Duplicates := dupIgnore;
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
for C := 0 to FProject.Tracks[T].Count - 1 do
|
||||
begin
|
||||
Clip := FProject.Tracks[T][C];
|
||||
if Clip.Selected and (Clip.GroupId <> '') then
|
||||
Groups.Add(Clip.GroupId);
|
||||
end;
|
||||
if Groups.Count = 0 then
|
||||
Exit;
|
||||
|
||||
for T2 := 0 to FProject.TrackCount - 1 do
|
||||
for C2 := 0 to FProject.Tracks[T2].Count - 1 do
|
||||
begin
|
||||
Clip := FProject.Tracks[T2][C2];
|
||||
if (Clip.GroupId <> '') and (Groups.IndexOf(Clip.GroupId) >= 0) then
|
||||
Clip.Selected := True;
|
||||
end;
|
||||
finally
|
||||
Groups.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.ToggleGroupSelected;
|
||||
var
|
||||
T, C, N: Integer;
|
||||
Clip: TClip;
|
||||
Group: string;
|
||||
AllSame: Boolean;
|
||||
begin
|
||||
if FProject = nil then
|
||||
Exit;
|
||||
|
||||
{ Сначала смотрим, что выделено: если все выделенные уже в одной
|
||||
связке, значит их надо развязать. }
|
||||
Group := '';
|
||||
AllSame := True;
|
||||
N := 0;
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
for C := 0 to FProject.Tracks[T].Count - 1 do
|
||||
begin
|
||||
Clip := FProject.Tracks[T][C];
|
||||
if not Clip.Selected then
|
||||
Continue;
|
||||
Inc(N);
|
||||
if Group = '' then
|
||||
Group := Clip.GroupId
|
||||
else if Clip.GroupId <> Group then
|
||||
AllSame := False;
|
||||
end;
|
||||
|
||||
if N < 1 then
|
||||
Exit;
|
||||
|
||||
FProject.BeginChange;
|
||||
if AllSame and (Group <> '') then
|
||||
Group := ''
|
||||
else
|
||||
Group := FProject.NewGroupId;
|
||||
|
||||
for T := 0 to FProject.TrackCount - 1 do
|
||||
for C := 0 to FProject.Tracks[T].Count - 1 do
|
||||
if FProject.Tracks[T][C].Selected then
|
||||
FProject.Tracks[T][C].GroupId := Group;
|
||||
|
||||
StructureChanged;
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.DeleteSelected;
|
||||
var
|
||||
T, C: Integer;
|
||||
|
|
@ -1807,6 +1963,10 @@ begin
|
|||
if FProject = nil then
|
||||
Exit;
|
||||
|
||||
{ Удалять надо связку целиком: половина связки — это разъехавшийся
|
||||
звук, который потом не найти. }
|
||||
SelectWholeGroups;
|
||||
|
||||
AnyChange := False;
|
||||
FProject.BeginChange;
|
||||
|
||||
|
|
@ -1946,6 +2106,12 @@ begin
|
|||
DeleteSelected;
|
||||
Key := 0;
|
||||
end;
|
||||
VK_G:
|
||||
if ssCtrl in Shift then
|
||||
begin
|
||||
ToggleGroupSelected;
|
||||
Key := 0;
|
||||
end;
|
||||
VK_A:
|
||||
if ssCtrl in Shift then
|
||||
begin
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue