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
+19 -1
View File
@@ -524,6 +524,7 @@ type
procedure UpdateVfoOverlayVisibility; // флаг A виден: панель скрыта ИЛИ есть слайсы B+
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
procedure OnVfoOverlaySquelchChange(SQLOn: Boolean; SQLLevel: Integer);
procedure OnVfoOverlayAGCChange(AGCMode: Integer);
procedure OnVfoOverlayVolume(Vol: Integer);
procedure OnVfoOverlayMute(Sender: TObject);
@@ -1515,10 +1516,12 @@ begin
// панели флаг правит себя сам (DoHit), синк не нужен. Только дискретные поля —
// SetState/SetDSPState безусловно инвалидируют (не звать на потоковых rfVfoA).
if Assigned(FVfoOverlay) and FVfoOverlay.Visible and (not FPanelHidden) and
(Field in [rfMode, rfFilter, rfNR, rfNB, rfSNB, rfANF, rfAGCMode]) then
(Field in [rfMode, rfFilter, rfNR, rfNB, rfSNB, rfANF, rfAGCMode,
rfFMSQ, rfFMSQLevel]) then
begin
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
end;
finally
FSyncingFromController := False;
@@ -5128,6 +5131,7 @@ begin
FVfoOverlay.Height := FVfoOverlay.BaseHeight; // растёт сам при открытии fly-out
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
FVfoOverlay.Visible := True;
PushVfoOverlayExtState;
PushVfoOverlayAudioDevices;
@@ -5292,6 +5296,7 @@ begin
begin
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
end;
end;
@@ -5310,6 +5315,17 @@ begin
FController.SetANF(ANFOn);
end;
procedure TMainForm.OnVfoOverlaySquelchChange(SQLOn: Boolean; SQLLevel: Integer);
begin
if CurSliceId <> 0 then
begin
FController.SetSliceFMSquelch(CurSliceId, SQLOn, SQLLevel);
Exit;
end;
FController.SetFMSquelchLevel(SQLLevel);
FController.SetFMSquelch(SQLOn);
end;
procedure TMainForm.OnVfoOverlayAGCChange(AGCMode: Integer);
begin
if CurSliceId <> 0 then
@@ -5436,6 +5452,7 @@ procedure TMainForm.WireOverlayEvents(O: TVfoOverlay);
begin
O.OnSelect := OnModeFilterSelect;
O.OnDSPChange := OnVfoOverlayDSPChange;
O.OnSquelchChange := OnVfoOverlaySquelchChange;
O.OnAGCChange := OnVfoOverlayAGCChange;
O.OnVolume := OnVfoOverlayVolume;
O.OnMute := OnVfoOverlayMute;
@@ -5565,6 +5582,7 @@ begin
end;
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
O.SetDSPState(0, 0, False, False, FController.FAGCMode);
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
O.SetExtState(Round(S.Volume * 100), S.Muted, False, False, False);
// Список аудио-устройств (общий PA-список) + текущее устройство слайса.
if Assigned(FController.FAudioOut) then
+16
View File
@@ -135,6 +135,8 @@ type
AGC: TWDSPAGCMode;
Volume: Double; // 0..1
Muted: Boolean;
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
FMSQLevel: Integer; // порог squelch 0..100
DeviceIndex: Integer; // PortAudio-индекс (-1 = default)
DevName: string;
Audio: TAudioOutput; // владеет; свой поток вывода
@@ -529,6 +531,7 @@ type
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
procedure SetSliceMute(Id: Integer; Mute: Boolean);
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
function SliceCount: Integer;
@@ -1153,6 +1156,8 @@ begin
FSlices[slot].AGC := AGC;
FSlices[slot].Volume := Vol;
FSlices[slot].Muted := False;
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
FSlices[slot].FMSQLevel := 40;
FSlices[slot].DeviceIndex := DevIndex;
FSlices[slot].DevName := DevName;
FSlices[slot].Audio := AO;
@@ -1247,6 +1252,17 @@ begin
end;
end;
procedure TRadioController.SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
var idx: Integer;
begin
idx := FindSliceIndex(Id);
if idx < 0 then Exit;
FSlices[idx].FMSQOn := On_;
FSlices[idx].FMSQLevel := Level;
if Assigned(FDSPEngine) then
FDSPEngine.SetSliceFMSquelch(Id, On_, Level);
end;
function TRadioController.SliceSMeter(Id: Integer): Double;
begin
if Assigned(FDSPEngine) then Result := FDSPEngine.GetSliceSMeter(Id)
+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)
+33
View File
@@ -169,6 +169,8 @@ type
NBMode: Integer; // 0=off,1=NB,2=NB2
SNBOn: Boolean;
ANFOn: Boolean;
FMSQOn: Boolean; // FM-шумоподавитель (squelch) включён
FMSQLevel: Integer; // порог squelch 0..100
SMeter: Double; // последнее показание S-метра канала (dBm), пишет DSP-поток
Opened: Boolean; // WDSP-канал реально открыт (после Open движка)
end;
@@ -384,6 +386,7 @@ type
procedure ApplyNBToChan(Chan, NBMode: Integer);
procedure ApplySNBToChan(Chan: Integer; On_: Boolean);
procedure ApplyANFToChan(Chan: Integer; On_: Boolean);
procedure ApplyFMSQToChan(Chan: Integer; Enable: Boolean; Level: Integer);
procedure ApplyNoiseFilterState;
procedure ProcessTXBlock; // вызывается из TTXDSPThread
procedure FeedTXDisplaySample(const I, Q: Double);
@@ -421,6 +424,7 @@ type
procedure SetSliceNBMode(Id, Mode: Integer);
procedure SetSliceSNB(Id: Integer; On_: Boolean);
procedure SetSliceANF(Id: Integer; On_: Boolean);
procedure SetSliceFMSquelch(Id: Integer; Enable: Boolean; Level: Integer);
function GetSliceSMeter(Id: Integer): Double;
function SliceCount: Integer;
function HasSlice(Id: Integer): Boolean;
@@ -1560,6 +1564,7 @@ begin
ApplyNBToChan(Chan, FSlices[Idx].NBMode);
ApplySNBToChan(Chan, FSlices[Idx].SNBOn);
ApplyANFToChan(Chan, FSlices[Idx].ANFOn);
ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel);
SetRXAPanelGain1(Chan, FSlices[Idx].Volume);
SetRXAPanelSelect(Chan, 3);
SetRXAPanelRun(Chan, 1);
@@ -1797,6 +1802,21 @@ begin
end;
end;
procedure TWDSPEngine.SetSliceFMSquelch(Id: Integer; Enable: Boolean; Level: Integer);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].FMSQOn := Enable;
FSlices[idx].FMSQLevel := Level;
if FSlices[idx].Opened then
ApplyFMSQToChan(FSlices[idx].Chan, Enable, Level);
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.GetSliceSMeter(Id: Integer): Double;
var idx: Integer;
begin
@@ -1985,6 +2005,19 @@ begin
SetRXAANFRun(Chan, Ord(On_));
end;
// FM-squelch на произвольном канале (RXA или слайс). Порог 0..100 → 0.0..0.5,
// как в SetFMSquelch для главного приёмника.
procedure TWDSPEngine.ApplyFMSQToChan(Chan: Integer; Enable: Boolean; Level: Integer);
begin
if Enable then
begin
SetRXAFMSQThreshold(Chan, Level / 200.0);
SetRXAFMSQRun(Chan, 1);
end
else
SetRXAFMSQRun(Chan, 0);
end;
procedure TWDSPEngine.ApplyNRState;
begin
if not FInitialized then Exit;