mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(slices): независимый rate панадаптеров (селектор в шапке)
- Создание пана: rate наследуется от главного с клампом ≤768к (вне сетки → 384к); за сменой rate главного паны не следуют - Клик по «NNN kHz» в шапке пана N → меню полной сетки железа (PanRateAllowed = Caps.RatePresets ∩ кратные 48 кГц; захардкоженный PAN_DDC_RATES удалён) - WDSPEngine.SetPanRate: под FSliceLock пересбор аккумулятора/анализатора пана + переоткрытие слайс-каналов на новом rate (state сохраняется) - RadioController.SetPanDDCRate: движок → реконфиг DDC (SetPanDDC) → кламп слайсов к новой полосе; FPanADC[] хранится (база под 3.4) - Шапка: rate отдельным лейблом (у панов N кликабелен, акцент/рука) Смоук-тест без железа ALL PASS (тон: спектр+аудио слайса живы через 384→192→768). Проверено юзером на экране. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+80
-6
@@ -184,6 +184,9 @@ type
|
|||||||
FPanSplitMinTop: Integer;
|
FPanSplitMinTop: Integer;
|
||||||
FPanSplitMaxTop: Integer;
|
FPanSplitMaxTop: Integer;
|
||||||
FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет)
|
FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет)
|
||||||
|
// Меню rate пана («NNN kHz» в шапке панов N; создаётся лениво).
|
||||||
|
FPanRateMenu: TPopupMenu;
|
||||||
|
FPanRateMenuPanId: Integer;
|
||||||
FSpecView: TSpectrumView;
|
FSpecView: TSpectrumView;
|
||||||
FWidebandView: TWidebandView;
|
FWidebandView: TWidebandView;
|
||||||
FUseOpenGLSpectrum: Boolean;
|
FUseOpenGLSpectrum: Boolean;
|
||||||
@@ -570,6 +573,8 @@ type
|
|||||||
procedure DestroyAllExtraPans; // stop/выход
|
procedure DestroyAllExtraPans; // stop/выход
|
||||||
procedure UpdatePanHeaders; // текст+видимость шапок
|
procedure UpdatePanHeaders; // текст+видимость шапок
|
||||||
procedure UpdateAddPanButton;
|
procedure UpdateAddPanButton;
|
||||||
|
procedure OnPanRateClick(Sender: TObject); // «NNN kHz» шапки пана N
|
||||||
|
procedure PanRateMenuItemClick(Sender: TObject);
|
||||||
procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
|
procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
|
||||||
procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N
|
procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N
|
||||||
procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
|
procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
|
||||||
@@ -2679,6 +2684,11 @@ begin
|
|||||||
begin
|
begin
|
||||||
FPans[i].HeaderPanel.Color := T.Panel;
|
FPans[i].HeaderPanel.Color := T.Panel;
|
||||||
FPans[i].HeaderLabel.Font.Color := T.Text;
|
FPans[i].HeaderLabel.Font.Color := T.Text;
|
||||||
|
// Rate-лейбл: кликабельный (паны N) подсвечиваем акцентом селектора.
|
||||||
|
if FPans[i].RateClickable then
|
||||||
|
FPans[i].HeaderRateLabel.Font.Color := T.BtnTextActive
|
||||||
|
else
|
||||||
|
FPans[i].HeaderRateLabel.Font.Color := T.Text;
|
||||||
end;
|
end;
|
||||||
if i > 0 then
|
if i > 0 then
|
||||||
begin
|
begin
|
||||||
@@ -5297,6 +5307,54 @@ begin
|
|||||||
FActiveSliceId := Id;
|
FActiveSliceId := Id;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnPanRateClick(Sender: TObject);
|
||||||
|
// Клик по «NNN kHz» в шапке пана N: меню с полной сеткой rate бэкенда
|
||||||
|
// (фильтр PanRateAllowed — кратные 48 кГц). Текущий rate отмечен галкой.
|
||||||
|
var
|
||||||
|
P: TPanafallPanel;
|
||||||
|
MI: TMenuItem;
|
||||||
|
i, kHz: Integer;
|
||||||
|
begin
|
||||||
|
if not (Sender is TPanafallPanel) then Exit;
|
||||||
|
P := TPanafallPanel(Sender);
|
||||||
|
if (P.PanId < 1) or not FController.PanDDCActive(P.PanId) then Exit;
|
||||||
|
if FPanRateMenu = nil then
|
||||||
|
FPanRateMenu := TPopupMenu.Create(Self);
|
||||||
|
FPanRateMenu.Items.Clear;
|
||||||
|
for i := 0 to High(FController.BackendCaps.RatePresets) do
|
||||||
|
begin
|
||||||
|
kHz := FController.BackendCaps.RatePresets[i] div 1000;
|
||||||
|
if not FController.PanRateAllowed(Word(kHz)) then Continue;
|
||||||
|
MI := TMenuItem.Create(FPanRateMenu);
|
||||||
|
MI.Caption := Format('%d kHz', [kHz]);
|
||||||
|
MI.Tag := kHz;
|
||||||
|
MI.Checked := kHz = FController.PanDDCRateKHz(P.PanId);
|
||||||
|
MI.OnClick := PanRateMenuItemClick;
|
||||||
|
FPanRateMenu.Items.Add(MI);
|
||||||
|
end;
|
||||||
|
if FPanRateMenu.Items.Count = 0 then Exit;
|
||||||
|
FPanRateMenuPanId := P.PanId;
|
||||||
|
FPanRateMenu.PopUp; // в позиции курсора
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.PanRateMenuItemClick(Sender: TObject);
|
||||||
|
var
|
||||||
|
P: TPanafallPanel;
|
||||||
|
PanId: Integer;
|
||||||
|
begin
|
||||||
|
PanId := FPanRateMenuPanId;
|
||||||
|
FPanRateMenuPanId := 0;
|
||||||
|
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||||
|
P := FPans[PanId];
|
||||||
|
if P = nil then Exit;
|
||||||
|
if not FController.SetPanDDCRate(PanId, Word(TMenuItem(Sender).Tag)) then Exit;
|
||||||
|
// Вьюха/шапка/флаги под новый span (слайсы могли клампнуться к краям).
|
||||||
|
SyncPanViews;
|
||||||
|
P.PushAllSliceFlagStates;
|
||||||
|
P.LayoutFlags;
|
||||||
|
MarkPanDirty(P);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.PanClickTune(P: TPanafallPanel; X, W: Integer);
|
procedure TMainForm.PanClickTune(P: TPanafallPanel; X, W: Integer);
|
||||||
// Клик по фону доп. пана = перестроить его слайс на частоту клика (аналог
|
// Клик по фону доп. пана = перестроить его слайс на частоту клика (аналог
|
||||||
// DoSpectrumClick главного: снап к 100 Гц). Цель — активный слайс, если он
|
// DoSpectrumClick главного: снап к 100 Гц). Цель — активный слайс, если он
|
||||||
@@ -5347,11 +5405,17 @@ begin
|
|||||||
P.HeaderVisible := N > 1;
|
P.HeaderVisible := N > 1;
|
||||||
if P.BtnClosePan <> nil then P.BtnClosePan.Visible := (N > 1) and (i > 0);
|
if P.BtnClosePan <> nil then P.BtnClosePan.Visible := (N > 1) and (i > 0);
|
||||||
if i = 0 then
|
if i = 0 then
|
||||||
P.SetHeaderText(Format('PAN 0 · %.6f MHz · %d kHz',
|
begin
|
||||||
[FController.FCenterFreq / 1e6, FController.FSampleRate div 1000]))
|
P.SetHeaderText(Format('PAN 0 · %.6f MHz ·',
|
||||||
|
[FController.FCenterFreq / 1e6]));
|
||||||
|
P.SetHeaderRate(Format('%d kHz', [FController.FSampleRate div 1000]));
|
||||||
|
end
|
||||||
else
|
else
|
||||||
P.SetHeaderText(Format('PAN %d · %.6f MHz · %d kHz',
|
begin
|
||||||
[i, FController.PanDDCFreq(i) / 1e6, FController.PanDDCRateKHz(i)]));
|
P.SetHeaderText(Format('PAN %d · %.6f MHz ·',
|
||||||
|
[i, FController.PanDDCFreq(i) / 1e6]));
|
||||||
|
P.SetHeaderRate(Format('%d kHz', [FController.PanDDCRateKHz(i)]));
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -5370,6 +5434,7 @@ procedure TMainForm.OnAddPanClick(Sender: TObject);
|
|||||||
// ADC 0; в центре автоматически создаётся первый слайс.
|
// ADC 0; в центре автоматически создаётся первый слайс.
|
||||||
var
|
var
|
||||||
PanId, i: Integer;
|
PanId, i: Integer;
|
||||||
|
RateKHz: Word;
|
||||||
Freq: Double;
|
Freq: Double;
|
||||||
P: TPanafallPanel;
|
P: TPanafallPanel;
|
||||||
Split: TPanel;
|
Split: TPanel;
|
||||||
@@ -5380,8 +5445,15 @@ begin
|
|||||||
if FPans[i] = nil then begin PanId := i; Break; end;
|
if FPans[i] = nil then begin PanId := i; Break; end;
|
||||||
if PanId < 0 then Exit;
|
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;
|
Freq := ActiveVfoFreq;
|
||||||
if not FController.AddPanDDC(PanId, Freq, 384, 0) then Exit;
|
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;
|
||||||
@@ -5392,6 +5464,8 @@ begin
|
|||||||
P.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
P.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||||
P.OnPanClose := OnPanHeaderClose;
|
P.OnPanClose := OnPanHeaderClose;
|
||||||
P.OnSplitterMoved := RightPanelResize; // ratio обновлён → полный re-layout
|
P.OnSplitterMoved := RightPanelResize; // ratio обновлён → полный re-layout
|
||||||
|
P.OnRateClick := OnPanRateClick; // «NNN kHz» в шапке → меню rate
|
||||||
|
P.RateClickable := True;
|
||||||
P.SpectrumMouseDown := PanNSpectrumMouseDown;
|
P.SpectrumMouseDown := PanNSpectrumMouseDown;
|
||||||
P.SpectrumMouseMove := PanNSpectrumMouseMove;
|
P.SpectrumMouseMove := PanNSpectrumMouseMove;
|
||||||
P.SpectrumMouseUp := PanNSpectrumMouseUp;
|
P.SpectrumMouseUp := PanNSpectrumMouseUp;
|
||||||
@@ -5416,7 +5490,7 @@ begin
|
|||||||
P.View.WfFrameInterval := EffectiveWfInterval;
|
P.View.WfFrameInterval := EffectiveWfInterval;
|
||||||
P.View.PAMaxPower := FController.FPAMaxPower;
|
P.View.PAMaxPower := FController.FPAMaxPower;
|
||||||
P.View.CenterFreq := Freq;
|
P.View.CenterFreq := Freq;
|
||||||
P.View.SpanHz := 384000;
|
P.View.SpanHz := FController.PanDDCRateKHz(PanId) * 1000.0;
|
||||||
// VFO/TX-маркеры главного на доп. пане не рисуем — уводим за пределы окна.
|
// VFO/TX-маркеры главного на доп. пане не рисуем — уводим за пределы окна.
|
||||||
P.View.VfoA := -1e15;
|
P.View.VfoA := -1e15;
|
||||||
P.View.VfoB := -1e15;
|
P.View.VfoB := -1e15;
|
||||||
|
|||||||
+62
-2
@@ -95,6 +95,9 @@ type
|
|||||||
FStackHeight: Integer; // высота региона (0 = до низа родителя)
|
FStackHeight: Integer; // высота региона (0 = до низа родителя)
|
||||||
FHeaderPanel: TPanel; // шапка пана (видна при панах >= 2)
|
FHeaderPanel: TPanel; // шапка пана (видна при панах >= 2)
|
||||||
FHeaderLabel: TLabel;
|
FHeaderLabel: TLabel;
|
||||||
|
FHeaderRateLabel: TLabel; // «NNN kHz»; у панов N>0 кликабелен (селектор rate)
|
||||||
|
FRateClickable: Boolean;
|
||||||
|
FOnRateClick: TNotifyEvent;
|
||||||
FBtnClosePan: TFlatButton; // «×» в шапке (только паны N>0)
|
FBtnClosePan: TFlatButton; // «×» в шапке (только паны N>0)
|
||||||
FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0)
|
FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0)
|
||||||
FHeaderVisible: Boolean;
|
FHeaderVisible: Boolean;
|
||||||
@@ -105,6 +108,8 @@ type
|
|||||||
|
|
||||||
procedure BtnClosePanClick(Sender: TObject);
|
procedure BtnClosePanClick(Sender: TObject);
|
||||||
procedure BtnAddPanClick(Sender: TObject);
|
procedure BtnAddPanClick(Sender: TObject);
|
||||||
|
procedure HeaderRateClick(Sender: TObject);
|
||||||
|
procedure SetRateClickable(AValue: Boolean);
|
||||||
procedure PositionHeaderChildren(RW, H: Integer);
|
procedure PositionHeaderChildren(RW, H: Integer);
|
||||||
// Видимое частотное окно ЭТОГО пана: пан 0 — окно главного (зум/пан),
|
// Видимое частотное окно ЭТОГО пана: пан 0 — окно главного (зум/пан),
|
||||||
// паны N — центр их DDC ± rate/2 (пан-зум — этап 3.3+).
|
// паны N — центр их DDC ± rate/2 (пан-зум — этап 3.3+).
|
||||||
@@ -169,11 +174,17 @@ type
|
|||||||
property PanId: Integer read FPanId write FPanId;
|
property PanId: Integer read FPanId write FPanId;
|
||||||
property StackTop: Integer read FStackTop write FStackTop;
|
property StackTop: Integer read FStackTop write FStackTop;
|
||||||
property StackHeight: Integer read FStackHeight write FStackHeight;
|
property StackHeight: Integer read FStackHeight write FStackHeight;
|
||||||
// Шапка пана: текст ставит хозяин (частота/rate), «×» → OnPanClose.
|
// Шапка пана: текст ставит хозяин (частота), rate — отдельным лейблом
|
||||||
|
// (SetHeaderRate); у панов N>0 хозяин делает его кликабельным (селектор
|
||||||
|
// rate, RateClickable + OnRateClick). «×» → OnPanClose.
|
||||||
property HeaderVisible: Boolean read FHeaderVisible write FHeaderVisible;
|
property HeaderVisible: Boolean read FHeaderVisible write FHeaderVisible;
|
||||||
procedure SetHeaderText(const S: string);
|
procedure SetHeaderText(const S: string);
|
||||||
|
procedure SetHeaderRate(const S: string);
|
||||||
|
property RateClickable: Boolean read FRateClickable write SetRateClickable;
|
||||||
|
property OnRateClick: TNotifyEvent read FOnRateClick write FOnRateClick;
|
||||||
property HeaderPanel: TPanel read FHeaderPanel;
|
property HeaderPanel: TPanel read FHeaderPanel;
|
||||||
property HeaderLabel: TLabel read FHeaderLabel;
|
property HeaderLabel: TLabel read FHeaderLabel;
|
||||||
|
property HeaderRateLabel: TLabel read FHeaderRateLabel;
|
||||||
property BtnClosePan: TFlatButton read FBtnClosePan;
|
property BtnClosePan: TFlatButton read FBtnClosePan;
|
||||||
property BtnAddPan: TFlatButton read FBtnAddPan;
|
property BtnAddPan: TFlatButton read FBtnAddPan;
|
||||||
// Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+).
|
// Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+).
|
||||||
@@ -210,6 +221,7 @@ type
|
|||||||
// Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+.
|
// Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+.
|
||||||
procedure UpdateMainFlagVisibility;
|
procedure UpdateMainFlagVisibility;
|
||||||
procedure PushSliceFlagState(SliceId: Integer);
|
procedure PushSliceFlagState(SliceId: Integer);
|
||||||
|
procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.)
|
||||||
procedure PushMainFlagExtState;
|
procedure PushMainFlagExtState;
|
||||||
procedure PushMainFlagAudioDevices;
|
procedure PushMainFlagAudioDevices;
|
||||||
// Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов.
|
// Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов.
|
||||||
@@ -362,6 +374,12 @@ begin
|
|||||||
FHeaderLabel.Parent := FHeaderPanel;
|
FHeaderLabel.Parent := FHeaderPanel;
|
||||||
FHeaderLabel.AutoSize := True;
|
FHeaderLabel.AutoSize := True;
|
||||||
FHeaderLabel.Left := 8;
|
FHeaderLabel.Left := 8;
|
||||||
|
// Rate — отдельным лейблом за основным текстом: у панов N>0 он кликабелен
|
||||||
|
// (селектор rate DDC), у пана 0 — просто текст.
|
||||||
|
FHeaderRateLabel := TLabel.Create(Self);
|
||||||
|
FHeaderRateLabel.Parent := FHeaderPanel;
|
||||||
|
FHeaderRateLabel.AutoSize := True;
|
||||||
|
FHeaderRateLabel.OnClick := HeaderRateClick;
|
||||||
FBtnClosePan := MakeFlatBtn(FHeaderPanel, '×', 0, 0, 10, 10, BtnClosePanClick);
|
FBtnClosePan := MakeFlatBtn(FHeaderPanel, '×', 0, 0, 10, 10, BtnClosePanClick);
|
||||||
FBtnClosePan.Visible := False; // хозяин включает у панов N>0
|
FBtnClosePan.Visible := False; // хозяин включает у панов N>0
|
||||||
end;
|
end;
|
||||||
@@ -378,7 +396,37 @@ end;
|
|||||||
|
|
||||||
procedure TPanafallPanel.SetHeaderText(const S: string);
|
procedure TPanafallPanel.SetHeaderText(const S: string);
|
||||||
begin
|
begin
|
||||||
if FHeaderLabel <> nil then FHeaderLabel.Caption := S;
|
if FHeaderLabel = nil then Exit;
|
||||||
|
if FHeaderLabel.Caption = S then Exit;
|
||||||
|
FHeaderLabel.Caption := S;
|
||||||
|
// Rate-лейбл прижат к основному тексту — сдвинуть за новой шириной.
|
||||||
|
if FHeaderRateLabel <> nil then
|
||||||
|
FHeaderRateLabel.Left := FHeaderLabel.Left + FHeaderLabel.Width + 6;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanafallPanel.SetHeaderRate(const S: string);
|
||||||
|
begin
|
||||||
|
if FHeaderRateLabel = nil then Exit;
|
||||||
|
if FHeaderRateLabel.Caption <> S then FHeaderRateLabel.Caption := S;
|
||||||
|
if FHeaderLabel <> nil then
|
||||||
|
FHeaderRateLabel.Left := FHeaderLabel.Left + FHeaderLabel.Width + 6;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanafallPanel.SetRateClickable(AValue: Boolean);
|
||||||
|
begin
|
||||||
|
FRateClickable := AValue;
|
||||||
|
if FHeaderRateLabel <> nil then
|
||||||
|
begin
|
||||||
|
if AValue then
|
||||||
|
FHeaderRateLabel.Cursor := crHandPoint
|
||||||
|
else
|
||||||
|
FHeaderRateLabel.Cursor := crArrow;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanafallPanel.HeaderRateClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if FRateClickable and Assigned(FOnRateClick) then FOnRateClick(Self);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPanafallPanel.ViewWindow(out VC, VS: Double);
|
procedure TPanafallPanel.ViewWindow(out VC, VS: Double);
|
||||||
@@ -399,6 +447,11 @@ var BtnS: Integer;
|
|||||||
begin
|
begin
|
||||||
if FHeaderLabel <> nil then
|
if FHeaderLabel <> nil then
|
||||||
FHeaderLabel.Top := (H - FHeaderLabel.Height) div 2;
|
FHeaderLabel.Top := (H - FHeaderLabel.Height) div 2;
|
||||||
|
if (FHeaderRateLabel <> nil) and (FHeaderLabel <> nil) then
|
||||||
|
begin
|
||||||
|
FHeaderRateLabel.Top := (H - FHeaderRateLabel.Height) div 2;
|
||||||
|
FHeaderRateLabel.Left := FHeaderLabel.Left + FHeaderLabel.Width + 6;
|
||||||
|
end;
|
||||||
if FBtnClosePan <> nil then
|
if FBtnClosePan <> nil then
|
||||||
begin
|
begin
|
||||||
BtnS := H - 4;
|
BtnS := H - 4;
|
||||||
@@ -1025,6 +1078,13 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TPanafallPanel.PushAllSliceFlagStates;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
for i := 0 to High(FSliceFlags) do
|
||||||
|
if FSliceFlags[i] <> nil then PushSliceFlagState(FSliceFlags[i].SliceId);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TPanafallPanel.PushMainFlagExtState;
|
procedure TPanafallPanel.PushMainFlagExtState;
|
||||||
begin
|
begin
|
||||||
if not (Assigned(FMainFlag) and FMainFlag.Visible) or (FController = nil) then Exit;
|
if not (Assigned(FMainFlag) and FMainFlag.Visible) or (FController = nil) then Exit;
|
||||||
|
|||||||
+54
-9
@@ -52,11 +52,10 @@ uses
|
|||||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
|
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
|
||||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||||
|
|
||||||
const
|
|
||||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||||||
// Допустимые rate доп. пан-DDC, кГц (дефолт 384).
|
// Допустимые rate пан-DDC — полная сетка железа Caps.RatePresets (кратные
|
||||||
PAN_DDC_RATES: array[0..2] of Word = (192, 384, 768);
|
// 48 кГц — требование движка), см. PanRateAllowed. Дефолт/фолбэк — 384 кГц.
|
||||||
|
|
||||||
type
|
type
|
||||||
// QO-100 beacon lock — статус контура (для UI/маркеров).
|
// QO-100 beacon lock — статус контура (для UI/маркеров).
|
||||||
@@ -353,6 +352,7 @@ type
|
|||||||
FPanDDCIdx: array[1..MAX_PANS-1] of Integer;
|
FPanDDCIdx: array[1..MAX_PANS-1] of Integer;
|
||||||
FPanFreqHz: array[1..MAX_PANS-1] of Double;
|
FPanFreqHz: array[1..MAX_PANS-1] of Double;
|
||||||
FPanRateKHz: array[1..MAX_PANS-1] of Word;
|
FPanRateKHz: array[1..MAX_PANS-1] of Word;
|
||||||
|
FPanADC: array[1..MAX_PANS-1] of Byte; // ADC-источник пана (3.4)
|
||||||
FDDCPanMap: array[0..MAX_DDCS-1] of Integer;
|
FDDCPanMap: array[0..MAX_DDCS-1] of Integer;
|
||||||
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
|
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
|
||||||
FLastDDCIndex: Integer; // последний DDC index
|
FLastDDCIndex: Integer; // последний DDC index
|
||||||
@@ -562,12 +562,17 @@ type
|
|||||||
|
|
||||||
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
|
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
|
||||||
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр (сырые Гц
|
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр (сырые Гц
|
||||||
// железа, XVTR-трансляции у панов нет), RateKHz ∈ PAN_DDC_RATES (иначе 384),
|
// железа, XVTR-трансляции у панов нет), RateKHz ∈ PanRateAllowed (иначе 384),
|
||||||
// ADCSource — АЦП платы. False = нет слота/бэкенд не умеет/не Running.
|
// ADCSource — АЦП платы. False = нет слота/бэкенд не умеет/не Running.
|
||||||
function AddPanDDC(PanId: Integer; FreqHz: Double; RateKHz: Word = 384;
|
function AddPanDDC(PanId: Integer; FreqHz: Double; RateKHz: Word = 384;
|
||||||
ADCSource: Byte = 0): Boolean;
|
ADCSource: Byte = 0): Boolean;
|
||||||
procedure RemovePanDDC(PanId: Integer);
|
procedure RemovePanDDC(PanId: Integer);
|
||||||
procedure SetPanDDCFreq(PanId: Integer; FreqHz: Double); // ретюн (HP-пакет)
|
procedure SetPanDDCFreq(PanId: Integer; FreqHz: Double); // ретюн (HP-пакет)
|
||||||
|
// Rate ∈ сетке бэкенда (Caps.RatePresets) и кратен 48 кГц (движок).
|
||||||
|
function PanRateAllowed(RateKHz: Word): Boolean;
|
||||||
|
// Смена rate живого пана: движок переоткрывает аккумулятор/анализатор/слайсы,
|
||||||
|
// DDC реконфигурируется; слайсы вне новой полосы клампятся к её краям.
|
||||||
|
function SetPanDDCRate(PanId: Integer; RateKHz: Word): Boolean;
|
||||||
function PanDDCActive(PanId: Integer): Boolean;
|
function PanDDCActive(PanId: Integer): Boolean;
|
||||||
function PanDDCFreq(PanId: Integer): Double;
|
function PanDDCFreq(PanId: Integer): Double;
|
||||||
function PanDDCRateKHz(PanId: Integer): Word;
|
function PanDDCRateKHz(PanId: Integer): Word;
|
||||||
@@ -1178,16 +1183,28 @@ begin
|
|||||||
FPanDDCIdx[i] := -1;
|
FPanDDCIdx[i] := -1;
|
||||||
FPanFreqHz[i] := 0;
|
FPanFreqHz[i] := 0;
|
||||||
FPanRateKHz[i] := 0;
|
FPanRateKHz[i] := 0;
|
||||||
|
FPanADC[i] := 0;
|
||||||
end;
|
end;
|
||||||
for i := 0 to MAX_DDCS - 1 do
|
for i := 0 to MAX_DDCS - 1 do
|
||||||
FDDCPanMap[i] := -1;
|
FDDCPanMap[i] := -1;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TRadioController.PanRateAllowed(RateKHz: Word): Boolean;
|
||||||
|
// Полная сетка железа (Caps.RatePresets) минус некратные 48 кГц: движок
|
||||||
|
// требует целого отношения rate пана к аудио-rate (блок/WDSP-каналы).
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if not Assigned(FNetwork) then Exit;
|
||||||
|
if (RateKHz = 0) or ((Integer(RateKHz) * 1000) mod 48000 <> 0) then Exit;
|
||||||
|
for i := 0 to High(FNetwork.Caps.RatePresets) do
|
||||||
|
if FNetwork.Caps.RatePresets[i] = Integer(RateKHz) * 1000 then Exit(True);
|
||||||
|
end;
|
||||||
|
|
||||||
function TRadioController.AddPanDDC(PanId: Integer; FreqHz: Double;
|
function TRadioController.AddPanDDC(PanId: Integer; FreqHz: Double;
|
||||||
RateKHz: Word; ADCSource: Byte): Boolean;
|
RateKHz: Word; ADCSource: Byte): Boolean;
|
||||||
var
|
var
|
||||||
i, ddc: Integer;
|
i, ddc: Integer;
|
||||||
RateOk: Boolean;
|
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||||
@@ -1195,10 +1212,7 @@ begin
|
|||||||
if not (Assigned(FNetwork) and FRunning) then Exit;
|
if not (Assigned(FNetwork) and FRunning) then Exit;
|
||||||
if PanId >= FNetwork.Caps.MaxPans then Exit; // бэкенд не умеет (Pluto=1)
|
if PanId >= FNetwork.Caps.MaxPans then Exit; // бэкенд не умеет (Pluto=1)
|
||||||
|
|
||||||
RateOk := False;
|
if not PanRateAllowed(RateKHz) then RateKHz := 384;
|
||||||
for i := Low(PAN_DDC_RATES) to High(PAN_DDC_RATES) do
|
|
||||||
if PAN_DDC_RATES[i] = RateKHz then begin RateOk := True; Break; end;
|
|
||||||
if not RateOk then RateKHz := 384;
|
|
||||||
|
|
||||||
// hw-индекс: следующий свободный после главного DDC (слот-стабильности не
|
// hw-индекс: следующий свободный после главного DDC (слот-стабильности не
|
||||||
// нужно — hw-индекс скрыт за PanId, наружу торчит только PanId).
|
// нужно — hw-индекс скрыт за PanId, наружу торчит только PanId).
|
||||||
@@ -1215,11 +1229,41 @@ begin
|
|||||||
FPanDDCIdx[PanId] := ddc;
|
FPanDDCIdx[PanId] := ddc;
|
||||||
FPanFreqHz[PanId] := FreqHz;
|
FPanFreqHz[PanId] := FreqHz;
|
||||||
FPanRateKHz[PanId] := RateKHz;
|
FPanRateKHz[PanId] := RateKHz;
|
||||||
|
FPanADC[PanId] := ADCSource;
|
||||||
FDDCPanMap[ddc] := PanId;
|
FDDCPanMap[ddc] := PanId;
|
||||||
FNetwork.SetPanDDC(ddc, True, FreqHz, RateKHz, ADCSource);
|
FNetwork.SetPanDDC(ddc, True, FreqHz, RateKHz, ADCSource);
|
||||||
Result := True;
|
Result := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TRadioController.SetPanDDCRate(PanId: Integer; RateKHz: Word): Boolean;
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
LoHz, HiHz: Double;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if not PanDDCActive(PanId) then Exit;
|
||||||
|
if not PanRateAllowed(RateKHz) then Exit;
|
||||||
|
if FPanRateKHz[PanId] = RateKHz then Exit(True);
|
||||||
|
// Движок первым: под FSliceLock пересобирает аккумулятор/анализатор и
|
||||||
|
// переоткрывает слайс-каналы; провалился — состояние не трогаем.
|
||||||
|
if FWDSPReady and not FDSPEngine.SetPanRate(PanId, Integer(RateKHz) * 1000) then
|
||||||
|
Exit;
|
||||||
|
FPanRateKHz[PanId] := RateKHz;
|
||||||
|
// DDC железа: реконфиг rate (RebuildDDCSpecific + freq-слово).
|
||||||
|
if Assigned(FNetwork) then
|
||||||
|
FNetwork.SetPanDDC(FPanDDCIdx[PanId], True, FPanFreqHz[PanId], RateKHz,
|
||||||
|
FPanADC[PanId]);
|
||||||
|
// Слайсы, выпавшие из сузившейся полосы, — к её краям (абс. TargetHz + NCO).
|
||||||
|
CapturedSpan(LoHz, HiHz, PanId);
|
||||||
|
for i := 0 to MAX_SLICES - 1 do
|
||||||
|
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||||||
|
begin
|
||||||
|
if FSlices[i].TargetHz < LoHz then SetSliceTarget(FSlices[i].Id, LoHz);
|
||||||
|
if FSlices[i].TargetHz > HiHz then SetSliceTarget(FSlices[i].Id, HiHz);
|
||||||
|
end;
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TRadioController.RemovePanDDC(PanId: Integer);
|
procedure TRadioController.RemovePanDDC(PanId: Integer);
|
||||||
var ddc, i: Integer;
|
var ddc, i: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -1232,6 +1276,7 @@ begin
|
|||||||
FPanDDCIdx[PanId] := -1;
|
FPanDDCIdx[PanId] := -1;
|
||||||
FPanFreqHz[PanId] := 0;
|
FPanFreqHz[PanId] := 0;
|
||||||
FPanRateKHz[PanId] := 0;
|
FPanRateKHz[PanId] := 0;
|
||||||
|
FPanADC[PanId] := 0;
|
||||||
for i := 0 to MAX_SLICES - 1 do
|
for i := 0 to MAX_SLICES - 1 do
|
||||||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||||||
RemoveSlice(FSlices[i].Id);
|
RemoveSlice(FSlices[i].Id);
|
||||||
|
|||||||
@@ -509,6 +509,9 @@ type
|
|||||||
function AddPan(PanId, RateHz: Integer): Boolean;
|
function AddPan(PanId, RateHz: Integer): Boolean;
|
||||||
// Убирает пан вместе с его слайсами (аудио-выходы слайсов — забота вызывающего).
|
// Убирает пан вместе с его слайсами (аудио-выходы слайсов — забота вызывающего).
|
||||||
procedure RemovePan(PanId: Integer);
|
procedure RemovePan(PanId: Integer);
|
||||||
|
// Смена rate живого пана: пересбор аккумулятора/анализатора + переоткрытие
|
||||||
|
// WDSP-каналов его слайсов на новом входном rate (state слайсов сохраняется).
|
||||||
|
function SetPanRate(PanId, RateHz: Integer): Boolean;
|
||||||
function PanActive(PanId: Integer): Boolean;
|
function PanActive(PanId: Integer): Boolean;
|
||||||
|
|
||||||
// --- Мультислайсы (софт-fan-out) ---
|
// --- Мультислайсы (софт-fan-out) ---
|
||||||
@@ -2220,6 +2223,41 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TWDSPEngine.SetPanRate(PanId, RateHz: Integer): Boolean;
|
||||||
|
var s: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||||
|
// Те же требования кратности, что в AddPan.
|
||||||
|
if (RateHz < FAudioRate) or (RateHz mod FAudioRate <> 0) then Exit;
|
||||||
|
FSliceLock.Enter;
|
||||||
|
try
|
||||||
|
if not FPans[PanId].Active then Exit;
|
||||||
|
if FPans[PanId].Rate = RateHz then Exit(True);
|
||||||
|
// Анализатор держит старый rate — снести до пересбора аккумулятора.
|
||||||
|
DestroyPanAnalyzer(PanId);
|
||||||
|
FPans[PanId].Rate := RateHz;
|
||||||
|
FPans[PanId].BufSize := FAudioBufSize * (RateHz div FAudioRate);
|
||||||
|
SetLength(FPans[PanId].AccI, FPans[PanId].BufSize);
|
||||||
|
SetLength(FPans[PanId].AccQ, FPans[PanId].BufSize);
|
||||||
|
FPans[PanId].AccPos := 0;
|
||||||
|
FPans[PanId].DispPos := 0;
|
||||||
|
AllocSliceBuffers; // FSliceIn/Out под новый макс. блок
|
||||||
|
if FAnalyzerOpen then CreatePanAnalyzer(PanId);
|
||||||
|
// Слайсы пана: их каналы открыты с InRate/InSize пана — переоткрываем
|
||||||
|
// (OpenSliceChannel восстанавливает mode/filter/AGC/DSP из записи слайса).
|
||||||
|
for s := 0 to MAX_SLICES - 1 do
|
||||||
|
if FSlices[s].Active and (FSlices[s].PanId = PanId) then
|
||||||
|
begin
|
||||||
|
CloseSliceChannel(s);
|
||||||
|
OpenSliceChannel(s);
|
||||||
|
end;
|
||||||
|
Result := True;
|
||||||
|
finally
|
||||||
|
FSliceLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function TWDSPEngine.PanActive(PanId: Integer): Boolean;
|
function TWDSPEngine.PanActive(PanId: Integer): Boolean;
|
||||||
begin
|
begin
|
||||||
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;
|
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;
|
||||||
|
|||||||
+28
-4
@@ -413,10 +413,34 @@ ChangeSampleRate).
|
|||||||
не `crDefault`. ⚠Урок на 3.5 (pop-out) и вообще: любой динамически
|
не `crDefault`. ⚠Урок на 3.5 (pop-out) и вообще: любой динамически
|
||||||
создаваемый нативный GL-виджет должен получать явный курсор при создании.
|
создаваемый нативный GL-виджет должен получать явный курсор при создании.
|
||||||
|
|
||||||
Статус: собирается (GUI+демон), НА ЭКРАНЕ НЕ ПРОВЕРЕНО (юзер проверяет).
|
Статус: все фиксы подтверждены юзером на экране, закоммичено 95b4f74
|
||||||
Открыто из фидбека: юзер видел курсор-«палец» на водопаде пана N — в коде
|
(+ dae0c19 банд-столл отдельно).
|
||||||
панов N `crHandPoint` нигде не ставится (подозрение: артефакт сломанной
|
|
||||||
раскладки, перекрытие чужим контролом); перепроверить после фиксов.
|
**RATE ПАНОВ (реализовано 2026-07-11, после коммита 3.3):**
|
||||||
|
|
||||||
|
Решение (обсуждено с юзером): (1) при создании пан наследует rate главного
|
||||||
|
с клампом ≤768к (48/96/192к как есть; вне сетки → 384к); (2) rate пана
|
||||||
|
независим и меняется кликом по «NNN kHz» в шапке пана → меню полной сетки
|
||||||
|
железа; (3) за сменой rate главного НЕ следует (издержки Ethernet/CPU, churn).
|
||||||
|
|
||||||
|
- `WDSPEngine.SetPanRate(PanId, RateHz)`: под FSliceLock — DestroyPanAnalyzer →
|
||||||
|
пересбор аккумулятора (BufSize/AccI/AccQ) → CreatePanAnalyzer → переоткрытие
|
||||||
|
слайс-каналов пана (Close+OpenSliceChannel, state слайсов сохраняется).
|
||||||
|
- `RadioController`: PAN_DDC_RATES (192/384/768) УДАЛЁН — валидация
|
||||||
|
`PanRateAllowed` = Caps.RatePresets ∩ кратные 48 кГц (полная сетка HPSDR
|
||||||
|
48-1536к). `SetPanDDCRate`: движок → FPanRateKHz → `SetPanDDC` (реконфиг
|
||||||
|
DDC тем же RebuildDDCSpecific+SendFullHP) → кламп слайсов к новой полосе.
|
||||||
|
`FPanADC[]` — ADC пана теперь хранится (нужен реконфигу; база под 3.4).
|
||||||
|
- UI: rate в шапке — отдельный TLabel (`SetHeaderRate`, у панов N кликабелен,
|
||||||
|
акцентный цвет, crHandPoint), `OnPanRateClick` → TPopupMenu из RatePresets
|
||||||
|
(текущий отмечен) → `PanRateMenuItemClick` → SetPanDDCRate + SyncPanViews +
|
||||||
|
PushAllSliceFlagStates + LayoutFlags. Пан 0 показывает rate некликабельно
|
||||||
|
(его rate — существующий SampleRateOverlay).
|
||||||
|
- ★СМОУК-ТЕСТ БЕЗ ЖЕЛЕЗА ALL PASS (scratchpad panratetest.pas, паттерн 3.2):
|
||||||
|
синтетический тон +10.8кГц → пан 384к: пик спектра на месте, слайс 800Гц
|
||||||
|
RMS 0.33 → SetPanRate 192к → пик/аудио живы → 768к → живы → RemovePan чисто.
|
||||||
|
НА ЭКРАНЕ НЕ ПРОВЕРЕНО, НЕ закоммичено. Смена rate пана на эфире = короткий
|
||||||
|
разрыв IQ этого пана (реконфиг DDC) — ожидаемо.
|
||||||
|
|
||||||
### 3.4 — ADC-селектор панадаптера (+ антенны Alex, уточнено 2026-07-11)
|
### 3.4 — ADC-селектор панадаптера (+ антенны Alex, уточнено 2026-07-11)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user