diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/MainForm.pas b/MainForm.pas index f80a302..cc7c65e 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -26,7 +26,7 @@ uses StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types, LCLIntf, LCLType, GraphType, HPSDRProtocol, HPSDRNetwork, - WDSPEngine, AudioOutput, AudioInput, + WDSP, WDSPEngine, AudioOutput, AudioInput, IntfGraphics, FPImage, Settings, WebServer; @@ -45,6 +45,7 @@ const CLR_METER_ON = TColor($0000CC44); CLR_METER_OVR= TColor($000000CC); CLR_SPECTRUM = TColor($0044FF44); + WDSP_WISDOM_FILE = 'wdspWisdom00'; BAND_COUNT = 11; MODE_COUNT = 8; @@ -229,11 +230,15 @@ type // ---- Spectrum ---- FSpectrumBuf: array[0..1023] of Single; - FWfAvgBuf: array[0..1023] of Single; - FWaterfallBuf: array[0..1023] of Single; // WDSP waterfall pixout=1 data + FWaterfallBuf: array[0..1023] of Single; // текущая строка водопада после локального сглаживания + FSpectrumBufCount: Integer; + FWaterfallBufCount: Integer; FSpectrumBitmap: TBitmap; FWfAGCEnabled: Boolean; FWfNFEnabled: Boolean; + FWfManualHigh: Double; + FWfManualLow: Double; + FWfAGCOffset: Double; FWfHigh: Double; FWfLow: Double; // Spectrum grid settings @@ -242,13 +247,16 @@ type FSpecGridStep: Double; // grid step in dB (e.g. 10) FWaterfallBitmap:TBitmap; FWaterfallTemp: TBitmap; + FWaterfallDirty: Boolean; + FWaterfallFrameInterval: Integer; + FWaterfallFrameCounter: Integer; FSpectrumWidth: Integer; FSpectrumHeight: Integer; FWaterfallHeight:Integer; // ---- Visibility settings ---- FShowSpectrum: Boolean; FShowWaterfall: Boolean; - FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30) + FDisplayFPS: Integer; // таймер спектра: кадров в секунду (1..100) // ---- Splitter между спектром и водопадом ---- FSplitter: TPanel; // визуальная полоса-разделитель FSplitterDrag: Boolean; // True = идёт перетаскивание @@ -403,6 +411,7 @@ type procedure ResetSpectrumBuf; procedure ResizeSpectrumPanels; procedure RecreateDSPEngine(ASampleRate: Integer); + function EnsureWDSPWisdom: Boolean; // Network callbacks procedure OnDeviceFound(const Dev: THPSDRDevice); @@ -498,6 +507,8 @@ type procedure WebOnFreqB(Hz: Double); procedure WebOnActiveVfo(Idx: Integer); procedure WebOnCenter(Hz: Double); + procedure WebOnMOX(On_: Boolean); + procedure WebOnDrive(V: Integer); // Synchronize-обёртки (выполняются в UI-потоке) procedure SyncWebFreq; procedure SyncWebMode; @@ -519,6 +530,8 @@ type procedure SyncWebFreqB; procedure SyncWebActiveVfo; procedure SyncWebCenter; + procedure SyncWebMOX; + procedure SyncWebDrive; procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject); procedure BtnHidePanelClick(Sender: TObject); @@ -555,10 +568,10 @@ type public // Методы для SettingsForm — немедленное применение настроек - procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode, - SpecAvgCount: Integer; SpecBackmult: Double); - procedure ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer; - WfBackmult: Double); + procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer; + SpecAvgTimeMS: Double); + procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer; + WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double); procedure ApplyGridParams(RefLevel, Range, GridStep: Double); procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string); procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string); @@ -575,6 +588,29 @@ type var Handled: Boolean); end; + TWisdomBuildThread = class(TThread) + private + FDirectory: string; + FError: string; + protected + procedure Execute; override; + public + constructor Create(const ADirectory: string); + property ErrorText: string read FError; + end; + + TWisdomProgressDialog = class(TForm) + private + FInfoLabel: TLabel; + FProgress: TProgressBar; + FTimer: TTimer; + FThread: TWisdomBuildThread; + FPhase: Integer; + procedure TimerTick(Sender: TObject); + public + constructor Create(AOwner: TComponent; AThread: TWisdomBuildThread); reintroduce; + end; + var MainForm: TMainForm; @@ -584,6 +620,123 @@ uses SettingsForm; {$R *.lfm} +function WaterfallColorThetis(Level: Integer): LongWord; +const + STOPS: array[0..5] of LongWord = ( + $000000, // black + $0A1A48, // deep blue + $0088CC, // cyan + $E0C020, // yellow + $D84A12, // orange-red + $FFFFFF // white + ); +var + Seg, Base: Integer; + T: Double; + C0, C1: LongWord; + R, G, B: Integer; +begin + Level := EnsureRange(Level, 0, 255); + Seg := Min(4, Level div 51); + Base := Seg * 51; + T := (Level - Base) / 51.0; + C0 := STOPS[Seg]; + C1 := STOPS[Seg + 1]; + + R := Round(((C0 shr 16) and $FF) * (1.0 - T) + ((C1 shr 16) and $FF) * T); + G := Round(((C0 shr 8) and $FF) * (1.0 - T) + ((C1 shr 8) and $FF) * T); + B := Round((C0 and $FF) * (1.0 - T) + (C1 and $FF) * T); + Result := (R shl 16) or (G shl 8) or B; +end; + +constructor TWisdomBuildThread.Create(const ADirectory: string); +begin + inherited Create(False); + FreeOnTerminate := False; + FDirectory := ADirectory; + FError := ''; +end; + +procedure TWisdomBuildThread.Execute; +var + DirA: AnsiString; +begin + try + if not Assigned(@WDSPwisdom) then Exit; + DirA := AnsiString(FDirectory); + WDSPwisdom(PAnsiChar(DirA)); + except + on E: Exception do + FError := E.ClassName + ': ' + E.Message; + end; +end; + +constructor TWisdomProgressDialog.Create(AOwner: TComponent; + AThread: TWisdomBuildThread); +begin + inherited CreateNew(AOwner); + FThread := AThread; + FPhase := 0; + + Caption := 'WDSP Wisdom'; + Width := 520; + Height := 140; + Position := poScreenCenter; + BorderStyle := bsDialog; + BorderIcons := []; + Color := CLR_BG; + Font.Name := 'Courier New'; + Font.Size := 9; + Font.Color := CLR_TEXT; + + FInfoLabel := TLabel.Create(Self); + FInfoLabel.Parent := Self; + FInfoLabel.SetBounds(16, 16, 480, 40); + FInfoLabel.AutoSize := False; + FInfoLabel.WordWrap := True; + FInfoLabel.Font.Color := CLR_TEXT; + FInfoLabel.Caption := + 'Creating FFTW wisdom for WDSP. This is done once and may take a while on the first run.'; + + FProgress := TProgressBar.Create(Self); + FProgress.Parent := Self; + FProgress.SetBounds(16, 72, 480, 22); + FProgress.Min := 0; + FProgress.Max := 100; + FProgress.Position := 0; + + FTimer := TTimer.Create(Self); + FTimer.Interval := 250; + FTimer.OnTimer := TimerTick; + FTimer.Enabled := True; +end; + +procedure TWisdomProgressDialog.TimerTick(Sender: TObject); +var + P: PAnsiChar; + S: string; +begin + FPhase := (FPhase + 7) mod 101; + FProgress.Position := FPhase; + + if Assigned(@wisdom_get_status) then + begin + P := wisdom_get_status; + if P <> nil then + begin + S := Trim(string(AnsiString(P))); + if S <> '' then + FInfoLabel.Caption := S; + end; + end; + + if Assigned(FThread) and FThread.Finished then + begin + FTimer.Enabled := False; + ModalResult := mrOk; + end; +end; + // =========================================================================== // Общая функция декодирования типа платы — используется везде // =========================================================================== @@ -713,26 +866,25 @@ begin Result.WindowType := FDSPEngine.WindowType; Result.SpecDetector := FDSPEngine.SpecDetector; Result.SpecAvgMode := FDSPEngine.SpecAvgMode; - Result.SpecAvgCount := FDSPEngine.SpecAvgCount; - Result.SpecBackmult := FDSPEngine.SpecBackmult; + Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS; Result.WfDetector := FDSPEngine.WfDetector; Result.WfAvgMode := FDSPEngine.WfAvgMode; - Result.WfAvgCount := FDSPEngine.WfAvgCount; - Result.WfBackmult := FDSPEngine.WfBackmult; + Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS; end else begin Result.FFTSize := 4096; - Result.WindowType := 5; + Result.WindowType := 2; Result.SpecDetector := 0; Result.SpecAvgMode := 3; - Result.SpecAvgCount := 2; - Result.SpecBackmult := 0.45; - Result.WfDetector := 2; + Result.SpecAvgTimeMS := 30.0; + Result.WfDetector := 0; Result.WfAvgMode := 3; - Result.WfAvgCount := 2; - Result.WfBackmult := 0.45; + Result.WfAvgTimeMS := 120.0; end; + Result.WfManualHigh := FWfManualHigh; + Result.WfManualLow := FWfManualLow; + Result.WfAGCOffset := FWfAGCOffset; Result.SpecRefLevel := FSpecRefLevel; Result.SpecRange := FSpecRange; Result.SpecGridStep := FSpecGridStep; @@ -763,9 +915,9 @@ begin // --- Кнопки диапазонов --- for i := 0 to BAND_COUNT - 1 do StyleButton(BtnBand[i], i = BandIdx); - // Сбрасываем waterfall AGC/NF при смене диапазона - FWfHigh := -50.0; - FWfLow := -120.0; + // Сбрасываем waterfall thresholds при смене диапазона + FWfHigh := FWfManualHigh; + FWfLow := FWfManualLow; // Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон ResetWfAvgBuf; @@ -859,12 +1011,17 @@ begin FSampleRate := 192000; FCurrentBand := 5; // 20m по умолчанию // Waterfall normalization - FWfHigh := -50.0; // начальная верхняя граница dBm - // WF avg buffer — инициализируем значением шумового пола - // FWfAvgBuf инициализируется в ResetSpectrumBuf - FWfLow := -120.0; // начальная нижняя граница dBm - FWfAGCEnabled := False; + FWfManualHigh := -80.0; + FWfManualLow := -130.0; + FWfAGCOffset := 0.0; + FWfHigh := FWfManualHigh; // начальная верхняя граница dBm + FWfLow := FWfManualLow; // начальная нижняя граница dBm + FWfAGCEnabled := True; FWfNFEnabled := False; + FSpectrumBufCount := 1024; + FWaterfallBufCount := 1024; + FWaterfallFrameInterval := 2; + FWaterfallFrameCounter := 0; FDevConnected := False; // Spectrum grid settings defaults FSpecRefLevel := -20.0; @@ -920,6 +1077,8 @@ begin FWebServer.OnFreqB := WebOnFreqB; FWebServer.OnActiveVfo := WebOnActiveVfo; FWebServer.OnCenter := WebOnCenter; + FWebServer.OnMOX := WebOnMOX; + FWebServer.OnDrive := WebOnDrive; FWebServer.Start; FSMeterPeak := -130; FSMeterMin := -130; @@ -940,6 +1099,7 @@ begin FShowSpectrum := True; FShowWaterfall := True; FDisplayFPS := 20; + FWaterfallDirty := True; FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию FSplitterDrag := False; @@ -2020,21 +2180,20 @@ begin end; procedure TMainForm.ResetWfAvgBuf; -var i: Integer; begin - for i := 0 to 1023 do FWfAvgBuf[i] := -120.0; - FWfHigh := -50.0; - FWfLow := -120.0; + FWfHigh := FWfManualHigh; + FWfLow := FWfManualLow; + FWaterfallDirty := True; end; procedure TMainForm.ResetSpectrumBuf; var i: Integer; begin for i := 0 to 1023 do FSpectrumBuf[i] := -130.0; - for i := 0 to 1023 do FWfAvgBuf[i] := -120.0; - for i := 0 to 1023 do FWaterfallBuf[i] := -120.0; - FWfHigh := -50.0; - FWfLow := -120.0; + for i := 0 to 1023 do FWaterfallBuf[i] := -130.0; + FWfHigh := FWfManualHigh; + FWfLow := FWfManualLow; + FWaterfallDirty := True; end; procedure TMainForm.InvalidateGridCache; @@ -2253,6 +2412,7 @@ var S0, S1: Integer; InvRange: Double; LabelBandW: Integer; + SrcCount: Integer; begin if FSpectrumBitmap = nil then Exit; W := FSpectrumBitmap.Width; @@ -2359,11 +2519,12 @@ begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end; + SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024); for i := 0 to W - 1 do begin - SrcF := i * 1023.0 / Max(1, W - 1); - S0 := Min(Trunc(SrcF), 1023); - S1 := Min(S0 + 1, 1023); + SrcF := i * (SrcCount - 1.0) / Max(1, W - 1); + S0 := Min(Trunc(SrcF), SrcCount - 1); + S1 := Min(S0 + 1, SrcCount - 1); Frac := SrcF - S0; dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac; FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H)))); @@ -2409,10 +2570,17 @@ var dB, frac: Double; WatSrcF: Double; WatS0, WatS1, V: Integer; - AvgDB: Double; - PeakDB: Double; - NFSum: Double; - NFCount: Integer; + TargetLow: Double; + TargetHigh: Double; + HistMinDB: Double; + HistMaxDB: Double; + HistStepDB: Double; + NoiseFloorDB: Double; + SignalTopDB: Double; + CumCount: Integer; + LowTargetCount: Integer; + HighTargetCount: Integer; + HistIdx: Integer; WfHigh, WfLow, InvRange: Double; Step: Double; Px: PLongWord; @@ -2422,53 +2590,78 @@ var Src: PLongWord; BPPi: Integer; Pal: LongWord; + Hist: array[0..191] of Integer; + SrcCount: Integer; const - ALPHA_SPEC = 0.25; - ALPHA_HIGH = 0.02; - ALPHA_LOW = 0.005; + ALPHA_HIGH = 0.10; + ALPHA_LOW = 0.08; + WF_AUTO_OFFSET = -4.0; + WF_MIN_RANGE = 48.0; + WF_MAX_RANGE = 62.0; begin if FWaterfallBitmap = nil then Exit; W := FWaterfallBitmap.Width; H := FWaterfallBitmap.Height; if (W <= 0) or (H <= 0) then Exit; + SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024); - // ── Источник данных водопада ───────────────────────────────────────────── - // При работе WDSP: FWaterfallBuf заполнен из pixout=1 (уже усреднённые WDSP данные) - // Без WDSP (demo): делаем EMA FSpectrumBuf → FWfAvgBuf - PeakDB := -200.0; NFSum := 0.0; NFCount := 0; - if FWDSPReady then + FillChar(Hist, SizeOf(Hist), 0); + HistMinDB := -170.0; + HistMaxDB := 22.0; + HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist); + for X := 0 to SrcCount - 1 do begin - // Используем WDSP waterfall данные напрямую - for X := 0 to 1023 do + HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist)); + Inc(Hist[HistIdx]); + end; + + LowTargetCount := Round(SrcCount * 0.30); + HighTargetCount := Round(SrcCount * 0.98); + CumCount := 0; + NoiseFloorDB := FWfLow; + SignalTopDB := FWfHigh; + for HistIdx := 0 to High(Hist) do + begin + CumCount := CumCount + Hist[HistIdx]; + if CumCount >= LowTargetCount then begin - AvgDB := FWaterfallBuf[X]; - FWfAvgBuf[X] := AvgDB; // синхронизируем для AGC логики - if AvgDB > PeakDB then PeakDB := AvgDB; - if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end; + NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; + Break; end; - end - else + end; + + CumCount := 0; + for HistIdx := 0 to High(Hist) do begin - // Fallback: Pascal EMA от демо-спектра - for X := 0 to 1023 do + CumCount := CumCount + Hist[HistIdx]; + if CumCount >= HighTargetCount then begin - AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC; - FWfAvgBuf[X] := AvgDB; - if AvgDB > PeakDB then PeakDB := AvgDB; - if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end; + SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; + Break; end; end; // ── AGC High/Low ──────────────────────────────────────────────────────── if FWfAGCEnabled then begin - if PeakDB > FWfHigh then FWfHigh := PeakDB + 5.0 - else FWfHigh := FWfHigh + ALPHA_HIGH * ((PeakDB + 5.0) - FWfHigh); + TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset; + if FWfNFEnabled then + TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0) + else + TargetHigh := TargetLow + 52.0; + if TargetHigh > TargetLow + WF_MAX_RANGE then + TargetHigh := TargetLow + WF_MAX_RANGE; + FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow); + FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh); end; - if FWfNFEnabled and (NFCount > 10) then - FWfLow := FWfLow + ALPHA_LOW * ((NFSum / NFCount - 5.0) - FWfLow); - WfHigh := FWfHigh; WfLow := FWfLow; - if WfHigh < WfLow + 15.0 then WfHigh := WfLow + 15.0; + WfHigh := FWfHigh; + WfLow := FWfLow; + if not FWfAGCEnabled then + begin + WfHigh := FWfManualHigh; + WfLow := FWfManualLow; + end; + if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0; if WfHigh > 0.0 then WfHigh := 0.0; if WfLow < -160 then WfLow := -160; InvRange := 255.0 / (WfHigh - WfLow); @@ -2489,21 +2682,21 @@ begin Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W); // ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ──────── - Step := 1023.0 / Max(1, W - 1); + Step := (SrcCount - 1.0) / Max(1, W - 1); WatSrcF := 0.0; Px := @FWfPixels[0]; for X := 0 to W - 1 do begin WatS0 := Trunc(WatSrcF); - if WatS0 > 1022 then WatS0 := 1022; + if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2; WatS1 := WatS0 + 1; frac := WatSrcF - WatS0; - dB := FWfAvgBuf[WatS0] * (1.0 - frac) + FWfAvgBuf[WatS1] * frac; + dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac; V := Trunc((dB - WfLow) * InvRange); if V < 0 then V := 0; if V > 255 then V := 255; - // Палитра задана как RGB, а в буфере нужен BGRA. - // Меняем местами R/B и ставим alpha=255, чтобы совпадало с web/Thetis-градиентом. + // WFALL_PALETTE задана в RGB-логике, а в буфере нужен BGRA. + // Поэтому меняем R/B местами и добавляем alpha=255. Pal := WFALL_PALETTE[V]; Px^ := ((Pal and $000000FF) shl 16) or (Pal and $0000FF00) or @@ -3084,7 +3277,6 @@ begin if FWDSPReady then begin - FDSPEngine.UpdateSpectrum; FLastSMeter := FDSPEngine.GetSMeterDBm; FDSPEngine.SetSpectrumWidth(FSpectrumWidth); // актуальная ширина для SetAGC/SetAGCTop FDSPEngine.UpdateAGCLines(FSpectrumWidth); @@ -3096,22 +3288,24 @@ begin FVfoOverlay.UpdateVfo(FVfoA); PositionVfoOverlay; end; - // Рисуем только при новых спектральных данных/изменении UI (FSpectrumDirty). - // Это резко снижает CPU в холостом режиме. + // Панорама и водопад теперь рисуются независимо по факту новых кадров. + // Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще. if FSpectrumDirty then begin DrawSpectrum; PbSpectrum.Invalidate; - // Водопад рисуем на каждом новом спектральном кадре для плавности. - DrawWaterfall; - PbWaterfall.Invalidate; - if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or (Abs(FVfoA - FRulerLastVfo) >= 0.5) then PbRuler.Invalidate; FSpectrumDirty := False; end; + if FWaterfallDirty then + begin + DrawWaterfall; + PbWaterfall.Invalidate; + FWaterfallDirty := False; + end; end else begin @@ -3139,15 +3333,20 @@ begin end; // Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем. - if FSpectrumDirty and (not FRunning) then + if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then begin - DrawSpectrum; - DrawWaterfall; - PbSpectrum.Invalidate; + if FSpectrumDirty then + DrawSpectrum; + if FWaterfallDirty then + DrawWaterfall; + if FSpectrumDirty then + PbSpectrum.Invalidate; if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or (Abs(FVfoA - FRulerLastVfo) >= 0.5) then PbRuler.Invalidate; - PbWaterfall.Invalidate; + if FWaterfallDirty then + PbWaterfall.Invalidate; + FWaterfallDirty := False; FSpectrumDirty := False; end; @@ -3155,7 +3354,7 @@ begin if Assigned(FWebServer) then FWebServer.PushSpectrum( FSpectrumBuf, 1024, - FWfAvgBuf, + FWaterfallBuf, FLastSMeter, FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop, FSpanHz, FVolume, @@ -3165,7 +3364,8 @@ begin FRunning, FMuted, FCTun, BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, FCenterFreq, FFilter, - FVfoB, FActiveVfo); + FVfoB, FActiveVfo, + FTransmitting, TrkDrive.Position); end; // =========================================================================== @@ -3412,6 +3612,67 @@ begin FDSPEngine.OnTXIQ := OnTXIQReady; end; +function TMainForm.EnsureWDSPWisdom: Boolean; +var + WisdomDir: string; + WisdomPath: string; + DirA: AnsiString; + BuildThread: TWisdomBuildThread; + Dlg: TWisdomProgressDialog; +begin + Result := True; + + if not Assigned(@WDSPwisdom) then Exit; + + WisdomDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))); + WisdomPath := WisdomDir + WDSP_WISDOM_FILE; + + if FileExists(WisdomPath) then + begin + // По рекомендации WDSP вызываем функцию на каждом старте: + // если файл уже есть, библиотека просто импортирует wisdom. + DirA := AnsiString(WisdomDir); + WDSPwisdom(PAnsiChar(DirA)); + Exit; + end; + + BtnStartStop.Enabled := False; + StatusBar1.Panels[3].Text := 'Creating WDSP wisdom...'; + + BuildThread := TWisdomBuildThread.Create(WisdomDir); + try + Dlg := TWisdomProgressDialog.Create(Self, BuildThread); + try + Dlg.ShowModal; + finally + Dlg.Free; + end; + + BuildThread.WaitFor; + + if BuildThread.ErrorText <> '' then + begin + ShowMessage('Failed to create WDSP wisdom.' + LineEnding + + BuildThread.ErrorText); + Result := False; + end + else if not FileExists(WisdomPath) then + begin + ShowMessage('WDSP wisdom was not created.' + LineEnding + + 'Expected file: ' + WisdomPath); + Result := False; + end; + finally + BuildThread.Free; + BtnStartStop.Enabled := True; + end; + + if Result then + StatusBar1.Panels[3].Text := 'WDSP wisdom ready' + else + StatusBar1.Panels[3].Text := 'WDSP wisdom missing'; +end; + procedure TMainForm.BtnDiscoverClick(Sender: TObject); begin // Открываем диалог выбора устройства @@ -3595,6 +3856,11 @@ begin BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton; FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); + FWfManualHigh := G_Settings.WfManualHigh; + FWfManualLow := G_Settings.WfManualLow; + FWfAGCOffset := G_Settings.WfAGCOffset; + FWfHigh := FWfManualHigh; + FWfLow := FWfManualLow; FShowSpectrum := G_Settings.ShowSpectrum; FShowWaterfall := G_Settings.ShowWaterfall; if G_Settings.DisplayFPS > 0 then @@ -3610,9 +3876,9 @@ begin begin FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType); FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode, - G_Settings.SpecAvgCount, G_Settings.SpecBackmult); + G_Settings.SpecAvgTimeMS); FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode, - G_Settings.WfAvgCount, G_Settings.WfBackmult); + G_Settings.WfAvgTimeMS); end; // Аудио устройство вывода FAudioOut.Close; @@ -4555,7 +4821,7 @@ begin FWfAGCEnabled := not FWfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); if not FWfAGCEnabled then - FWfHigh := -50.0; // сбрасываем к умолчанию при выключении + FWfHigh := FWfManualHigh; // сбрасываем к ручному уровню при выключении end; procedure TMainForm.BtnWfNFClick(Sender: TObject); @@ -4563,7 +4829,7 @@ begin FWfNFEnabled := not FWfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); if not FWfNFEnabled then - FWfLow := -120.0; + FWfLow := FWfManualLow; end; procedure TMainForm.BtnHidePanelClick(Sender: TObject); @@ -5114,11 +5380,41 @@ begin FSpectrumDirty := True; end; +procedure TMainForm.WebOnMOX(On_: Boolean); +begin + FWebSyncBool := On_; + FWebSyncM := SyncWebMOX; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebMOX; +begin + ApplyMOX(FWebSyncBool); +end; + +procedure TMainForm.WebOnDrive(V: Integer); +begin + FWebSyncInt := V; + FWebSyncM := SyncWebDrive; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebDrive; +begin + // Установка Position вызывает TrkDriveChange через OnChange автоматически + TrkDrive.Position := FWebSyncInt; +end; + procedure TMainForm.TrkDriveChange(Sender: TObject); begin FDriveLevel := CalcDriveByte; if FWDSPReady then FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0); + if FNetwork.Connected and FNetwork.Running then + begin + FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.SendFullHP; + end; end; function TMainForm.CalcDriveByte: Byte; @@ -5283,6 +5579,7 @@ begin N := Min(Count, 1024); for i := 0 to N - 1 do FSpectrumBuf[i] := Pixels[i]; + FSpectrumBufCount := N; FSpectrumDirty := True; end; @@ -5291,31 +5588,47 @@ procedure TMainForm.OnWaterfallReady(const Pixels: array of Single; var i, N: Integer; begin - // Заполняем буфер водопада из WDSP pixout=1. - // Вызывается из DSP-потока — Single-запись атомарна. N := Min(Count, 1024); for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i]; + FWaterfallBufCount := N; + Inc(FWaterfallFrameCounter); + if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then + begin + FWaterfallFrameCounter := 0; + FWaterfallDirty := True; + end; end; // =========================================================================== // Settings apply methods (вызываются из TSettingsForm) // =========================================================================== -procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode, - SpecAvgCount: Integer; SpecBackmult: Double); +procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer; + SpecAvgTimeMS: Double); begin + if FWDSPReady then + FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS); if FWDSPReady then FDSPEngine.SetFFTParams(FFTSize, WinType); - if FWDSPReady then - FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgCount, SpecBackmult); + FSpectrumDirty := True; end; -procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer; - WfBackmult: Double); +procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer; + WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double); begin if FWDSPReady then - FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgCount, WfBackmult); + FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS); + FWfManualHigh := WfHigh; + FWfManualLow := WfLow; + FWfAGCOffset := WfAGCOffset; + if not FWfAGCEnabled then + begin + FWfHigh := FWfManualHigh; + FWfLow := FWfManualLow; + end; + ResetWfAvgBuf; + FSpectrumDirty := True; end; procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double); @@ -5375,9 +5688,15 @@ end; procedure TMainForm.ApplyFPS(FPS: Integer); begin if FPS < 1 then FPS := 1; - if FPS > 60 then FPS := 60; + if FPS > 100 then FPS := 100; FDisplayFPS := FPS; FSpectrumTimer.Interval := 1000 div FPS; + // Thetis по умолчанию обновляет водопад через один display frame. + FWaterfallFrameInterval := 2; + FWaterfallFrameCounter := 0; + FWaterfallDirty := True; + if FWDSPReady then + FDSPEngine.SetDisplayFPS(FPS); end; procedure TMainForm.BtnSettingsClick(Sender: TObject); @@ -5410,18 +5729,19 @@ begin FDSPEngine.WindowType, FDSPEngine.SpecDetector, FDSPEngine.SpecAvgMode, - FDSPEngine.SpecAvgCount, - FDSPEngine.SpecBackmult, + FDSPEngine.SpecAvgTimeMS, FDSPEngine.WfDetector, FDSPEngine.WfAvgMode, - FDSPEngine.WfAvgCount, - FDSPEngine.WfBackmult, + FDSPEngine.WfAvgTimeMS, + FWfManualHigh, + FWfManualLow, + FWfAGCOffset, FSpecRefLevel, FSpecRange, FSpecGridStep, FAudioOutDevName, FAudioInDevName) else SF.LoadValues( - 4096, 5, 0, 3, 2, 0.45, - 2, 3, 2, 0.45, + 4096, 2, 0, 3, 30.0, + 0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset, FSpecRefLevel, FSpecRange, FSpecGridStep, FAudioOutDevName, FAudioInDevName); SF.Show; @@ -5442,6 +5762,8 @@ begin on E: Exception do StatusBar1.Panels[3].Text := 'Audio: ' + E.Message; end; + + EnsureWDSPWisdom; end; diff --git a/Settings.pas b/Settings.pas index 5fa8166..b2ef484 100644 --- a/Settings.pas +++ b/Settings.pas @@ -6,7 +6,7 @@ unit Settings; } {$mode objfpc}{$H+} interface -uses SysUtils, Classes, fpJSON, jsonparser, jsonscanner; +uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner; const SETTINGS_FILE = 'hpsdr_settings.json'; @@ -53,12 +53,13 @@ type WindowType: Integer; // 0=Rect,1=BH4,2=Hann,3=FlatTop,4=Hamming,5=Kaiser,6=BH7 SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive - SpecAvgCount: Integer; // frames to average - SpecBackmult: Double; // 0.0..1.0 recursive averaging multiplier + SpecAvgTimeMS: Double; // averaging time in milliseconds WfDetector: Integer; // waterfall detector WfAvgMode: Integer; // waterfall average mode - WfAvgCount: Integer; // waterfall average count - WfBackmult: Double; // waterfall backmult + WfAvgTimeMS: Double; // waterfall averaging time in milliseconds + WfManualHigh: Double; // manual waterfall high threshold + WfManualLow: Double; // manual waterfall low threshold + WfAGCOffset: Double; // AGC offset in dB SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20 SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range) SpecGridStep: Double; // grid step in dB, e.g. 10 @@ -70,7 +71,7 @@ type ShowSpectrum: Boolean; // True = draw spectrum on main form ShowWaterfall: Boolean; // True = draw waterfall on main form // --- Display FPS --- - DisplayFPS: Integer; // spectrum/waterfall timer fps (5..30) + DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100) // --- PA (Power Amplifier) settings --- PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200) PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0 @@ -117,6 +118,26 @@ type implementation +function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer; + Backmult, DefaultMS: Double): Double; +var + FPSv: Integer; +begin + if AvgMode <= 0 then + Exit(0.0); + + FPSv := EnsureRange(FPS, 1, 100); + if (Backmult > 0.0) and (Backmult < 1.0) then + Result := -1000.0 / (FPSv * Ln(Backmult)) + else + Result := (1000.0 * Max(1, AvgCount)) / FPSv; + + if IsNan(Result) or IsInfinite(Result) or (Result <= 0.0) then + Result := DefaultMS; + + Result := EnsureRange(Result, 10.0, 5000.0); +end; + class function TSettingsManager.MacToStr(const MAC: array of Byte): string; begin Result := Format('%02X:%02X:%02X:%02X:%02X:%02X', @@ -144,20 +165,23 @@ begin 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.WfAGCEnabled := True; + G.WfNFEnabled := False; G.SampleRate := 192000; G.PAMaxPower := 100.0; for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0; // Display defaults G.FFTSize := 4096; - G.WindowType := 5; // Kaiser + G.WindowType := 2; // Hann G.SpecDetector := 0; // Peak G.SpecAvgMode := 3; // Log Recursive - G.SpecAvgCount := 2; - G.SpecBackmult := 0.45; - G.WfDetector := 2; // Average + G.SpecAvgTimeMS := 30.0; + G.WfDetector := 0; // Peak G.WfAvgMode := 3; // Log Recursive - G.WfAvgCount := 2; - G.WfBackmult := 0.45; + G.WfAvgTimeMS := 120.0; + G.WfManualHigh := -80.0; + G.WfManualLow := -130.0; + G.WfAGCOffset := 0.0; G.SpecRefLevel := -20.0; G.SpecRange := 110.0; G.SpecGridStep := 10.0; @@ -307,7 +331,7 @@ begin G.ANFEnabled := JB(GObj,'anf_enabled',False); G.AGCSlope := JI(GObj,'agc_slope',0); G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100); - G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',False); + G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',True); G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False); G.LastBand := JI(GObj,'last_band',5); G.SampleRate := JI(GObj,'sample_rate',192000); @@ -315,13 +339,30 @@ begin G.FFTSize := JI(GObj,'fft_size',4096); G.WindowType := JI(GObj,'win_type',5); G.SpecDetector := JI(GObj,'spec_detector',0); - G.SpecAvgMode := JI(GObj,'spec_avg_mode',3); - G.SpecAvgCount := JI(GObj,'spec_avg_count',2); - G.SpecBackmult := JD(GObj,'spec_backmult',0.45); + G.SpecAvgMode := JI(GObj,'spec_avg_mode',0); + if GObj.Find('spec_avg_time_ms') <> nil then + G.SpecAvgTimeMS := JD(GObj,'spec_avg_time_ms',120.0) + else + G.SpecAvgTimeMS := LegacyAverageTimeMS( + JI(GObj,'display_fps',20), + G.SpecAvgMode, + JI(GObj,'spec_avg_count',1), + JD(GObj,'spec_backmult',0.0), + 120.0); G.WfDetector := JI(GObj,'wf_detector',2); - G.WfAvgMode := JI(GObj,'wf_avg_mode',3); - G.WfAvgCount := JI(GObj,'wf_avg_count',2); - G.WfBackmult := JD(GObj,'wf_backmult',0.45); + G.WfAvgMode := JI(GObj,'wf_avg_mode',1); + if GObj.Find('wf_avg_time_ms') <> nil then + G.WfAvgTimeMS := JD(GObj,'wf_avg_time_ms',120.0) + else + G.WfAvgTimeMS := LegacyAverageTimeMS( + JI(GObj,'display_fps',20), + G.WfAvgMode, + JI(GObj,'wf_avg_count',2), + JD(GObj,'wf_backmult',0.25), + 120.0); + G.WfManualHigh := JD(GObj,'wf_manual_high',-80.0); + G.WfManualLow := JD(GObj,'wf_manual_low',-130.0); + G.WfAGCOffset := JD(GObj,'wf_agc_offset',0.0); G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0); G.SpecRange := JD(GObj,'spec_range',110.0); G.SpecGridStep := JD(GObj,'spec_grid_step',10.0); @@ -376,12 +417,13 @@ begin JW(O,'win_type',G.WindowType); JW(O,'spec_detector',G.SpecDetector); JW(O,'spec_avg_mode',G.SpecAvgMode); - JW(O,'spec_avg_count',G.SpecAvgCount); - JW(O,'spec_backmult',G.SpecBackmult); + JW(O,'spec_avg_time_ms',G.SpecAvgTimeMS); JW(O,'wf_detector',G.WfDetector); JW(O,'wf_avg_mode',G.WfAvgMode); - JW(O,'wf_avg_count',G.WfAvgCount); - JW(O,'wf_backmult',G.WfBackmult); + JW(O,'wf_avg_time_ms',G.WfAvgTimeMS); + JW(O,'wf_manual_high',G.WfManualHigh); + JW(O,'wf_manual_low',G.WfManualLow); + JW(O,'wf_agc_offset',G.WfAGCOffset); JW(O,'spec_ref_level',G.SpecRefLevel); JW(O,'spec_range',G.SpecRange); JW(O,'spec_grid_step',G.SpecGridStep); diff --git a/SettingsForm.pas b/SettingsForm.pas index 9546596..2430a87 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -35,9 +35,9 @@ type const BandCal: array of Double) of object; TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector, - SpecAvgMode, SpecAvgCount: Integer; SpecBackmult: Double) of object; - TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer; - WfBackmult: Double) of object; + SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object; + TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer; + WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object; TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object; TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object; TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object; @@ -79,13 +79,14 @@ type // Spectrum group FCmbSpecDet: TComboBox; FCmbSpecAvg: TComboBox; - FSpinSpecCount: TSpinEdit; - FEdSpecBackmult: TEdit; + FSpecAvgTime: TFloatSpinEdit; // Waterfall group FCmbWfDet: TComboBox; FCmbWfAvg: TComboBox; - FSpinWfCount: TSpinEdit; - FEdWfBackmult: TEdit; + FWfAvgTime: TFloatSpinEdit; + FWfHigh: TFloatSpinEdit; + FWfLow: TFloatSpinEdit; + FWfAGCOffset: TFloatSpinEdit; // Grid group FEdRefLevel: TEdit; FEdRange: TEdit; @@ -136,12 +137,11 @@ type procedure OnWindowChange(Sender: TObject); procedure OnSpecDetChange(Sender: TObject); procedure OnSpecAvgModeChange(Sender: TObject); - procedure OnSpecCountChange(Sender: TObject); - procedure OnSpecBackmultChange(Sender: TObject); + procedure OnSpecAvgTimeChange(Sender: TObject); procedure OnWfDetChange(Sender: TObject); procedure OnWfAvgModeChange(Sender: TObject); - procedure OnWfCountChange(Sender: TObject); - procedure OnWfBackmultChange(Sender: TObject); + procedure OnWfAvgTimeChange(Sender: TObject); + procedure OnWfThresholdChange(Sender: TObject); procedure OnRefLevelChange(Sender: TObject); procedure OnRangeChange(Sender: TObject); procedure OnGridStepChange(Sender: TObject); @@ -157,8 +157,11 @@ type procedure FireWaterfallChange; procedure FireGridChange; - function GetSpecBackmult: Double; - function GetWfBackmult: Double; + function GetSpecAvgTimeMS: Double; + function GetWfAvgTimeMS: Double; + function GetWfHigh: Double; + function GetWfLow: Double; + function GetWfAGCOffset: Double; function GetRefLevel: Double; function GetRange: Double; function GetGridStep: Double; @@ -169,10 +172,10 @@ type // Заполнить элементы управления текущими значениями (без firing events) procedure LoadValues( - FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer; - SpecBackmult: Double; - WfDet, WfAvgMode, WfAvgCount: Integer; - WfBackmult: Double; + FFTSize, WinType, SpecDet, SpecAvgMode: Integer; + SpecAvgTimeMS: Double; + WfDet, WfAvgMode: Integer; + WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double; RefLevel, Range, GridStep: Double; const AudioOutDevName: string; const AudioInDevName: string); @@ -208,9 +211,8 @@ const CLR_ACCENT = TColor($0040FF80); BTN_H = 24; - // FFT sizes (index → value) - FFT_SIZES: array[0..5] of Integer = (1024, 2048, 4096, 8192, 16384, 32768); - FFT_NAMES: array[0..5] of string = ('1024','2048','4096','8192','16384','32768'); + FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144); + FFT_NAMES: array[0..6] of string = ('4096','8192','16384','32768','65536','131072','262144'); WINDOW_NAMES: array[0..6] of string = ( 'Rectangular', '4-term Blackman-Harris', 'Hann', @@ -219,17 +221,19 @@ const DETECTOR_NAMES: array[0..4] of string = ( 'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS'); - AVGMODE_NAMES: array[0..4] of string = ( - 'Peak Hold', 'None', 'Recursive', 'Time Window', 'Log Recursive'); - // Note: AVERAGE_PEAK_HOLD=-1, NONE=0, RECURSIVE=1, TIME_WINDOW=2, LOG_RECURSIVE=3 - // Index → WDSP value mapping: 0→-1, 1→0, 2→1, 3→2, 4→3 - AVGMODE_VALUES: array[0..4] of Integer = (-1, 0, 1, 2, 3); + AVGMODE_NAMES: array[0..3] of string = ( + 'None', 'Recursive', 'Time Window', 'Log Recursive'); + AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3); + WF_AVGMODE_NAMES: array[0..3] of string = ( + 'None', 'Recursive', 'Time Window', 'Log Recursive'); + WF_AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3); GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0); GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB'); - FPS_VALUES: array[0..5] of Integer = (5, 10, 15, 20, 25, 30); - FPS_NAMES: array[0..5] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps'); + FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100); + FPS_NAMES: array[0..12] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps', + '40 fps','50 fps','60 fps','75 fps','80 fps','90 fps','100 fps'); PA_BAND_NAMES: array[0..10] of string = ('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'); @@ -240,8 +244,8 @@ constructor TSettingsForm.Create(AOwner: TComponent); begin inherited CreateNew(AOwner); Caption := 'Settings'; - Width := 700; - Height := 560; + Width := 760; + Height := 640; Position := poScreenCenter; BorderStyle := bsSizeable; Color := CLR_BG; @@ -457,31 +461,20 @@ begin end; procedure TSettingsForm.BuildRX1Tab; -// Layout (alTop stack in FTabDispRX1): -// 8px top padding -// FFT group — full width -// 12px separator -// PnlMid — Spectrum (alLeft) | 10px gap | Waterfall (alClient), same height -// 12px separator -// Spectrum Grid group — full width -// hint (alBottom) -// -// Row step: STEP = BTN_H + 10 = 34px. Inner padding: R1 = 10. const - R1 = 10; // top padding inside groups - LX = 10; // left margin inside group - LW = 88; // label column width - CX = LX + LW + 4; // combo/edit column start (= 102) - STEP = BTN_H + 10; // row step (= 34) - SEP = 12; // gap between groups - COLW = 328; // left column panel width - COLGAP = 10; // horizontal gap between columns - GH2 = 100; // GroupBox height for 2-row groups - GH3 = 136; // GroupBox height for 3-row groups + R1 = 12; + LX = 12; + LW = 112; + CX = LX + LW + 8; + STEP = 34; + SEP = 12; + GH_FFT = 100; + GH_SPEC = 136; + GH_WF = 170; + GH_GRID = 100; var - PnlFFT, PnlMid, PnlL, PnlSepH, PnlR: TPanel; + PnlFFT, PnlSpec, PnlWF: TPanel; Grp: TGroupBox; - Lbl: TLabel; i: Integer; function MkGrp(AParent: TWinControl; const Cap: string; AH: Integer): TGroupBox; @@ -508,28 +501,17 @@ var S.Height := AH; end; - function MkEdit(AParent: TWinControl; AX, AY, AW: Integer; - AOnExit: TNotifyEvent): TEdit; + function MkFloat(AParent: TWinControl; AX, AY, AW: Integer; AValue: Double; + AMin, AMax, AInc: Double; AOnChange: TNotifyEvent): TFloatSpinEdit; begin - Result := TEdit.Create(Self); + Result := TFloatSpinEdit.Create(Self); Result.Parent := AParent; Result.SetBounds(AX, AY, AW, BTN_H); - Result.Color := CLR_PANEL; - Result.Font.Color := CLR_TEXT; - Result.Font.Name := 'Courier New'; - Result.Font.Size := 8; - Result.OnExit := AOnExit; - end; - - function MkSpin(AParent: TWinControl; AX, AY, AW: Integer; - AOnChange: TNotifyEvent): TSpinEdit; - begin - Result := TSpinEdit.Create(Self); - Result.Parent := AParent; - Result.SetBounds(AX, AY, AW, BTN_H); - Result.MinValue := 1; - Result.MaxValue := 256; - Result.Value := 2; + Result.Value := AValue; + Result.MinValue := AMin; + Result.MaxValue := AMax; + Result.Increment := AInc; + Result.DecimalPlaces := 1; Result.Color := CLR_PANEL; Result.Font.Color := CLR_TEXT; Result.Font.Name := 'Courier New'; @@ -538,24 +520,15 @@ var end; begin - // Hint at bottom (added first so alBottom is honoured by later alTop panels) - Lbl := MakeLbl(FTabDispRX1, - 'Backmult: higher value = slower decay (range 0.0 .. 1.0)', LX, 0, 0); - Lbl.Align := alBottom; - Lbl.Height := 22; - Lbl.Font.Color := CLR_TEXTDIM; - - // ── Top padding ─────────────────────────────────────────────────────────── MkSep(FTabDispRX1, 8); - // ── FFT (full width, alTop) ─────────────────────────────────────────────── PnlFFT := TPanel.Create(Self); PnlFFT.Parent := FTabDispRX1; PnlFFT.BevelOuter := bvNone; PnlFFT.Color := CLR_BG; PnlFFT.Align := alTop; - PnlFFT.Height := GH2; - Grp := MkGrp(PnlFFT, 'FFT', GH2); + PnlFFT.Height := GH_FFT; + Grp := MkGrp(PnlFFT, 'FFT / Analyzer', GH_FFT); MakeLbl(Grp, 'FFT Size:', LX, R1+4, LW); FCmbFFTSize := MakeCombo(Grp, CX, R1, 130, OnFFTSizeChange); for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]); @@ -563,75 +536,70 @@ begin FCmbWindow := MakeCombo(Grp, CX, R1+STEP, 250, OnWindowChange); for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]); - // ── Separator ───────────────────────────────────────────────────────────── MkSep(FTabDispRX1, SEP); - // ── Middle row: Spectrum (left) | Waterfall (right) at the same level ───── - PnlMid := TPanel.Create(Self); - PnlMid.Parent := FTabDispRX1; - PnlMid.BevelOuter := bvNone; - PnlMid.Color := CLR_BG; - PnlMid.Align := alTop; - PnlMid.Height := GH3; - - PnlL := TPanel.Create(Self); - PnlL.Parent := PnlMid; - PnlL.BevelOuter := bvNone; - PnlL.Color := CLR_BG; - PnlL.Align := alLeft; - PnlL.Width := COLW; - - PnlSepH := TPanel.Create(Self); - PnlSepH.Parent := PnlMid; - PnlSepH.BevelOuter := bvNone; - PnlSepH.Color := CLR_BG; - PnlSepH.Align := alLeft; - PnlSepH.Width := COLGAP; - - PnlR := TPanel.Create(Self); - PnlR.Parent := PnlMid; - PnlR.BevelOuter := bvNone; - PnlR.Color := CLR_BG; - PnlR.Align := alClient; - - // ── Spectrum (left column) ──────────────────────────────────────────────── - Grp := MkGrp(PnlL, 'Spectrum', GH3); + PnlSpec := TPanel.Create(Self); + PnlSpec.Parent := FTabDispRX1; + PnlSpec.BevelOuter := bvNone; + PnlSpec.Color := CLR_BG; + PnlSpec.Align := alTop; + PnlSpec.Height := GH_SPEC; + Grp := MkGrp(PnlSpec, 'Spectrum', GH_SPEC); MakeLbl(Grp, 'Detector:', LX, R1+4, LW); FCmbSpecDet := MakeCombo(Grp, CX, R1, 172, OnSpecDetChange); for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]); MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); FCmbSpecAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnSpecAvgModeChange); for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]); - MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW); - FSpinSpecCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnSpecCountChange); - MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64); - FEdSpecBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnSpecBackmultChange); - FEdSpecBackmult.Text := '0.45'; + MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW); + FSpecAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange); - // ── Waterfall (right column) ────────────────────────────────────────────── - Grp := MkGrp(PnlR, 'Waterfall', GH3); + MkSep(FTabDispRX1, SEP); + + PnlWF := TPanel.Create(Self); + PnlWF.Parent := FTabDispRX1; + PnlWF.BevelOuter := bvNone; + PnlWF.Color := CLR_BG; + PnlWF.Align := alTop; + PnlWF.Height := GH_WF; + Grp := MkGrp(PnlWF, 'Waterfall', GH_WF); MakeLbl(Grp, 'Detector:', LX, R1+4, LW); FCmbWfDet := MakeCombo(Grp, CX, R1, 172, OnWfDetChange); for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]); MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); FCmbWfAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnWfAvgModeChange); - for i := 0 to High(AVGMODE_NAMES) do FCmbWfAvg.Items.Add(AVGMODE_NAMES[i]); - MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW); - FSpinWfCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnWfCountChange); - MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64); - FEdWfBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnWfBackmultChange); - FEdWfBackmult.Text := '0.45'; + for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]); + MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW); + FWfAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange); + MakeLbl(Grp, 'High (dBm):', LX, R1+3*STEP+4, LW); + FWfHigh := MkFloat(Grp, CX, R1+3*STEP, 92, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange); + MakeLbl(Grp, 'Low (dBm):', CX+120, R1+3*STEP+4, 72); + FWfLow := MkFloat(Grp, CX+196, R1+3*STEP, 92, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange); + MakeLbl(Grp, 'AGC Offset:', LX, R1+4*STEP+4, LW); + FWfAGCOffset := MkFloat(Grp, CX, R1+4*STEP, 92, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange); - // ── Separator ───────────────────────────────────────────────────────────── MkSep(FTabDispRX1, SEP); - // ── Spectrum Grid (full width, alTop) ───────────────────────────────────── - Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH2); + Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH_GRID); MakeLbl(Grp, 'Ref Level (dBm):', LX, R1+4, 116); - FEdRefLevel := MkEdit(Grp, LX+120, R1, 80, OnRefLevelChange); + FEdRefLevel := TEdit.Create(Self); + FEdRefLevel.Parent := Grp; + FEdRefLevel.SetBounds(LX+120, R1, 80, BTN_H); + FEdRefLevel.Color := CLR_PANEL; + FEdRefLevel.Font.Color := CLR_TEXT; + FEdRefLevel.Font.Name := 'Courier New'; + FEdRefLevel.Font.Size := 8; + FEdRefLevel.OnExit := OnRefLevelChange; FEdRefLevel.Text := '-20'; MakeLbl(Grp, 'Range (dB):', LX, R1+STEP+4, LW); - FEdRange := MkEdit(Grp, CX, R1+STEP, 80, OnRangeChange); + FEdRange := TEdit.Create(Self); + FEdRange.Parent := Grp; + FEdRange.SetBounds(CX, R1+STEP, 80, BTN_H); + FEdRange.Color := CLR_PANEL; + FEdRange.Font.Color := CLR_TEXT; + FEdRange.Font.Name := 'Courier New'; + FEdRange.Font.Size := 8; + FEdRange.OnExit := OnRangeChange; FEdRange.Text := '110'; MakeLbl(Grp, 'Grid Step:', CX+90, R1+STEP+4, 72); FCmbGridStep := MakeCombo(Grp, CX+90+76, R1+STEP, 80, OnGridStepChange); @@ -656,10 +624,10 @@ end; // --------------------------------------------------------------------------- procedure TSettingsForm.LoadValues( - FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer; - SpecBackmult: Double; - WfDet, WfAvgMode, WfAvgCount: Integer; - WfBackmult: Double; + FFTSize, WinType, SpecDet, SpecAvgMode: Integer; + SpecAvgTimeMS: Double; + WfDet, WfAvgMode: Integer; + WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double; RefLevel, Range, GridStep: Double; const AudioOutDevName: string; const AudioInDevName: string); @@ -669,7 +637,7 @@ begin FLoading := True; try // FFT Size - Idx := 2; // default 4096 + Idx := 0; // default 4096 for i := 0 to High(FFT_SIZES) do if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end; FCmbFFTSize.ItemIndex := Idx; @@ -678,7 +646,7 @@ begin if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then FCmbWindow.ItemIndex := WinType else - FCmbWindow.ItemIndex := 5; // Kaiser + FCmbWindow.ItemIndex := 2; // Hann // Spectrum Detector if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then @@ -687,13 +655,11 @@ begin FCmbSpecDet.ItemIndex := 0; // Spectrum Avg Mode (WDSP value → combo index) - Idx := 2; // default Recursive + Idx := 0; // default None for i := 0 to High(AVGMODE_VALUES) do if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end; FCmbSpecAvg.ItemIndex := Idx; - - FSpinSpecCount.Value := EnsureRange(SpecAvgCount, 1, 256); - FEdSpecBackmult.Text := FormatFloat('0.##', SpecBackmult); + FSpecAvgTime.Value := EnsureRange(SpecAvgTimeMS, 0.0, 5000.0); // Waterfall Detector if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then @@ -702,13 +668,14 @@ begin FCmbWfDet.ItemIndex := 2; // Average // Waterfall Avg Mode - Idx := 4; // default Log Recursive - for i := 0 to High(AVGMODE_VALUES) do - if AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end; + Idx := 1; // default Recursive + for i := 0 to High(WF_AVGMODE_VALUES) do + if WF_AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end; FCmbWfAvg.ItemIndex := Idx; - - FSpinWfCount.Value := EnsureRange(WfAvgCount, 1, 256); - FEdWfBackmult.Text := FormatFloat('0.##', WfBackmult); + FWfAvgTime.Value := EnsureRange(WfAvgTimeMS, 0.0, 5000.0); + FWfHigh.Value := EnsureRange(WfHigh, -160.0, 20.0); + FWfLow.Value := EnsureRange(WfLow, -180.0, 0.0); + FWfAGCOffset.Value := EnsureRange(WfAGCOffset, -40.0, 40.0); // Grid FEdRefLevel.Text := FormatFloat('0.#', RefLevel); @@ -817,20 +784,29 @@ begin Result := 4096; end; -function TSettingsForm.GetSpecBackmult: Double; -var V: Double; Err: Integer; +function TSettingsForm.GetSpecAvgTimeMS: Double; begin - Val(FEdSpecBackmult.Text, V, Err); - if Err <> 0 then V := 0.45; - Result := EnsureRange(V, 0.0, 1.0); + Result := EnsureRange(FSpecAvgTime.Value, 0.0, 5000.0); end; -function TSettingsForm.GetWfBackmult: Double; -var V: Double; Err: Integer; +function TSettingsForm.GetWfAvgTimeMS: Double; begin - Val(FEdWfBackmult.Text, V, Err); - if Err <> 0 then V := 0.45; - Result := EnsureRange(V, 0.0, 1.0); + Result := EnsureRange(FWfAvgTime.Value, 0.0, 5000.0); +end; + +function TSettingsForm.GetWfHigh: Double; +begin + Result := EnsureRange(FWfHigh.Value, -160.0, 20.0); +end; + +function TSettingsForm.GetWfLow: Double; +begin + Result := EnsureRange(FWfLow.Value, -180.0, 0.0); +end; + +function TSettingsForm.GetWfAGCOffset: Double; +begin + Result := EnsureRange(FWfAGCOffset.Value, -40.0, 40.0); end; function TSettingsForm.GetRefLevel: Double; @@ -880,8 +856,7 @@ begin FCmbWindow.ItemIndex, FCmbSpecDet.ItemIndex, AvgVal, - FSpinSpecCount.Value, - GetSpecBackmult); + GetSpecAvgTimeMS); end; procedure TSettingsForm.FireWaterfallChange; @@ -892,15 +867,17 @@ begin if FLoading then Exit; if not Assigned(FOnWaterfallChange) then Exit; AvgIdx := FCmbWfAvg.ItemIndex; - if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then - AvgVal := AVGMODE_VALUES[AvgIdx] + if (AvgIdx >= 0) and (AvgIdx <= High(WF_AVGMODE_VALUES)) then + AvgVal := WF_AVGMODE_VALUES[AvgIdx] else - AvgVal := 3; + AvgVal := 1; FOnWaterfallChange( FCmbWfDet.ItemIndex, AvgVal, - FSpinWfCount.Value, - GetWfBackmult); + GetWfAvgTimeMS, + GetWfHigh, + GetWfLow, + GetWfAGCOffset); end; procedure TSettingsForm.FireGridChange; @@ -926,10 +903,7 @@ begin FireDisplayChange; end; procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject); begin FireDisplayChange; end; -procedure TSettingsForm.OnSpecCountChange(Sender: TObject); -begin FireDisplayChange; end; - -procedure TSettingsForm.OnSpecBackmultChange(Sender: TObject); +procedure TSettingsForm.OnSpecAvgTimeChange(Sender: TObject); begin FireDisplayChange; end; procedure TSettingsForm.OnWfDetChange(Sender: TObject); @@ -938,10 +912,10 @@ begin FireWaterfallChange; end; procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject); begin FireWaterfallChange; end; -procedure TSettingsForm.OnWfCountChange(Sender: TObject); +procedure TSettingsForm.OnWfAvgTimeChange(Sender: TObject); begin FireWaterfallChange; end; -procedure TSettingsForm.OnWfBackmultChange(Sender: TObject); +procedure TSettingsForm.OnWfThresholdChange(Sender: TObject); begin FireWaterfallChange; end; procedure TSettingsForm.OnRefLevelChange(Sender: TObject); diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 6e7226a..61b7b4b 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -98,6 +98,7 @@ const DISP_ID = 0; // ID для XCreateAnalyzer DSP_BUFSIZE = 1024; SPECTRUM_PIXELS = 1024; + DISPLAY_BLOCK_SIZE = 64; // Очередь IQ пакетов между сетевым и DSP потоком IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски) @@ -150,11 +151,15 @@ type // Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает) FSpecBuf: array of Double; + // Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize. + FDispBuf: array of Double; + FDispPos: Integer; // Аудио выходные буферы — аллоцируем один раз FOutL: array of Single; FOutR: array of Single; // DSP поток + очередь пакетов FDSPThread: TThread; + FDisplayThread:TThread; FQueue: array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem; FQueueHead: Integer; // пишет сетевой поток FQueueTail: Integer; // читает DSP поток @@ -168,14 +173,15 @@ type // Настройки анализатора FFFTSize: Integer; FWindowType: Integer; + FDisplayFPS: Integer; + FEffectiveDisplayFPS: Double; + FDisplayPixelCount: Integer; FSpecDetector: Integer; FSpecAvgMode: Integer; - FSpecAvgCount: Integer; - FSpecBackmult: Double; + FSpecAvgTimeMS: Double; FWfDetector: Integer; FWfAvgMode: Integer; - FWfAvgCount: Integer; - FWfBackmult: Double; + FWfAvgTimeMS: Double; FOnAudio: TOnAudioReady; FOnSpectrum: TOnSpectrumReady; @@ -213,11 +219,20 @@ type function ModeToWDSP(Mode: Integer): Integer; procedure ApplyDefaultFilter; + procedure FeedDisplaySample(const I, Q: Double); procedure ProcessRXBlock; procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure OpenAnalyzer; procedure CloseAnalyzer; procedure ApplyAnalyzerSettings; + function CalcDisplayFFTSize: Integer; + procedure CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer; + out EffectiveFPS: Double); + function NormalizeFFTSize(FFTSize: Integer): Integer; + procedure NormalizeDisplayParams(var Detector, AvgMode: Integer; + var AvgTimeMS: Double; ForWaterfall: Boolean); + procedure AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double; + out AvgCount: Integer; out Backmult: Double); procedure ApplyNRState; procedure ApplyNBState; procedure ApplySNBState; @@ -274,8 +289,9 @@ type // Настройки анализатора (применяются немедленно если открыт) procedure SetFFTParams(FFTSize, WinType: Integer); - procedure SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double); - procedure SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double); + procedure SetDisplayFPS(FPS: Integer); + procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double); + procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double); // Spectrum — вызывать из таймера (~20 fps) procedure UpdateSpectrum; @@ -304,12 +320,10 @@ type property WindowType: Integer read FWindowType; property SpecDetector: Integer read FSpecDetector; property SpecAvgMode: Integer read FSpecAvgMode; - property SpecAvgCount: Integer read FSpecAvgCount; - property SpecBackmult: Double read FSpecBackmult; + property SpecAvgTimeMS: Double read FSpecAvgTimeMS; property WfDetector: Integer read FWfDetector; property WfAvgMode: Integer read FWfAvgMode; - property WfAvgCount: Integer read FWfAvgCount; - property WfBackmult: Double read FWfBackmult; + property WfAvgTimeMS: Double read FWfAvgTimeMS; property OnAudio: TOnAudioReady read FOnAudio write FOnAudio; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall; @@ -325,6 +339,15 @@ implementation // Аналог tx_thread в piHPSDR/transmitter.c // =========================================================================== type + TDisplayThread = class(TThread) + private + FEngine: TWDSPEngine; + protected + procedure Execute; override; + public + constructor Create(AEngine: TWDSPEngine); + end; + TTXDSPThread = class(TThread) private FEngine: TWDSPEngine; @@ -334,6 +357,48 @@ type constructor Create(AEngine: TWDSPEngine); end; +function UsePanNormOneHz(Detector: Integer): Integer; +begin + // В Thetis нормализация к 1 Гц включается только для panadapter + // и только для detector modes 2/3/4. + if Detector in [2, 3, 4] then + Result := 1 + else + Result := 0; +end; + +constructor TDisplayThread.Create(AEngine: TWDSPEngine); +begin + FEngine := AEngine; + FreeOnTerminate := False; + inherited Create(False); +end; + +procedure TDisplayThread.Execute; +var + NextTick: QWord; + NowTick: QWord; + DelayMS: Integer; +begin + NextTick := GetTickCount64; + while not Terminated do + begin + if FEngine.FInitialized and FEngine.FAnalyzerOpen then + FEngine.UpdateSpectrum; + + Inc(NextTick, QWord(Max(1, Round(1000.0 / Max(1, FEngine.FDisplayFPS))))); + NowTick := GetTickCount64; + if NextTick > NowTick then + begin + DelayMS := Integer(NextTick - NowTick); + if DelayMS > 0 then + Sleep(DelayMS); + end + else + NextTick := NowTick; + end; +end; + constructor TTXDSPThread.Create(AEngine: TWDSPEngine); begin FEngine := AEngine; @@ -342,24 +407,44 @@ begin end; procedure TTXDSPThread.Execute; +// Аналог tx_thread в piHPSDR/transmitter.c: +// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate). +// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину. +// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде. var - Avail: Integer; + Avail: Integer; + PeriodMs: Integer; begin + // 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс + PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate); + while not Terminated do begin - RTLEventWaitFor(FEngine.FTXMicSem, 50); + // Ждём сигнала от mic-данных ИЛИ истечения периода — + // в piHPSDR timing идёт от receive thread; здесь — от таймаута + RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs); if Terminated then Break; if not FEngine.FTXActive then Continue; - // Обрабатываем все накопившиеся блоки + // Обрабатываем накопившиеся блоки с реальными mic-данными repeat Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); - if Avail >= FEngine.FAudioBufSize then + if (Avail >= FEngine.FAudioBufSize) and not Terminated then FEngine.ProcessTXBlock else Break; - until Terminated; + until False; + + if Terminated then Break; + + // Если mic-данных не было — всё равно обрабатываем один блок тишины. + // Аналог piHPSDR: если mic_ring пуст, tx_add_mic_sample получает 0.0. + // Это держит DUC IQ поток и обновляет спектр/водопад TX-сигналом. + Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) + and (TX_MIC_RING - 1); + if Avail < FEngine.FAudioBufSize then + FEngine.ProcessTXBlock; end; end; @@ -467,15 +552,16 @@ begin FRXAccPos := 0; // Analyzer defaults FFFTSize := 4096; - FWindowType := 5; // Kaiser + FWindowType := 2; // Hann + FDisplayFPS := 20; + FEffectiveDisplayFPS := 20.0; + FDisplayPixelCount := SPECTRUM_PIXELS; FSpecDetector := 0; // Peak FSpecAvgMode := 3; // Log Recursive - FSpecAvgCount := 2; - FSpecBackmult := 0.45; - FWfDetector := 2; // Average + FSpecAvgTimeMS := 30.0; + FWfDetector := 0; // Peak FWfAvgMode := 3; // Log Recursive - FWfAvgCount := 2; - FWfBackmult := 0.45; + FWfAvgTimeMS := 120.0; SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2) SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2) @@ -484,6 +570,8 @@ begin SetLength(FRXAccI, FBufSize); SetLength(FRXAccQ, FBufSize); SetLength(FSpecBuf, FBufSize * 2); + SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2); + FDispPos := 0; SetLength(FOutL, FAudioBufSize); SetLength(FOutR, FAudioBufSize); @@ -493,6 +581,7 @@ begin FDSPRunning := False; FQueueSem := RTLEventCreate; FDSPThread := nil; + FDisplayThread := nil; // TX инициализация FTXMicHead := 0; @@ -523,47 +612,75 @@ begin if FAnalyzerOpen then Exit; Success := -1; - XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil); + XCreateAnalyzer(DISP_ID, @Success, 262144, 1, 1, nil); if Success <> 0 then Exit; FFlp[0] := 0; ApplyAnalyzerSettings; FAnalyzerOpen := True; + if FDisplayThread = nil then + FDisplayThread := TDisplayThread.Create(Self); end; procedure TWDSPEngine.ApplyAnalyzerSettings; // Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров var + AnalyzerFFT: Integer; + AvgCount: Integer; + Backmult: Double; + WaterfallAvgCount: Integer; + WaterfallBackmult: Double; MaxW: Integer; + Overlap: Integer; +const + KEEP_TIME_SEC = 0.10; +var + DisplaySampleRate: Integer; + DisplayPixels: Integer; begin - // MaxW = FFTSize — минимально допустимо (piHPSDR approach) - MaxW := FFFTSize; + // FFT Size пользователя управляет именно display-analyzer, как в Thetis, + // но визуальная нормализация должна идти к ширине видимого окна, как в piHPSDR. + AnalyzerFFT := CalcDisplayFFTSize; + FDispPos := 0; + MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate, + KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100))); + CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS); + DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS); + FDisplayPixelCount := DisplayPixels; + DisplaySampleRate := FSampleRate; - // n_pixout=2: pixout=0 — спектр, pixout=1 — водопад - SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], FFFTSize, FAudioBufSize, FWindowType, 14.0, - 0, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW); + // Раздельные выходы display: pixout=0 панорама, pixout=1 водопад. + SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE, FWindowType, 14.0, + Overlap, 0, 0.0, 0.0, DisplayPixels, 1, 0, 0.0, 0.0, MaxW); - // Спектр (pixout=0) + AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult); + AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WaterfallAvgCount, WaterfallBackmult); SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector); SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode); - SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount); - SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult); - SetDisplayNormOneHz(DISP_ID, 0, 0); + SetDisplayNumAverage(DISP_ID, 0, AvgCount); + SetDisplayAvBackmult(DISP_ID, 0, Backmult); + SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector)); - // Водопад (pixout=1) SetDisplayDetectorMode(DISP_ID, 1, FWfDetector); SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode); - SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount); - SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult); + SetDisplayNumAverage(DISP_ID, 1, WaterfallAvgCount); + SetDisplayAvBackmult(DISP_ID, 1, WaterfallBackmult); SetDisplayNormOneHz(DISP_ID, 1, 0); - SetDisplaySampleRate(DISP_ID, FSampleRate); + // В Thetis сюда идёт реальный sample rate приёмника. + SetDisplaySampleRate(DISP_ID, DisplaySampleRate); end; procedure TWDSPEngine.CloseAnalyzer; begin if not FAnalyzerOpen then Exit; + if Assigned(FDisplayThread) then + begin + FDisplayThread.Terminate; + FDisplayThread.WaitFor; + FreeAndNil(FDisplayThread); + end; DestroyAnalyzer(DISP_ID); FAnalyzerOpen := False; end; @@ -719,6 +836,8 @@ begin SetLength(FSpecBuf, FBufSize * 2); SetLength(FRXAccI, FBufSize); SetLength(FRXAccQ, FBufSize); + SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2); + FDispPos := 0; // Переинициализируем WDSP с новым rate Open; @@ -771,6 +890,8 @@ begin if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000); if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000); + FeedDisplaySample(IR * SCALE, QR * SCALE); + FRXAccI[FRXAccPos] := IR * SCALE; FRXAccQ[FRXAccPos] := QR * SCALE; Inc(FRXAccPos); @@ -780,6 +901,22 @@ begin end; end; +procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double); +begin + if not FAnalyzerOpen then Exit; + if FTXActive then Exit; + + FDispBuf[FDispPos * 2] := I; + FDispBuf[FDispPos * 2 + 1] := Q; + Inc(FDispPos); + + if FDispPos >= DISPLAY_BLOCK_SIZE then + begin + Spectrum0(1, DISP_ID, 0, 0, @FDispBuf[0]); + FDispPos := 0; + end; +end; + procedure TWDSPEngine.ProcessRXBlock; var i: Integer; @@ -797,21 +934,10 @@ begin FRXIn[i * 2 + 1] := FRXAccQ[i]; end; - // Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn - if FAnalyzerOpen then - begin - Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double)); - end; - // DSP обработка Err := 0; fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); - // Подаём RX IQ в анализатор только когда не передаём. - // Во время TX анализатор кормит ProcessTXBlock (TX сигнал на той же частоте). - if FAnalyzerOpen and not FTXActive then - Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]); - // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter. // GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй // вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь! @@ -933,7 +1059,11 @@ end; procedure TWDSPEngine.SetSpectrumWidth(W: Integer); begin - if W > 0 then FLastSpectrumW := W; + if W <= 0 then Exit; + if FLastSpectrumW = W then Exit; + FLastSpectrumW := W; + if FAnalyzerOpen then + ApplyAnalyzerSettings; end; procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer); @@ -1104,6 +1234,7 @@ end; procedure TWDSPEngine.SetTXRun(Run: Boolean); begin FTXActive := Run; + FDispPos := 0; if Run then begin @@ -1135,22 +1266,35 @@ end; procedure TWDSPEngine.ProcessTXBlock; // Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов -// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback +// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback. +// Аналог tx_full_buffer() в piHPSDR/transmitter.c: +// если mic ring buffer содержит достаточно данных — берём их, +// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0. var i, tail: Integer; Err: Integer; + Avail: Integer; begin if not FInitialized then Exit; - // Читаем FAudioBufSize сэмплов из ring buffer → FTXIn - tail := FTXMicTail; - for i := 0 to FAudioBufSize - 1 do + Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); + + if Avail >= FAudioBufSize then begin - FTXIn[i * 2] := FTXMicRing[tail]; - FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал - tail := (tail + 1) and (TX_MIC_RING - 1); + // Читаем FAudioBufSize сэмплов из ring buffer → FTXIn + tail := FTXMicTail; + for i := 0 to FAudioBufSize - 1 do + begin + FTXIn[i * 2] := FTXMicRing[tail]; + FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал + tail := (tail + 1) and (TX_MIC_RING - 1); + end; + FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread) + end else + begin + // Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями) + FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); end; - FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread) Err := 0; fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err); @@ -1205,30 +1349,29 @@ procedure TWDSPEngine.UpdateSpectrum; var PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single; Flag: Integer; - i: Integer; + i, N: Integer; begin if not FInitialized or not FAnalyzerOpen then Exit; + N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); - // Спектр (pixout=0) Flag := 0; GetPixels(DISP_ID, 0, @PixBuf[0], @Flag); if Flag <> 0 then begin - for i := 0 to SPECTRUM_PIXELS - 1 do + for i := 0 to N - 1 do FSpectrumPixels[i] := PixBuf[i]; if Assigned(FOnSpectrum) then - FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS); + FOnSpectrum(FSpectrumPixels, N); end; - // Водопад (pixout=1) Flag := 0; GetPixels(DISP_ID, 1, @PixBuf[0], @Flag); if Flag <> 0 then begin - for i := 0 to SPECTRUM_PIXELS - 1 do + for i := 0 to N - 1 do FWaterfallPixels[i] := PixBuf[i]; if Assigned(FOnWaterfall) then - FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS); + FOnWaterfall(FWaterfallPixels, N); end; end; @@ -1254,46 +1397,150 @@ end; // Настройки анализатора (применяются немедленно) // --------------------------------------------------------------------------- -procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer); +function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer; begin - // Ограничения: FFT size должен быть >= bf_sz (FAudioBufSize = 1024) - // и степенью двойки, не превышать m_size=16384 (из XCreateAnalyzer) - if FFTSize < FAudioBufSize then FFTSize := FAudioBufSize; - if FFTSize > 16384 then FFTSize := 16384; + // Thetis-style display FFT range. + if FFTSize < 4096 then FFTSize := 4096; + Result := 4096; + while Result < FFTSize do + Result := Result shl 1; + if Result > 262144 then + Result := 262144; +end; + +function TWDSPEngine.CalcDisplayFFTSize: Integer; +begin + // FFT Size теперь снова управляет display-analyzer как в Thetis, + // но не влияет на аудио DSP цепочку. + Result := FFFTSize; +end; + +procedure TWDSPEngine.CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer; + out EffectiveFPS: Double); +var + TargetFPS: Double; +begin + TargetFPS := EnsureRange(FDisplayFPS, 5, 100); + // Thetis/piHPSDR-style timing: + // целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT. + Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS))); + EffectiveFPS := TargetFPS; +end; + +procedure TWDSPEngine.NormalizeDisplayParams(var Detector, AvgMode: Integer; + var AvgTimeMS: Double; ForWaterfall: Boolean); +begin + Detector := EnsureRange(Detector, 0, 4); + + if ForWaterfall then + begin + // Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis. + if AvgMode < 0 then AvgMode := 1; + end + else + if AvgMode < -1 then AvgMode := -1; + + if AvgMode > 3 then AvgMode := 3; + + if AvgMode <= 0 then + AvgTimeMS := 0.0 + else + AvgTimeMS := EnsureRange(AvgTimeMS, 10.0, 5000.0); +end; + +procedure TWDSPEngine.AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double; + out AvgCount: Integer; out Backmult: Double); +var + TimeSec: Double; + Frames: Double; + FPSv: Double; +begin + if AvgMode <= 0 then + begin + AvgCount := 1; + Backmult := 0.0; + Exit; + end; + + TimeSec := EnsureRange(AvgTimeMS, 10.0, 5000.0) * 0.001; + FPSv := Max(1.0, FEffectiveDisplayFPS); + Frames := FPSv * TimeSec; + AvgCount := EnsureRange(Round(Frames), 2, 60); + Backmult := Exp(-1.0 / Max(1E-6, FPSv * TimeSec)); + Backmult := EnsureRange(Backmult, 0.0, 0.9999); +end; + +procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer); +var + NewFFT: Integer; +begin + NewFFT := NormalizeFFTSize(FFTSize); if WinType < 0 then WinType := 0; if WinType > 6 then WinType := 6; - FFFTSize := FFTSize; + + if (FFFTSize = NewFFT) and (FWindowType = WinType) then Exit; + + FFFTSize := NewFFT; FWindowType := WinType; if FAnalyzerOpen then ApplyAnalyzerSettings; end; -procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; - Backmult: Double); +procedure TWDSPEngine.SetDisplayFPS(FPS: Integer); begin - FSpecDetector := Detector; - FSpecAvgMode := AvgMode; - FSpecAvgCount := AvgCount; - FSpecBackmult := Backmult; - if not FAnalyzerOpen then Exit; - SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector); - SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode); - SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount); - SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult); + FPS := EnsureRange(FPS, 1, 100); + if FDisplayFPS = FPS then Exit; + FDisplayFPS := FPS; + if FAnalyzerOpen then + ApplyAnalyzerSettings; end; -procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; - Backmult: Double); +procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode: Integer; + AvgTimeMS: Double); +var + AvgCount: Integer; + Backmult: Double; begin + NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False); + + if (FSpecDetector = Detector) and + (FSpecAvgMode = AvgMode) and + (Abs(FSpecAvgTimeMS - AvgTimeMS) < 1E-6) then Exit; + + FSpecDetector := Detector; + FSpecAvgMode := AvgMode; + FSpecAvgTimeMS := AvgTimeMS; + if not FAnalyzerOpen then Exit; + AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult); + SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector); + SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode); + SetDisplayNumAverage(DISP_ID, 0, AvgCount); + SetDisplayAvBackmult(DISP_ID, 0, Backmult); + SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector)); +end; + +procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer; + AvgTimeMS: Double); +var + AvgCount: Integer; + Backmult: Double; +begin + NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True); + + if (FWfDetector = Detector) and + (FWfAvgMode = AvgMode) and + (Abs(FWfAvgTimeMS - AvgTimeMS) < 1E-6) then Exit; + FWfDetector := Detector; FWfAvgMode := AvgMode; - FWfAvgCount := AvgCount; - FWfBackmult := Backmult; + FWfAvgTimeMS := AvgTimeMS; if not FAnalyzerOpen then Exit; + AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult); SetDisplayDetectorMode(DISP_ID, 1, FWfDetector); SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode); - SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount); - SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult); + SetDisplayNumAverage(DISP_ID, 1, AvgCount); + SetDisplayAvBackmult(DISP_ID, 1, Backmult); + SetDisplayNormOneHz(DISP_ID, 1, 0); end; end. diff --git a/WebPageHtml.pas b/WebPageHtml.pas index cc23154..8ee627e 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -56,6 +56,7 @@ begin 'button:hover{background:linear-gradient(180deg,#4c4c4c,#383838)}' + 'button.on{background:linear-gradient(180deg,#1e4d1e,#163216);border-color:#3d7a3d;color:#7fff7f}' + 'button.rx-tx{background:linear-gradient(180deg,#4d1e1e,#321616);border-color:#7a3d3d;color:#ff8080}' + + 'button.mox-on{background:linear-gradient(180deg,#6d1e1e,#4d0a0a);border-color:#cc3333;color:#ffaaaa;font-weight:bold}' + 'select{background:#222;color:#bbb;border:1px solid #484848;border-radius:3px;padding:0 4px;font-size:11px;height:22px;cursor:pointer}' + '.sl-g{display:flex;align-items:center;gap:3px}' + '.sl-lb{font-size:10px;color:#777;white-space:nowrap}' + @@ -110,6 +111,7 @@ begin '' + '|' + '
TX50
' + + '' + '|' + '
' + '