Звуковые дорожки смешиваются, а не побеждают друг друга
Правило «верхний побеждает» я перенёс на звук механически, и это была ошибка: оно придумано для картинки, где и делает возможной сборку копированием. Для звука оно бессмысленно — под голос нельзя было положить фоновую музыку. И стоит смешивание ничего: звук всё равно перекодируется. В сборке куски со звуком нескольких дорожек сводятся тем же путём, что переходы: слоями, каждый со своей громкостью. В предпросмотре сложнее: плеер играет одну звуковую дорожку за раз. Поэтому каждая дорожка монтажа подаётся своим списком кусков, а смесь собирается цепочкой фильтров плеера. Громкость клипов подставляется в ту же цепочку — иначе предпросмотр врал бы про баланс. Проверено: два тона на разных дорожках, второй с громкостью 50%. В готовом ролике -21.2 и -27.1 dB, в предпросмотре -21.2 и -27.2. Совпало. Найдена по дороге ошибка в собственном условии: я считал слои без победителя и потому обнулял список, когда звучали ровно две дорожки.
This commit is contained in:
parent
79fe091fbb
commit
3583f74864
4 changed files with 355 additions and 33 deletions
12
README.md
12
README.md
|
|
@ -292,6 +292,18 @@ Linux потом искала по нему `.dll`.
|
|||
громкость. Клип с непрозрачностью меньше единицы смешивается с тем, что
|
||||
под ним.
|
||||
|
||||
**Звуковые дорожки смешиваются.** Правило «верхний побеждает» придумано
|
||||
для картинки — оно и делает возможной сборку копированием. Для звука оно
|
||||
бессмысленно: без смешивания нельзя положить фоновую музыку под голос. И
|
||||
стоит оно ничего, потому что звук всё равно перекодируется.
|
||||
|
||||
Плеер играет одну звуковую дорожку за раз, поэтому в предпросмотре смесь
|
||||
собирается его же цепочкой фильтров: каждая дорожка монтажа подаётся
|
||||
своим списком кусков, дальше `amix`. Громкость клипов подставляется в ту
|
||||
же цепочку — иначе предпросмотр врал бы про баланс. Проверено: в
|
||||
предпросмотре и в готовом ролике уровни совпадают до десятой доли
|
||||
децибела.
|
||||
|
||||
Два клипа **на одной дорожке**, наползшие друг на друга, дают переход:
|
||||
один плавно перетекает в другой. На разных дорожках перекрытие означает
|
||||
не переход, а наложение — там по-прежнему побеждает верхний, если он
|
||||
|
|
|
|||
166
src/uFlatten.pas
166
src/uFlatten.pas
|
|
@ -71,6 +71,15 @@ function FlattenStream(AProject: TProject; AKind: TTrackKind;
|
|||
const AMinLength: Double;
|
||||
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean;
|
||||
|
||||
{ Уплощение одной звуковой дорожки — для предпросмотра.
|
||||
|
||||
Плеер умеет играть только одну звуковую дорожку за раз, поэтому смесь
|
||||
ему приходится собирать самому: каждая дорожка монтажа подаётся своим
|
||||
списком кусков, а дальше они складываются его же средствами. }
|
||||
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
|
||||
const ATotal: Double;
|
||||
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean;
|
||||
|
||||
{ Длина слышимого материала одного вида дорожек. }
|
||||
function AudibleEnd(AProject: TProject; AKind: TTrackKind): Double;
|
||||
|
||||
|
|
@ -274,6 +283,70 @@ var
|
|||
AProject.Master.Height);
|
||||
end;
|
||||
|
||||
{ Все звучащие клипы в этот момент, со всех слышимых звуковых дорожек.
|
||||
|
||||
Если звучит ровно один и без перехода, слои не нужны — кусок описан
|
||||
обычным способом. }
|
||||
procedure CollectAudioLayers(AWinner, APartner: TClip;
|
||||
const AAt, ASegStart: Double; var ASeg: TFlatSegment);
|
||||
var
|
||||
Ti, C, N: Integer;
|
||||
Track: TTrack;
|
||||
Cl: TClip;
|
||||
|
||||
procedure Push(const ALayer: TSegmentLayer);
|
||||
begin
|
||||
SetLength(ASeg.Layers, N + 1);
|
||||
ASeg.Layers[N] := ALayer;
|
||||
Inc(N);
|
||||
end;
|
||||
|
||||
begin
|
||||
N := 0;
|
||||
ASeg.Layers := nil;
|
||||
|
||||
if APartner <> nil then
|
||||
begin
|
||||
{ Переход на дорожке победителя: один уходит, другой приходит. }
|
||||
if APartner.TlStart < AWinner.TlStart then
|
||||
begin
|
||||
Push(LayerOf(APartner, ASegStart, APartner.Volume, 0));
|
||||
Push(LayerOf(AWinner, ASegStart, 0, AWinner.Volume));
|
||||
end
|
||||
else
|
||||
begin
|
||||
Push(LayerOf(AWinner, ASegStart, AWinner.Volume, 0));
|
||||
Push(LayerOf(APartner, ASegStart, 0, APartner.Volume));
|
||||
end;
|
||||
end
|
||||
else
|
||||
Push(LayerOf(AWinner, ASegStart, AWinner.Volume, AWinner.Volume));
|
||||
|
||||
{ Всё, что звучит одновременно на других звуковых дорожках. }
|
||||
for Ti := 0 to AProject.TrackCount - 1 do
|
||||
begin
|
||||
Track := AProject.Tracks[Ti];
|
||||
if Track.Kind <> tkAudio then
|
||||
Continue;
|
||||
if not AProject.TrackAudible(Track) then
|
||||
Continue;
|
||||
for C := 0 to Track.Count - 1 do
|
||||
begin
|
||||
Cl := Track[C];
|
||||
if not Cl.Covers(AAt) then
|
||||
Continue;
|
||||
if (Cl = AWinner) or (Cl = APartner) then
|
||||
Continue;
|
||||
Push(LayerOf(Cl, ASegStart, Cl.Volume, Cl.Volume));
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Звучит ровно один клип и без перехода — смешивать нечего, кусок
|
||||
описывается обычным способом и обходится без пересборки. }
|
||||
if N < 2 then
|
||||
ASeg.Layers := nil;
|
||||
end;
|
||||
|
||||
{ Клип, побеждающий в указанный момент: с самой верхней дорожки. }
|
||||
function WinnerAt(const ATime: Double): TClip;
|
||||
var
|
||||
|
|
@ -531,7 +604,19 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
if (Partner <> nil) or (Under <> nil) then
|
||||
{ Звуковые дорожки смешиваются, а не побеждают друг друга.
|
||||
|
||||
Правило «верхний побеждает» придумано для картинки: оно и делает
|
||||
возможной сборку копированием. Для звука оно бессмысленно — без
|
||||
смешивания нельзя положить фоновую музыку под голос. И стоит оно
|
||||
ничего: звук у нас всё равно перекодируется. }
|
||||
if AKind = tkAudio then
|
||||
begin
|
||||
CollectAudioLayers(Winner, Partner, Mid, SegStart, Seg);
|
||||
if Length(Seg.Layers) > 0 then
|
||||
Seg.Transcode := True;
|
||||
end
|
||||
else if (Partner <> nil) or (Under <> nil) then
|
||||
begin
|
||||
SetLength(Seg.Layers, 2);
|
||||
if Partner <> nil then
|
||||
|
|
@ -626,6 +711,85 @@ begin
|
|||
Result := True;
|
||||
end;
|
||||
|
||||
function FlattenAudioTrack(AProject: TProject; ATrack: TTrack;
|
||||
const ATotal: Double;
|
||||
out ASegments: TFlatSegmentArray; out AMissing: TGenRequests): Boolean;
|
||||
var
|
||||
C, N: Integer;
|
||||
At: Double;
|
||||
Clip: TClip;
|
||||
Item: TMediaItem;
|
||||
Seg: TFlatSegment;
|
||||
Req: TGenRequest;
|
||||
Path: string;
|
||||
|
||||
procedure PushSilence(const ALen: Double);
|
||||
begin
|
||||
if ALen <= 0.0005 then
|
||||
Exit;
|
||||
Seg := Default(TFlatSegment);
|
||||
Seg.Volume := 1;
|
||||
Seg.Kind := skBlack;
|
||||
Seg.Duration := ALen;
|
||||
Req.Kind := gkBlack;
|
||||
Req.SourcePath := '';
|
||||
Req.Duration := BucketFor(ALen);
|
||||
Req.TargetPath := GeneratedPath(AProject.CacheDir, Req);
|
||||
Seg.FilePath := Req.TargetPath;
|
||||
if not FileExists(Req.TargetPath) then
|
||||
begin
|
||||
SetLength(AMissing, Length(AMissing) + 1);
|
||||
AMissing[High(AMissing)] := Req;
|
||||
end;
|
||||
SetLength(ASegments, N + 1);
|
||||
ASegments[N] := Seg;
|
||||
Inc(N);
|
||||
end;
|
||||
|
||||
begin
|
||||
ASegments := nil;
|
||||
AMissing := nil;
|
||||
Result := False;
|
||||
if (AProject = nil) or (ATrack = nil) then
|
||||
Exit;
|
||||
|
||||
N := 0;
|
||||
At := 0;
|
||||
ATrack.Sort;
|
||||
for C := 0 to ATrack.Count - 1 do
|
||||
begin
|
||||
Clip := ATrack[C];
|
||||
{ Клипы на дорожке могут наползать друг на друга. В предпросмотре это
|
||||
разбирается проще некуда: наползший кусок начинается там, где
|
||||
кончился предыдущий. Переход в предпросмотре не показывается — он
|
||||
виден в готовом ролике. }
|
||||
if Clip.TlEnd <= At + 0.0005 then
|
||||
Continue;
|
||||
if Clip.TlStart > At then
|
||||
PushSilence(Clip.TlStart - At);
|
||||
|
||||
Item := AProject.FindMedia(Clip.MediaId);
|
||||
if Item = nil then
|
||||
Continue;
|
||||
|
||||
Seg := Default(TFlatSegment);
|
||||
Seg.Kind := skVideo;
|
||||
Seg.Volume := Clip.Volume;
|
||||
Seg.FilePath := Item.EffectivePath;
|
||||
Seg.SrcStart := Clip.SrcIn + Max(0, At - Clip.TlStart);
|
||||
Seg.Duration := Clip.TlEnd - Max(At, Clip.TlStart);
|
||||
SetLength(ASegments, N + 1);
|
||||
ASegments[N] := Seg;
|
||||
Inc(N);
|
||||
At := Clip.TlEnd;
|
||||
end;
|
||||
|
||||
if ATotal > At then
|
||||
PushSilence(ATotal - At);
|
||||
|
||||
Result := N > 0;
|
||||
end;
|
||||
|
||||
function FlattenProject(AProject: TProject;
|
||||
out AVideo, AAudio: TFlatSegmentArray;
|
||||
out AMissing: TGenRequests): Boolean;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ type
|
|||
находить звучащий кусок и брать у него громкость. }
|
||||
FPreviewSegments: TFlatSegmentArray;
|
||||
FLastVolumeSent: Double;
|
||||
FLastVolumeKey: string;
|
||||
|
||||
FGenerate: TGenerateThread;
|
||||
FGenerateBusy: Boolean;
|
||||
|
|
@ -1337,8 +1338,12 @@ end;
|
|||
procedure TMainForm.LoadPreviewEdl(const ASegments, AAudio: TFlatSegmentArray);
|
||||
var
|
||||
Edl, AudioEdl: string;
|
||||
Pos, Total: Double;
|
||||
Pos, Total, TotalLen: Double;
|
||||
WasPaused: Boolean;
|
||||
Urls: array of string;
|
||||
TrackSegs: TFlatSegmentArray;
|
||||
TrackMissing: TGenRequests;
|
||||
I: Integer;
|
||||
begin
|
||||
{ Кадр проекта задаётся до загрузки: mpv должен показывать то, что
|
||||
получится в готовом ролике, а не размер очередного исходника. Файл
|
||||
|
|
@ -1347,13 +1352,32 @@ begin
|
|||
ApplyPreviewFrame;
|
||||
|
||||
Edl := BuildEdlUri(ASegments);
|
||||
{ Звук идёт отдельным списком кусков и подаётся плееру как внешняя
|
||||
звуковая дорожка. Иначе картинку и звук из разных мест монтажа
|
||||
одновременно не показать: у одного файла звук всегда свой.
|
||||
{ Каждая звуковая дорожка идёт своим списком кусков.
|
||||
|
||||
Проверено замером: mpv принимает edl:// в качестве внешнего звука и
|
||||
берёт его именно оттуда. }
|
||||
AudioEdl := BuildEdlUri(AAudio);
|
||||
Одним общим списком не обойтись: звуковые дорожки смешиваются, а не
|
||||
побеждают друг друга, иначе под голос не положить музыку. Плеер играет
|
||||
одну дорожку за раз, поэтому смесь он собирает своими средствами, а
|
||||
мы даём ему каждую по отдельности. }
|
||||
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] + '|';
|
||||
|
||||
{ Список кусков запоминаем всегда, даже если строка не изменилась:
|
||||
громкость в неё не входит, а брать её потом неоткуда. }
|
||||
|
|
@ -1374,7 +1398,7 @@ begin
|
|||
перемоткой следом. Иначе mpv успевает начать с нуля, и на каждой
|
||||
правке монтажа слышен и виден кусок не оттуда. }
|
||||
{ Внешний звук назначается до загрузки: он часть того, что открывается. }
|
||||
FPlayer.SetAudioSource(AudioEdl);
|
||||
FPlayer.SetAudioSources(Urls);
|
||||
FPlayer.LoadUrl(Edl, Format('start=%s,pause=%s',
|
||||
[SecondsToStr(Pos), BoolToStr(WasPaused, 'yes', 'no')]));
|
||||
FLastLoadedEdl := Edl;
|
||||
|
|
@ -1852,32 +1876,50 @@ begin
|
|||
AddClipFromPool(FProject.PoolCount - 1, TrackIdx, DropTime);
|
||||
end;
|
||||
|
||||
{ Громкость клипов в предпросмотре.
|
||||
|
||||
Дорожки смешиваются, поэтому одной общей громкостью не обойтись: у
|
||||
каждой дорожки под курсором свой клип со своей громкостью. Значения
|
||||
собираются по дорожкам и отдаются плееру; он подставляет их в свою
|
||||
цепочку смешивания.
|
||||
|
||||
Пересобирать цепочку на каждый кадр нельзя — она создаётся заново и
|
||||
звук щёлкает. Поэтому только при изменении. }
|
||||
procedure TMainForm.ApplyVolumeAt(const ATime: Double);
|
||||
var
|
||||
I: Integer;
|
||||
Acc, Vol: Double;
|
||||
Clip: TClip;
|
||||
Vols: array of Double;
|
||||
Key: string;
|
||||
begin
|
||||
if (FPlayer = nil) or (not FPlayer.Initialized) then
|
||||
Exit;
|
||||
if Length(FPreviewSegments) = 0 then
|
||||
if FProject = nil then
|
||||
Exit;
|
||||
|
||||
Vol := 1;
|
||||
Acc := 0;
|
||||
for I := 0 to High(FPreviewSegments) do
|
||||
Vols := nil;
|
||||
Key := '';
|
||||
for I := 0 to FProject.TrackCount - 1 do
|
||||
begin
|
||||
if (ATime >= Acc) and (ATime < Acc + FPreviewSegments[I].Duration) then
|
||||
begin
|
||||
Vol := FPreviewSegments[I].Volume;
|
||||
Break;
|
||||
end;
|
||||
Acc := Acc + FPreviewSegments[I].Duration;
|
||||
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);
|
||||
SetLength(Vols, Length(Vols) + 1);
|
||||
if Clip <> nil then
|
||||
Vols[High(Vols)] := Clip.Volume
|
||||
else
|
||||
Vols[High(Vols)] := 1;
|
||||
Key := Key + SecondsToStr(Vols[High(Vols)]) + ',';
|
||||
end;
|
||||
|
||||
if Abs(Vol - FLastVolumeSent) < 0.001 then
|
||||
if Key = FLastVolumeKey then
|
||||
Exit;
|
||||
FLastVolumeSent := Vol;
|
||||
FPlayer.SetVolume(Vol * 100);
|
||||
FLastVolumeKey := Key;
|
||||
FPlayer.SetTrackVolumes(Vols);
|
||||
end;
|
||||
|
||||
procedure TMainForm.GenerateProgress(Sender: TObject);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ type
|
|||
FVideoFilter: string;
|
||||
FHwdec: string;
|
||||
FAudioSource: string;
|
||||
FAudioSources: array of string;
|
||||
FTrackVolumes: array of Double;
|
||||
{ Номера подключённых звуковых дорожек, в порядке подключения. }
|
||||
FAudioIds: array of string;
|
||||
FLastGraph: string;
|
||||
{ Что произошло с выбором звуковой дорожки. Читается окном для
|
||||
журнала: без этого понять, откуда взялся звук, нечем. }
|
||||
FAudioTrackNote: string;
|
||||
|
|
@ -91,6 +96,8 @@ type
|
|||
усмотрению. Получалось, что звук слышен там, где на монтаже его нет
|
||||
вовсе — он приходил из видеофайла, а не с звуковой дорожки. }
|
||||
procedure SelectExternalAudio;
|
||||
{ Цепочка смешивания звуковых дорожек с их громкостями. }
|
||||
procedure BuildAudioGraph;
|
||||
function SetOption(const AName, AValue: string): Integer;
|
||||
public
|
||||
constructor Create(AVideoParent: TWinControl);
|
||||
|
|
@ -153,6 +160,14 @@ type
|
|||
Пустая строка снимает внешний звук. }
|
||||
procedure SetAudioSource(const AUrl: string);
|
||||
|
||||
{ Несколько звуковых дорожек монтажа разом. Плеер играет только одну,
|
||||
поэтому смесь собирается его же средствами — см. AttachAudio. }
|
||||
procedure SetAudioSources(const AUrls: array of string);
|
||||
|
||||
{ Громкость каждой звуковой дорожки монтажа. Подставляется в цепочку
|
||||
смешивания: одной общей громкостью тут не обойтись. }
|
||||
procedure SetTrackVolumes(const AVolumes: array of Double);
|
||||
|
||||
function GetPropString(const AName: string): string;
|
||||
function GetPropDouble(const AName: string; out AValue: Double): Boolean;
|
||||
|
||||
|
|
@ -657,10 +672,39 @@ begin
|
|||
end;
|
||||
|
||||
procedure TMpvPlayer.SetAudioSource(const AUrl: string);
|
||||
begin
|
||||
SetAudioSources([AUrl]);
|
||||
end;
|
||||
|
||||
procedure TMpvPlayer.SetAudioSources(const AUrls: array of string);
|
||||
var
|
||||
I, N: Integer;
|
||||
begin
|
||||
{ Здесь только запоминаем. Подключается звук после загрузки картинки,
|
||||
командой audio-add — см. AttachAudioSource. }
|
||||
FAudioSource := AUrl;
|
||||
командой audio-add — см. SelectExternalAudio. }
|
||||
SetLength(FAudioSources, Length(AUrls));
|
||||
N := 0;
|
||||
for I := 0 to High(AUrls) do
|
||||
if AUrls[I] <> '' then
|
||||
begin
|
||||
FAudioSources[N] := AUrls[I];
|
||||
Inc(N);
|
||||
end;
|
||||
SetLength(FAudioSources, N);
|
||||
if N > 0 then
|
||||
FAudioSource := FAudioSources[0]
|
||||
else
|
||||
FAudioSource := '';
|
||||
end;
|
||||
|
||||
procedure TMpvPlayer.SetTrackVolumes(const AVolumes: array of Double);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
SetLength(FTrackVolumes, Length(AVolumes));
|
||||
for I := 0 to High(AVolumes) do
|
||||
FTrackVolumes[I] := AVolumes[I];
|
||||
BuildAudioGraph;
|
||||
end;
|
||||
|
||||
procedure TMpvPlayer.SetHwdec(const AMode: string);
|
||||
|
|
@ -743,9 +787,52 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
procedure TMpvPlayer.BuildAudioGraph;
|
||||
var
|
||||
I: Integer;
|
||||
Graph, Vol: string;
|
||||
begin
|
||||
if FHandle = nil then
|
||||
Exit;
|
||||
if Length(FAudioIds) = 0 then
|
||||
Exit;
|
||||
|
||||
{ Одна дорожка играется как есть: цепочка фильтров тут только мешала бы,
|
||||
да и громкость для неё задаётся обычным способом. }
|
||||
if Length(FAudioIds) = 1 then
|
||||
begin
|
||||
Graph := '';
|
||||
if (Length(FTrackVolumes) > 0) and (Abs(FTrackVolumes[0] - 1) > 0.001) then
|
||||
SetVolume(FTrackVolumes[0] * 100)
|
||||
else
|
||||
SetVolume(100);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Graph := '';
|
||||
for I := 0 to High(FAudioIds) do
|
||||
begin
|
||||
if I <= High(FTrackVolumes) then
|
||||
Vol := SecondsToStr(FTrackVolumes[I])
|
||||
else
|
||||
Vol := '1';
|
||||
Graph := Graph + Format('[aid%s]volume=%s[t%d];', [FAudioIds[I], Vol, I]);
|
||||
end;
|
||||
for I := 0 to High(FAudioIds) do
|
||||
Graph := Graph + Format('[t%d]', [I]);
|
||||
Graph := Graph + Format('amix=inputs=%d:normalize=0[ao]',
|
||||
[Length(FAudioIds)]);
|
||||
end;
|
||||
|
||||
if Graph = FLastGraph then
|
||||
Exit;
|
||||
FLastGraph := Graph;
|
||||
mpv_set_property_string(FHandle, 'lavfi-complex', PAnsiChar(Graph));
|
||||
end;
|
||||
|
||||
procedure TMpvPlayer.SelectExternalAudio;
|
||||
var
|
||||
Count, I: Integer;
|
||||
Count, I, Found: Integer;
|
||||
Kind, Ext, Id: string;
|
||||
begin
|
||||
if FHandle = nil then
|
||||
|
|
@ -755,8 +842,7 @@ begin
|
|||
if FAudioSource = '' then
|
||||
Exit;
|
||||
|
||||
{ Подключаем свой список кусков как отдельную звуковую дорожку и сразу
|
||||
выбираем её.
|
||||
{ Подключаем каждую звуковую дорожку монтажа отдельным списком кусков.
|
||||
|
||||
Через свойство audio-files это не работает: проверено на живом mpv —
|
||||
список дорожек после загрузки остаётся прежним, внешний файл не
|
||||
|
|
@ -766,8 +852,15 @@ begin
|
|||
|
||||
Команда audio-add с признаком select делает ровно то, что нужно:
|
||||
добавляет дорожку, выбирает её и снимает выбор с внутренней. }
|
||||
Command(['audio-add', FAudioSource, 'select']);
|
||||
for I := 0 to High(FAudioSources) do
|
||||
if I = 0 then
|
||||
Command(['audio-add', FAudioSources[I], 'select'])
|
||||
else
|
||||
Command(['audio-add', FAudioSources[I], 'auto']);
|
||||
|
||||
{ Собираем номера всех подключённых звуковых дорожек. }
|
||||
FAudioIds := nil;
|
||||
Found := 0;
|
||||
Count := StrToIntDef(GetPropString('track-list/count'), 0);
|
||||
for I := 0 to Count - 1 do
|
||||
begin
|
||||
|
|
@ -778,11 +871,22 @@ begin
|
|||
if (Ext <> 'yes') and (Ext <> 'true') then
|
||||
Continue;
|
||||
Id := GetPropString(Format('track-list/%d/id', [I]));
|
||||
if Id <> '' then
|
||||
begin
|
||||
if Id = '' then
|
||||
Continue;
|
||||
if Found = 0 then
|
||||
mpv_set_property_string(FHandle, 'aid', PAnsiChar(Id));
|
||||
FAudioTrackNote := 'Звук: отдельная дорожка монтажа (' + Id + ').';
|
||||
end;
|
||||
SetLength(FAudioIds, Found + 1);
|
||||
FAudioIds[Found] := Id;
|
||||
Inc(Found);
|
||||
end;
|
||||
|
||||
if Found > 0 then
|
||||
begin
|
||||
FAudioTrackNote := 'Звук: дорожек монтажа ' + IntToStr(Found) + '.';
|
||||
{ Цепочка строится отдельно: она зависит ещё и от громкостей, а те
|
||||
меняются по ходу воспроизведения. }
|
||||
FLastGraph := '';
|
||||
BuildAudioGraph;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue