feat(vfo): кнопка SQL + ползунок FM-squelch в DSP-меню флага (main + слайсы)

В fly-out DSP появляется ряд SQL (тумблер + порог 0..100) только в режиме FM.
Squelch работает per-slice: у слайсов свои WDSP-каналы, поэтому добавлены
поля/сеттеры FMSQ в TSliceChan/TCtrlSlice, ApplyFMSQToChan и восстановление
при переоткрытии канала. Роутинг события по SliceId (0=RXA, иначе слайс).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 12:51:02 +03:00
co-authored by Claude Opus 4.8
parent ce9cfd730f
commit b2b3339a2a
4 changed files with 158 additions and 2 deletions
+90 -1
View File
@@ -41,6 +41,7 @@ function SliceColor(L: Char): TColor;
type
TModeFilterEvent = procedure(Mode: Integer; FilterBW: Integer) of object;
TDSPChangeEvent = procedure(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean) of object;
TSquelchEvent = procedure(SQLOn: Boolean; SQLLevel: Integer) of object;
TAGCChangeEvent = procedure(AGCMode: Integer) of object;
TVolumeEvent = procedure(Vol: Integer) of object;
TAudioDeviceEvent = procedure(DevIndex: Integer; const DevName: string) of object;
@@ -61,6 +62,9 @@ type
FANFOn: Boolean;
FAGCMode: Integer; // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF
FSQLOn: Boolean; // FM-шумоподавитель (squelch) включён
FSQLLevel: Integer; // порог squelch 0..100
FVolume: Integer; // 0..100
FMuted: Boolean;
FSplit: Boolean;
@@ -77,9 +81,12 @@ type
FAudScroll: Integer; // прокрутка списка аудио-устройств (верхний индекс)
FVolTrack: TRect; // трек громкости (локальные координаты)
FVolDrag: Boolean;
FSqlTrack: TRect; // трек squelch в DSP-панели (лок. координаты)
FSqlDrag: Boolean;
FOnSelect: TModeFilterEvent;
FOnDSPChange: TDSPChangeEvent;
FOnSquelchChange: TSquelchEvent;
FOnAGCChange: TAGCChangeEvent;
FOnVolume: TVolumeEvent;
FOnMute: TNotifyEvent;
@@ -131,6 +138,8 @@ type
function ComputeHeight: Integer;
procedure SetPanel(P: TFlyPanel);
procedure ApplyVolFromX(LX: Integer);
procedure ApplySqlFromX(LX: Integer);
function IsFM: Boolean;
procedure RequestInvalidate;
procedure RequestMeterInvalidate;
procedure RefreshMeter(C: TCanvas);
@@ -159,6 +168,7 @@ type
procedure SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
AAGCMode: Integer);
procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean);
procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
const CurName: string);
@@ -185,6 +195,7 @@ type
property BaseHeight: Integer read ComputeHeight;
property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect;
property OnDSPChange: TDSPChangeEvent read FOnDSPChange write FOnDSPChange;
property OnSquelchChange: TSquelchEvent read FOnSquelchChange write FOnSquelchChange;
property OnAGCChange: TAGCChangeEvent read FOnAGCChange write FOnAGCChange;
property OnVolume: TVolumeEvent read FOnVolume write FOnVolume;
property OnMute: TNotifyEvent read FOnMute write FOnMute;
@@ -228,6 +239,8 @@ const
HIT_AUD_DN = -28;
HIT_SLICE_SEL = -29; // клик по бейджу буквы (выбрать слайс активным)
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели)
HIT_SQL_SLIDER = -32; // трек порога FM-squelch
HIT_AGC_PICK_BASE = 100000;
HIT_DEV_BASE = 200000;
@@ -402,6 +415,7 @@ begin
FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000;
FModeFilter[6] := 8000; FModeFilter[7] := 8000;
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
FTxSel := True; FSlice := 'A';
FSliceId := 0; FIsMain := True; // по умолчанию — главный приёмник (слайс A)
@@ -532,11 +546,19 @@ end;
{ ---- Высота с учётом fly-out ---- }
function TVfoOverlay.IsFM: Boolean;
begin
Result := FMode = 5; // индекс FM в OVL_MODE_NAMES (= MODE_FM контроллера)
end;
function TVfoOverlay.PanelContentH: Integer;
begin
case FPanel of
fpMode: Result := ROW_H*2 + IGAP + ROW_H + IGAP; // 2 ряда режимов + ряд фильтров
fpDsp: Result := ROW_H + IGAP + ROW_H; // toggles + AGC
fpDsp: begin
Result := ROW_H + IGAP + ROW_H; // toggles + AGC
if IsFM then Inc(Result, IGAP + ROW_H); // + ряд SQL (только FM)
end;
fpAud: Result := Min(Max(1, FDevNames.Count), AUD_MAX_VIS) * AUD_ROW_H;
else Result := 0;
end;
@@ -796,6 +818,34 @@ begin
RegisterHit(HR, HIT_AGC_PICK_BASE + I);
BX := BRight;
end;
// Ряд 3 (только FM): тумблер SQL + ползунок порога squelch
if IsFM then
begin
Inc(Y, ROW_H + IGAP);
// кнопка SQL слева (1/4 ширины)
BRight := X + RowW div 4;
HR := Rect(X, Y, BRight - 2, Y + ROW_H);
DrawButton(C, HR, 'SQL', FSQLOn,
(FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_SQL), 7);
RegisterHit(HR, HIT_SQL);
// ползунок порога справа
BX := BRight + 4;
FSqlTrack := Rect(BX, Y + ROW_H div 2 - 3, X + RowW, Y + ROW_H div 2 + 3);
C.Brush.Color := CLR_BTN_NORM; C.Brush.Style := bsSolid;
C.Pen.Color := CLR_BTN_NORM; C.Pen.Style := psSolid;
C.RoundRect(FSqlTrack.Left, FSqlTrack.Top, FSqlTrack.Right, FSqlTrack.Bottom, 3, 3);
BX := FSqlTrack.Left + Round(FSQLLevel/100.0 * (FSqlTrack.Right - FSqlTrack.Left));
if FSQLOn then
begin
C.Brush.Color := CLR_ACCENT; C.Pen.Color := CLR_ACCENT;
C.RoundRect(FSqlTrack.Left, FSqlTrack.Top, BX, FSqlTrack.Bottom, 3, 3);
end;
C.Brush.Color := CLR_TEXT; C.Pen.Color := CLR_TEXT;
C.Ellipse(BX-4, Y + ROW_H div 2 - 5, BX+4, Y + ROW_H div 2 + 5);
RegisterHit(Rect(FSqlTrack.Left - 4, Y, FSqlTrack.Right + 4, Y + ROW_H),
HIT_SQL_SLIDER);
end;
end;
procedure TVfoOverlay.DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
@@ -1081,6 +1131,9 @@ begin
HIT_BAR_AUD: begin SetPanel(fpAud); Exit; end;
HIT_MUTE: begin FMuted := not FMuted; RequestInvalidate;
if Assigned(FOnMute) then FOnMute(Self); Exit; end;
HIT_SQL: begin FSQLOn := not FSQLOn; RequestInvalidate;
if Assigned(FOnSquelchChange) then
FOnSquelchChange(FSQLOn, FSQLLevel); Exit; end;
HIT_SPLIT: begin if Assigned(FOnSplit) then FOnSplit(Self); Exit; end;
HIT_TX: begin if Assigned(FOnTxSelect) then FOnTxSelect(Self); Exit; end;
HIT_SLICE_SEL: begin if Assigned(FOnSliceSelect) then FOnSliceSelect(Self); Exit; end;
@@ -1176,6 +1229,21 @@ begin
end;
end;
procedure TVfoOverlay.ApplySqlFromX(LX: Integer);
var
V: Integer;
begin
if FSqlTrack.Right <= FSqlTrack.Left then Exit;
V := Round((LX - FSqlTrack.Left) / (FSqlTrack.Right - FSqlTrack.Left) * 100);
if V < 0 then V := 0 else if V > 100 then V := 100;
if V <> FSQLLevel then
begin
FSQLLevel := V;
RequestInvalidate;
if Assigned(FOnSquelchChange) then FOnSquelchChange(FSQLOn, FSQLLevel);
end;
end;
{ ---- Внешний ввод (маршрутизируется MainForm поверх спектра) ---- }
function TVfoOverlay.HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
@@ -1200,6 +1268,12 @@ begin
Result := True;
Exit;
end;
if FSqlDrag then
begin
ApplySqlFromX(LX);
Result := True;
Exit;
end;
OldHot := FHotIdx;
FHotIdx := -1;
if (LX >= 0) and (LY >= 0) and (LX < Width) and (LY < Height) then
@@ -1218,6 +1292,7 @@ begin
Result := False;
if not Visible then Exit;
if FVolDrag then begin FVolDrag := False; Result := True; Exit; end;
if FSqlDrag then begin FSqlDrag := False; Result := True; Exit; end;
if (X >= Left) and (Y >= Top) and (X < Left + Width) and (Y < Top + Height) then
Result := True; // клик был по накладке — не пускаем его в спектр
end;
@@ -1226,6 +1301,7 @@ function TVfoOverlay.HandleMouseLeave: Boolean;
begin
Result := False;
FVolDrag := False;
FSqlDrag := False;
if FHotIdx >= 0 then
begin
FHotIdx := -1;
@@ -1250,6 +1326,11 @@ begin
FVolDrag := True;
ApplyVolFromX(X);
end
else if HV = HIT_SQL_SLIDER then
begin
FSqlDrag := True;
ApplySqlFromX(X);
end
else
DoHit(HV);
Break;
@@ -1298,6 +1379,14 @@ begin
RequestInvalidate;
end;
procedure TVfoOverlay.SetSquelchState(AOn: Boolean; ALevel: Integer);
begin
if (FSQLOn = AOn) and ((FSQLLevel = ALevel) or FSqlDrag) then Exit;
FSQLOn := AOn;
if not FSqlDrag then FSQLLevel := ALevel; // во время drag ведём сами
RequestInvalidate;
end;
procedure TVfoOverlay.SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean);
begin
if (AVolume = FVolume) and (AMuted = FMuted) and (ASplit = FSplit)