From ad88683e8b0f8599f92b577c1716bbc74f2dfbac Mon Sep 17 00:00:00 2001 From: 4aiman Date: Fri, 21 Aug 2026 12:58:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=81=D1=82=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B8=D1=88=D0=B8=D0=BD=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=B2=D1=83=D0=BA=D0=B0=20=D0=B4=D0=B5=D0=BB=D0=B0=D1=8E?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B1=D0=B5=D0=B7=20=D0=BA=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D0=BD=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Звук пропадал кусками, и причина оказалась не в громкости и не в раскладке дорожек: обе были верны. Дыры на звуковой дорожке заполнялись тем же чёрным клипом, что и дыры на видеодорожке, а опорные кадры в нём стоят через 4.171 с. Перемотка внутри такой вставки откатывается к ближайшему опорному кадру, дорожка начинает отдавать звук на несколько секунд раньше соседних, смешивание равняется по ней — и содержимое соседней дорожки в это окно не попадает. Замерено на файле, без запуска окна. Смешивание трёх дорожек проекта: старт 0 с: -23.3 dB старт 24 с: -91.0 dB (полная тишина) старт 12 с: -10.1 dB старт 30 с: -18.0 dB При этом та же дорожка A2 отдельно на 24-й секунде даёт -11.7 dB, то есть содержимое на месте. Смешивание A2 с A4 работает, A1 с A2 — нет: у A1 в этой точке идёт длинная чёрная вставка, и перемотка в ней откатывается на 3.1 с назад. Теперь у звуковых дорожек своя вставка — anullsrc в звуковом кодеке проекта, без видеопотока. В звуке опорным является каждый кадр, и отката нет. После правки во всех проверенных точках звук есть: 0 с -24.1 7 с -19.0 12 с -11.9 18 с -17.5 24 с -11.7 25 с -9.6 30 с -9.5 40 с -15.6 60 с -19.5 72 с -15.9 Сборка ролика от этого не страдает: она декодирует куски в отсчёты, и файл без картинки ей подходит так же. Заодно в служебную программу добавлена команда audio: раскладка звуковых дорожек проекта на куски вместе со строками edl://. Ею это и было найдено. --- src/uFfmpegCmd.pas | 33 ++++++++++++++++++ src/uFlatten.pas | 19 +++++++++-- src/uGenerate.pas | 7 ++++ tests/nlvetool.lpr | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) diff --git a/src/uFfmpegCmd.pas b/src/uFfmpegCmd.pas index ec88632..ef0a4e6 100644 --- a/src/uFfmpegCmd.pas +++ b/src/uFfmpegCmd.pas @@ -184,6 +184,13 @@ function ArgsBlackClip(const ADst: string; ADuration: Double; const AVideoCodec, APixFmt, AAudioCodec: string; ASampleRate, AChannels: Integer): TArgs; +{ Вставка тишины для звуковой дорожки: только звук, без картинки. + + Отдельно от чёрного клипа, потому что перемотка внутри видео + откатывается к опорному кадру, а в звуке опорным является каждый. } +function ArgsSilenceClip(const ADst: string; ADuration: Double; + const AAudioCodec: string; ASampleRate, AChannels: Integer): TArgs; + { Кадр-миниатюра для списка файлов проекта. Кадр берётся не с нуля: у многих съёмок первые секунды это крышка @@ -869,6 +876,32 @@ begin Result := Concat(Result, [ADst]); end; +function ArgsSilenceClip(const ADst: string; ADuration: Double; + const AAudioCodec: string; ASampleRate, AChannels: Integer): TArgs; +var + Codec: string; + Ch: Integer; +begin + Codec := AAudioCodec; + if Codec = '' then + Codec := 'aac'; + Ch := AChannels; + if Ch < 1 then + Ch := 2; + + Result := Concat(CommonFfmpegPrefix, [ + '-f', 'lavfi', + '-i', Format('anullsrc=channel_layout=%s:sample_rate=%d', + [IfThen(Ch = 1, 'mono', 'stereo'), ASampleRate]), + '-c:a', Codec, + '-ac', IntToStr(Ch), + '-ar', IntToStr(ASampleRate), + '-t', SecondsToStr(ADuration) + ]); + Result := Concat(Result, ProgressArgs); + Result := Concat(Result, [ADst]); +end; + function ArgsThumbnail(const ASrc, ADst: string; const ATime: Double; AWidth, AHeight: Integer): TArgs; begin diff --git a/src/uFlatten.pas b/src/uFlatten.pas index ba0b030..a9f4994 100644 --- a/src/uFlatten.pas +++ b/src/uFlatten.pas @@ -39,7 +39,19 @@ uses type { Что нужно создать, чтобы кусок стал воспроизводимым файлом. } - TGenKind = (gkNone, gkBlack, gkImage); + { gkSilence — вставка тишины без картинки, только для звуковых дорожек. + + Отдельный вид, а не чёрный клип, потому что перемотка внутри чёрного + клипа откатывается к ближайшему опорному кадру, а те стоят через + четыре секунды. Дорожка после этого отдаёт звук на несколько секунд + раньше соседних, смешивание равняется по ней, и содержимое соседней + дорожки в это окно не попадает — звук пропадал целыми кусками. + Замерено: на 24-й секунде смешивание трёх дорожек давало -91 dB, то + есть полную тишину, при том что каждая дорожка по отдельности звучала. + Со звуковой вставкой в той же точке -11.7 dB. + + В звуковом файле опорным является каждый кадр, и такой беды нет. } + TGenKind = (gkNone, gkBlack, gkImage, gkSilence); TGenRequest = record Kind: TGenKind; @@ -150,6 +162,8 @@ begin case AReq.Kind of gkBlack: Name := Format('black_%d.mkv', [Ms]); + gkSilence: + Name := Format('silence_%d.m4a', [Ms]); gkImage: begin Key := AReq.SourcePath + '|' + IntToStr(Ms); @@ -731,7 +745,8 @@ var Seg.Volume := 1; Seg.Kind := skBlack; Seg.Duration := ALen; - Req.Kind := gkBlack; + { Тишина без картинки: см. пояснение у gkSilence. } + Req.Kind := gkSilence; Req.SourcePath := ''; Req.Duration := BucketFor(ALen); Req.TargetPath := GeneratedPath(AProject.CacheDir, Req); diff --git a/src/uGenerate.pas b/src/uGenerate.pas index 6042bd3..7e9afb1 100644 --- a/src/uGenerate.pas +++ b/src/uGenerate.pas @@ -175,6 +175,9 @@ begin gkImage: FStageText := Format('Делаю клип из картинки %d из %d', [I + 1, Length(FRequests)]); + gkSilence: + FStageText := Format('Делаю вставку тишины %d из %d', + [I + 1, Length(FRequests)]); end; Publish; @@ -196,6 +199,10 @@ begin FMaster.Width, FMaster.Height, FMaster.FpsNum, FMaster.FpsDen, FMaster.VideoCodec, FMaster.PixFmt, AudioCodec, FMaster.SampleRate, FMaster.Channels); + gkSilence: + Args := ArgsSilenceClip(FRequests[I].TargetPath, + FRequests[I].Duration, AudioCodec, + FMaster.SampleRate, FMaster.Channels); else Continue; end; diff --git a/tests/nlvetool.lpr b/tests/nlvetool.lpr index c9dc794..305dcd3 100644 --- a/tests/nlvetool.lpr +++ b/tests/nlvetool.lpr @@ -23,6 +23,9 @@ render <проект.nlve> <выход> уплотить проект, создать вставки, собрать ролик и сверить число кадров + audio <проект.nlve> + разложить звуковые дорожки проекта на куски и + напечатать их вместе со строками edl:// buildcut <проект.nlve> <видео> проект из одного файла с точным резом в заведомо неопорной точке @@ -858,6 +861,85 @@ begin WriteLn(' ', FThread.PendingLog); end; + +{ Раскладка звуковых дорожек. + + Нужна потому, что «звука нет вот здесь» глазами не разобрать: громкость + может быть верной, а кусок в этом месте — ссылаться не туда. Здесь + печатается ровно то, что уходит в плеер: место на монтаже, файл, место + в файле, длина. } +procedure CmdAudio; +var + Proj: TProject; + Segs: TFlatSegmentArray; + Missing: TGenRequests; + Err: string; + I, J: Integer; + Total, At: Double; + Kind: string; +begin + Proj := TProject.Create; + try + if not Proj.LoadFromFile(ExpandFileName(ParamStr(2)), Err) then + begin + WriteLn('Не удалось прочитать проект: ', Err); + Halt(1); + end; + + Total := Max(AudibleEnd(Proj, tkVideo), AudibleEnd(Proj, tkAudio)); + WriteLn('проект: ', Proj.FileName); + WriteLn('длина слышимого: ', SecondsToStr(Total), ' с'); + WriteLn; + + for I := 0 to Proj.TrackCount - 1 do + begin + if Proj.Tracks[I].Kind <> tkAudio then + Continue; + WriteLn('=== ', Proj.Tracks[I].Name, ' ==='); + if not Proj.TrackAudible(Proj.Tracks[I]) then + begin + WriteLn(' не слышна (заглушена или пуста)'); + WriteLn; + Continue; + end; + if Proj.Tracks[I].Count = 0 then + begin + WriteLn(' клипов нет'); + WriteLn; + Continue; + end; + if not FlattenAudioTrack(Proj, Proj.Tracks[I], Total, Segs, Missing) then + begin + WriteLn(' разложить не удалось'); + WriteLn; + Continue; + end; + + At := 0; + for J := 0 to High(Segs) do + begin + if Segs[J].Kind = skBlack then + Kind := 'тишина' + else + Kind := 'звук '; + WriteLn(Format(' [%2d] %s монтаж %8s..%8s длина %8s из %8s %s', + [J, Kind, SecondsToStr(At), SecondsToStr(At + Segs[J].Duration), + SecondsToStr(Segs[J].Duration), SecondsToStr(Segs[J].SrcStart), + ExtractFileName(Segs[J].FilePath)])); + if (Segs[J].Kind = skBlack) and (not FileExists(Segs[J].FilePath)) then + WriteLn(' ВНИМАНИЕ: вставки тишины нет на диске'); + At := At + Segs[J].Duration; + end; + WriteLn(' всего по дорожке: ', SecondsToStr(At), ' с'); + WriteLn; + WriteLn(' ', BuildEdlUri(Segs)); + WriteLn; + end; + finally + Proj.Free; + end; +end; + procedure CmdRender; var Proj: TProject; @@ -999,6 +1081,8 @@ begin CmdBuild else if (Cmd = 'buildcut') and (ParamCount >= 3) then CmdBuildCut + else if (Cmd = 'audio') and (ParamCount >= 2) then + CmdAudio else if (Cmd = 'render') and (ParamCount >= 3) then CmdRender else