Этапы 3 и 4: звук отдельно от картинки

У дорожки появился вид — картинка или звук, у клипа принадлежность к
связке. Проект заводит две дорожки картинки и две звука; при добавлении
файла со звуком клип кладётся парой, связанной общим номером.

Уплощение теперь делается двумя проходами: отдельно для картинки,
отдельно для звука. Одним проходом такое не выразить — в один и тот же
момент картинка берётся с одной дорожки, а звук с другой.

Сборка соответственно тоже раздваивается. Картинка собирается прежним
способом, копированием. Звук собирается в сырой поток и сжимается одним
проходом: куски дописываются друг за другом байтами, и длина каждого
считается из длительности, а не берётся у ffmpeg. Поэтому стык получается
с точностью до отсчёта, а не до звукового кадра, и звук не уезжает от
картинки. Тишина в дырах пишется нулями, без запуска ffmpeg вовсе.

Готовые потоки сшиваются копированием.

Предпросмотр получает второй список кусков как внешнюю звуковую дорожку.

Проекты, записанные раньше, переносятся при чтении: звук там шёл вместе с
клипом картинки и без пары просто пропал бы.

Проверено сборкой:
- картинка из молчащего синего файла 0..6 с, звук из другого файла с 2 по
  6 с. В результате тишина первые две секунды и тон дальше — звук встал
  туда, куда положен, а не туда, где картинка;
- обычный парный случай: клип с 2 по 6 с, тишина до него и тон в нём;
- проверки уплощения пройдены.
This commit is contained in:
chaiman 2026-08-20 09:50:03 +03:00
commit a06ecfd720
7 changed files with 547 additions and 38 deletions

View file

@ -306,7 +306,8 @@ begin
WriteLn('остановка через: ', SecondsToStr(CancelAfter), ' с');
WriteLn;
Thread := TExportThread.Create(Segments, ParamStr(FirstArg - 1), True,
{ Проверка вырезания и склейки картинки: звук здесь не собирается. }
Thread := TExportThread.Create(Segments, nil, ParamStr(FirstArg - 1), True,
Default(TMediaInfo));
Watcher := TExportWatcher.Create(Thread);
try
@ -563,7 +564,7 @@ begin
try
Proj.AddClip(Proj.Tracks[0], Media, 10, 5, 0);
Proj.AddClip(Proj.Tracks[0], Media, 15, 5, 5);
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) = 1, 'склеились в один кусок');
if Length(Segs) = 1 then
@ -582,7 +583,7 @@ begin
try
Proj.AddClip(Proj.Tracks[0], Media, 100, 5, 0);
Proj.AddClip(Proj.Tracks[0], Media, 200, 5, 5);
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) = 2, 'остались двумя кусками');
finally
@ -597,7 +598,7 @@ begin
{ Дорожка 0 — верхняя, она побеждает. }
Proj.AddClip(Proj.Tracks[1], Media, 0, 30, 0);
Proj.AddClip(Proj.Tracks[0], Media, 100, 10, 10);
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) = 3, 'получилось три куска');
if Length(Segs) = 3 then
@ -621,7 +622,7 @@ begin
try
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 3);
Proj.AddClip(Proj.Tracks[0], Media, 50, 5, 12);
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) = 4, 'четыре куска: чёрное, видео, чёрное, видео');
if Length(Segs) = 4 then
@ -651,7 +652,7 @@ begin
try
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, 0);
Proj.AddClip(Proj.Tracks[0], Media, 200, 10, 0);
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) = 1, 'остался один кусок');
if Length(Segs) = 1 then
@ -668,7 +669,7 @@ begin
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, 0);
Proj.AddClip(Proj.Tracks[0], Media, 200, 10, 0);
Proj.Tracks[0].Muted := True;
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) = 1, 'остался один кусок');
if Length(Segs) = 1 then
@ -688,7 +689,7 @@ begin
Proj.Tracks[0][0].EdgeIn := edPrecise;
Proj.RippleMode := rmAllTracks;
Proj.RippleEnabled := True;
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
Check(Length(Segs) = 3, 'до записи три куска');
Check(Proj.SaveToFile(TempFile, Err), 'записан: ' + Err);
finally
@ -698,13 +699,17 @@ begin
Proj := TProject.Create;
try
Check(Proj.LoadFromFile(TempFile, Err), 'прочитан: ' + Err);
Check(Proj.TrackCount = 2, 'дорожек снова две');
{ Четыре: две картинки и две звука. Звуковые заводятся сразу, чтобы
каждому клипу картинки было куда положить парный звук. }
Check(Proj.TrackCount = 4, 'дорожек снова четыре');
Check(Proj.TrackCountOf(tkVideo) = 2, 'из них две для картинки');
Check(Proj.TrackCountOf(tkAudio) = 2, 'и две для звука');
Check(Proj.PoolCount = 1, 'файл в списке один');
Check(Proj.RippleMode = rmAllTracks, 'режим схлопывания сохранился');
Check(Proj.RippleEnabled, 'состояние схлопывания сохранилось');
if Proj.TrackCount = 2 then
Check(Proj.Tracks[0][0].EdgeIn = edPrecise, 'признак точного реза сохранился');
FlattenProject(Proj, Segs, Missing);
FlattenStream(Proj, tkVideo, Segs, Missing);
DumpSegments(Segs);
Check(Length(Segs) = 3, 'после чтения снова три куска');
if Length(Segs) = 3 then
@ -739,6 +744,7 @@ begin
Proj.Clear;
Proj.AddTrack('V1');
Proj.AddTrack('V2');
Proj.EnsureAudioTracks;
if not ProbeMedia(ExpandFileName(ParamStr(3)), Info, Err) then
begin
@ -750,8 +756,10 @@ begin
{ Нижняя дорожка: два куска видео из разных мест, между ними дыра
в две секунды, и ещё дыра в начале. }
Proj.AddClip(Proj.Tracks[1], Video, 100, 6, 1.5);
Proj.AddClip(Proj.Tracks[1], Video, 400, 6, 9.5);
{ Парой: клип картинки и его звук на соответствующей звуковой
дорожке. Так собранный проект похож на настоящий. }
Proj.AddClipPair(Proj.Tracks[1], Video, 100, 6, 1.5);
Proj.AddClipPair(Proj.Tracks[1], Video, 400, 6, 9.5);
if ParamCount >= 4 then
begin
@ -853,7 +861,7 @@ end;
procedure CmdRender;
var
Proj: TProject;
Segs: TFlatSegmentArray;
Segs, AudioSegs: TFlatSegmentArray;
Missing: TGenRequests;
Err: string;
Gen: TGenerateThread;
@ -877,7 +885,7 @@ begin
WriteLn('длина: ', SecondsToStr(Proj.EndTime), ' с');
WriteLn;
if not FlattenProject(Proj, Segs, Missing) then
if not FlattenProject(Proj, Segs, AudioSegs, Missing) then
begin
WriteLn('Уплотить нечего.');
Halt(1);
@ -914,7 +922,7 @@ begin
WriteLn;
{ После создания уплощаем заново: теперь все файлы на месте. }
FlattenProject(Proj, Segs, Missing);
FlattenProject(Proj, Segs, AudioSegs, Missing);
if Length(Missing) > 0 then
begin
WriteLn('Часть вставок так и не появилась.');
@ -922,8 +930,8 @@ begin
end;
end;
Thread := TExportThread.Create(Segs, ExpandFileName(ParamStr(3)), True,
Proj.Master);
Thread := TExportThread.Create(Segs, AudioSegs,
ExpandFileName(ParamStr(3)), True, Proj.Master);
Watcher := TExportWatcher.Create(Thread);
try
Thread.OnProgress := @Watcher.Progress;