From e09e61103d970ccfae4f9dc219518ffb30dedfca Mon Sep 17 00:00:00 2001 From: chaiman Date: Thu, 20 Aug 2026 19:30:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B8=D0=B4=20=D0=B7=D0=B2=D1=83=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BB=D0=B8=D0=BF=D0=B0?= =?UTF-8?q?=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=8F=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F,=20=D0=B2=D1=81=D1=82=D0=B0=D0=B2=D0=BA=D0=B8=20=D1=82?= =?UTF-8?q?=D0=B8=D1=88=D0=B8=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=8E=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вид клипа «звук» не умел записываться: в файле проекта он превращался в «видео». После открытия такой клип терял форму звука, получал ненужную кнопку кадрирования, а ползунок начинал править прозрачность вместо громкости. Найдено прямо в файле проекта пользователя. Уже записанные проекты чинятся при чтении: клип на звуковой дорожке обязан быть звуковым. Вторая правка: списки кусков звуковых дорожек ссылаются на вставки тишины, но о недостающих никто не сообщал, и они не создавались. Список тогда указывает на несуществующий файл, плеер его не открывает, и дорожка молчит целиком. Теперь недостающие собираются вместе с остальными. Третья: громкости дорожек и сами дорожки собирались разными проходами и могли разъехаться — тогда дорожка с нулевой громкостью заглушила бы чужую. Теперь громкости берутся ровно по тому списку, из которого построены списки кусков. Проверено на проекте пользователя: виды клипов после чтения верные, список звуковой дорожки A2 играет сам по себе (-16 dB), и вся связка с смешиванием тоже (-8.8 dB). --- src/uMainForm.pas | 88 ++++++++++++++++++++++++++--------------------- src/uProject.pas | 28 +++++++++++++++ 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/src/uMainForm.pas b/src/uMainForm.pas index 6d7d9e8..e483f55 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -79,6 +79,9 @@ type FUpdatingFromPlayer: Boolean; FLastLoadedEdl: string; FLastLoadedAudioEdl: string; + { Списки кусков звуковых дорожек для плеера. } + FPreviewAudioUrls: array of string; + FPreviewAudioTracks: array of TTrack; FLastHwdec: string; FLastAudioNote: string; FLogFilePath: string; @@ -1331,8 +1334,10 @@ end; procedure TMainForm.RebuildPreview; var - Segments, AudioSegs: TFlatSegmentArray; - Missing: TGenRequests; + Segments, AudioSegs, TrackSegs: TFlatSegmentArray; + Missing, TrackMissing: TGenRequests; + I, J: Integer; + TotalLen: Double; begin if (FPlayer = nil) or (not FPlayer.Initialized) then Exit; @@ -1353,6 +1358,39 @@ begin Exit; end; + { Каждая звуковая дорожка идёт своим списком кусков: они смешиваются, а + не побеждают друг друга. Дыры в каждой дорожке свои, и вставки тишины + под них нужны свои — без этого список ссылается на несуществующий + файл, плеер его не открывает, и дорожка молчит целиком. } + FPreviewAudioUrls := nil; + FPreviewAudioTracks := nil; + TotalLen := Max(AudibleEnd(FProject, tkVideo), AudibleEnd(FProject, tkAudio)); + for I := 0 to FProject.TrackCount - 1 do + begin + if FProject.Tracks[I].Kind <> tkAudio then + Continue; + if not FProject.TrackAudible(FProject.Tracks[I]) then + Continue; + if FProject.Tracks[I].Count = 0 then + Continue; + if not FlattenAudioTrack(FProject, FProject.Tracks[I], TotalLen, + TrackSegs, TrackMissing) then + Continue; + for J := 0 to High(TrackMissing) do + begin + SetLength(Missing, Length(Missing) + 1); + Missing[High(Missing)] := TrackMissing[J]; + end; + SetLength(FPreviewAudioUrls, Length(FPreviewAudioUrls) + 1); + FPreviewAudioUrls[High(FPreviewAudioUrls)] := BuildEdlUri(TrackSegs); + { Запоминаем сами дорожки: громкости потом берутся ровно по этому + списку. Пересчитывать его отдельным проходом нельзя — стоит одному + проходу пропустить дорожку, и громкости съедут на соседние. Тогда + дорожка с нулевой громкостью заглушила бы чужую. } + SetLength(FPreviewAudioTracks, Length(FPreviewAudioTracks) + 1); + FPreviewAudioTracks[High(FPreviewAudioTracks)] := FProject.Tracks[I]; + end; + if Length(Missing) > 0 then begin FGenerateBusy := True; @@ -1371,11 +1409,8 @@ end; procedure TMainForm.LoadPreviewEdl(const ASegments, AAudio: TFlatSegmentArray); var Edl, AudioEdl: string; - Pos, Total, TotalLen: Double; + Pos, Total: Double; WasPaused: Boolean; - Urls: array of string; - TrackSegs: TFlatSegmentArray; - TrackMissing: TGenRequests; I: Integer; begin { Кадр проекта задаётся до загрузки: mpv должен показывать то, что @@ -1385,32 +1420,11 @@ begin ApplyPreviewFrame; Edl := BuildEdlUri(ASegments); - { Каждая звуковая дорожка идёт своим списком кусков. - - Одним общим списком не обойтись: звуковые дорожки смешиваются, а не - побеждают друг друга, иначе под голос не положить музыку. Плеер играет - одну дорожку за раз, поэтому смесь он собирает своими средствами, а - мы даём ему каждую по отдельности. } - Urls := nil; - TotalLen := Max(AudibleEnd(FProject, tkVideo), AudibleEnd(FProject, tkAudio)); - for I := 0 to FProject.TrackCount - 1 do - begin - if FProject.Tracks[I].Kind <> tkAudio then - Continue; - if not FProject.TrackAudible(FProject.Tracks[I]) then - Continue; - if FProject.Tracks[I].Count = 0 then - Continue; - if not FlattenAudioTrack(FProject, FProject.Tracks[I], TotalLen, - TrackSegs, TrackMissing) then - Continue; - SetLength(Urls, Length(Urls) + 1); - Urls[High(Urls)] := BuildEdlUri(TrackSegs); - end; - + { Списки кусков звуковых дорожек уже построены при пересборке: там же + проверено, что все вставки тишины существуют. } AudioEdl := ''; - for I := 0 to High(Urls) do - AudioEdl := AudioEdl + Urls[I] + '|'; + for I := 0 to High(FPreviewAudioUrls) do + AudioEdl := AudioEdl + FPreviewAudioUrls[I] + '|'; { Список кусков запоминаем всегда, даже если строка не изменилась: громкость в неё не входит, а брать её потом неоткуда. } @@ -1431,7 +1445,7 @@ begin перемоткой следом. Иначе mpv успевает начать с нуля, и на каждой правке монтажа слышен и виден кусок не оттуда. } { Внешний звук назначается до загрузки: он часть того, что открывается. } - FPlayer.SetAudioSources(Urls); + FPlayer.SetAudioSources(FPreviewAudioUrls); FPlayer.LoadUrl(Edl, Format('start=%s,pause=%s', [SecondsToStr(Pos), BoolToStr(WasPaused, 'yes', 'no')])); FLastLoadedEdl := Edl; @@ -1932,15 +1946,9 @@ begin Vols := nil; Key := ''; - for I := 0 to FProject.TrackCount - 1 do + for I := 0 to High(FPreviewAudioTracks) do begin - if FProject.Tracks[I].Kind <> tkAudio then - Continue; - if not FProject.TrackAudible(FProject.Tracks[I]) then - Continue; - if FProject.Tracks[I].Count = 0 then - Continue; - Clip := FProject.Tracks[I].ClipAt(ATime); + Clip := FPreviewAudioTracks[I].ClipAt(ATime); SetLength(Vols, Length(Vols) + 1); if Clip <> nil then Vols[High(Vols)] := Clip.Volume diff --git a/src/uProject.pas b/src/uProject.pas index f9316d9..bb8b969 100644 --- a/src/uProject.pas +++ b/src/uProject.pas @@ -226,6 +226,11 @@ type { Перенос проектов, записанных до разделения звука и картинки: там звук шёл вместе с клипом картинки, и без пары он бы просто пропал. } procedure MigrateAudio; + { Клип на звуковой дорожке обязан быть звуковым. В проектах, записанных + до того, как вид клипа научился сохраняться, он остался «видео» — и + тогда у клипа пропадает форма звука, появляется лишняя кнопка + кадрирования, а ползунок правит прозрачность вместо громкости. } + procedure FixClipKinds; { Кладёт клип и, если в файле есть звук, парный звуковой клип на соответствующую звуковую дорожку. Возвращает клип картинки. } function AddClipPair(ATrack: TTrack; AMedia: TMediaItem; @@ -735,6 +740,25 @@ begin TTrack(FTracks[T]).Sort; end; +procedure TProject.FixClipKinds; +var + T, C: Integer; + Track: TTrack; +begin + for T := 0 to FTracks.Count - 1 do + begin + Track := TTrack(FTracks[T]); + for C := 0 to Track.Count - 1 do + if Track.Kind = tkAudio then + begin + if Track[C].Kind <> ckBlack then + Track[C].Kind := ckAudio; + end + else if Track[C].Kind = ckAudio then + Track[C].Kind := ckVideo; + end; +end; + procedure TProject.EnsureAudioTracks; var Need, Have, I: Integer; @@ -1050,6 +1074,7 @@ begin case AKind of ckImage: Result := 'image'; ckBlack: Result := 'black'; + ckAudio: Result := 'audio'; else Result := 'video'; end; @@ -1061,6 +1086,8 @@ begin Result := ckImage else if AValue = 'black' then Result := ckBlack + else if AValue = 'audio' then + Result := ckAudio else Result := ckVideo; end; @@ -1332,6 +1359,7 @@ begin end; MigrateAudio; + FixClipKinds; Settings := Obj.Find('settings', jtObject) as TJSONObject; if Settings <> nil then