From 1e28dd098b9e7ae6147fbcd92a1182b32a8754e1 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Sun, 30 Aug 2026 21:59:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=85=D0=BB=D0=BE=D0=BF=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B8=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8,=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=20=D0=B2=D0=BD=D1=83=D1=82=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=B8=D1=85=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA,=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=BE=D0=B6=D0=BA=D0=B8=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Схлопывание при удалении. При включённом автосхлопывании удаление куска сдвигает всё, что правее, влево до первого касания — дыра не остаётся, и сборке не приходится заполнять её чернотой. Сдвиг считается по всем дорожкам сразу, а не по каждой отдельно: иначе развалилось бы выверенное соответствие картинки и звука. Насколько можно сдвинуть, решает та дорожка, где запас меньше всего. Дыра не обязана быть пустой везде: клип, который её перекрывает, никуда не едет, и запас на его дорожке считается от его конца, поэтому сдвиг сам останавливается там, где упёрся. Проверено: клип 0..4 с парным звуком разрезан на 2 с, удалён первый кусок. Хвост уехал в ноль, следующий клип встал вплотную, дыры нет. Внутренние ошибки. Окно «Access violation» без единого слова о том, где именно, не даёт ничего, а при воспроизведении оно повторяется на каждый кадр и его невозможно закрыть. Теперь ошибка перехватывается: в журнал пишется разбор стека с именами файлов и номерами строк — сборка идёт с отладочными сведениями. Окно показывается только на первую ошибку, дальнейшие лишь считаются. Звуковые дорожки предпросмотра хранятся номерами, а не указателями. Указатели были ловушкой: отмена и открытие проекта пересоздают все дорожки, а список продолжал показывать на освобождённую память — и обращаются к нему на каждое обновление времени, то есть десятки раз в секунду. --- src/uMainForm.pas | 73 +++++++++++++++++++++++++---- src/uTimelineCtrl.pas | 105 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 161 insertions(+), 17 deletions(-) diff --git a/src/uMainForm.pas b/src/uMainForm.pas index ce085b0..3075bc6 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -95,7 +95,14 @@ type FLastLoadedAudioEdl: string; { Списки кусков звуковых дорожек для плеера. } FPreviewAudioUrls: array of string; - FPreviewAudioTracks: array of TTrack; + { Звуковые дорожки предпросмотра — их номера в проекте, а не сами + объекты. + + Указатели здесь были ловушкой: отмена и открытие проекта + пересоздают все дорожки, а этот список продолжал показывать на + освобождённую память. Обращение к нему идёт на каждое обновление + времени при воспроизведении, то есть десятки раз в секунду. } + FPreviewAudioTracks: array of string; FLastHwdec: string; FLastAudioNote: string; FLogFilePath: string; @@ -112,6 +119,9 @@ type { Что выбрали в окне сборки в прошлый раз. Держится между вызовами, чтобы не задавать один и тот же вопрос заново. } FExportChoice: TExportChoice; + { Сколько внутренних ошибок уже случилось. Окно показываем только на + первую. } + FErrorCount: Integer; FLogButton: TThemedButton; { Показанная доля выполнения. Отдельно от настоящей, потому что растёт она по своим правилам — см. AdvanceJobBar. } @@ -213,6 +223,8 @@ type { --- окно и меню --- } procedure BuildMenu; + { Разбор внутренней ошибки: пишет в журнал место, где она случилась. } + procedure HandleException(Sender: TObject; E: Exception); procedure ApplyWindowIcon; procedure BuildJobIndicator; procedure JobTimerTick(Sender: TObject); @@ -592,6 +604,7 @@ begin { Точный замер текста ставится до первой сборки субтитров: без него размер подбирался бы по грубой оценке. } InstallTextMeasure; + Application.OnException := @HandleException; ApplyWindowIcon; @@ -681,6 +694,41 @@ end; Место справа в нижней полосе отводится сразу и насовсем, а надпись с полоской показываются только когда работа есть. Так строка состояния и показатель не наезжают друг на друга. } +{ Внутренняя ошибка не должна оставаться загадкой. + + Окно «Access violation» без единого слова о том, где именно, не даёт + ничего: ошибка повторяется на каждый кадр воспроизведения, а найти её + негде. Поэтому мы перехватываем её сами и пишем в журнал разбор стека — + сборка идёт с отладочными сведениями, поэтому в нём видны имена файлов + и номера строк. + + Окно показывается только на первую ошибку. Дальше они лишь считаются: + повторяющееся окно поверх воспроизведения невозможно закрыть. } +procedure TMainForm.HandleException(Sender: TObject; E: Exception); +var + Trace: string; + I: Integer; + Frames: PPointer; +begin + Inc(FErrorCount); + + Trace := BackTraceStrFunc(ExceptAddr); + Frames := ExceptFrames; + for I := 0 to ExceptFrameCount - 1 do + Trace := Trace + LineEnding + ' ' + BackTraceStrFunc(Frames[I]); + + Log(Format('ВНУТРЕННЯЯ ОШИБКА %d: %s: %s', [FErrorCount, E.ClassName, + E.Message])); + Log(' где: ' + Trace); + + if FErrorCount = 1 then + ThemedMessageDlg('nlve', + E.ClassName + ': ' + E.Message + LineEnding + LineEnding + + 'Разбор записан в журнал (nlve.log рядом с программой). ' + + 'Следующие такие же ошибки будут только записываться.', + mtError, [mbOK], 0); +end; + { Значок окна. Вшит в программу, потому что рядом с ней может ничего не лежать. Значок самого файла программы под Windows задаётся не здесь, а ресурсом при сборке — это отдельная история. } @@ -1864,7 +1912,7 @@ begin проходу пропустить дорожку, и громкости съедут на соседние. Тогда дорожка с нулевой громкостью заглушила бы чужую. } SetLength(FPreviewAudioTracks, Length(FPreviewAudioTracks) + 1); - FPreviewAudioTracks[High(FPreviewAudioTracks)] := FProject.Tracks[I]; + FPreviewAudioTracks[High(FPreviewAudioTracks)] := FProject.Tracks[I].Id; end; if Length(Missing) > 0 then @@ -2452,6 +2500,7 @@ procedure TMainForm.ApplyVolumeAt(const ATime: Double); var I: Integer; Clip: TClip; + Track: TTrack; Vols: array of Double; Key: string; begin @@ -2464,12 +2513,15 @@ begin Key := ''; for I := 0 to High(FPreviewAudioTracks) do begin - Clip := FPreviewAudioTracks[I].ClipAt(ATime); + Track := FProject.FindTrack(FPreviewAudioTracks[I]); SetLength(Vols, Length(Vols) + 1); - if Clip <> nil then - Vols[High(Vols)] := Clip.Volume - else - Vols[High(Vols)] := 1; + Vols[High(Vols)] := 1; + if Track <> nil then + begin + Clip := Track.ClipAt(ATime); + if Clip <> nil then + Vols[High(Vols)] := Clip.Volume; + end; Key := Key + SecondsToStr(Vols[High(Vols)]) + ','; end; @@ -2485,7 +2537,12 @@ begin begin if Key <> '' then Key := Key + ', '; - Key := Key + FPreviewAudioTracks[I].Name + '=' + SecondsToStr(Vols[I]); + Track := FProject.FindTrack(FPreviewAudioTracks[I]); + if Track <> nil then + Key := Key + Track.Name + '=' + else + Key := Key + '?='; + Key := Key + SecondsToStr(Vols[I]); end; Log(Format('Громкости на %s: %s', [SecondsToStr(ATime), Key])); diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas index 1a3c5d8..4b69769 100644 --- a/src/uTimelineCtrl.pas +++ b/src/uTimelineCtrl.pas @@ -155,6 +155,9 @@ type procedure DrawOverlaps(ACanvas: TCanvas; ATrack: TTrack; ATop: Integer); { Полоса поверх участка, который прямо сейчас пересобирается. } procedure DrawBusyRange(ACanvas: TCanvas); + { Сдвиг всего, что правее дыры, влево до первого касания. } + procedure RippleAfterDelete(const AHoleEnd: Double; + const ATracks: TStringList); public { Отметить участок монтажа, который сейчас пересобирается. Нули снимают отметку. } @@ -2295,11 +2298,80 @@ begin StructureChanged; end; +{ Сдвиг всего, что правее дыры, влево до первого касания. + + Считается по всем дорожкам сразу, а не по каждой отдельно: сдвинуть + одну дорожку значило бы развалить выверенное соответствие картинки и + звука. Насколько можно сдвинуть — решает та дорожка, где запас меньше + всего: до стыка с ближайшим клипом слева от дыры. + + Дыра при этом не обязана быть пустой на всех дорожках. Клип, который её + перекрывает, никуда не едет, и запас на его дорожке считается от его + конца — так сдвиг сам собой останавливается там, где упёрся. } +procedure TTimelineControl.RippleAfterDelete(const AHoleEnd: Double; + const ATracks: TStringList); +var + T, C: Integer; + Track: TTrack; + PrevEnd, NextStart, Shift, Room: Double; + HasMoving: Boolean; +begin + if FProject = nil then + Exit; + + Shift := -1; + for T := 0 to FProject.TrackCount - 1 do + begin + if (FProject.RippleMode = rmCurrentTrack) and + (ATracks.IndexOf(IntToStr(T)) < 0) then + Continue; + + Track := FProject.Tracks[T]; + PrevEnd := 0; + NextStart := 0; + HasMoving := False; + for C := 0 to Track.Count - 1 do + if Track[C].TlStart >= AHoleEnd - 0.0005 then + begin + if (not HasMoving) or (Track[C].TlStart < NextStart) then + NextStart := Track[C].TlStart; + HasMoving := True; + end + else + PrevEnd := Max(PrevEnd, Track[C].TlEnd); + + if not HasMoving then + Continue; + + Room := NextStart - PrevEnd; + if (Shift < 0) or (Room < Shift) then + Shift := Room; + end; + + if Shift <= 0.0005 then + Exit; + + for T := 0 to FProject.TrackCount - 1 do + begin + if (FProject.RippleMode = rmCurrentTrack) and + (ATracks.IndexOf(IntToStr(T)) < 0) then + Continue; + + Track := FProject.Tracks[T]; + for C := 0 to Track.Count - 1 do + if Track[C].TlStart >= AHoleEnd - 0.0005 then + Track[C].TlStart := Track[C].TlStart - Shift; + Track.Sort; + end; +end; + procedure TTimelineControl.DeleteSelected; var T, C: Integer; Track: TTrack; AnyChange: Boolean; + HoleEnd: Double; + Touched: TStringList; begin if FProject = nil then Exit; @@ -2309,17 +2381,32 @@ begin SelectWholeGroups; AnyChange := False; + HoleEnd := 0; FProject.BeginChange; + Touched := TStringList.Create; + try + for T := 0 to FProject.TrackCount - 1 do + begin + Track := FProject.Tracks[T]; + for C := Track.Count - 1 downto 0 do + if Track[C].Selected then + begin + { Правый край дыры: дальше него всё поедет влево. } + HoleEnd := Max(HoleEnd, Track[C].TlEnd); + if Touched.IndexOf(IntToStr(T)) < 0 then + Touched.Add(IntToStr(T)); + Track.Remove(Track[C]); + AnyChange := True; + end; + end; - for T := 0 to FProject.TrackCount - 1 do - begin - Track := FProject.Tracks[T]; - for C := Track.Count - 1 downto 0 do - if Track[C].Selected then - begin - Track.Remove(Track[C]); - AnyChange := True; - end; + { Схлопывание после удаления — то же самое автосхлопывание, что и при + перетаскивании: если оно выключено, дыра остаётся, и сборка потом + заполнит её чернотой. } + if AnyChange and FProject.RippleEnabled then + RippleAfterDelete(HoleEnd, Touched); + finally + Touched.Free; end; if AnyChange then