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
+62 -2
View File
@@ -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;