Вставки тишины для звука делаются без картинки

Звук пропадал кусками, и причина оказалась не в громкости и не в
раскладке дорожек: обе были верны. Дыры на звуковой дорожке заполнялись
тем же чёрным клипом, что и дыры на видеодорожке, а опорные кадры в нём
стоят через 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://. Ею это и
было найдено.
This commit is contained in:
4aiman 2026-08-21 12:58:13 +03:00
commit ad88683e8b
4 changed files with 141 additions and 2 deletions

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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