diff --git a/BACKLOG.md b/BACKLOG.md index ef3652d..f507c80 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -76,6 +76,28 @@ ffmpeg проблемой не является: он запускается о MP4/H.264 нельзя — контейнер и кодек альфу не хранят. Для этого нужен отдельный вывод в VP9 или ProRes 4444. +## Окно настроек программы + +Переключателей, которые стоят машинного времени, становится больше, а +живут они кто где: прилипание и схлопывание — кнопками внизу, ключ в +предпросмотре — в меню «Вид», выбор кодировщика вообще нигде (берётся +NVENC, если он есть). Это разрозненно и не даёт увидеть общую цену. + +Нужно одно окно, где собрано всё, что можно выключить ради скорости: + +- ключ и прозрачность в предпросмотре (сейчас в меню «Вид»); +- наложение в предпросмотре вообще — на слабой машине может не хватать и + его, тогда честнее показывать одну основу; +- кодировщик для пересборки: аппаратный или процессорный, и какой + именно; +- где держать кэш и когда его убирать; +- прилипание клипов и курсора (сейчас кнопками, но им место и здесь). + +Почему это важно: разработка идёт на машине с быстрой видеокартой, и +всё, что здесь считается «недорогим», на другой может оказаться +неподъёмным. Пока цифры замеряны только на одной машине, у человека +должна быть возможность выключить любую из этих трат. + ## Мелочи, до которых не дошли руки **Значок файла программы под Windows.** Значок окна задаётся из diff --git a/README.md b/README.md index 3a66955..a4f52b7 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,18 @@ xdg-mime query default application/x-nlve-project липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без другого. +**Ключ и прозрачность в предпросмотре** (меню «Вид»). Включено — окно +показывает то же, что даст сборка: выбитый цвет и приглушение считаются +на лету. Выключено — оверлей виден целиком, со своим фоном: так понятно, +куда он встал и где его края. + +Считается это по площади самого оверлея, а не всего кадра. Замер на +2688x1512 (600 кадров, десять секунд материала): основа 2.07 с, с +наложением 2.43 с, с ключом и прозрачностью 2.66 с. То есть наложение +стоит 0.36 с, ключ добавляет ещё 0.23 с — вчетверо быстрее реального +времени. На слабой машине это может быть заметнее, потому выключатель и +есть. + **Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена — автосхлопывание включено, и тогда при перетаскивании клипа всё, что начинается не раньше него, едет следом. Стрелка рядом открывает выбор diff --git a/src/uFfmpegCmd.pas b/src/uFfmpegCmd.pas index c902428..b08f31e 100644 --- a/src/uFfmpegCmd.pas +++ b/src/uFfmpegCmd.pas @@ -235,7 +235,7 @@ function ParseProgressLine(const ALine: string; var ASeconds: Double; implementation uses - StrUtils; + StrUtils, uTransform; { Общие параметры вывода в формате проекта. Объявлено заранее, потому что используется выше по тексту, чем определено. } @@ -318,12 +318,8 @@ begin масштабом. Цвет записывается как 0xRRGGBB — порядок обратный тому, в каком он хранится у нас. } if ALayers[I].KeyEnabled then - Fit := Fit + Format(',colorkey=0x%.2x%.2x%.2x:%s:%s', - [ALayers[I].KeyColor and $FF, - (ALayers[I].KeyColor shr 8) and $FF, - (ALayers[I].KeyColor shr 16) and $FF, - SecondsToStr(ALayers[I].KeySimilarity / 100), - SecondsToStr(ALayers[I].KeyBlend / 100)]); + Fit := Fit + ',' + ColorKeyFilter(ALayers[I].KeyColor, + ALayers[I].KeySimilarity, ALayers[I].KeyBlend); Graph := Graph + Format('[%d:v]%s,format=yuva420p[v%d];', [I, Fit, I]); end; diff --git a/src/uFlatten.pas b/src/uFlatten.pas index 8386ef6..b639f8b 100644 --- a/src/uFlatten.pas +++ b/src/uFlatten.pas @@ -140,11 +140,17 @@ function FlattenProject(AProject: TProject; под ними видно то, что лежит ниже, — а выдаются отдельным списком со временем и местом в кадре. + AWithKey — применять ли к оверлею цветовой ключ и прозрачность прямо в + плеере. Тогда окно показывает то же, что даст сборка. Стоит это + недорого: фильтр работает по площади самого оверлея, а не всего кадра, + но на слабой машине лишним не будет, поэтому решает вызывающий. + Для сборки ролика это не применяется: там всё сводится по-настоящему. } function FlattenPreview(AProject: TProject; out AVideo, AAudio: TFlatSegmentArray; out AOverlays: TPreviewOverlays; - out AMissing: TGenRequests): Boolean; + out AMissing: TGenRequests; + AWithKey: Boolean = False): Boolean; { Путь к сгенерированному клипу. Имя выводится из содержимого запроса, поэтому одинаковые запросы дают один и тот же файл, и он делается @@ -1034,7 +1040,8 @@ end; function FlattenPreview(AProject: TProject; out AVideo, AAudio: TFlatSegmentArray; out AOverlays: TPreviewOverlays; - out AMissing: TGenRequests): Boolean; + out AMissing: TGenRequests; + AWithKey: Boolean): Boolean; var Hidden: TFPList; MissA: TGenRequests; @@ -1108,6 +1115,17 @@ begin 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); diff --git a/src/uMainForm.pas b/src/uMainForm.pas index 2a34a93..c60e5c0 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -129,6 +129,9 @@ type первую. } FErrorCount: Integer; FLogButton: TThemedButton; + { Пункт «Ключ и прозрачность в предпросмотре»: галку в нём приходится + держать за руку — она не следует за настройкой сама. } + FKeyPreviewItem: TMenuItem; { Показанная доля выполнения. Отдельно от настоящей, потому что растёт она по своим правилам — см. AdvanceJobBar. } FJobShown: Double; @@ -238,6 +241,7 @@ type procedure AdvanceJobBar; procedure LogButtonClick(Sender: TObject); procedure MenuToggleLog(Sender: TObject); + procedure MenuTogglePreviewKey(Sender: TObject); procedure MenuAbout(Sender: TObject); procedure MenuAddText(Sender: TObject); { Открывает редактор текста для клипа. True — что-то изменилось. } @@ -583,8 +587,10 @@ procedure TMainForm.BuildMenu; FMenu.Items.Add(Result); end; - procedure AddItem(AParent: TMenuItem; const ACaption: string; - AHandler: TNotifyEvent; AShortCut: TShortCut = 0); + { Возвращает созданный пункт: у пунктов с галкой её потом надо ставить + и снимать, а искать пункт по названию — способ ненадёжный. } + function AddItem(AParent: TMenuItem; const ACaption: string; + AHandler: TNotifyEvent; AShortCut: TShortCut = 0): TMenuItem; var Item: TMenuItem; begin @@ -593,6 +599,7 @@ procedure TMainForm.BuildMenu; Item.OnClick := AHandler; Item.ShortCut := AShortCut; AParent.Add(Item); + Result := Item; end; procedure AddSep(AParent: TMenuItem); @@ -675,6 +682,11 @@ begin AddItem(MView, 'Отдалить', @MenuZoomOut, ShortCut(VK_SUBTRACT, [ssCtrl])); AddSep(MView); AddItem(MView, 'Журнал сообщений', @MenuToggleLog, ShortCut(VK_L, [ssCtrl])); + AddSep(MView); + FKeyPreviewItem := AddItem(MView, 'Ключ и прозрачность в предпросмотре', + @MenuTogglePreviewKey); + FKeyPreviewItem.AutoCheck := False; + FKeyPreviewItem.Checked := FSettings.PreviewKey; MProject := AddTop('Проект'); AddItem(MProject, 'Сведения о проекте...', @MenuProjectInfo); @@ -1882,7 +1894,8 @@ begin { Уплощение для предпросмотра: клипы, которые кладутся поверх, идут отдельно. Плеер положит их сам, ничего не собирая. } - if not FlattenPreview(FProject, Segments, AudioSegs, Overlays, Missing) then + if not FlattenPreview(FProject, Segments, AudioSegs, Overlays, Missing, + FSettings.PreviewKey) then begin FPlayer.Stop; FLastLoadedEdl := ''; @@ -3111,6 +3124,29 @@ end; { К началу самого раннего клипа по всем дорожкам. Если монтаж начинается с пятой секунды, ноль — не то место, куда хотят попасть. } +{ Показывать ли в предпросмотре то, что даст сборка. + + Выключено — оверлей виден целиком, со своим фоном: так понятно, куда он + встал и где его края. Включено — выбитый цвет и прозрачность считаются + на лету, и окно совпадает с готовым роликом. + + Считается это не бесплатно, хотя и недорого: фильтр работает по площади + оверлея, а не всего кадра. Замерено на 2688x1512: наложение стоит 0.36 с + на десять секунд материала, ключ добавляет ещё 0.23 с. } +procedure TMainForm.MenuTogglePreviewKey(Sender: TObject); +begin + FSettings.PreviewKey := not FSettings.PreviewKey; + FKeyPreviewItem.Checked := FSettings.PreviewKey; + { Список кусков не изменился, а показывать надо иначе — заставляем + пересобрать цепочку. } + FLastLoadedEdl := ''; + SchedulePreview; + if FSettings.PreviewKey then + SetStatus('Предпросмотр показывает ключ и прозрачность.') + else + SetStatus('Предпросмотр показывает оверлей как есть.'); +end; + procedure TMainForm.MenuToggleRipple(Sender: TObject); begin ToggleRipple; diff --git a/src/uSettings.pas b/src/uSettings.pas index 7b3ca08..b4b3684 100644 --- a/src/uSettings.pas +++ b/src/uSettings.pas @@ -37,6 +37,7 @@ type FVideoPanelHeight: Integer; FLogVisible: Boolean; FPlayheadSnap: Boolean; + FPreviewKey: Boolean; FDepsReminder: Boolean; FRecentProjects: TStringList; function FileName: string; @@ -70,6 +71,8 @@ type property LogVisible: Boolean read FLogVisible write FLogVisible; { Прилипание курсора воспроизведения к границам клипов. } property PlayheadSnap: Boolean read FPlayheadSnap write FPlayheadSnap; + { Применять ли ключ и прозрачность прямо в предпросмотре. } + property PreviewKey: Boolean read FPreviewKey write FPreviewKey; { Напоминать ли о недостающих зависимостях при запуске. Гасится кнопкой «Не напоминать» в самом напоминании. } property DepsReminder: Boolean read FDepsReminder write FDepsReminder; @@ -103,6 +106,7 @@ begin FVideoPanelHeight := DefaultVideoHeight; FLogVisible := True; FPlayheadSnap := True; + FPreviewKey := True; FDepsReminder := True; end; @@ -184,6 +188,7 @@ begin FVideoPanelHeight := Obj.Get('video_panel_height', FVideoPanelHeight); FLogVisible := Obj.Get('log_visible', FLogVisible); FPlayheadSnap := Obj.Get('playhead_snap', FPlayheadSnap); + FPreviewKey := Obj.Get('preview_key', FPreviewKey); FDepsReminder := Obj.Get('deps_reminder', FDepsReminder); Arr := Obj.Find('recent_projects', jtArray) as TJSONArray; @@ -236,6 +241,7 @@ begin Root.Add('video_panel_height', FVideoPanelHeight); Root.Add('log_visible', FLogVisible); Root.Add('playhead_snap', FPlayheadSnap); + Root.Add('preview_key', FPreviewKey); Root.Add('deps_reminder', FDepsReminder); Arr := TJSONArray.Create; diff --git a/src/uTransform.pas b/src/uTransform.pas index 0c8b91a..acddee3 100644 --- a/src/uTransform.pas +++ b/src/uTransform.pas @@ -22,7 +22,7 @@ unit uTransform; interface uses - Classes, SysUtils; + Classes, SysUtils, uAppTypes; type { Как соотносятся кадр исходника и кадр проекта. } @@ -82,6 +82,16 @@ function OverlayFilter(const AT: TMediaTransform; ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer; out AX, AY, AWidth, AHeight: Integer): string; +{ Фильтр цветового ключа: этот цвет становится прозрачным. + + Один на всех: и сборка, и предпросмотр должны выбивать одинаково, иначе + окно обещает одно, а готовый ролик даёт другое. Цвет записывается как + 0xRRGGBB — порядок обратный тому, в каком он хранится у нас. } +function ColorKeyFilter(AColor: TRgb; ASimilarity, ABlend: Integer): string; + +{ Фильтр приглушения по альфа-каналу. Пусто, если приглушать нечего. } +function OpacityFilter(const AOpacity: Double): string; + function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string; function FitModeToStr(AMode: TFitMode): string; @@ -91,7 +101,7 @@ function FitModeCaption(AMode: TFitMode): string; implementation uses - Math, uAppTypes; + Math; function DefaultTransform: TMediaTransform; begin @@ -143,6 +153,28 @@ begin (ASrcWidth = ADstWidth) and (ASrcHeight = ADstHeight); end; +{ Дробное число через точку: у ffmpeg разделитель всегда точка, а в + русской локали Format вставил бы запятую и развалил бы всю цепочку. } +function FilterNum(const AValue: Double): string; +begin + Result := SecondsToStr(AValue); +end; + +function ColorKeyFilter(AColor: TRgb; ASimilarity, ABlend: Integer): string; +begin + Result := Format('colorkey=0x%.2x%.2x%.2x:%s:%s', + [AColor and $FF, (AColor shr 8) and $FF, (AColor shr 16) and $FF, + FilterNum(ASimilarity / 100), FilterNum(ABlend / 100)]); +end; + +function OpacityFilter(const AOpacity: Double): string; +begin + if AOpacity >= 0.999 then + Result := '' + else + Result := 'colorchannelmixer=aa=' + FilterNum(Max(0, AOpacity)); +end; + function PreviewFitFilter(ADstWidth, ADstHeight: Integer): string; var W, H: string;