Вид звукового клипа сохраняется, вставки тишины заказываются

Вид клипа «звук» не умел записываться: в файле проекта он превращался в
«видео». После открытия такой клип терял форму звука, получал ненужную
кнопку кадрирования, а ползунок начинал править прозрачность вместо
громкости. Найдено прямо в файле проекта пользователя.

Уже записанные проекты чинятся при чтении: клип на звуковой дорожке
обязан быть звуковым.

Вторая правка: списки кусков звуковых дорожек ссылаются на вставки
тишины, но о недостающих никто не сообщал, и они не создавались. Список
тогда указывает на несуществующий файл, плеер его не открывает, и
дорожка молчит целиком. Теперь недостающие собираются вместе с
остальными.

Третья: громкости дорожек и сами дорожки собирались разными проходами и
могли разъехаться — тогда дорожка с нулевой громкостью заглушила бы
чужую. Теперь громкости берутся ровно по тому списку, из которого
построены списки кусков.

Проверено на проекте пользователя: виды клипов после чтения верные,
список звуковой дорожки A2 играет сам по себе (-16 dB), и вся связка с
смешиванием тоже (-8.8 dB).
This commit is contained in:
chaiman 2026-08-20 19:30:30 +03:00
commit e09e61103d
2 changed files with 76 additions and 40 deletions

View file

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

View file

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