From 01de71c60f0a1f9846ee5294a8f0428ac819643e Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Mon, 9 Mar 2026 20:55:19 +0300 Subject: [PATCH] add nb nr --- MainForm.pas | 211 ++++++++++++++++++++++++++++++++++++------------ Settings.pas | 38 +++++++-- WDSPEngine.pas | 168 +++++++++++++++++++++++++++++++++++--- WebPageHtml.pas | 21 +++-- WebServer.pas | 76 ++++++++++++----- 5 files changed, 421 insertions(+), 93 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 710cbd1..b11fe1d 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -315,6 +315,7 @@ type TrkVolume: TTrackBar; BtnNR: TFlatButton; BtnNB: TFlatButton; + BtnSNB: TFlatButton; BtnANF: TFlatButton; BtnMute: TFlatButton; @@ -436,8 +437,14 @@ type procedure BtnVfoBCopyAClick(Sender: TObject); procedure BtnMOXClick(Sender: TObject); procedure BtnMuteClick(Sender: TObject); + procedure UpdateNRButton; + procedure UpdateNBButton; + procedure UpdateSNBButton; + procedure UpdateANFButton; + procedure ApplyNoiseFilterButtonsToDSP; procedure BtnNRClick(Sender: TObject); procedure BtnNBClick(Sender: TObject); + procedure BtnSNBClick(Sender: TObject); procedure BtnANFClick(Sender: TObject); procedure BtnSpanClick(Sender: TObject); // Веб-интерфейс: callbacks от TWebServer @@ -454,8 +461,9 @@ type procedure WebOnRun(On_: Boolean); procedure WebOnMute(On_: Boolean); procedure WebOnCtun(On_: Boolean); - procedure WebOnNR(On_: Boolean); - procedure WebOnNB(On_: Boolean); + procedure WebOnNR(Mode: Integer); + procedure WebOnNB(Mode: Integer); + procedure WebOnSNB(On_: Boolean); procedure WebOnANF(On_: Boolean); // Synchronize-обёртки (выполняются в UI-потоке) procedure SyncWebFreq; @@ -473,6 +481,7 @@ type procedure SyncWebCtun; procedure SyncWebNR; procedure SyncWebNB; + procedure SyncWebSNB; procedure SyncWebANF; procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject); @@ -618,9 +627,10 @@ begin Result.Volume := FVolume; Result.DriveLevel := FDriveLevel; Result.ActiveVfo := FActiveVfo; - Result.NREnabled := BtnNR.Tag = 1; - Result.NBEnabled := BtnNB.Tag = 1; - Result.ANFEnabled := BtnANF.Tag = 1; + Result.NRMode := BtnNR.Tag; + Result.NBMode := BtnNB.Tag; + Result.SNBEnabled := BtnSNB.Tag <> 0; + Result.ANFEnabled := BtnANF.Tag <> 0; Result.AGCSlope := 0; Result.AGCHangThreshold := 100; Result.WfAGCEnabled := FWfAGCEnabled; @@ -717,7 +727,8 @@ end; procedure TMainForm.FormCreate(Sender: TObject); var - bi_: Integer; + bi_, StartupRate: Integer; + StartupVfoA, StartupVfoB: Double; begin FVfoA := 14200000; FVfoB := 7100000; @@ -757,6 +768,17 @@ begin // Загружаем JSON настройки FSettings := TSettingsManager.Create; FSettings.Load; + if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then + begin + FVfoA := StartupVfoA; + FVfoB := StartupVfoB; + FCenterFreq := FVfoA; + if StartupRate > 0 then + begin + FSampleRate := StartupRate; + FSpanHz := StartupRate; + end; + end; // Восстанавливаем размер/позицию окна при старте (до подключения устройства) RestoreWindowBounds; // S-метр позиционируем после рестора размера окна @@ -780,6 +802,7 @@ begin FWebServer.OnCtun := WebOnCtun; FWebServer.OnNR := WebOnNR; FWebServer.OnNB := WebOnNB; + FWebServer.OnSNB := WebOnSNB; FWebServer.OnANF := WebOnANF; FWebServer.Start; FSMeterPeak := -130; @@ -824,6 +847,12 @@ begin FNetwork.OnMicPacket := OnMicPacketCB; BuildUI; + StyleSpanButton(BtnSpan48k, FSampleRate = 48000); + StyleSpanButton(BtnSpan96k, FSampleRate = 96000); + StyleSpanButton(BtnSpan192k, FSampleRate = 192000); + StyleSpanButton(BtnSpan384k, FSampleRate = 384000); + StyleSpanButton(BtnSpan768k, FSampleRate = 768000); + StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000); ResizeSMeter; ApplyDarkTheme; UpdateVfoDisplay; @@ -835,7 +864,7 @@ begin // BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz. // Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA // (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR. - FDSPEngine := TWDSPEngine.Create(192000, 48000, 512); + FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512); FDSPEngine.OnAudio := OnAudioReady; FDSPEngine.OnSpectrum := OnSpectrumReady; @@ -898,6 +927,7 @@ begin FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); end; // Размер окна сохраняем всегда (не зависит от подключения) + FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate); FSettings.SaveWindowBounds(Left, Top, Width, Height); FSettings.Save; FSettings.Free; @@ -1133,22 +1163,21 @@ begin PanelRX.Parent := PanelLeft; PanelRX.SetBounds(0, Y, LEFT_W, 120); PanelRX.BevelOuter := bvNone; - MakeLbl(PanelRX, 'RX', 4, 2); // AGC mode — 5 кнопок в ряд - MakeLbl(PanelRX, 'AGC', 4, 18); - W := (LEFT_W - 36) div 5; - BtnAGCMode[0] := MakeBtn(PanelRX,'FAST', 34+0*(W+1),14,W,BTN_SM,BtnAGCModeClick); BtnAGCMode[0].Tag:=0; - BtnAGCMode[1] := MakeBtn(PanelRX,'MED', 34+1*(W+1),14,W,BTN_SM,BtnAGCModeClick); BtnAGCMode[1].Tag:=1; - BtnAGCMode[2] := MakeBtn(PanelRX,'SLOW', 34+2*(W+1),14,W,BTN_SM,BtnAGCModeClick); BtnAGCMode[2].Tag:=2; - BtnAGCMode[3] := MakeBtn(PanelRX,'LONG', 34+3*(W+1),14,W,BTN_SM,BtnAGCModeClick); BtnAGCMode[3].Tag:=3; - BtnAGCMode[4] := MakeBtn(PanelRX,'OFF', 34+4*(W+1),14,W,BTN_SM,BtnAGCModeClick); BtnAGCMode[4].Tag:=4; + MakeLbl(PanelRX, 'AGC', 4, 4); + W := (LEFT_W - 10) div 5; + BtnAGCMode[0] := MakeBtn(PanelRX,'FAST', 2, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[0].Tag:=0; + BtnAGCMode[1] := MakeBtn(PanelRX,'MED', W+4, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[1].Tag:=1; + BtnAGCMode[2] := MakeBtn(PanelRX,'SLOW', 2*W+6, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[2].Tag:=2; + BtnAGCMode[3] := MakeBtn(PanelRX,'LONG', 3*W+8, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[3].Tag:=3; + BtnAGCMode[4] := MakeBtn(PanelRX,'OFF', 4*W+10, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[4].Tag:=4; for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode); // AGC level slider - MakeLbl(PanelRX, 'THRESH', 4, 41); + MakeLbl(PanelRX, 'THRESH', 4, 50); TrkAGC := TTrackBar.Create(Self); - TrkAGC.Parent := PanelRX; TrkAGC.Left := 56; TrkAGC.Top := 37; + TrkAGC.Parent := PanelRX; TrkAGC.Left := 56; TrkAGC.Top := 46; TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 20; TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → −20..-120 dBm TrkAGC.Position := FAGCTop; @@ -1157,26 +1186,27 @@ begin LblAGCTop := TLabel.Create(Self); LblAGCTop.Parent := PanelRX; - LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 41; + LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 50; LblAGCTop.Caption := Format('-%ddB', [FAGCTop]); LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New'; LblAGCTop.Font.Size := 7; // VOL - MakeLbl(PanelRX, 'VOL', 4, 65); + MakeLbl(PanelRX, 'VOL', 4, 74); TrkVolume := TTrackBar.Create(Self); - TrkVolume.Parent := PanelRX; TrkVolume.Left := 34; TrkVolume.Top := 61; + TrkVolume.Parent := PanelRX; TrkVolume.Left := 34; TrkVolume.Top := 70; TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 20; TrkVolume.Min := 0; TrkVolume.Max := 100; TrkVolume.Position := FVolume; TrkVolume.TickStyle := tsNone; TrkVolume.OnChange := TrkVolumeChange; - // NR NB ANF MUTE - W := (LEFT_W - 8) div 4; - BtnNR := MakeBtn(PanelRX, 'NR', 2, 88, W, BTN_SM, BtnNRClick); - BtnNB := MakeBtn(PanelRX, 'NB', W+4, 88, W, BTN_SM, BtnNBClick); - BtnANF := MakeBtn(PanelRX, 'ANF',2*W+6, 88, W, BTN_SM, BtnANFClick); - BtnMute:= MakeBtn(PanelRX, 'MUTE',3*W+8,88, W, BTN_SM, BtnMuteClick); + // NR NB SNB ANF MUTE + W := (LEFT_W - 10) div 5; + BtnNR := MakeBtn(PanelRX, 'NR', 2, 96, W, BTN_SM, BtnNRClick); + BtnNB := MakeBtn(PanelRX, 'NB', W+4, 96, W, BTN_SM, BtnNBClick); + BtnSNB := MakeBtn(PanelRX, 'SNB', 2*W+6, 96, W, BTN_SM, BtnSNBClick); + BtnANF := MakeBtn(PanelRX, 'ANF', 3*W+8, 96, W, BTN_SM, BtnANFClick); + BtnMute:= MakeBtn(PanelRX, 'MUTE', 4*W+10, 96, W, BTN_SM, BtnMuteClick); Inc(Y, 124); @@ -2661,7 +2691,7 @@ begin FCurrentBand, FRunning and FNetwork.Connected, FRunning, FMuted, FCTun, - BtnNR.Tag = 1, BtnNB.Tag = 1, BtnANF.Tag = 1, + BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, FCenterFreq, FFilter); end; @@ -3067,9 +3097,10 @@ begin FSampleRate := G_Settings.SampleRate; FSpanHz := FSampleRate; end; - BtnNR.Tag := Ord(G_Settings.NREnabled); StyleButton(BtnNR, G_Settings.NREnabled); - BtnNB.Tag := Ord(G_Settings.NBEnabled); StyleButton(BtnNB, G_Settings.NBEnabled); - BtnANF.Tag := Ord(G_Settings.ANFEnabled); StyleButton(BtnANF, G_Settings.ANFEnabled); + BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton; + BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton; + BtnSNB.Tag := Ord(G_Settings.SNBEnabled); UpdateSNBButton; + BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton; FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); RestoreBand(FCurrentBand); @@ -3109,6 +3140,7 @@ begin FDSPEngine.SetMode(FMode); FDSPEngine.SetVolume(FVolume / 100.0); ApplyModeFilter; + ApplyNoiseFilterButtonsToDSP; // ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки FDSPEngine.SetAGCTop(FAGCTop); FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); @@ -3617,28 +3649,81 @@ begin if FWDSPReady then FDSPEngine.SetMute(FMuted); end; +procedure TMainForm.UpdateNRButton; +begin + case BtnNR.Tag of + 1: BtnNR.Caption := 'NR'; + 2: BtnNR.Caption := 'NR2'; + 3: BtnNR.Caption := 'NR3'; + 4: BtnNR.Caption := 'NR4'; + else + BtnNR.Caption := 'NR'; + end; + StyleButton(BtnNR, BtnNR.Tag <> 0); + BtnNR.Invalidate; +end; + +procedure TMainForm.UpdateNBButton; +begin + case BtnNB.Tag of + 1: BtnNB.Caption := 'NB'; + 2: BtnNB.Caption := 'NB2'; + else + BtnNB.Caption := 'NB'; + end; + StyleButton(BtnNB, BtnNB.Tag <> 0); + BtnNB.Invalidate; +end; + +procedure TMainForm.UpdateSNBButton; +begin + BtnSNB.Caption := 'SNB'; + StyleButton(BtnSNB, BtnSNB.Tag <> 0); + BtnSNB.Invalidate; +end; + +procedure TMainForm.UpdateANFButton; +begin + BtnANF.Caption := 'ANF'; + StyleButton(BtnANF, BtnANF.Tag <> 0); + BtnANF.Invalidate; +end; + +procedure TMainForm.ApplyNoiseFilterButtonsToDSP; +begin + if not FWDSPReady then Exit; + FDSPEngine.SetNRMode(BtnNR.Tag); + FDSPEngine.SetNBMode(BtnNB.Tag); + FDSPEngine.SetSNB(BtnSNB.Tag <> 0); + FDSPEngine.SetANF(BtnANF.Tag <> 0); +end; + procedure TMainForm.BtnNRClick(Sender: TObject); begin - // Используем BtnNR напрямую — корректно работает и при вызове из веб-синхронизации (Sender=nil) - BtnNR.Tag := 1 - BtnNR.Tag; - StyleButton(BtnNR, BtnNR.Tag = 1); - if FWDSPReady then FDSPEngine.SetNR(BtnNR.Tag = 1); + BtnNR.Tag := (BtnNR.Tag + 1) mod 5; + UpdateNRButton; + if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag); end; procedure TMainForm.BtnNBClick(Sender: TObject); begin - // Используем BtnNB напрямую — корректно работает и при вызове из веб-синхронизации (Sender=nil) - BtnNB.Tag := 1 - BtnNB.Tag; - StyleButton(BtnNB, BtnNB.Tag = 1); - if FWDSPReady then FDSPEngine.SetNB(BtnNB.Tag = 1); + BtnNB.Tag := (BtnNB.Tag + 1) mod 3; + UpdateNBButton; + if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag); +end; + +procedure TMainForm.BtnSNBClick(Sender: TObject); +begin + BtnSNB.Tag := 1 - BtnSNB.Tag; + UpdateSNBButton; + if FWDSPReady then FDSPEngine.SetSNB(BtnSNB.Tag <> 0); end; procedure TMainForm.BtnANFClick(Sender: TObject); begin - // Используем BtnANF напрямую — корректно работает и при вызове из веб-синхронизации (Sender=nil) BtnANF.Tag := 1 - BtnANF.Tag; - StyleButton(BtnANF, BtnANF.Tag = 1); - if FWDSPReady then FDSPEngine.SetANF(BtnANF.Tag = 1); + UpdateANFButton; + if FWDSPReady then FDSPEngine.SetANF(BtnANF.Tag <> 0); end; @@ -3766,6 +3851,7 @@ begin FDSPEngine.SetMode(FMode); FDSPEngine.SetVolume(FVolume / 100.0); ApplyModeFilter; + ApplyNoiseFilterButtonsToDSP; // ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC FDSPEngine.SetAGCTop(FAGCTop); FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0); @@ -4046,32 +4132,57 @@ end; // ── NR ──────────────────────────────────────────────────────────────────── -procedure TMainForm.WebOnNR(On_: Boolean); +procedure TMainForm.WebOnNR(Mode: Integer); begin - FWebSyncBool := On_; + FWebSyncInt := Mode; FWebSyncM := SyncWebNR; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebNR; begin - if FWebSyncBool <> (BtnNR.Tag = 1) then - BtnNRClick(BtnNR); + if FWebSyncInt < 0 then FWebSyncInt := 0; + if FWebSyncInt > 4 then FWebSyncInt := 4; + if BtnNR.Tag <> FWebSyncInt then + begin + BtnNR.Tag := FWebSyncInt; + UpdateNRButton; + if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag); + end; end; // ── NB ──────────────────────────────────────────────────────────────────── -procedure TMainForm.WebOnNB(On_: Boolean); +procedure TMainForm.WebOnNB(Mode: Integer); begin - FWebSyncBool := On_; + FWebSyncInt := Mode; FWebSyncM := SyncWebNB; TThread.Synchronize(nil, FWebSyncM); end; procedure TMainForm.SyncWebNB; begin - if FWebSyncBool <> (BtnNB.Tag = 1) then - BtnNBClick(BtnNB); + if FWebSyncInt < 0 then FWebSyncInt := 0; + if FWebSyncInt > 2 then FWebSyncInt := 2; + if BtnNB.Tag <> FWebSyncInt then + begin + BtnNB.Tag := FWebSyncInt; + UpdateNBButton; + if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag); + end; +end; + +procedure TMainForm.WebOnSNB(On_: Boolean); +begin + FWebSyncBool := On_; + FWebSyncM := SyncWebSNB; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebSNB; +begin + if FWebSyncBool <> (BtnSNB.Tag <> 0) then + BtnSNBClick(BtnSNB); end; // ── ANF ─────────────────────────────────────────────────────────────────── @@ -4085,7 +4196,7 @@ end; procedure TMainForm.SyncWebANF; begin - if FWebSyncBool <> (BtnANF.Tag = 1) then + if FWebSyncBool <> (BtnANF.Tag <> 0) then BtnANFClick(BtnANF); end; diff --git a/Settings.pas b/Settings.pas index db38cab..917f71e 100644 --- a/Settings.pas +++ b/Settings.pas @@ -34,8 +34,9 @@ type Volume: Integer; DriveLevel: Integer; ActiveVfo: Integer; - NREnabled: Boolean; - NBEnabled: Boolean; + NRMode: Integer; + NBMode: Integer; + SNBEnabled: Boolean; ANFEnabled: Boolean; AGCSlope: Integer; AGCHangThreshold: Integer; @@ -82,6 +83,8 @@ type // Размер окна — не привязан к MAC, хранится в корне JSON procedure SaveWindowBounds(L, T, W, H: Integer); procedure LoadWindowBounds(out L, T, W, H: Integer); + procedure SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer); + function LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean; end; implementation @@ -110,6 +113,7 @@ class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings); begin FillChar(G, SizeOf(G), 0); G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0; + G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False; G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5; G.SampleRate := 192000; end; @@ -235,8 +239,9 @@ begin G.Volume := JI(GObj,'volume',70); G.DriveLevel := JI(GObj,'drive_level',50); G.ActiveVfo := JI(GObj,'active_vfo',0); - G.NREnabled := JB(GObj,'nr_enabled',False); - G.NBEnabled := JB(GObj,'nb_enabled',False); + G.NRMode := JI(GObj,'nr_mode',Ord(JB(GObj,'nr_enabled',False))); + G.NBMode := JI(GObj,'nb_mode',Ord(JB(GObj,'nb_enabled',False))); + G.SNBEnabled := JB(GObj,'snb_enabled',False); G.ANFEnabled := JB(GObj,'anf_enabled',False); G.AGCSlope := JI(GObj,'agc_slope',0); G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100); @@ -268,7 +273,9 @@ begin O := GetGlobalObj(GetDevObj(MacToStr(MAC))); JW(O,'volume',G.Volume); JW(O,'drive_level',G.DriveLevel); JW(O,'active_vfo',G.ActiveVfo); - JW(O,'nr_enabled',G.NREnabled); JW(O,'nb_enabled',G.NBEnabled); + JW(O,'nr_mode',G.NRMode); JW(O,'nb_mode',G.NBMode); + JW(O,'nr_enabled',G.NRMode <> 0); JW(O,'nb_enabled',G.NBMode <> 0); + JW(O,'snb_enabled',G.SNBEnabled); JW(O,'anf_enabled',G.ANFEnabled); JW(O,'agc_slope',G.AGCSlope); JW(O,'agc_hang_threshold',G.AGCHangThreshold); @@ -333,4 +340,25 @@ begin H := JI(O, 'height', 900); end; +procedure TSettingsManager.SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer); +var O: TJSONObject; +begin + O := EnsureObj(FRoot, 'startup'); + JW(O, 'vfo_a', VfoA); + JW(O, 'vfo_b', VfoB); + JW(O, 'sample_rate', SampleRate); +end; + +function TSettingsManager.LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean; +var O: TJSONObject; +begin + Result := FRoot.Find('startup') <> nil; + if not Result then Exit; + + O := EnsureObj(FRoot, 'startup'); + VfoA := JD(O, 'vfo_a', 14200000); + VfoB := JD(O, 'vfo_b', 7100000); + SampleRate := JI(O, 'sample_rate', 192000); +end; + end. diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 27b0a0d..466da27 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -55,6 +55,39 @@ const AGC_MEDIUM = 3; AGC_FAST = 4; + // RX filter modes + NR_MODE_OFF = 0; + NR_MODE_1 = 1; + NR_MODE_2 = 2; + NR_MODE_3 = 3; + NR_MODE_4 = 4; + + NB_MODE_OFF = 0; + NB_MODE_1 = 1; + NB_MODE_2 = 2; + + FILTER_NR_AGC_POSITION = 1; + FILTER_NB_TAU = 0.00001; + FILTER_NB_ADVTIME = 0.00001; + FILTER_NB_HANGTIME = 0.00001; + FILTER_NB_THRESHOLD = 4.95; + FILTER_NB2_MODE = 0; + FILTER_NR2_GAIN_METHOD = 2; + FILTER_NR2_NPE_METHOD = 0; + FILTER_NR2_POST_RUN = 0; + FILTER_NR2_POST_TAPER = 12; + FILTER_NR2_POST_NLEVEL = 15.0; + FILTER_NR2_POST_FACTOR = 15.0; + FILTER_NR2_POST_RATE = 5.0; + FILTER_NR2_TRAINED_THRESHOLD = -0.5; + FILTER_NR2_TRAINED_T2 = 0.2; + FILTER_NR4_REDUCTION_AMOUNT = 10.0; + FILTER_NR4_SMOOTHING_FACTOR = 20.0; + FILTER_NR4_WHITENING_FACTOR = 0.0; + FILTER_NR4_NOISE_RESCALE = 2.0; + FILTER_NR4_POST_THRESHOLD = -3.0; + FILTER_NR4_NOISE_SCALING_TYPE = 0; + // S-meter types для GetRXAMeter RXA_S_PK = 0; // peak, dBm RXA_S_AV = 1; // average, dBm @@ -140,8 +173,9 @@ type FAGCThresh: Double; // читается из WDSP (GetRXAAGCThresh) FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс) FShiftHz: Double; // NCO shift для CTUN - FNREnabled: Boolean; - FNBEnabled: Boolean; + FNRMode: Integer; + FNBMode: Integer; + FSNBEnabled: Boolean; FANFEnabled: Boolean; FMuted: Boolean; FVolume: Double; @@ -153,6 +187,11 @@ type procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure OpenAnalyzer; procedure CloseAnalyzer; + procedure ApplyNRState; + procedure ApplyNBState; + procedure ApplySNBState; + procedure ApplyANFState; + procedure ApplyNoiseFilterState; public constructor Create(SampleRate: Integer = 192000; @@ -181,8 +220,11 @@ type procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг + procedure SetNRMode(Mode: Integer); procedure SetNR(Enable: Boolean); + procedure SetNBMode(Mode: Integer); procedure SetNB(Enable: Boolean); + procedure SetSNB(Enable: Boolean); procedure SetANF(Enable: Boolean); procedure SetVolume(Vol: Double); procedure SetMute(Mute: Boolean); @@ -212,6 +254,9 @@ type property Mode: Integer read FMode; property FilterLow: Integer read FFilterLow; property FilterHigh: Integer read FFilterHigh; + property NRMode: Integer read FNRMode; + property NBMode: Integer read FNBMode; + property SNBEnabled: Boolean read FSNBEnabled; property LastError: string read FLastError; property SMeter: Double read FSMeter; property OnAudio: TOnAudioReady read FOnAudio write FOnAudio; @@ -317,6 +362,10 @@ begin FAGCTop := -90.0; FVolume := 0.7; FSMeter := -130.0; + FNRMode := NR_MODE_OFF; + FNBMode := NB_MODE_OFF; + FSNBEnabled := False; + FANFEnabled := False; FRXAccPos := 0; SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2) @@ -438,6 +487,10 @@ begin 0 ); + create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20); + create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20); + + SetRXABandpassWindow(RXA_CHAN, 1); SetRXAMode(RXA_CHAN, ModeToWDSP(FMode)); SetRXAShiftRun(RXA_CHAN, 1); SetRXAShiftFreq(RXA_CHAN, 0.0); @@ -474,6 +527,7 @@ begin SetTXAPanelSelect(TXA_CHAN, 1); FInitialized := True; + ApplyNoiseFilterState; Result := True; // Запускаем DSP поток после успешной инициализации WDSP @@ -505,6 +559,8 @@ begin end; CloseAnalyzer; + destroy_anbEXT(RXA_CHAN); + destroy_nobEXT(RXA_CHAN); CloseChannel(RXA_CHAN); CloseChannel(TXA_CHAN); FInitialized := False; @@ -760,25 +816,117 @@ begin FSampleRate); end; +procedure TWDSPEngine.ApplyNRState; +begin + if not FInitialized then Exit; + + SetRXAANRVals(RXA_CHAN, 64, 16, 16e-4, 10e-7); + SetRXAANRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); + SetRXAANRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_1)); + + SetRXAEMNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); + SetRXAEMNRgainMethod(RXA_CHAN, FILTER_NR2_GAIN_METHOD); + SetRXAEMNRnpeMethod(RXA_CHAN, FILTER_NR2_NPE_METHOD); + SetRXAEMNRtrainZetaThresh(RXA_CHAN, FILTER_NR2_TRAINED_THRESHOLD); + SetRXAEMNRtrainT2(RXA_CHAN, FILTER_NR2_TRAINED_T2); + SetRXAEMNRpost2Taper(RXA_CHAN, FILTER_NR2_POST_TAPER); + SetRXAEMNRpost2Nlevel(RXA_CHAN, FILTER_NR2_POST_NLEVEL); + SetRXAEMNRpost2Factor(RXA_CHAN, FILTER_NR2_POST_FACTOR); + SetRXAEMNRpost2Rate(RXA_CHAN, FILTER_NR2_POST_RATE); + SetRXAEMNRaeRun(RXA_CHAN, 1); + SetRXAEMNRpost2Run(RXA_CHAN, FILTER_NR2_POST_RUN); + SetRXAEMNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_2)); + + SetRXARNNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); + SetRXARNNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_3)); + + SetRXASBNRreductionAmount(RXA_CHAN, FILTER_NR4_REDUCTION_AMOUNT); + SetRXASBNRsmoothingFactor(RXA_CHAN, FILTER_NR4_SMOOTHING_FACTOR); + SetRXASBNRwhiteningFactor(RXA_CHAN, FILTER_NR4_WHITENING_FACTOR); + SetRXASBNRnoiseRescale(RXA_CHAN, FILTER_NR4_NOISE_RESCALE); + SetRXASBNRpostFilterThreshold(RXA_CHAN, FILTER_NR4_POST_THRESHOLD); + SetRXASBNRnoiseScalingType(RXA_CHAN, FILTER_NR4_NOISE_SCALING_TYPE); + SetRXASBNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); + SetRXASBNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_4)); +end; + +procedure TWDSPEngine.ApplyNBState; +begin + if not FInitialized then Exit; + + SetEXTANBTau(RXA_CHAN, FILTER_NB_TAU); + SetEXTANBHangtime(RXA_CHAN, FILTER_NB_HANGTIME); + SetEXTANBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME); + SetEXTANBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD); + SetEXTANBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_1)); + + SetEXTNOBMode(RXA_CHAN, FILTER_NB2_MODE); + SetEXTNOBTau(RXA_CHAN, FILTER_NB_TAU); + SetEXTNOBHangtime(RXA_CHAN, FILTER_NB_HANGTIME); + SetEXTNOBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME); + SetEXTNOBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD); + SetEXTNOBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_2)); +end; + +procedure TWDSPEngine.ApplySNBState; +begin + if not FInitialized then Exit; + SetRXASNBARun(RXA_CHAN, Ord(FSNBEnabled)); +end; + +procedure TWDSPEngine.ApplyANFState; +begin + if not FInitialized then Exit; + SetRXAANFPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); + SetRXAANFRun(RXA_CHAN, Ord(FANFEnabled)); +end; + +procedure TWDSPEngine.ApplyNoiseFilterState; +begin + ApplyNRState; + ApplyNBState; + ApplySNBState; + ApplyANFState; +end; + +procedure TWDSPEngine.SetNRMode(Mode: Integer); +begin + if Mode < NR_MODE_OFF then Mode := NR_MODE_OFF; + if Mode > NR_MODE_4 then Mode := NR_MODE_4; + FNRMode := Mode; + ApplyNRState; +end; + procedure TWDSPEngine.SetNR(Enable: Boolean); begin - FNREnabled := Enable; - if not FInitialized then Exit; - SetRXAEMNRRun(RXA_CHAN, Ord(Enable)); + if Enable then SetNRMode(NR_MODE_1) + else SetNRMode(NR_MODE_OFF); +end; + +procedure TWDSPEngine.SetNBMode(Mode: Integer); +begin + if Mode < NB_MODE_OFF then Mode := NB_MODE_OFF; + if Mode > NB_MODE_2 then Mode := NB_MODE_2; + FNBMode := Mode; + ApplyNBState; end; procedure TWDSPEngine.SetNB(Enable: Boolean); begin - FNBEnabled := Enable; - if not FInitialized then Exit; - SetEXTANBRun(RXA_CHAN, Ord(Enable)); + if Enable then SetNBMode(NB_MODE_1) + else SetNBMode(NB_MODE_OFF); +end; + +procedure TWDSPEngine.SetSNB(Enable: Boolean); +begin + FSNBEnabled := Enable; + ApplySNBState; end; procedure TWDSPEngine.SetANF(Enable: Boolean); begin FANFEnabled := Enable; - if not FInitialized then Exit; - SetRXAANFRun(RXA_CHAN, Ord(Enable)); + ApplyANFState; end; procedure TWDSPEngine.SetVolume(Vol: Double); diff --git a/WebPageHtml.pas b/WebPageHtml.pas index 9cfc18d..0e7d913 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -126,6 +126,7 @@ begin '' + '' + '' + + '' + '' + '' + '|' + @@ -185,6 +186,10 @@ begin 'function sw(id,on){const b=q(id);if(b)b.classList.toggle("on",!!on);}' + 'function ws2(obj){if(ws&&ws.readyState===1)ws.send(JSON.stringify(obj));}' + 'function cmd(c,v){ws2({cmd:c,on:v});}' + + 'function updNrBtn(){const b=q("nrBtn");if(!b)return;const m=cur.nr_mode|0;b.textContent=(m===2)?"NR2":(m===3)?"NR3":(m===4)?"NR4":"NR";sw("nrBtn",m!==0);}' + + 'function updNbBtn(){const b=q("nbBtn");if(!b)return;const m=cur.nb_mode|0;b.textContent=(m===2)?"NB2":"NB";sw("nbBtn",m!==0);}' + + 'function updSnbBtn(){sw("snbBtn",!!cur.snb);}' + + 'function updAnfBtn(){sw("anfBtn",!!cur.anf);}' + 'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM"];' + 'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,5],am:[6,7]};' + @@ -203,9 +208,10 @@ begin 'q("txSl").oninput=function(e){var v=q("txVal");if(v)v.textContent=e.target.value;};' + // DSP кнопки — cur обновляется локально сразу, не ждём ответа сервера - 'q("nrBtn").onclick=function(){cur.nr=!cur.nr;sw("nrBtn",cur.nr);pend("nr");cmd("set_nr",cur.nr);};' + - 'q("nbBtn").onclick=function(){cur.nb=!cur.nb;sw("nbBtn",cur.nb);pend("nb");cmd("set_nb",cur.nb);};' + - 'q("anfBtn").onclick=function(){cur.anf=!cur.anf;sw("anfBtn",cur.anf);pend("anf");cmd("set_anf",cur.anf);};' + + 'q("nrBtn").onclick=function(){cur.nr_mode=((cur.nr_mode|0)+1)%5;cur.nr=cur.nr_mode!==0;updNrBtn();pend("nr");ws2({cmd:"set_nr_mode",mode:cur.nr_mode});};' + + 'q("nbBtn").onclick=function(){cur.nb_mode=((cur.nb_mode|0)+1)%3;cur.nb=cur.nb_mode!==0;updNbBtn();pend("nb");ws2({cmd:"set_nb_mode",mode:cur.nb_mode});};' + + 'q("snbBtn").onclick=function(){cur.snb=!cur.snb;updSnbBtn();pend("snb");cmd("set_snb",cur.snb);};' + + 'q("anfBtn").onclick=function(){cur.anf=!cur.anf;updAnfBtn();pend("anf");cmd("set_anf",cur.anf);};' + // START/STOP 'function updRunBtn(r){var b=q("startStopBtn");if(!b)return;b.textContent=r?"STOP":"START";b.classList.toggle("rx-tx",!!r);b.classList.toggle("on",!r);}' + @@ -324,7 +330,7 @@ begin 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' + // Маппинг: server JSON field → pend key - 'var PEND_MAP={vfo_a_hz:"freq",nr:"nr",nb:"nb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc"};' + + 'var PEND_MAP={vfo_a_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc"};' + 'function applyState(msg){' + // Мержим только поля которые не pending @@ -337,9 +343,10 @@ begin 'const as=q("agcSel");if(as&&cur.agc_mode!=null&&!isPending("agc"))as.value=cur.agc_mode;' + 'const vs=q("volSl");if(vs&&!vs.matches(":active")){vs.value=cur.volume||70;var vv=q("volVal");if(vv)vv.textContent=cur.volume||70;}' + 'const rs=q("rfSl");if(rs&&!rs.matches(":active")){rs.value=cur.agc_top||90;var rv=q("rfVal");if(rv)rv.textContent=(cur.agc_top||90)+"dB";}' + - 'if(!isPending("nr"))sw("nrBtn",!!cur.nr);' + - 'if(!isPending("nb"))sw("nbBtn",!!cur.nb);' + - 'if(!isPending("anf"))sw("anfBtn",!!cur.anf);' + + 'if(!isPending("nr"))updNrBtn();' + + 'if(!isPending("nb"))updNbBtn();' + + 'if(!isPending("snb"))updSnbBtn();' + + 'if(!isPending("anf"))updAnfBtn();' + 'if(!isPending("run"))updRunBtn(!!cur.running);' + // syncModGrp syncs dropdown AND calls updModTglLabel internally 'if(!isPending("mode")){syncModGrp(cur.mode||0);}' + diff --git a/WebServer.pas b/WebServer.pas index a4aa14c..d0f4187 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -117,8 +117,9 @@ type TWebCmdRun = procedure(On_: Boolean) of object; TWebCmdMute = procedure(On_: Boolean) of object; TWebCmdCtun = procedure(On_: Boolean) of object; - TWebCmdNR = procedure(On_: Boolean) of object; - TWebCmdNB = procedure(On_: Boolean) of object; + TWebCmdNRMode = procedure(Mode: Integer) of object; + TWebCmdNBMode = procedure(Mode: Integer) of object; + TWebCmdSNB = procedure(On_: Boolean) of object; TWebCmdANF = procedure(On_: Boolean) of object; // ── Главный класс сервера ───────────────────────────────────────────────── @@ -169,8 +170,9 @@ type FTrxRunning: Boolean; FMuted: Boolean; FCtun: Boolean; - FNR: Boolean; - FNB: Boolean; + FNRMode: Integer; + FNBMode: Integer; + FSNB: Boolean; FANF: Boolean; FCenterHz: Double; FFilterIdx: Integer; @@ -190,8 +192,9 @@ type FOnRun: TWebCmdRun; FOnMute: TWebCmdMute; FOnCtun: TWebCmdCtun; - FOnNR: TWebCmdNR; - FOnNB: TWebCmdNB; + FOnNR: TWebCmdNRMode; + FOnNB: TWebCmdNBMode; + FOnSNB: TWebCmdSNB; FOnANF: TWebCmdANF; FWebClientActive: Boolean; @@ -232,7 +235,8 @@ type SpanHz: Double; Volume: Integer; WfAGC, WfNF: Boolean; BandIdx: Integer; TrxConnected: Boolean; - TrxRunning, Muted, Ctun, NR, NB, ANF: Boolean; + TrxRunning, Muted, Ctun: Boolean; + NRMode, NBMode: Integer; SNB, ANF: Boolean; CenterHz: Double; FilterIdx: Integer); property WebClientActive: Boolean read FWebClientActive; @@ -250,8 +254,9 @@ type property OnRun: TWebCmdRun read FOnRun write FOnRun; property OnMute: TWebCmdMute read FOnMute write FOnMute; property OnCtun: TWebCmdCtun read FOnCtun write FOnCtun; - property OnNR: TWebCmdNR read FOnNR write FOnNR; - property OnNB: TWebCmdNB read FOnNB write FOnNB; + property OnNR: TWebCmdNRMode read FOnNR write FOnNR; + property OnNB: TWebCmdNBMode read FOnNB write FOnNB; + property OnSNB: TWebCmdSNB read FOnSNB write FOnSNB; property OnANF: TWebCmdANF read FOnANF write FOnANF; end; @@ -905,15 +910,39 @@ begin end else if Cmd = 'set_nr' then begin - On_ := JsonGetBool(Json, 'on', FNR); - FStateLock.Enter; FNR := On_; FStateLock.Leave; - if Assigned(FOnNR) then FOnNR(On_); + On_ := JsonGetBool(Json, 'on', FNRMode <> 0); + ModeValue := Ord(On_); + FStateLock.Enter; FNRMode := ModeValue; FStateLock.Leave; + if Assigned(FOnNR) then FOnNR(ModeValue); + end + else if Cmd = 'set_nr_mode' then + begin + ModeValue := JsonGetInt(Json, 'mode', FNRMode); + if ModeValue < 0 then ModeValue := 0; + if ModeValue > 4 then ModeValue := 4; + FStateLock.Enter; FNRMode := ModeValue; FStateLock.Leave; + if Assigned(FOnNR) then FOnNR(ModeValue); end else if Cmd = 'set_nb' then begin - On_ := JsonGetBool(Json, 'on', FNB); - FStateLock.Enter; FNB := On_; FStateLock.Leave; - if Assigned(FOnNB) then FOnNB(On_); + On_ := JsonGetBool(Json, 'on', FNBMode <> 0); + ModeValue := Ord(On_); + FStateLock.Enter; FNBMode := ModeValue; FStateLock.Leave; + if Assigned(FOnNB) then FOnNB(ModeValue); + end + else if Cmd = 'set_nb_mode' then + begin + ModeValue := JsonGetInt(Json, 'mode', FNBMode); + if ModeValue < 0 then ModeValue := 0; + if ModeValue > 2 then ModeValue := 2; + FStateLock.Enter; FNBMode := ModeValue; FStateLock.Leave; + if Assigned(FOnNB) then FOnNB(ModeValue); + end + else if Cmd = 'set_snb' then + begin + On_ := JsonGetBool(Json, 'on', FSNB); + FStateLock.Enter; FSNB := On_; FStateLock.Leave; + if Assigned(FOnSNB) then FOnSNB(On_); end else if Cmd = 'set_anf' then begin @@ -1015,7 +1044,7 @@ begin '"span_hz":%.0f,"center_hz":%.0f,"volume":%d,' + '"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' + '"running":%s,"mute":%s,"ctun":%s,' + - '"nr":%s,"nb":%s,"anf":%s,"connected":%s}', + '"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s}', [FFreq, FMode, MODE_N[FMode mod 8], FFilterIdx, FFilterBW, FAGCMode, FAGCTop, @@ -1026,8 +1055,11 @@ begin BoolToStr(FTrxRunning, 'true', 'false'), BoolToStr(FMuted, 'true', 'false'), BoolToStr(FCtun, 'true', 'false'), - BoolToStr(FNR, 'true', 'false'), - BoolToStr(FNB, 'true', 'false'), + FNRMode, + BoolToStr(FNRMode <> 0, 'true', 'false'), + FNBMode, + BoolToStr(FNBMode <> 0, 'true', 'false'), + BoolToStr(FSNB, 'true', 'false'), BoolToStr(FANF, 'true', 'false'), BoolToStr(FConnected, 'true', 'false') ]); @@ -1091,7 +1123,8 @@ procedure TWebServer.PushSpectrum( SpanHz: Double; Volume: Integer; WfAGC, WfNF: Boolean; BandIdx: Integer; TrxConnected: Boolean; - TrxRunning, Muted, Ctun, NR, NB, ANF: Boolean; + TrxRunning, Muted, Ctun: Boolean; + NRMode, NBMode: Integer; SNB, ANF: Boolean; CenterHz: Double; FilterIdx: Integer); var N, i: Integer; begin @@ -1116,8 +1149,9 @@ begin FTrxRunning := TrxRunning; FMuted := Muted; FCtun := Ctun; - FNR := NR; - FNB := NB; + FNRMode := NRMode; + FNBMode := NBMode; + FSNB := SNB; FANF := ANF; FCenterHz := CenterHz; FFilterIdx := FilterIdx;