mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(slices): персист панов+слайсов, раздельно КВ/трансвертеры
Сохранение и восстановление панадаптеров, слайсов и раскладки per-device,
раздельно по контексту: КВ ('hf') и каждый трансвертер ('xvtr_N') — паны
одного контекста не имеют смысла в другом.
Settings: секция "pans"/<ctx> под MAC (grid-режим/доли, на пан —
freq/rate/adc/zoom/pan/доли стека+колонки+сплиттер спектр/водопад, на слайс —
freq/mode/фильтр/agc/vol/mute/squelch/аудиоустройство). TSliceCfg/TPanCfg/
TPansConfig + Default/Save/LoadPans(Ctx).
MainForm: CreatePanCore (вынос ядра из OnAddPanClick), SavePanWorkspaceCtx/
RestorePanWorkspaceCtx/TearDownWorkspace, PanContextKey. Хук контроллера
OnBeforeStop (снимок до гашения DDC в StopRunning). Переходы ActivateXvtrBand/
DeactivateXvtr обёрнуты save-старого/teardown/restore-нового (гейт FRunning);
restore XVTR-панов после ActivateXvtr (FCurrentXvtr целевой ⇒ XvtrTranslate
верный). PanafallPanel.DestroyAllSliceFlags. Смена КВ-банда контекст не меняет.
Round-trip сериализации и разделения контекстов проверены автономно; на
экране не проверено.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+235
-22
@@ -189,6 +189,7 @@ type
|
|||||||
FPanGridRowShare: Double; // доля высоты верхнего ряда (пан 0), 0.15..0.85
|
FPanGridRowShare: Double; // доля высоты верхнего ряда (пан 0), 0.15..0.85
|
||||||
FPanColShare: array[1..MAX_PANS-1] of Double; // доли ширины колонок
|
FPanColShare: array[1..MAX_PANS-1] of Double; // доли ширины колонок
|
||||||
FPanVSplitters: array[1..MAX_PANS-1] of TPanel; // слева от колонки пана i
|
FPanVSplitters: array[1..MAX_PANS-1] of TPanel; // слева от колонки пана i
|
||||||
|
FPansRestored: Boolean; // персист панов восстановлен в этой Run-сессии
|
||||||
// Drag вертикального сплиттера (индекс пана справа; 0 = нет).
|
// Drag вертикального сплиттера (индекс пана справа; 0 = нет).
|
||||||
FPanVSplitDrag: Integer;
|
FPanVSplitDrag: Integer;
|
||||||
FPanVSplitX0: Integer;
|
FPanVSplitX0: Integer;
|
||||||
@@ -583,10 +584,20 @@ type
|
|||||||
function PanOfOverlay(Sender: TObject): TPanafallPanel;
|
function PanOfOverlay(Sender: TObject): TPanafallPanel;
|
||||||
procedure PushFlagStateAllPans(SliceId: Integer);
|
procedure PushFlagStateAllPans(SliceId: Integer);
|
||||||
procedure LayoutFlagsAllPans;
|
procedure LayoutFlagsAllPans;
|
||||||
|
function CreatePanCore(PanId: Integer; Freq: Double;
|
||||||
|
RateKHz: Word; ADC: Byte): TPanafallPanel; // DDC+панель (без слайса)
|
||||||
procedure OnAddPanClick(Sender: TObject); // «⊞» пана 0
|
procedure OnAddPanClick(Sender: TObject); // «⊞» пана 0
|
||||||
procedure OnPanHeaderClose(Sender: TObject); // «×» шапки пана
|
procedure OnPanHeaderClose(Sender: TObject); // «×» шапки пана
|
||||||
procedure ClosePanadapter(PanId: Integer);
|
procedure ClosePanadapter(PanId: Integer);
|
||||||
procedure DestroyAllExtraPans; // stop/выход
|
procedure DestroyAllExtraPans; // stop/выход
|
||||||
|
function PanContextKey: string; // 'hf' | 'xvtr_N'
|
||||||
|
procedure TearDownWorkspace; // снос панов+слайсов контекста
|
||||||
|
procedure SavePanWorkspace; // OnBeforeStop: текущий контекст
|
||||||
|
procedure SavePanWorkspaceCtx(const Ctx: string); // снимок панов+слайсов контекста
|
||||||
|
procedure RestorePanWorkspace; // восстановление при Run (once)
|
||||||
|
procedure RestorePanWorkspaceCtx(const Ctx: string); // восстановление контекста Ctx
|
||||||
|
procedure RestoreSliceInto(P: TPanafallPanel; PanId: Integer;
|
||||||
|
const SC: TSliceCfg);
|
||||||
procedure UpdatePanHeaders; // текст+видимость шапок
|
procedure UpdatePanHeaders; // текст+видимость шапок
|
||||||
procedure ApplyPanHeaderTheme(P: TPanafallPanel); // цвета шапки пана
|
procedure ApplyPanHeaderTheme(P: TPanafallPanel); // цвета шапки пана
|
||||||
procedure UpdateAddPanButton;
|
procedure UpdateAddPanButton;
|
||||||
@@ -879,6 +890,8 @@ begin
|
|||||||
FChannelController.OnActiveChanged := OnChannelActiveChanged;
|
FChannelController.OnActiveChanged := OnChannelActiveChanged;
|
||||||
// Канал-оркестрация фаером из контроллера в конце SetVfoA (любой путь тюнинга).
|
// Канал-оркестрация фаером из контроллера в конце SetVfoA (любой путь тюнинга).
|
||||||
FController.OnAfterTune := FChannelController.OnVfoTuned;
|
FController.OnAfterTune := FChannelController.OnVfoTuned;
|
||||||
|
// Снимок панов+слайсов перед гашением DDC в StopRunning (персист).
|
||||||
|
FController.OnBeforeStop := SavePanWorkspace;
|
||||||
FController.FVfoA := 14200000;
|
FController.FVfoA := 14200000;
|
||||||
FController.FVfoB := 7100000;
|
FController.FVfoB := 7100000;
|
||||||
FController.FActiveVfo := 0;
|
FController.FActiveVfo := 0;
|
||||||
@@ -1337,6 +1350,7 @@ begin
|
|||||||
BtnMOX.Enabled := True;
|
BtnMOX.Enabled := True;
|
||||||
UpdateAddPanButton; // «⊞» доступна только в Run
|
UpdateAddPanButton; // «⊞» доступна только в Run
|
||||||
ResizeSpectrumPanels;
|
ResizeSpectrumPanels;
|
||||||
|
RestorePanWorkspace; // паны+слайсы прошлой сессии
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
@@ -1344,6 +1358,10 @@ begin
|
|||||||
BtnStartStop.Caption := 'START';
|
BtnStartStop.Caption := 'START';
|
||||||
StyleButton(BtnStartStop, False);
|
StyleButton(BtnStartStop, False);
|
||||||
BtnMOX.Enabled := False;
|
BtnMOX.Enabled := False;
|
||||||
|
// Снимок панов уже сделан хуком OnBeforeStop=SavePanWorkspace (до
|
||||||
|
// гашения DDC в StopRunning). Здесь только разрешаем повторный
|
||||||
|
// restore на следующем старте.
|
||||||
|
FPansRestored := False;
|
||||||
// Доп. паны живут только в Run-сессии (их DDC уже погашены
|
// Доп. паны живут только в Run-сессии (их DDC уже погашены
|
||||||
// контроллером в StopRunning) — убираем и UI.
|
// контроллером в StopRunning) — убираем и UI.
|
||||||
DestroyAllExtraPans;
|
DestroyAllExtraPans;
|
||||||
@@ -5841,31 +5859,18 @@ begin
|
|||||||
FController.FRunning and HasSlot;
|
FController.FRunning and HasSlot;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnAddPanClick(Sender: TObject);
|
function TMainForm.CreatePanCore(PanId: Integer; Freq: Double;
|
||||||
// «⊞»: новый пан на своём DDC. Центр = частота активного VFO, rate 384к,
|
RateKHz: Word; ADC: Byte): TPanafallPanel;
|
||||||
// ADC 0; в центре автоматически создаётся первый слайс.
|
// Ядро создания доп. пана: DDC + панель + проводка + сплиттеры + регистрация
|
||||||
|
// в FPans. НЕ создаёт слайс, НЕ трогает раскладку/шапки — это делают
|
||||||
|
// вызывающие (OnAddPanClick — новый пан; RestorePanWorkspace — восстановление).
|
||||||
|
// Возвращает nil, если DDC не создался.
|
||||||
var
|
var
|
||||||
PanId, i: Integer;
|
|
||||||
RateKHz: Word;
|
|
||||||
Freq: Double;
|
|
||||||
P: TPanafallPanel;
|
P: TPanafallPanel;
|
||||||
Split: TPanel;
|
Split: TPanel;
|
||||||
begin
|
begin
|
||||||
if not FController.FRunning then Exit;
|
Result := nil;
|
||||||
PanId := -1;
|
if not FController.AddPanDDC(PanId, Freq, RateKHz, ADC) then Exit;
|
||||||
for i := 1 to MAX_PANS - 1 do
|
|
||||||
if FPans[i] = nil then begin PanId := i; Break; end;
|
|
||||||
if PanId < 0 then Exit;
|
|
||||||
|
|
||||||
// Rate наследуется от главного пана с клампом сверху (768к — потолок доп.
|
|
||||||
// пана по Ethernet/CPU; 48/96/192к наследуются как есть). Не из сетки
|
|
||||||
// (напр. Pluto-подобные) — контроллер откатит на 384к. Дальше rate пана
|
|
||||||
// независим: селектор «NNN kHz» в шапке, за сменой rate главного НЕ следует.
|
|
||||||
RateKHz := FController.FSampleRate div 1000;
|
|
||||||
if RateKHz > 768 then RateKHz := 768;
|
|
||||||
|
|
||||||
Freq := ActiveVfoFreq;
|
|
||||||
if not FController.AddPanDDC(PanId, Freq, RateKHz, 0) then Exit;
|
|
||||||
|
|
||||||
P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
||||||
P.PanId := PanId;
|
P.PanId := PanId;
|
||||||
@@ -5942,13 +5947,42 @@ begin
|
|||||||
FPanVSplitters[PanId] := Split;
|
FPanVSplitters[PanId] := Split;
|
||||||
FPanColShare[PanId] := 1.0;
|
FPanColShare[PanId] := 1.0;
|
||||||
|
|
||||||
// Публикуем пан для пиксельных колбэков (display-поток) и делим высоту поровну.
|
// Публикуем пан для пиксельных колбэков (display-поток).
|
||||||
FPanCbLock.Enter;
|
FPanCbLock.Enter;
|
||||||
try
|
try
|
||||||
FPans[PanId] := P;
|
FPans[PanId] := P;
|
||||||
finally
|
finally
|
||||||
FPanCbLock.Leave;
|
FPanCbLock.Leave;
|
||||||
end;
|
end;
|
||||||
|
Result := P;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnAddPanClick(Sender: TObject);
|
||||||
|
// «⊞»: новый пан на своём DDC. Центр = частота активного VFO, rate наследуется
|
||||||
|
// от главного (кламп 768к); ADC 0; в центре автоматически создаётся слайс.
|
||||||
|
var
|
||||||
|
PanId, i: Integer;
|
||||||
|
RateKHz: Word;
|
||||||
|
Freq: Double;
|
||||||
|
P: TPanafallPanel;
|
||||||
|
begin
|
||||||
|
if not FController.FRunning then Exit;
|
||||||
|
PanId := -1;
|
||||||
|
for i := 1 to MAX_PANS - 1 do
|
||||||
|
if FPans[i] = nil then begin PanId := i; Break; end;
|
||||||
|
if PanId < 0 then Exit;
|
||||||
|
|
||||||
|
// Rate наследуется от главного пана с клампом сверху (768к — потолок доп.
|
||||||
|
// пана по Ethernet/CPU; 48/96/192к наследуются как есть). Не из сетки
|
||||||
|
// (напр. Pluto-подобные) — контроллер откатит на 384к. Дальше rate пана
|
||||||
|
// независим: селектор «NNN kHz» в шапке, за сменой rate главного НЕ следует.
|
||||||
|
RateKHz := FController.FSampleRate div 1000;
|
||||||
|
if RateKHz > 768 then RateKHz := 768;
|
||||||
|
|
||||||
|
Freq := ActiveVfoFreq;
|
||||||
|
P := CreatePanCore(PanId, Freq, RateKHz, 0);
|
||||||
|
if P = nil then Exit;
|
||||||
|
|
||||||
for i := 0 to MAX_PANS - 1 do
|
for i := 0 to MAX_PANS - 1 do
|
||||||
if FPans[i] <> nil then FPanShare[i] := 1.0 / PanCount;
|
if FPans[i] <> nil then FPanShare[i] := 1.0 / PanCount;
|
||||||
|
|
||||||
@@ -6011,6 +6045,163 @@ begin
|
|||||||
if FPans[i] <> nil then ClosePanadapter(i);
|
if FPans[i] <> nil then ClosePanadapter(i);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TMainForm.PanContextKey: string;
|
||||||
|
// Ключ контекста персиста панов: КВ и каждый трансвертер — раздельно
|
||||||
|
// (паны/слайсы одного контекста не имеют смысла в другом).
|
||||||
|
begin
|
||||||
|
if FController.FCurrentXvtr < 0 then
|
||||||
|
Result := 'hf'
|
||||||
|
else
|
||||||
|
Result := 'xvtr_' + IntToStr(FController.FCurrentXvtr);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.TearDownWorkspace;
|
||||||
|
// Полный снос панов+слайсов текущего контекста (доп. паны + слайсы пана 0).
|
||||||
|
begin
|
||||||
|
DestroyAllExtraPans;
|
||||||
|
FPan.DestroyAllSliceFlags;
|
||||||
|
FActiveSliceId := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.SavePanWorkspace;
|
||||||
|
// OnBeforeStop-хук: снимок текущего контекста перед гашением панов.
|
||||||
|
begin
|
||||||
|
SavePanWorkspaceCtx(PanContextKey);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.SavePanWorkspaceCtx(const Ctx: string);
|
||||||
|
// Снимок панов+слайсов+раскладки в секцию "pans"/Ctx под MAC. Зовётся при STOP/
|
||||||
|
// закрытии/смене контекста (пока железо ещё подключено), до разрушения панов.
|
||||||
|
var
|
||||||
|
Cfg: TPansConfig;
|
||||||
|
i, s, n: Integer;
|
||||||
|
begin
|
||||||
|
if not FController.FNetwork.Connected then Exit; // MAC валиден только при девайсе
|
||||||
|
TSettingsManager.DefaultPans(Cfg);
|
||||||
|
Cfg.GridMode := FPanGridMode;
|
||||||
|
Cfg.GridRowShare := FPanGridRowShare;
|
||||||
|
for i := 0 to MAX_PANS - 1 do
|
||||||
|
begin
|
||||||
|
Cfg.Pans[i].StackShare := FPanShare[i];
|
||||||
|
if FPans[i] <> nil then Cfg.Pans[i].SplitRatio := FPans[i].SplitterRatio;
|
||||||
|
if i > 0 then
|
||||||
|
begin
|
||||||
|
if not FController.PanDDCActive(i) then Continue; // слайсы мёртвого пана не пишем
|
||||||
|
Cfg.Pans[i].Active := True;
|
||||||
|
Cfg.Pans[i].FreqHz := FController.PanDDCFreq(i);
|
||||||
|
Cfg.Pans[i].RateKHz := FController.PanDDCRateKHz(i);
|
||||||
|
Cfg.Pans[i].ADC := FController.PanADC(i);
|
||||||
|
Cfg.Pans[i].Zoom := FController.PanZoomFactor(i);
|
||||||
|
Cfg.Pans[i].PanSlider := FController.PanPanSlider(i);
|
||||||
|
Cfg.Pans[i].ColShare := FPanColShare[i];
|
||||||
|
end;
|
||||||
|
// Слайсы этого пана (главный VFO пана 0 — не слайс, его тут нет).
|
||||||
|
n := 0;
|
||||||
|
for s := 0 to High(FController.FSlices) do
|
||||||
|
if FController.FSlices[s].Active and (FController.FSlices[s].PanId = i) then
|
||||||
|
Inc(n);
|
||||||
|
SetLength(Cfg.Pans[i].Slices, n);
|
||||||
|
n := 0;
|
||||||
|
for s := 0 to High(FController.FSlices) do
|
||||||
|
if FController.FSlices[s].Active and (FController.FSlices[s].PanId = i) then
|
||||||
|
begin
|
||||||
|
Cfg.Pans[i].Slices[n].TargetHz := FController.FSlices[s].TargetHz;
|
||||||
|
Cfg.Pans[i].Slices[n].Mode := FController.FSlices[s].Mode;
|
||||||
|
Cfg.Pans[i].Slices[n].FilterLo := FController.FSlices[s].FilterLo;
|
||||||
|
Cfg.Pans[i].Slices[n].FilterHi := FController.FSlices[s].FilterHi;
|
||||||
|
Cfg.Pans[i].Slices[n].AGC := Ord(FController.FSlices[s].AGC);
|
||||||
|
Cfg.Pans[i].Slices[n].Volume := FController.FSlices[s].Volume;
|
||||||
|
Cfg.Pans[i].Slices[n].Muted := FController.FSlices[s].Muted;
|
||||||
|
Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn;
|
||||||
|
Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel;
|
||||||
|
Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName;
|
||||||
|
Inc(n);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
FController.FSettings.SavePans(FController.FDevMAC, Ctx, Cfg);
|
||||||
|
FController.FSettings.Save;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.RestoreSliceInto(P: TPanafallPanel; PanId: Integer;
|
||||||
|
const SC: TSliceCfg);
|
||||||
|
// Один слайс в пан P (PanId): создаёт WDSP-канал + флаг, применяет mute/squelch.
|
||||||
|
var Id, DevIdx: Integer;
|
||||||
|
begin
|
||||||
|
if not FController.SliceFitsCapture(SC.TargetHz, PanId) then Exit; // вне полосы захвата
|
||||||
|
DevIdx := -1;
|
||||||
|
if SC.DevName <> '' then
|
||||||
|
DevIdx := FController.FAudioOut.FindDeviceByName(SC.DevName);
|
||||||
|
Id := FController.AddSlice(SC.TargetHz, SC.Mode, SC.FilterLo, SC.FilterHi,
|
||||||
|
TWDSPAGCMode(EnsureRange(SC.AGC, 0, Ord(High(TWDSPAGCMode)))),
|
||||||
|
EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId);
|
||||||
|
if Id < 0 then Exit;
|
||||||
|
if SC.Muted then FController.SetSliceMute(Id, True);
|
||||||
|
if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel);
|
||||||
|
P.CreateSliceFlag(Id);
|
||||||
|
FActiveSliceId := Id;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.RestorePanWorkspace;
|
||||||
|
// Восстановление контекста при выходе в Run (один раз за сессию). Контекст
|
||||||
|
// (КВ|трансвертер) уже финальный — ConnectAndRun зовёт StartRunning последним.
|
||||||
|
begin
|
||||||
|
if not FController.FRunning then Exit;
|
||||||
|
if FPansRestored then Exit;
|
||||||
|
FPansRestored := True;
|
||||||
|
// Слайсы пана 0 переживают STOP (софт-fan-out) — снести выживших, иначе дубли.
|
||||||
|
TearDownWorkspace;
|
||||||
|
RestorePanWorkspaceCtx(PanContextKey);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.RestorePanWorkspaceCtx(const Ctx: string);
|
||||||
|
// Восстановление панов+слайсов+раскладки контекста Ctx. Вызывающий обязан
|
||||||
|
// СНАЧАЛА снести текущий воркспейс (TearDownWorkspace). При отсутствии
|
||||||
|
// сохранёнки применяются дефолты (чистый стек, без панов).
|
||||||
|
var
|
||||||
|
Cfg: TPansConfig;
|
||||||
|
i, k: Integer;
|
||||||
|
P: TPanafallPanel;
|
||||||
|
begin
|
||||||
|
if not FController.FRunning then Exit;
|
||||||
|
// LoadPans заполняет Cfg дефолтами и при отсутствии секции — применяем как есть
|
||||||
|
// (грид off, панов нет) ради чистого целевого контекста.
|
||||||
|
FController.FSettings.LoadPans(FController.FDevMAC, Ctx, Cfg);
|
||||||
|
|
||||||
|
FPanGridMode := Cfg.GridMode;
|
||||||
|
FPanGridRowShare := Cfg.GridRowShare;
|
||||||
|
|
||||||
|
// Пан 0: восстановить его доп. слайсы (главный VFO — не слайс) + доли.
|
||||||
|
FPanShare[0] := Cfg.Pans[0].StackShare;
|
||||||
|
FPan.SplitterRatio := Cfg.Pans[0].SplitRatio;
|
||||||
|
for k := 0 to High(Cfg.Pans[0].Slices) do
|
||||||
|
RestoreSliceInto(FPan, 0, Cfg.Pans[0].Slices[k]);
|
||||||
|
|
||||||
|
// Доп. паны на своих DDC.
|
||||||
|
for i := 1 to MAX_PANS - 1 do
|
||||||
|
begin
|
||||||
|
if not Cfg.Pans[i].Active then Continue;
|
||||||
|
if FPans[i] <> nil then Continue;
|
||||||
|
P := CreatePanCore(i, Cfg.Pans[i].FreqHz,
|
||||||
|
Word(EnsureRange(Cfg.Pans[i].RateKHz, 48, 1536)),
|
||||||
|
Byte(EnsureRange(Cfg.Pans[i].ADC, 0, 3)));
|
||||||
|
if P = nil then Continue;
|
||||||
|
FPanShare[i] := Cfg.Pans[i].StackShare;
|
||||||
|
FPanColShare[i] := Cfg.Pans[i].ColShare;
|
||||||
|
P.SplitterRatio := Cfg.Pans[i].SplitRatio;
|
||||||
|
FController.SetPanZoomPan(i, Cfg.Pans[i].Zoom, Cfg.Pans[i].PanSlider);
|
||||||
|
for k := 0 to High(Cfg.Pans[i].Slices) do
|
||||||
|
RestoreSliceInto(P, i, Cfg.Pans[i].Slices[k]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
ApplyDarkTheme;
|
||||||
|
UpdatePanHeaders;
|
||||||
|
UpdateAddPanButton;
|
||||||
|
ResizeSpectrumPanels;
|
||||||
|
// Зум-бары/флаги — после раскладки (размеры вьюх уже верны).
|
||||||
|
for i := 1 to MAX_PANS - 1 do
|
||||||
|
if FPans[i] <> nil then RefreshPanZoom(FPans[i]);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.SyncPanViews;
|
procedure TMainForm.SyncPanViews;
|
||||||
// Центр/span вьюх доп. панов (аналог SyncSpecViewFreq главного, но без
|
// Центр/span вьюх доп. панов (аналог SyncSpecViewFreq главного, но без
|
||||||
// VFO/TX-маркеров — они уведены за окно при создании).
|
// VFO/TX-маркеров — они уведены за окно при создании).
|
||||||
@@ -6860,20 +7051,42 @@ end;
|
|||||||
// Активирует XVTR-band: устанавливает видимую частоту по LastFreq или
|
// Активирует XVTR-band: устанавливает видимую частоту по LastFreq или
|
||||||
// середине [FreqBegin..FreqEnd], применяет network XVTR-режим.
|
// середине [FreqBegin..FreqEnd], применяет network XVTR-режим.
|
||||||
procedure TMainForm.ActivateXvtrBand(Idx: Integer);
|
procedure TMainForm.ActivateXvtrBand(Idx: Integer);
|
||||||
|
var Running: Boolean;
|
||||||
begin
|
begin
|
||||||
|
// Смена контекста персиста панов: сохранить старый (КВ|прежний XVTR), снести,
|
||||||
|
// войти в слот, восстановить целевой. Паны живут только в Run.
|
||||||
|
Running := FController.FRunning;
|
||||||
|
if Running then
|
||||||
|
begin
|
||||||
|
SavePanWorkspaceCtx(PanContextKey); // текущий контекст (до смены FCurrentXvtr)
|
||||||
|
TearDownWorkspace;
|
||||||
|
end;
|
||||||
// Тонкий делегат: вход в XVTR-слот + рендер кнопок/частот/спектра/wideband/web —
|
// Тонкий делегат: вход в XVTR-слот + рендер кнопок/частот/спектра/wideband/web —
|
||||||
// всё в команде/событиях (rfXvtr/rfBand/rfActiveVfo).
|
// всё в команде/событиях (rfXvtr/rfBand/rfActiveVfo).
|
||||||
FController.ActivateXvtr(Idx);
|
FController.ActivateXvtr(Idx);
|
||||||
|
if Running then
|
||||||
|
RestorePanWorkspaceCtx(PanContextKey); // новый контекст (xvtr_Idx)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Деактивирует XVTR — возвращает к HF-режиму. FController.FCurrentBand указывает
|
// Деактивирует XVTR — возвращает к HF-режиму. FController.FCurrentBand указывает
|
||||||
// на HF-диапазон, с которого пользователь зашёл в трансвертер. RestoreBand вернёт
|
// на HF-диапазон, с которого пользователь зашёл в трансвертер. RestoreBand вернёт
|
||||||
// VFO/Mode/Filter/AGC/CTUN/Span этого диапазона и обновит подсветку HF band-кнопки.
|
// VFO/Mode/Filter/AGC/CTUN/Span этого диапазона и обновит подсветку HF band-кнопки.
|
||||||
procedure TMainForm.DeactivateXvtr;
|
procedure TMainForm.DeactivateXvtr;
|
||||||
|
var Running: Boolean;
|
||||||
begin
|
begin
|
||||||
|
// Выход из XVTR в КВ = смена контекста персиста панов: сохранить XVTR-контекст,
|
||||||
|
// снести, вернуться в HF, восстановить 'hf'.
|
||||||
|
Running := FController.FRunning;
|
||||||
|
if Running then
|
||||||
|
begin
|
||||||
|
SavePanWorkspaceCtx(PanContextKey); // текущий XVTR-контекст (до сброса FCurrentXvtr)
|
||||||
|
TearDownWorkspace;
|
||||||
|
end;
|
||||||
// Тонкий делегат: выход в HF (SaveCurrentBand + HF-возврат RestoreBand + рендер
|
// Тонкий делегат: выход в HF (SaveCurrentBand + HF-возврат RestoreBand + рендер
|
||||||
// кнопок/wideband/web) — всё в команде/событиях.
|
// кнопок/wideband/web) — всё в команде/событиях.
|
||||||
FController.DeactivateXvtr;
|
FController.DeactivateXvtr;
|
||||||
|
if Running then
|
||||||
|
RestorePanWorkspaceCtx(PanContextKey); // контекст 'hf'
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings);
|
procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings);
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ type
|
|||||||
function FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
function FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
||||||
procedure CreateSliceFlag(SliceId: Integer);
|
procedure CreateSliceFlag(SliceId: Integer);
|
||||||
procedure DestroySliceFlag(SliceId: Integer);
|
procedure DestroySliceFlag(SliceId: Integer);
|
||||||
|
procedure DestroyAllSliceFlags; // сброс всех слайсов панели (+RemoveSlice)
|
||||||
// «×» на флаге: нельзя free внутри его же обработчика мыши — пометить...
|
// «×» на флаге: нельзя free внутри его же обработчика мыши — пометить...
|
||||||
procedure RequestSliceClose(SliceId: Integer);
|
procedure RequestSliceClose(SliceId: Integer);
|
||||||
// …и снять после возврата из диспетчера.
|
// …и снять после возврата из диспетчера.
|
||||||
@@ -1004,6 +1005,15 @@ begin
|
|||||||
if Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self); // Self = панель (роутинг по пану)
|
if Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self); // Self = панель (роутинг по пану)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TPanafallPanel.DestroyAllSliceFlags;
|
||||||
|
// Полный сброс слайсов панели (каждый DestroySliceFlag зовёт RemoveSlice +
|
||||||
|
// сжимает FSliceFlags). Нужен персисту: слайсы пана 0 переживают STOP, а
|
||||||
|
// восстановление создаёт их заново — иначе дубли.
|
||||||
|
begin
|
||||||
|
while Length(FSliceFlags) > 0 do
|
||||||
|
DestroySliceFlag(FSliceFlags[0].SliceId);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TPanafallPanel.RequestSliceClose(SliceId: Integer);
|
procedure TPanafallPanel.RequestSliceClose(SliceId: Integer);
|
||||||
begin
|
begin
|
||||||
FPendingSliceClose := SliceId;
|
FPendingSliceClose := SliceId;
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ type
|
|||||||
FOnStateChanged: TRadioStateEvent;
|
FOnStateChanged: TRadioStateEvent;
|
||||||
FStateListeners: array of TRadioStateEvent; // доп. наблюдатели (CAT/Andromeda), помимо FOnStateChanged (MainForm)
|
FStateListeners: array of TRadioStateEvent; // доп. наблюдатели (CAT/Andromeda), помимо FOnStateChanged (MainForm)
|
||||||
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
|
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
|
||||||
|
FOnBeforeStop: TThreadMethod; // хук перед гашением панов в StopRunning (персист панов)
|
||||||
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
|
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
|
||||||
FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False
|
FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False
|
||||||
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
|
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
|
||||||
@@ -740,6 +741,7 @@ type
|
|||||||
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
|
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
|
||||||
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
|
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
|
||||||
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
|
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
|
||||||
|
property OnBeforeStop: TThreadMethod read FOnBeforeStop write FOnBeforeStop;
|
||||||
property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume;
|
property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume;
|
||||||
// Локальный звук ПК (динамик RX / микрофон TX). Демон ставит False: аудио
|
// Локальный звук ПК (динамик RX / микрофон TX). Демон ставит False: аудио
|
||||||
// только через web (OnAudioConsume), локальная звуковая карта не трогается.
|
// только через web (OnAudioConsume), локальная звуковая карта не трогается.
|
||||||
@@ -3915,6 +3917,8 @@ var
|
|||||||
begin
|
begin
|
||||||
if FRunning then
|
if FRunning then
|
||||||
begin
|
begin
|
||||||
|
// Персист панов/слайсов ДО их гашения (UI-снимок живого состояния).
|
||||||
|
if Assigned(FOnBeforeStop) then FOnBeforeStop;
|
||||||
// Паны на доп. DDC живут только внутри Run-сессии — гасим до Run=0
|
// Паны на доп. DDC живут только внутри Run-сессии — гасим до Run=0
|
||||||
// (3.3 UI пересоздаст по персисту при следующем старте).
|
// (3.3 UI пересоздаст по персисту при следующем старте).
|
||||||
for p := 1 to MAX_PANS - 1 do
|
for p := 1 to MAX_PANS - 1 do
|
||||||
|
|||||||
+178
@@ -12,6 +12,7 @@ const
|
|||||||
SETTINGS_FILE = 'hpsdr_settings.json';
|
SETTINGS_FILE = 'hpsdr_settings.json';
|
||||||
CFG_BAND_COUNT = 12;
|
CFG_BAND_COUNT = 12;
|
||||||
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
||||||
|
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
|
||||||
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
|
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
|
||||||
// (split-LO транспондер; настраивается на
|
// (split-LO транспондер; настраивается на
|
||||||
// отдельной странице, не в общей XVTR-таблице)
|
// отдельной странице, не в общей XVTR-таблице)
|
||||||
@@ -83,6 +84,41 @@ type
|
|||||||
PanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
|
PanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
|
||||||
|
// один раз при выходе в Run. Слайс = приёмный WDSP-канал внутри пана; частоты
|
||||||
|
// абсолютные (видимые). См. [[project_slices_plan]].
|
||||||
|
TSliceCfg = record
|
||||||
|
TargetHz: Double; // абсолютная видимая частота слайса
|
||||||
|
Mode: Integer;
|
||||||
|
FilterLo: Integer;
|
||||||
|
FilterHi: Integer;
|
||||||
|
AGC: Integer; // Ord(TWDSPAGCMode)
|
||||||
|
Volume: Double; // 0..1
|
||||||
|
Muted: Boolean;
|
||||||
|
FMSQOn: Boolean;
|
||||||
|
FMSQLevel: Integer;
|
||||||
|
DevName: string; // имя PortAudio-устройства ('' = default)
|
||||||
|
end;
|
||||||
|
|
||||||
|
TPanCfg = record
|
||||||
|
Active: Boolean; // пан 1..N существует (пан 0 всегда есть)
|
||||||
|
FreqHz: Double; // видимый центр DDC (только паны N)
|
||||||
|
RateKHz: Integer;
|
||||||
|
ADC: Integer;
|
||||||
|
Zoom: Double; // 0..1
|
||||||
|
PanSlider: Double; // 0..1
|
||||||
|
StackShare: Double; // доля высоты в стеке панов
|
||||||
|
ColShare: Double; // доля ширины в грид-раскладке
|
||||||
|
SplitRatio: Double; // доля высоты спектра (сплиттер спектр/водопад)
|
||||||
|
Slices: array of TSliceCfg;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TPansConfig = record
|
||||||
|
GridMode: Boolean; // грид-раскладка (▦) vs вертикальный стек (▤)
|
||||||
|
GridRowShare: Double; // доля высоты пана 0 в гриде
|
||||||
|
Pans: array[0..CFG_PANS_MAX-1] of TPanCfg; // [0]=главный (только Slices)
|
||||||
|
end;
|
||||||
|
|
||||||
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
||||||
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
|
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
|
||||||
TTXSettings = record
|
TTXSettings = record
|
||||||
@@ -371,6 +407,11 @@ type
|
|||||||
class procedure DefaultXvtr(out X: TXvtrSettings);
|
class procedure DefaultXvtr(out X: TXvtrSettings);
|
||||||
function LoadXvtr(const MAC: array of Byte; out X: TXvtrSettings): Boolean;
|
function LoadXvtr(const MAC: array of Byte; out X: TXvtrSettings): Boolean;
|
||||||
procedure SaveXvtr(const MAC: array of Byte; const X: TXvtrSettings);
|
procedure SaveXvtr(const MAC: array of Byte; const X: TXvtrSettings);
|
||||||
|
// Панадаптеры + слайсы per-device (JSON-секция "pans" под MAC). Ctx —
|
||||||
|
// ключ контекста ('hf' или 'xvtr_N'): КВ и каждый трансвертер раздельно.
|
||||||
|
class procedure DefaultPans(out P: TPansConfig);
|
||||||
|
function LoadPans(const MAC: array of Byte; const Ctx: string; out P: TPansConfig): Boolean;
|
||||||
|
procedure SavePans(const MAC: array of Byte; const Ctx: string; const P: TPansConfig);
|
||||||
// Web-сервер — глобальные настройки (секция "web" в корне JSON).
|
// Web-сервер — глобальные настройки (секция "web" в корне JSON).
|
||||||
class procedure DefaultWeb(out W: TWebSettings);
|
class procedure DefaultWeb(out W: TWebSettings);
|
||||||
procedure LoadWebSettings(out W: TWebSettings);
|
procedure LoadWebSettings(out W: TWebSettings);
|
||||||
@@ -1547,4 +1588,141 @@ begin
|
|||||||
Result := JB(O, 'light_theme', False);
|
Result := JB(O, 'light_theme', False);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Панадаптеры + слайсы (per-device, JSON-секция "pans" под MAC)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class procedure TSettingsManager.DefaultPans(out P: TPansConfig);
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
P.GridMode := False;
|
||||||
|
P.GridRowShare := 0.5;
|
||||||
|
for i := 0 to CFG_PANS_MAX-1 do
|
||||||
|
begin
|
||||||
|
P.Pans[i].Active := False;
|
||||||
|
P.Pans[i].FreqHz := 0.0;
|
||||||
|
P.Pans[i].RateKHz := 384;
|
||||||
|
P.Pans[i].ADC := 0;
|
||||||
|
P.Pans[i].Zoom := 0.0;
|
||||||
|
P.Pans[i].PanSlider := 0.5;
|
||||||
|
P.Pans[i].StackShare := 1.0;
|
||||||
|
P.Pans[i].ColShare := 1.0;
|
||||||
|
P.Pans[i].SplitRatio := 0.40; // = TPanafallPanel дефолт
|
||||||
|
SetLength(P.Pans[i].Slices, 0);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsManager.SavePans(const MAC: array of Byte; const Ctx: string;
|
||||||
|
const P: TPansConfig);
|
||||||
|
var
|
||||||
|
Root, PanObj, SliceObj: TJSONObject;
|
||||||
|
PanArr, SliceArr: TJSONArray;
|
||||||
|
i, j, Idx: Integer;
|
||||||
|
begin
|
||||||
|
Root := EnsureObj(EnsureObj(GetDevObj(MacToStr(MAC)), 'pans'), Ctx);
|
||||||
|
JW(Root, 'grid_mode', P.GridMode);
|
||||||
|
JW(Root, 'grid_row_share', P.GridRowShare);
|
||||||
|
// Список панов пересоздаётся целиком (проще, чем merge по индексам).
|
||||||
|
Idx := Root.IndexOfName('list');
|
||||||
|
if Idx >= 0 then Root.Delete(Idx);
|
||||||
|
PanArr := TJSONArray.Create;
|
||||||
|
Root.Add('list', PanArr);
|
||||||
|
for i := 0 to CFG_PANS_MAX-1 do
|
||||||
|
begin
|
||||||
|
// Пан 0 пишем всегда (ради его доли стека/слайсов), паны N — только активные.
|
||||||
|
if (i > 0) and not P.Pans[i].Active then Continue;
|
||||||
|
PanObj := TJSONObject.Create;
|
||||||
|
PanArr.Add(PanObj);
|
||||||
|
PanObj.Add('id', i);
|
||||||
|
PanObj.Add('active', P.Pans[i].Active);
|
||||||
|
PanObj.Add('freq', P.Pans[i].FreqHz);
|
||||||
|
PanObj.Add('rate_khz', P.Pans[i].RateKHz);
|
||||||
|
PanObj.Add('adc', P.Pans[i].ADC);
|
||||||
|
PanObj.Add('zoom', P.Pans[i].Zoom);
|
||||||
|
PanObj.Add('pan', P.Pans[i].PanSlider);
|
||||||
|
PanObj.Add('stack_share', P.Pans[i].StackShare);
|
||||||
|
PanObj.Add('col_share', P.Pans[i].ColShare);
|
||||||
|
PanObj.Add('split_ratio', P.Pans[i].SplitRatio);
|
||||||
|
SliceArr := TJSONArray.Create;
|
||||||
|
PanObj.Add('slices', SliceArr);
|
||||||
|
for j := 0 to High(P.Pans[i].Slices) do
|
||||||
|
begin
|
||||||
|
SliceObj := TJSONObject.Create;
|
||||||
|
SliceArr.Add(SliceObj);
|
||||||
|
SliceObj.Add('target_hz', P.Pans[i].Slices[j].TargetHz);
|
||||||
|
SliceObj.Add('mode', P.Pans[i].Slices[j].Mode);
|
||||||
|
SliceObj.Add('filter_lo', P.Pans[i].Slices[j].FilterLo);
|
||||||
|
SliceObj.Add('filter_hi', P.Pans[i].Slices[j].FilterHi);
|
||||||
|
SliceObj.Add('agc', P.Pans[i].Slices[j].AGC);
|
||||||
|
SliceObj.Add('volume', P.Pans[i].Slices[j].Volume);
|
||||||
|
SliceObj.Add('muted', P.Pans[i].Slices[j].Muted);
|
||||||
|
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
|
||||||
|
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
|
||||||
|
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TSettingsManager.LoadPans(const MAC: array of Byte; const Ctx: string;
|
||||||
|
out P: TPansConfig): Boolean;
|
||||||
|
var
|
||||||
|
MacStr: string;
|
||||||
|
DevObj, PansObj, Root, PanObj, SliceObj: TJSONObject;
|
||||||
|
ListData, SlData: TJSONData;
|
||||||
|
Arr, SArr: TJSONArray;
|
||||||
|
i, id, k, n: Integer;
|
||||||
|
begin
|
||||||
|
DefaultPans(P);
|
||||||
|
MacStr := MacToStr(MAC);
|
||||||
|
Result := FRoot.Find(MacStr) <> nil;
|
||||||
|
if not Result then Exit;
|
||||||
|
DevObj := GetDevObj(MacStr);
|
||||||
|
if DevObj.Find('pans') = nil then begin Result := False; Exit; end;
|
||||||
|
PansObj := EnsureObj(DevObj, 'pans');
|
||||||
|
if PansObj.Find(Ctx) = nil then begin Result := False; Exit; end;
|
||||||
|
Root := EnsureObj(PansObj, Ctx);
|
||||||
|
P.GridMode := JB(Root, 'grid_mode', False);
|
||||||
|
P.GridRowShare := EnsureRange(JD(Root, 'grid_row_share', 0.5), 0.15, 0.85);
|
||||||
|
ListData := Root.Find('list');
|
||||||
|
if (ListData = nil) or not (ListData is TJSONArray) then Exit;
|
||||||
|
Arr := TJSONArray(ListData);
|
||||||
|
for i := 0 to Arr.Count-1 do
|
||||||
|
begin
|
||||||
|
if not (Arr.Items[i] is TJSONObject) then Continue;
|
||||||
|
PanObj := TJSONObject(Arr.Items[i]);
|
||||||
|
id := JI(PanObj, 'id', -1);
|
||||||
|
if (id < 0) or (id >= CFG_PANS_MAX) then Continue;
|
||||||
|
P.Pans[id].Active := JB(PanObj, 'active', id > 0);
|
||||||
|
P.Pans[id].FreqHz := JD(PanObj, 'freq', 0.0);
|
||||||
|
P.Pans[id].RateKHz := JI(PanObj, 'rate_khz', 384);
|
||||||
|
P.Pans[id].ADC := JI(PanObj, 'adc', 0);
|
||||||
|
P.Pans[id].Zoom := EnsureRange(JD(PanObj, 'zoom', 0.0), 0.0, 1.0);
|
||||||
|
P.Pans[id].PanSlider := EnsureRange(JD(PanObj, 'pan', 0.5), 0.0, 1.0);
|
||||||
|
P.Pans[id].StackShare := JD(PanObj, 'stack_share', 1.0);
|
||||||
|
P.Pans[id].ColShare := JD(PanObj, 'col_share', 1.0);
|
||||||
|
P.Pans[id].SplitRatio := EnsureRange(JD(PanObj, 'split_ratio', 0.40), 0.05, 0.95);
|
||||||
|
SlData := PanObj.Find('slices');
|
||||||
|
if (SlData = nil) or not (SlData is TJSONArray) then Continue;
|
||||||
|
SArr := TJSONArray(SlData);
|
||||||
|
SetLength(P.Pans[id].Slices, 0);
|
||||||
|
for k := 0 to SArr.Count-1 do
|
||||||
|
begin
|
||||||
|
if not (SArr.Items[k] is TJSONObject) then Continue;
|
||||||
|
SliceObj := TJSONObject(SArr.Items[k]);
|
||||||
|
n := Length(P.Pans[id].Slices);
|
||||||
|
SetLength(P.Pans[id].Slices, n+1);
|
||||||
|
P.Pans[id].Slices[n].TargetHz := JD(SliceObj, 'target_hz', 0.0);
|
||||||
|
P.Pans[id].Slices[n].Mode := JI(SliceObj, 'mode', 0);
|
||||||
|
P.Pans[id].Slices[n].FilterLo := JI(SliceObj, 'filter_lo', -1500);
|
||||||
|
P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500);
|
||||||
|
P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1);
|
||||||
|
P.Pans[id].Slices[n].Volume := EnsureRange(JD(SliceObj, 'volume', 0.7), 0.0, 1.0);
|
||||||
|
P.Pans[id].Slices[n].Muted := JB(SliceObj, 'muted', False);
|
||||||
|
P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False);
|
||||||
|
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
|
||||||
|
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
Reference in New Issue
Block a user