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;
|
||||
FPanSplitMaxTop: Integer;
|
||||
FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет)
|
||||
// Меню rate пана («NNN kHz» в шапке панов N; создаётся лениво).
|
||||
FPanRateMenu: TPopupMenu;
|
||||
FPanRateMenuPanId: Integer;
|
||||
FSpecView: TSpectrumView;
|
||||
FWidebandView: TWidebandView;
|
||||
FUseOpenGLSpectrum: Boolean;
|
||||
@@ -570,6 +573,8 @@ type
|
||||
procedure DestroyAllExtraPans; // stop/выход
|
||||
procedure UpdatePanHeaders; // текст+видимость шапок
|
||||
procedure UpdateAddPanButton;
|
||||
procedure OnPanRateClick(Sender: TObject); // «NNN kHz» шапки пана N
|
||||
procedure PanRateMenuItemClick(Sender: TObject);
|
||||
procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
|
||||
procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N
|
||||
procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
|
||||
@@ -2679,6 +2684,11 @@ begin
|
||||
begin
|
||||
FPans[i].HeaderPanel.Color := T.Panel;
|
||||
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;
|
||||
if i > 0 then
|
||||
begin
|
||||
@@ -5297,6 +5307,54 @@ begin
|
||||
FActiveSliceId := Id;
|
||||
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);
|
||||
// Клик по фону доп. пана = перестроить его слайс на частоту клика (аналог
|
||||
// DoSpectrumClick главного: снап к 100 Гц). Цель — активный слайс, если он
|
||||
@@ -5347,11 +5405,17 @@ begin
|
||||
P.HeaderVisible := N > 1;
|
||||
if P.BtnClosePan <> nil then P.BtnClosePan.Visible := (N > 1) and (i > 0);
|
||||
if i = 0 then
|
||||
P.SetHeaderText(Format('PAN 0 · %.6f MHz · %d kHz',
|
||||
[FController.FCenterFreq / 1e6, FController.FSampleRate div 1000]))
|
||||
begin
|
||||
P.SetHeaderText(Format('PAN 0 · %.6f MHz ·',
|
||||
[FController.FCenterFreq / 1e6]));
|
||||
P.SetHeaderRate(Format('%d kHz', [FController.FSampleRate div 1000]));
|
||||
end
|
||||
else
|
||||
P.SetHeaderText(Format('PAN %d · %.6f MHz · %d kHz',
|
||||
[i, FController.PanDDCFreq(i) / 1e6, FController.PanDDCRateKHz(i)]));
|
||||
begin
|
||||
P.SetHeaderText(Format('PAN %d · %.6f MHz ·',
|
||||
[i, FController.PanDDCFreq(i) / 1e6]));
|
||||
P.SetHeaderRate(Format('%d kHz', [FController.PanDDCRateKHz(i)]));
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -5370,6 +5434,7 @@ procedure TMainForm.OnAddPanClick(Sender: TObject);
|
||||
// ADC 0; в центре автоматически создаётся первый слайс.
|
||||
var
|
||||
PanId, i: Integer;
|
||||
RateKHz: Word;
|
||||
Freq: Double;
|
||||
P: TPanafallPanel;
|
||||
Split: TPanel;
|
||||
@@ -5380,8 +5445,15 @@ begin
|
||||
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, 384, 0) then Exit;
|
||||
if not FController.AddPanDDC(PanId, Freq, RateKHz, 0) then Exit;
|
||||
|
||||
P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
||||
P.PanId := PanId;
|
||||
@@ -5392,6 +5464,8 @@ begin
|
||||
P.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||
P.OnPanClose := OnPanHeaderClose;
|
||||
P.OnSplitterMoved := RightPanelResize; // ratio обновлён → полный re-layout
|
||||
P.OnRateClick := OnPanRateClick; // «NNN kHz» в шапке → меню rate
|
||||
P.RateClickable := True;
|
||||
P.SpectrumMouseDown := PanNSpectrumMouseDown;
|
||||
P.SpectrumMouseMove := PanNSpectrumMouseMove;
|
||||
P.SpectrumMouseUp := PanNSpectrumMouseUp;
|
||||
@@ -5416,7 +5490,7 @@ begin
|
||||
P.View.WfFrameInterval := EffectiveWfInterval;
|
||||
P.View.PAMaxPower := FController.FPAMaxPower;
|
||||
P.View.CenterFreq := Freq;
|
||||
P.View.SpanHz := 384000;
|
||||
P.View.SpanHz := FController.PanDDCRateKHz(PanId) * 1000.0;
|
||||
// VFO/TX-маркеры главного на доп. пане не рисуем — уводим за пределы окна.
|
||||
P.View.VfoA := -1e15;
|
||||
P.View.VfoB := -1e15;
|
||||
|
||||
+62
-2
@@ -95,6 +95,9 @@ type
|
||||
FStackHeight: Integer; // высота региона (0 = до низа родителя)
|
||||
FHeaderPanel: TPanel; // шапка пана (видна при панах >= 2)
|
||||
FHeaderLabel: TLabel;
|
||||
FHeaderRateLabel: TLabel; // «NNN kHz»; у панов N>0 кликабелен (селектор rate)
|
||||
FRateClickable: Boolean;
|
||||
FOnRateClick: TNotifyEvent;
|
||||
FBtnClosePan: TFlatButton; // «×» в шапке (только паны N>0)
|
||||
FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0)
|
||||
FHeaderVisible: Boolean;
|
||||
@@ -105,6 +108,8 @@ type
|
||||
|
||||
procedure BtnClosePanClick(Sender: TObject);
|
||||
procedure BtnAddPanClick(Sender: TObject);
|
||||
procedure HeaderRateClick(Sender: TObject);
|
||||
procedure SetRateClickable(AValue: Boolean);
|
||||
procedure PositionHeaderChildren(RW, H: Integer);
|
||||
// Видимое частотное окно ЭТОГО пана: пан 0 — окно главного (зум/пан),
|
||||
// паны N — центр их DDC ± rate/2 (пан-зум — этап 3.3+).
|
||||
@@ -169,11 +174,17 @@ type
|
||||
property PanId: Integer read FPanId write FPanId;
|
||||
property StackTop: Integer read FStackTop write FStackTop;
|
||||
property StackHeight: Integer read FStackHeight write FStackHeight;
|
||||
// Шапка пана: текст ставит хозяин (частота/rate), «×» → OnPanClose.
|
||||
// Шапка пана: текст ставит хозяин (частота), rate — отдельным лейблом
|
||||
// (SetHeaderRate); у панов N>0 хозяин делает его кликабельным (селектор
|
||||
// rate, RateClickable + OnRateClick). «×» → OnPanClose.
|
||||
property HeaderVisible: Boolean read FHeaderVisible write FHeaderVisible;
|
||||
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 HeaderLabel: TLabel read FHeaderLabel;
|
||||
property HeaderRateLabel: TLabel read FHeaderRateLabel;
|
||||
property BtnClosePan: TFlatButton read FBtnClosePan;
|
||||
property BtnAddPan: TFlatButton read FBtnAddPan;
|
||||
// Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+).
|
||||
@@ -210,6 +221,7 @@ type
|
||||
// Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+.
|
||||
procedure UpdateMainFlagVisibility;
|
||||
procedure PushSliceFlagState(SliceId: Integer);
|
||||
procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.)
|
||||
procedure PushMainFlagExtState;
|
||||
procedure PushMainFlagAudioDevices;
|
||||
// Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов.
|
||||
@@ -362,6 +374,12 @@ begin
|
||||
FHeaderLabel.Parent := FHeaderPanel;
|
||||
FHeaderLabel.AutoSize := True;
|
||||
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.Visible := False; // хозяин включает у панов N>0
|
||||
end;
|
||||
@@ -378,7 +396,37 @@ end;
|
||||
|
||||
procedure TPanafallPanel.SetHeaderText(const S: string);
|
||||
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;
|
||||
|
||||
procedure TPanafallPanel.ViewWindow(out VC, VS: Double);
|
||||
@@ -399,6 +447,11 @@ var BtnS: Integer;
|
||||
begin
|
||||
if FHeaderLabel <> nil then
|
||||
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
|
||||
begin
|
||||
BtnS := H - 4;
|
||||
@@ -1025,6 +1078,13 @@ begin
|
||||
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;
|
||||
begin
|
||||
if not (Assigned(FMainFlag) and FMainFlag.Visible) or (FController = nil) then Exit;
|
||||
|
||||
+54
-9
@@ -52,11 +52,10 @@ uses
|
||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
|
||||
const
|
||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||||
// Допустимые rate доп. пан-DDC, кГц (дефолт 384).
|
||||
PAN_DDC_RATES: array[0..2] of Word = (192, 384, 768);
|
||||
// Допустимые rate пан-DDC — полная сетка железа Caps.RatePresets (кратные
|
||||
// 48 кГц — требование движка), см. PanRateAllowed. Дефолт/фолбэк — 384 кГц.
|
||||
|
||||
type
|
||||
// QO-100 beacon lock — статус контура (для UI/маркеров).
|
||||
@@ -353,6 +352,7 @@ type
|
||||
FPanDDCIdx: array[1..MAX_PANS-1] of Integer;
|
||||
FPanFreqHz: array[1..MAX_PANS-1] of Double;
|
||||
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;
|
||||
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
|
||||
FLastDDCIndex: Integer; // последний DDC index
|
||||
@@ -562,12 +562,17 @@ type
|
||||
|
||||
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
|
||||
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр (сырые Гц
|
||||
// железа, XVTR-трансляции у панов нет), RateKHz ∈ PAN_DDC_RATES (иначе 384),
|
||||
// железа, XVTR-трансляции у панов нет), RateKHz ∈ PanRateAllowed (иначе 384),
|
||||
// ADCSource — АЦП платы. False = нет слота/бэкенд не умеет/не Running.
|
||||
function AddPanDDC(PanId: Integer; FreqHz: Double; RateKHz: Word = 384;
|
||||
ADCSource: Byte = 0): Boolean;
|
||||
procedure RemovePanDDC(PanId: Integer);
|
||||
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 PanDDCFreq(PanId: Integer): Double;
|
||||
function PanDDCRateKHz(PanId: Integer): Word;
|
||||
@@ -1178,16 +1183,28 @@ begin
|
||||
FPanDDCIdx[i] := -1;
|
||||
FPanFreqHz[i] := 0;
|
||||
FPanRateKHz[i] := 0;
|
||||
FPanADC[i] := 0;
|
||||
end;
|
||||
for i := 0 to MAX_DDCS - 1 do
|
||||
FDDCPanMap[i] := -1;
|
||||
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;
|
||||
RateKHz: Word; ADCSource: Byte): Boolean;
|
||||
var
|
||||
i, ddc: Integer;
|
||||
RateOk: Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
@@ -1195,10 +1212,7 @@ begin
|
||||
if not (Assigned(FNetwork) and FRunning) then Exit;
|
||||
if PanId >= FNetwork.Caps.MaxPans then Exit; // бэкенд не умеет (Pluto=1)
|
||||
|
||||
RateOk := False;
|
||||
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;
|
||||
if not PanRateAllowed(RateKHz) then RateKHz := 384;
|
||||
|
||||
// hw-индекс: следующий свободный после главного DDC (слот-стабильности не
|
||||
// нужно — hw-индекс скрыт за PanId, наружу торчит только PanId).
|
||||
@@ -1215,11 +1229,41 @@ begin
|
||||
FPanDDCIdx[PanId] := ddc;
|
||||
FPanFreqHz[PanId] := FreqHz;
|
||||
FPanRateKHz[PanId] := RateKHz;
|
||||
FPanADC[PanId] := ADCSource;
|
||||
FDDCPanMap[ddc] := PanId;
|
||||
FNetwork.SetPanDDC(ddc, True, FreqHz, RateKHz, ADCSource);
|
||||
Result := True;
|
||||
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);
|
||||
var ddc, i: Integer;
|
||||
begin
|
||||
@@ -1232,6 +1276,7 @@ begin
|
||||
FPanDDCIdx[PanId] := -1;
|
||||
FPanFreqHz[PanId] := 0;
|
||||
FPanRateKHz[PanId] := 0;
|
||||
FPanADC[PanId] := 0;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||||
RemoveSlice(FSlices[i].Id);
|
||||
|
||||
@@ -509,6 +509,9 @@ type
|
||||
function AddPan(PanId, RateHz: Integer): Boolean;
|
||||
// Убирает пан вместе с его слайсами (аудио-выходы слайсов — забота вызывающего).
|
||||
procedure RemovePan(PanId: Integer);
|
||||
// Смена rate живого пана: пересбор аккумулятора/анализатора + переоткрытие
|
||||
// WDSP-каналов его слайсов на новом входном rate (state слайсов сохраняется).
|
||||
function SetPanRate(PanId, RateHz: Integer): Boolean;
|
||||
function PanActive(PanId: Integer): Boolean;
|
||||
|
||||
// --- Мультислайсы (софт-fan-out) ---
|
||||
@@ -2220,6 +2223,41 @@ begin
|
||||
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;
|
||||
begin
|
||||
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;
|
||||
|
||||
+28
-4
@@ -413,10 +413,34 @@ ChangeSampleRate).
|
||||
не `crDefault`. ⚠Урок на 3.5 (pop-out) и вообще: любой динамически
|
||||
создаваемый нативный GL-виджет должен получать явный курсор при создании.
|
||||
|
||||
Статус: собирается (GUI+демон), НА ЭКРАНЕ НЕ ПРОВЕРЕНО (юзер проверяет).
|
||||
Открыто из фидбека: юзер видел курсор-«палец» на водопаде пана N — в коде
|
||||
панов N `crHandPoint` нигде не ставится (подозрение: артефакт сломанной
|
||||
раскладки, перекрытие чужим контролом); перепроверить после фиксов.
|
||||
Статус: все фиксы подтверждены юзером на экране, закоммичено 95b4f74
|
||||
(+ dae0c19 банд-столл отдельно).
|
||||
|
||||
**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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user