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:
2026-07-11 23:28:25 +03:00
co-authored by Claude Fable 5
parent 95b4f749be
commit e5921b8196
5 changed files with 264 additions and 23 deletions
+80 -6
View File
@@ -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
View File
@@ -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;
+56 -11
View File
@@ -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 — полная сетка железа Caps.RatePresets (кратные
// Допустимые rate доп. пан-DDC, кГц (дефолт 384). // 48 кГц — требование движка), см. PanRateAllowed. Дефолт/фолбэк — 384 кГц.
PAN_DDC_RATES: array[0..2] of Word = (192, 384, 768);
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);
+38
View File
@@ -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
View File
@@ -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)