diff --git a/src/uMainForm.pas b/src/uMainForm.pas index dce91ba..6d7d9e8 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -108,6 +108,8 @@ type FThumbs: TImageList; FThumbThread: TThumbThread; FPeaksThread: TPeaksThread; + FPeaksRestart: Boolean; + FPeaksFinished: Boolean; { Соответствие «путь к файлу -> номер картинки в списке изображений». По пути, а не по номеру в списке проекта: пока миниатюра делается, файлы могут добавиться или пропасть. Номер хранится увеличенным на @@ -158,6 +160,7 @@ type procedure StartThumbs; procedure StartPeaks; procedure PeaksReady(Sender: TObject; const ASourcePath: string); + procedure PeaksDone(Sender: TObject); procedure ThumbReady(Sender: TObject; const ASourcePath, AThumbPath: string); procedure SeekPlayerTo(const ATime: Double); @@ -368,6 +371,9 @@ begin end; if Assigned(FPeaksThread) then begin + { При закрытии ждать приходится: поток обращается к окну. Ждать + недолго — запущенный ffmpeg прерывается по тому же признаку. } + FPeaksRestart := False; FPeaksThread.Terminate; FPeaksThread.WaitFor; FreeAndNil(FPeaksThread); @@ -847,13 +853,26 @@ var Item: TMediaItem; Dummy: TPeaks; begin + { Ждать окончания разбора нельзя: он читает файл целиком, а это + гигабайты и десяток секунд. Ожидание идёт в главном потоке, и всё это + время окно не отвечает — меню не открывается вовсе. + + Поэтому просто отмечаем, что список изменился: текущий проход + доработает и запустится заново. } if Assigned(FPeaksThread) then begin - FPeaksThread.Terminate; + if not FPeaksFinished then + begin + FPeaksRestart := True; + Exit; + end; + { Уже завершился — ожидание мгновенное. } FPeaksThread.WaitFor; FreeAndNil(FPeaksThread); end; + FPeaksRestart := False; + FPeaksFinished := False; if FProject.PoolCount = 0 then Exit; @@ -876,9 +895,23 @@ begin FPeaksThread := TPeaksThread.Create(Requests); FPeaksThread.OnReady := @PeaksReady; + FPeaksThread.OnTerminate := @PeaksDone; + { Разбор звука не должен мешать работе: он тяжёлый и никуда не спешит. } + FPeaksThread.Priority := tpLowest; FPeaksThread.Start; end; +{ Проход разбора закончился. Освобождаем поток и, если за это время + список файлов менялся, начинаем заново. } +procedure TMainForm.PeaksDone(Sender: TObject); +begin + { Освобождать поток прямо здесь нельзя: он ещё доживает свой цикл. + Отмечаем, что он закончил, а освободит его следующий заход. } + FPeaksFinished := True; + if FPeaksRestart then + StartPeaks; +end; + procedure TMainForm.PeaksReady(Sender: TObject; const ASourcePath: string); begin { Запомненную неудачу надо забыть: пики только что появились. } diff --git a/src/uPanCropForm.pas b/src/uPanCropForm.pas index 61bd6a9..86feeb1 100644 --- a/src/uPanCropForm.pas +++ b/src/uPanCropForm.pas @@ -91,6 +91,10 @@ type { Пересчёт между рамкой и настройкой. } procedure SelFromTransform; procedure TransformFromSel; + { Загоняет настройку в те пределы, которые умеют ползунки. Иначе + рамка и ползунки расходятся: рамка от неограниченного значения, + фильтр от обрезанного. } + procedure ClampTransform; procedure ReadControls; procedure WriteControls; procedure ScheduleRefresh; @@ -134,7 +138,11 @@ const ZoomMin = 50; ZoomMax = 400; - OffsetRange = 50; + { Предел смещения. При сильном увеличении кадр гуляет далеко за половину + своей ширины, и полсотни процентов не хватало: рамку можно было + утащить туда, куда ползунок не дотягивается, и они расходились — + рамка показывала одно, а получалось другое. } + OffsetRange = 200; procedure TPanCropForm.FormCreate(Sender: TObject); begin @@ -302,17 +310,12 @@ end; { Размеры кадра после поворота: рамка живёт именно в них, иначе после поворота на 90 она оказалась бы поперёк картинки. } procedure TPanCropForm.UpdateRotatedSize; -var - Rot: Integer; begin + { Рамка живёт в координатах исходника как он есть. Поворот на неё не + влияет: он применяется к уже вырезанному куску, а не к кадру целиком — + иначе рамка показывала бы не то, что останется. } FRotW := FSourceInfo.Width; FRotH := FSourceInfo.Height; - Rot := ((FTransform.Rotation mod 360) + 360) mod 360; - if (Rot = 90) or (Rot = 270) then - begin - FRotW := FSourceInfo.Height; - FRotH := FSourceInfo.Width; - end; end; { Кадр источника, вписанный в панель с сохранением пропорций. } @@ -447,6 +450,18 @@ begin FTransform.OffsetY := (FSel.Top * K - (SH - CH) / 2) / FDstHeight; end; +procedure TPanCropForm.ClampTransform; +begin + if FTransform.Zoom < ZoomMin / 100 then + FTransform.Zoom := ZoomMin / 100; + if FTransform.Zoom > ZoomMax / 100 then + FTransform.Zoom := ZoomMax / 100; + FTransform.OffsetX := Max(-OffsetRange / 100, Min(OffsetRange / 100, + FTransform.OffsetX)); + FTransform.OffsetY := Max(-OffsetRange / 100, Min(OffsetRange / 100, + FTransform.OffsetY)); +end; + { Кадр источника без обработки: по нему тянут рамку. } procedure TPanCropForm.LoadSourceFrame; var @@ -578,6 +593,11 @@ begin end; TransformFromSel; + ClampTransform; + { Рамка пересчитывается обратно из настройки. Так она всегда показывает + ровно то, что получится: если настройка упёрлась в предел, это сразу + видно, а не расходится молча. } + SelFromTransform; FUpdating := True; try WriteControls; diff --git a/src/uTransform.pas b/src/uTransform.pas index bff9c7c..87b01e5 100644 --- a/src/uTransform.pas +++ b/src/uTransform.pas @@ -164,11 +164,10 @@ function TransformFilter(const AT: TMediaTransform; ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): string; var Chain: string; - W0, H0, Tmp: Integer; K, KFit, KFill: Double; SW, SH: Integer; CW, CH, CX, CY: Integer; - PX, PY: Integer; + PW, PH, PX, PY: Integer; Rot: Integer; procedure Add(const AFilter: string); @@ -185,32 +184,14 @@ begin Exit; Chain := ''; - W0 := ASrcWidth; - H0 := ASrcHeight; - { --- Поворот и отражение --- } - Rot := ((AT.Rotation mod 360) + 360) mod 360; - case Rot of - 90: - begin - { transpose=1 — поворот на 90 по часовой стрелке. } - Add('transpose=1'); - Tmp := W0; W0 := H0; H0 := Tmp; - end; - 180: - { Два поворота подряд дешевле и точнее, чем поворот на произвольный - угол: последний пересчитывает каждый пиксель. } - Add('transpose=1,transpose=1'); - 270: - begin - Add('transpose=2'); - Tmp := W0; W0 := H0; H0 := Tmp; - end; - end; - if AT.FlipH then - Add('hflip'); - if AT.FlipV then - Add('vflip'); + { Порядок действий: сначала выбирается кусок кадра, и только потом он + поворачивается и отражается. + + Так это устроено в монтажных программах, и так понятнее: рамка + показывает, что́ останется, а поворот применяется к оставшемуся. При + обратном порядке обрезался бы уже перевёрнутый кадр, и в рамку + попадало бы совсем не то, что видно. } { --- Масштаб --- } if AT.Fit = fmStretch then @@ -220,16 +201,16 @@ begin end else begin - KFit := Min(ADstWidth / W0, ADstHeight / H0); - KFill := Max(ADstWidth / W0, ADstHeight / H0); + KFit := Min(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight); + KFill := Max(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight); if AT.Fit = fmFill then K := KFill else K := KFit; if AT.Zoom > 0 then K := K * AT.Zoom; - SW := MakeEven(Round(W0 * K)); - SH := MakeEven(Round(H0 * K)); + SW := MakeEven(Round(ASrcWidth * K)); + SH := MakeEven(Round(ASrcHeight * K)); end; Add(Format('scale=%d:%d', [SW, SH])); @@ -247,13 +228,51 @@ begin Add(Format('crop=%d:%d:%d:%d', [CW, CH, CX, CY])); end; - { --- Поля, если картинка меньше кадра --- } - if (CW < ADstWidth) or (CH < ADstHeight) then + { --- Поворот и отражение оставшегося куска --- } + Rot := ((AT.Rotation mod 360) + 360) mod 360; + case Rot of + 90: + begin + { transpose=1 — поворот на 90 по часовой стрелке. } + Add('transpose=1'); + PW := CH; PH := CW; + end; + 180: + begin + { Два поворота подряд дешевле и точнее, чем поворот на + произвольный угол: последний пересчитывает каждый пиксель. } + Add('transpose=1,transpose=1'); + PW := CW; PH := CH; + end; + 270: + begin + Add('transpose=2'); + PW := CH; PH := CW; + end; + else + PW := CW; PH := CH; + end; + if AT.FlipH then + Add('hflip'); + if AT.FlipV then + Add('vflip'); + + { --- Приведение к кадру проекта --- } + + { После поворота на 90 кусок лежит поперёк и в кадр проекта уже не + вписывается. Вписываем целиком, с полями. } + if (PW > ADstWidth) or (PH > ADstHeight) then begin - PX := Round((ADstWidth - CW) / 2 + AT.OffsetX * ADstWidth); - PY := Round((ADstHeight - CH) / 2 + AT.OffsetY * ADstHeight); - PX := Max(0, Min(ADstWidth - CW, PX)); - PY := Max(0, Min(ADstHeight - CH, PY)); + K := Min(ADstWidth / PW, ADstHeight / PH); + PW := MakeEven(Round(PW * K)); + PH := MakeEven(Round(PH * K)); + Add(Format('scale=%d:%d', [PW, PH])); + end; + + if (PW < ADstWidth) or (PH < ADstHeight) then + begin + PX := Max(0, (ADstWidth - PW) div 2); + PY := Max(0, (ADstHeight - PH) div 2); Add(Format('pad=%d:%d:%d:%d', [ADstWidth, ADstHeight, PX, PY])); end; diff --git a/src/uWaveform.pas b/src/uWaveform.pas index 3c913e4..9977f87 100644 --- a/src/uWaveform.pas +++ b/src/uWaveform.pas @@ -335,6 +335,8 @@ var I: Integer; begin inherited Create(True); + { Освобождает поток окно, а не он сам: окно обращается к нему и после + завершения, чтобы понять, надо ли начинать заново. } FreeOnTerminate := False; SetLength(FRequests, Length(ARequests)); for I := 0 to High(ARequests) do