diff --git a/MainForm.pas b/MainForm.pas index 6d560dc..502fdaf 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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 diff --git a/RadioController.pas b/RadioController.pas index 9d3c4a9..550731f 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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) diff --git a/VfoOverlay.pas b/VfoOverlay.pas index b166f8f..2815043 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -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) diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 423c4b6..7f4d22b 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -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;