diff --git a/MainForm.pas b/MainForm.pas index d07d9dd..8dc854a 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -22,15 +22,15 @@ unit MainForm; interface uses - Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, WinFirewall, Forms, Controls, Graphics, Dialogs, + Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types, LCLIntf, LCLType, GraphType, HPSDRProtocol, HPSDRNetwork, WDSP, WDSPEngine, AudioOutput, AudioInput, - IntfGraphics, FPImage, Settings, WebServer, - CATEngine, CATSerial, CATTcp; + CATEngine, CATSerial, CATTcp, + SpectrumView; const CLR_BG = TColor($00101010); @@ -90,43 +90,6 @@ const MODE_NAMES: array[0..MODE_COUNT-1] of string = ( 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM'); - - - WFALL_PALETTE: array[0..255] of LongWord = ( - $00000000, $00020000, $00050000, $00080000, $000A0000, $000D0000, $00100000, $00120000, - $00150000, $00180000, $001A0000, $001D0000, $00200000, $00220000, $00250000, $00280000, - $002A0000, $002D0000, $00300000, $00320000, $00350000, $00380000, $003A0000, $003D0000, - $00400000, $00420000, $00450000, $00480000, $004A0000, $004D0000, $00500000, $00530000, - $00560000, $005A0000, $005D0000, $00600000, $00640000, $00670000, $006A0000, $006E0000, - $00710000, $00740000, $00780000, $007B0000, $007E0000, $00820000, $00850000, $00880000, - $008C0000, $008F0000, $00920000, $00960000, $00990000, $009C0000, $00A00000, $00A30000, - $00A60000, $00AA0000, $00AD0000, $00B00000, $00B40000, $00B60200, $00B90500, $00BB0700, - $00BE0A00, $00C00C00, $00C30F00, $00C51100, $00C81400, $00CA1600, $00CD1900, $00CF1B00, - $00D21E00, $00D42000, $00D72300, $00D92500, $00DC2800, $00DE2A00, $00E12D00, $00E32F00, - $00E63200, $00E73700, $00E83C00, $00E94100, $00EB4600, $00EC4B00, $00ED5000, $00EE5500, - $00F05A00, $00F15F00, $00F26400, $00F36900, $00F56E00, $00F67300, $00F77800, $00F87D00, - $00FA8200, $00FB8700, $00FC8C00, $00FD9100, $00FF9600, $00FD9A00, $00FB9E00, $00F9A200, - $00F8A600, $00F6AA00, $00F4AE00, $00F2B200, $00F1B600, $00EFBA00, $00EDBE00, $00EBC200, - $00EAC600, $00E8CA00, $00E6CE00, $00E4D200, $00E3D600, $00E1DA00, $00DFDE00, $00DDE200, - $00DCE600, $00D6E700, $00D0E800, $00CAE900, $00C4EB00, $00BEEC00, $00B8ED00, $00B2EE00, - $00ACF000, $00A6F100, $00A0F200, $009AF300, $0094F500, $008EF600, $0088F700, $0082F800, - $007CFA00, $0076FB00, $0070FC00, $006AFD00, $0064FF00, $005FFF04, $005AFF08, $0055FF0C, - $0050FF10, $004BFF14, $0046FF18, $0041FF1C, $003CFF20, $0037FF24, $0032FF28, $002CFF2C, - $0028FF30, $0023FF34, $001EFF38, $0019FF3C, $0014FF40, $000FFF44, $000AFF48, $0005FF4C, - $0000FF50, $0000FF56, $0000FF5C, $0000FF62, $0000FF68, $0000FF6E, $0000FF74, $0000FF7A, - $0000FF80, $0000FF86, $0000FF8C, $0000FF92, $0000FF98, $0000FF9E, $0000FFA4, $0000FFAA, - $0000FFB0, $0000FFB6, $0000FFBC, $0000FFC2, $0000FFC8, $0000FDCA, $0000FBCD, $0000F9D0, - $0000F8D3, $0000F6D5, $0000F4D8, $0000F2DB, $0000F1DE, $0000EFE0, $0000EDE3, $0000EBE6, - $0000EAE9, $0000E8EB, $0000E6EE, $0000E4F1, $0000E3F4, $0000E1F6, $0000DFF9, $0000DDFC, - $0000DCFF, $0000D7FF, $0000D2FF, $0000CDFF, $0000C8FF, $0000C3FF, $0000BEFF, $0000B9FF, - $0000B4FF, $0000AFFF, $0000AAFF, $0000A5FF, $0000A0FF, $00009BFF, $000096FF, $000091FF, - $00008CFF, $000087FF, $000082FF, $00007DFF, $000078FF, $000073FF, $00006FFF, $00006AFF, - $000066FF, $000061FF, $00005DFF, $000058FF, $000054FF, $00004FFF, $00004BFF, $000046FF, - $000042FF, $00003DFF, $000039FF, $000034FF, $000030FF, $00002BFF, $000027FF, $000022FF, - $00001EFF, $00112DFF, $00223CFF, $00334BFF, $00445AFF, $005569FF, $006678FF, $007787FF, - $008896FF, $0099A5FF, $00AAB4FF, $00BBC3FF, $00CCD2FF, $00DDE1FF, $00EEF0FF, $00FFFFFF - ); - type TDeviceItem = record Dev: THPSDRDevice; @@ -205,9 +168,6 @@ type FSpecDragX0: Integer; // X при нажатии FSpecDragFreq: Double; // FCenterFreq при нажатии FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад - // Маркер правой кнопки: вертикальная линия на спектре и водопаде - FMarkerActive: Boolean; // True = линия видима - FMarkerX: Integer; // X в пикселях (относительно ширины панели) FDriveLevel: Byte; FPAMaxPower: Double; FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон @@ -229,61 +189,38 @@ type FLastFwdW: Double; FLastSWR: Double; - // ---- Spectrum ---- - FSpectrumBuf: array[0..1023] of Single; - 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 - FSpecRefLevel: Double; // dBm top of scale (e.g. -20) - FSpecRange: Double; // dB range (e.g. 110 → bottom = -130) - FSpecGridStep: Double; // grid step in dB (e.g. 10) - FWaterfallBitmap:TBitmap; - FWaterfallTemp: TBitmap; - FWaterfallDirty: Boolean; - FWaterfallFrameInterval: Integer; - FWaterfallFrameCounter: Integer; - FSpectrumWidth: Integer; + // ---- Spectrum / Waterfall view ---- + FSpecView: TSpectrumView; + FSpectrumWidth: Integer; // актуальная ширина для FDSPEngine и resize-детектора FSpectrumHeight: Integer; FWaterfallHeight:Integer; - // ---- Visibility settings ---- FShowSpectrum: Boolean; FShowWaterfall: Boolean; - FDisplayFPS: Integer; // таймер спектра: кадров в секунду (1..100) - // ---- Splitter между спектром и водопадом ---- - FSplitter: TPanel; // визуальная полоса-разделитель - FSplitterDrag: Boolean; // True = идёт перетаскивание - FSplitterDragY0: Integer; // Y мыши в начале перетаскивания (в координатах PanelRight) - FSplitterSH0: Integer; // FSpectrumHeight в начале перетаскивания - FSplitterRatio: Double; // соотношение высоты спектра (0.15..0.85), сохраняется - // Водопад: прямой массив пикселей BGRA + RawImage bitmap без IntfImage roundtrip - FWfPixels: array of LongWord; // W×H пикселей, строка 0 = верх (новейшая) - FWfBitmap: TBitmap; - FWfBitmapW: Integer; - FWfBitmapH: Integer; - // Спектр: кэш фона (сетка+фон) — перерисовывается только при ресайзе - FGridBitmap: TBitmap; - FGridBitmapW: Integer; - FGridBitmapH: Integer; - FSpPts: array of TPoint; - FSpPtsLen: Integer; - // Градиент спектра: кэшированный IntfImage — без alloc каждый кадр - FSpecGradImg: TLazIntfImage; - FSpecGradBmp: TBitmap; // кэш bitmap для градиента — без alloc каждый кадр - // Водопад: кэшированный IntfImage — без alloc/free каждый кадр - FWfIntfImg: TLazIntfImage; - // Кэш линейки частот — перерисовывается только при смене FCenterFreq/FVfoA - FRulerBitmap: TBitmap; - FRulerLastFreq: Double; - FRulerLastVfo: Double; + FDisplayFPS: Integer; + FWaterfallDirty: Boolean; + // Waterfall settings (kept here for MakeGlobalSettings, buttons, settings dialog) + FWfAGCEnabled: Boolean; + FWfNFEnabled: Boolean; + FWfManualHigh: Double; + FWfManualLow: Double; + FWfAGCOffset: Double; + // Spectrum display settings (for settings save/load) + FSpecRefLevel: Double; + FSpecRange: Double; + FSpecGridStep: Double; + // DSP data buffers (copy of last DSP output, for WebServer.PushSpectrum) + FSpectrumBuf: array[0..1023] of Single; + FWaterfallBuf: array[0..1023] of Single; + FSpectrumBufCount: Integer; + FWaterfallBufCount:Integer; + // Waterfall frame timing + FWaterfallFrameInterval: Integer; + FWaterfallFrameCounter: Integer; + // ---- Splitter ---- + FSplitterDrag: Boolean; + FSplitterDragY0: Integer; + FSplitterSH0: Integer; + FSplitterRatio: Double; // --- Settings --- FSettings: TSettingsManager; FWebServer: TWebServer; // веб-интерфейс (порт 8080) @@ -346,11 +283,9 @@ type BtnCTun: TFlatButton; PanelRX: TPanel; - LblAGC: TLabel; BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF LblAGCTop: TLabel; // показывает значение уровня TrkAGC: TTrackBar; // ползунок уровня AGC - LblVol: TLabel; TrkVolume: TTrackBar; BtnNR: TFlatButton; BtnNB: TFlatButton; @@ -360,9 +295,8 @@ type BtnWfAGC: TFlatButton; BtnWfNF: TFlatButton; - PanelSMeterRight: TPanel; // контейнер S-метра справа - PbSMeterRight: TPaintBox;// новый большой S-метр справа - FSmBitmap: TBitmap; // off-screen буфер для alpha-blend + PanelSMeterRight: TPanel; + PbSMeterRight: TPaintBox; PanelTX: TPanel; LblDrv: TLabel; @@ -400,23 +334,11 @@ type procedure PositionSpanOverlayButtons; procedure UpdateVfoDisplay; function FormatFreq(Hz: Double): string; - function ScaleX(X, Total, Width: Integer): Integer; - procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect; Value, Peak, MinVal: Double; - out ZX1, ZX2, ZY1, ZY2: Integer); - procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect; X1, X2, Y1, Y2: Integer); - procedure PbSMeterRightPaint(Sender: TObject); - procedure DrawBarMeter(ACanvas: TCanvas; R: TRect; - Value, MaxVal: Double; BarColor: TColor); - procedure DrawSpectrum; - procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); - procedure DrawMarkerLine(C: TCanvas; W, H: Integer); - procedure DrawWaterfall; - procedure ResetWfAvgBuf; procedure ResizeSMeter; - procedure FillDemoSpectrum; - procedure ResetSpectrumBuf; procedure ResizeSpectrumPanels; + procedure InvalidateGridCache; + procedure SyncSpecViewFreq; procedure RecreateDSPEngine(ASampleRate: Integer); function EnsureWDSPWisdom: Boolean; @@ -469,7 +391,6 @@ type X, Y: Integer); procedure PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); - procedure DrawRuler; procedure PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState; @@ -582,7 +503,6 @@ type procedure BtnWfNFClick(Sender: TObject); procedure BtnHidePanelClick(Sender: TObject); procedure BtnSettingsClick(Sender: TObject); - procedure InvalidateGridCache; procedure PositionVfoOverlay; procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer); procedure PbSpectrumDblClick(Sender: TObject); @@ -595,11 +515,6 @@ type procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); procedure BtnAGCModeClick(Sender: TObject); procedure TrkAGCChange(Sender: TObject); - procedure PbFwdPowerPaint(Sender: TObject); - procedure PbSWRPaint(Sender: TObject); - procedure PbSpectrumPaint(Sender: TObject); - procedure PbRulerPaint(Sender: TObject); - procedure PbWaterfallPaint(Sender: TObject); procedure MeterTimerTick(Sender: TObject); procedure SpectrumTimerTick(Sender: TObject); procedure RightPanelResize(Sender: TObject); @@ -666,114 +581,6 @@ 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); - // Для сырого буфера водопада нужен BGRA32 в памяти: - // LongWord = $AARRGGBB, в little-endian байты идут как B,G,R,A. - Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; -end; - -function WaterfallEnhancedColorThetis(ValueDB, LowDB, HighDB: Double): LongWord; -var - Overall, Local: Double; - R, G, B: Integer; -begin - if ValueDB <= LowDB then - begin - Result := $FF000000; - Exit; - end; - - if ValueDB >= HighDB then - begin - Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; - Exit; - end; - - Overall := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB); - - if Overall < (2.0 / 9.0) then - begin - Local := Overall / (2.0 / 9.0); - R := 0; - G := 0; - B := Round(Local * 255.0); - end - else if Overall < (3.0 / 9.0) then - begin - Local := (Overall - (2.0 / 9.0)) / (1.0 / 9.0); - R := 0; - G := Round(Local * 255.0); - B := 255; - end - else if Overall < (4.0 / 9.0) then - begin - Local := (Overall - (3.0 / 9.0)) / (1.0 / 9.0); - R := 0; - G := 255; - B := Round((1.0 - Local) * 255.0); - end - else if Overall < (5.0 / 9.0) then - begin - Local := (Overall - (4.0 / 9.0)) / (1.0 / 9.0); - R := Round(Local * 255.0); - G := 255; - B := 0; - end - else if Overall < (7.0 / 9.0) then - begin - Local := (Overall - (5.0 / 9.0)) / (2.0 / 9.0); - R := 255; - G := Round((1.0 - Local) * 255.0); - B := 0; - end - else if Overall < (8.0 / 9.0) then - begin - Local := (Overall - (7.0 / 9.0)) / (1.0 / 9.0); - R := 255; - G := 0; - B := Round(Local * 255.0); - end - else - begin - Local := (Overall - (8.0 / 9.0)) / (1.0 / 9.0); - R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0); - G := Round(Local * 255.0 * 0.5); - B := 255; - end; - - R := EnsureRange(R, 0, 255); - G := EnsureRange(G, 0, 255); - B := EnsureRange(B, 0, 255); - // Для сырого буфера водопада нужен BGRA32 в памяти: - // LongWord = $AARRGGBB, в little-endian байты идут как B,G,R,A. - Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; -end; - constructor TWisdomBuildThread.Create(const ADirectory: string); begin inherited Create(False); @@ -1050,10 +857,7 @@ begin for i := 0 to BAND_COUNT - 1 do StyleButton(BtnBand[i], i = BandIdx); // Сбрасываем waterfall thresholds при смене диапазона - FWfHigh := FWfManualHigh; - FWfLow := FWfManualLow; - // Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон - ResetWfAvgBuf; + FSpecView.ResetWfAvgBuf; // --- Режим: сначала устанавливаем FMode, потом кнопки и WDSP --- FMode := B.Mode; @@ -1148,8 +952,6 @@ begin FWfManualHigh := -80.0; FWfManualLow := -130.0; FWfAGCOffset := 0.0; - FWfHigh := FWfManualHigh; // начальная верхняя граница dBm - FWfLow := FWfManualLow; // начальная нижняя граница dBm FWfAGCEnabled := True; FWfNFEnabled := False; FSpectrumBufCount := 1024; @@ -1235,27 +1037,28 @@ begin FShowSpectrum := True; FShowWaterfall := True; FDisplayFPS := 60; - FWaterfallDirty := True; - FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию + FSplitterRatio := 0.40; FSplitterDrag := False; - FSpectrumBitmap := TBitmap.Create; - FWaterfallBitmap := TBitmap.Create; - FWaterfallTemp := TBitmap.Create; - FSpecGradImg := nil; - FSpecGradBmp := TBitmap.Create; - FWfIntfImg := nil; - FWfBitmap := TBitmap.Create; - FWfBitmapW := 0; - FWfBitmapH := 0; - FGridBitmap := TBitmap.Create; - FGridBitmapW := 0; - FGridBitmapH := 0; - FSpPtsLen := 0; - FRulerBitmap := TBitmap.Create; - FRulerLastFreq := -1.0; - FRulerLastVfo := -1.0; - ResetSpectrumBuf; + FSpecView := TSpectrumView.Create; + FSpecView.VfoA := FVfoA; + FSpecView.VfoB := FVfoB; + FSpecView.ActiveVfo := FActiveVfo; + FSpecView.CenterFreq := FCenterFreq; + FSpecView.SpanHz := FSpanHz; + FSpecView.Mode := FMode; + FSpecView.FilterBW := FFilterBW; + FSpecView.AGCTop := FAGCTop; + FSpecView.WfAGCEnabled := FWfAGCEnabled; + FSpecView.WfNFEnabled := FWfNFEnabled; + FSpecView.WfManualHigh := FWfManualHigh; + FSpecView.WfManualLow := FWfManualLow; + FSpecView.WfAGCOffset := FWfAGCOffset; + FSpecView.SpecRefLevel := FSpecRefLevel; + FSpecView.SpecRange := FSpecRange; + FSpecView.SpecGridStep := FSpecGridStep; + FSpecView.WfFrameInterval := FWaterfallFrameInterval; + FSpecView.FillDemoSpectrum; FNetwork := THPSDRNetwork.Create; FNetwork.OnDeviceFound := OnDeviceFound; @@ -1332,15 +1135,7 @@ begin FAudioIn.Free; FDSPEngine.Close; FDSPEngine.Free; - FSpectrumBitmap.Free; - FWaterfallBitmap.Free; - FWaterfallTemp.Free; - FRulerBitmap.Free; - FWfBitmap.Free; - FGridBitmap.Free; - FreeAndNil(FSpecGradImg); - FSpecGradBmp.Free; - FreeAndNil(FWfIntfImg); + FreeAndNil(FSpecView); // Сохраняем настройки при закрытии if FDevConnected then @@ -1679,13 +1474,13 @@ begin PbFwdPower := TPaintBox.Create(Self); PbFwdPower.Parent := PanelTX; PbFwdPower.SetBounds(34, 74, LEFT_W - 38, 12); - PbFwdPower.OnPaint := PbFwdPowerPaint; + PbFwdPower.OnPaint := FSpecView.PaintFwdPower; MakeLbl(PanelTX, 'SWR', 4, 92); PbSWR := TPaintBox.Create(Self); PbSWR.Parent := PanelTX; PbSWR.SetBounds(34, 90, LEFT_W - 38, 12); - PbSWR.OnPaint := PbSWRPaint; + PbSWR.OnPaint := FSpecView.PaintSWR; // ---- Right panel ---- PanelRight := TPanel.Create(Self); @@ -1743,12 +1538,12 @@ begin PbSMeterRight := TPaintBox.Create(Self); PbSMeterRight.Parent := PanelSMeterRight; PbSMeterRight.Align := alClient; - PbSMeterRight.OnPaint := PbSMeterRightPaint; + PbSMeterRight.OnPaint := FSpecView.PaintSMeterRight; PbSMeterRight.Color := CLR_PANEL; PbSpectrum := TPaintBox.Create(Self); PbSpectrum.Parent := PanelRight; - PbSpectrum.OnPaint := PbSpectrumPaint; + PbSpectrum.OnPaint := FSpecView.PaintSpectrum; PbSpectrum.OnMouseDown := PbSpectrumMouseDown; PbSpectrum.OnDblClick := PbSpectrumDblClick; PbSpectrum.OnMouseMove := PbSpectrumMouseMove; @@ -1756,8 +1551,9 @@ begin PbRuler := TPaintBox.Create(Self); PbRuler.Parent := PanelRight; - PbRuler.OnPaint := PbRulerPaint; + PbRuler.OnPaint := FSpecView.PaintRuler; PbRuler.Cursor := crDefault; + FSpecView.PbRuler := PbRuler; // ---- Splitter между спектром+линейкой и водопадом ---- PanelSplitter := TPanel.Create(Self); @@ -1772,7 +1568,7 @@ begin PbWaterfall := TPaintBox.Create(Self); PbWaterfall.Parent := PanelRight; - PbWaterfall.OnPaint := PbWaterfallPaint; + PbWaterfall.OnPaint := FSpecView.PaintWaterfall; PbWaterfall.OnMouseDown := PbWaterfallMouseDown; PbWaterfall.OnMouseMove := PbWaterfallMouseMove; PbWaterfall.OnMouseUp := PbWaterfallMouseUp; @@ -1938,6 +1734,7 @@ var AvailH: Integer; begin if PanelRight = nil then Exit; + if Assigned(FSpecView) then SyncSpecViewFreq; RW := PanelRight.ClientWidth; RH := PanelRight.ClientHeight; TopOff := PanelSpanButtons.Height; @@ -1975,32 +1772,12 @@ begin FSpectrumWidth := RW; FSpectrumHeight := SH; FWaterfallHeight := 0; - if (FSpectrumBitmap <> nil) and (RW > 0) and (SH > 0) then - begin - FSpectrumBitmap.SetSize(RW, SH); - FSpectrumBitmap.Canvas.Brush.Color := CLR_BG; - FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, RW, SH)); - FreeAndNil(FSpecGradImg); - FGridBitmapW := 0; - FGridBitmapH := 0; - FSpPtsLen := 0; - end; - if (FWaterfallBitmap <> nil) then - begin - FWaterfallBitmap.SetSize(1, 1); - FWfBitmapW := 0; - FWfBitmapH := 0; - SetLength(FWfPixels, 0); - end; - if (FRulerBitmap <> nil) and (RW > 0) then - begin - FRulerBitmap.SetSize(RW, RULER_H); - FRulerLastFreq := -1.0; - FRulerLastVfo := -1.0; - end; if RW > 0 then begin - DrawSpectrum; + FSpecView.SetSpectrumBitmapSize(RW, SH); + FSpecView.SetWaterfallBitmapSize(1, 1); + FSpecView.SetRulerSize(RW, RULER_H); + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; PbRuler.Invalidate; end; @@ -2024,32 +1801,12 @@ begin FSpectrumWidth := RW; FSpectrumHeight := 0; FWaterfallHeight := WH; - if (FSpectrumBitmap <> nil) then - begin - FSpectrumBitmap.SetSize(1, 1); - FreeAndNil(FSpecGradImg); - FGridBitmapW := 0; - FGridBitmapH := 0; - FSpPtsLen := 0; - end; - if (FWaterfallBitmap <> nil) and (RW > 0) and (WH > 0) then - begin - FWaterfallBitmap.SetSize(RW, WH); - FWaterfallBitmap.Canvas.Brush.Color := clBlack; - FWaterfallBitmap.Canvas.FillRect(Rect(0, 0, RW, WH)); - FWfBitmapW := 0; - FWfBitmapH := 0; - SetLength(FWfPixels, 0); - end; - if (FRulerBitmap <> nil) and (RW > 0) then - begin - FRulerBitmap.SetSize(RW, RULER_H); - FRulerLastFreq := -1.0; - FRulerLastVfo := -1.0; - end; if RW > 0 then begin - DrawWaterfall; + FSpecView.SetSpectrumBitmapSize(1, 1); + FSpecView.SetWaterfallBitmapSize(RW, WH); + FSpecView.SetRulerSize(RW, RULER_H); + FSpecView.DrawWaterfall; PbWaterfall.Invalidate; PbRuler.Invalidate; end; @@ -2092,38 +1849,12 @@ begin FWaterfallHeight := WH; // Пересоздаём bitmap точно под новый размер - if (FSpectrumBitmap <> nil) and (RW > 0) and (SH > 0) then - begin - FSpectrumBitmap.SetSize(RW, SH); - FSpectrumBitmap.Canvas.Brush.Color := CLR_BG; - FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, RW, SH)); - FreeAndNil(FSpecGradImg); - // Сбрасываем кэш фона/сетки и кэш точек кривой - FGridBitmapW := 0; - FGridBitmapH := 0; - FSpPtsLen := 0; - end; - if (FWaterfallBitmap <> nil) and (RW > 0) and (WH > 0) then - begin - FWaterfallBitmap.SetSize(RW, WH); - FWaterfallBitmap.Canvas.Brush.Color := clBlack; - FWaterfallBitmap.Canvas.FillRect(Rect(0, 0, RW, WH)); - // Сбрасываем буфер пикселей водопада - FWfBitmapW := 0; - FWfBitmapH := 0; - SetLength(FWfPixels, 0); - end; - if (FRulerBitmap <> nil) and (RW > 0) then - begin - FRulerBitmap.SetSize(RW, 18); - FRulerLastFreq := -1.0; - FRulerLastVfo := -1.0; - end; - - // Немедленно перерисовываем с новыми размерами if RW > 0 then begin - DrawSpectrum; + if SH > 0 then FSpecView.SetSpectrumBitmapSize(RW, SH); + if WH > 0 then FSpecView.SetWaterfallBitmapSize(RW, WH); + FSpecView.SetRulerSize(RW, RULER_H); + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; PbWaterfall.Invalidate; @@ -2280,1036 +2011,25 @@ begin end; // =========================================================================== -// Helpers +// Rendering helpers — delegated to TSpectrumView // =========================================================================== -// Замена MulDiv (Windows API) — простая целочисленная пропорция -function TMainForm.ScaleX(X, Total, Width: Integer): Integer; -begin - if Total = 0 then Result := 0 - else Result := Round(X / Total * Width); -end; - -// =========================================================================== -// Drawing -// =========================================================================== - - -procedure TMainForm.DrawBarMeter(ACanvas: TCanvas; R: TRect; - Value, MaxVal: Double; BarColor: TColor); -var - Pct, BarW: Integer; -begin - ACanvas.Brush.Color := TColor($00080808); - ACanvas.FillRect(R); - - if MaxVal > 0 then - Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2)) - else - Pct := 0; - - BarW := Pct; - ACanvas.Brush.Color := BarColor; - ACanvas.Pen.Color := BarColor; - ACanvas.FillRect(Rect(R.Left+1, R.Top+1, R.Left+1+BarW, R.Bottom-1)); - - ACanvas.Brush.Style := bsClear; - ACanvas.Pen.Color := CLR_BORDER; - ACanvas.Rectangle(R); -end; - -procedure TMainForm.ResetWfAvgBuf; -begin - 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 FWaterfallBuf[i] := -130.0; - FWfHigh := FWfManualHigh; - FWfLow := FWfManualLow; - FWaterfallDirty := True; -end; - procedure TMainForm.InvalidateGridCache; -// Сбрасываем кэш фона/сетки — перерисуется на следующем тике DrawSpectrum begin - FGridBitmapW := 0; - FGridBitmapH := 0; - FSpectrumDirty := True; + FSpecView.InvalidateGridCache; end; -procedure TMainForm.FillDemoSpectrum; -var - i: Integer; - FreqOff, Noise, Sig: Double; +procedure TMainForm.SyncSpecViewFreq; begin - for i := 0 to FSpectrumWidth - 1 do - begin - if FSpectrumWidth > 1 then - FreqOff := (i / (FSpectrumWidth - 1) - 0.5) * FSpanHz - else - FreqOff := 0; - Noise := -110 + (Random - 0.5) * 6; - if Abs(FreqOff) < 2000 then - Sig := -50 - Abs(FreqOff) / 200 - else - Sig := -999; - FSpectrumBuf[i mod 1024] := Max(Noise, Sig); - end; + FSpecView.VfoA := FVfoA; + FSpecView.VfoB := FVfoB; + FSpecView.ActiveVfo := FActiveVfo; + FSpecView.CenterFreq := FCenterFreq; + FSpecView.SpanHz := FSpanHz; + FSpecView.Mode := FMode; + FSpecView.FilterBW := FFilterBW; end; -procedure TMainForm.DrawMarkerLine(C: TCanvas; W, H: Integer); -var - MX: Integer; - MarkerFreq: Double; - MarkerLbl: string; -begin - MX := Round(FMarkerX / 1000.0 * W); - if (MX < 0) or (MX >= W) then Exit; - - MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz; - MarkerLbl := FormatFreq(Round(MarkerFreq)); - - C.Pen.Color := TColor($004444FF); // красный (BGR формат) - C.Pen.Width := 1; - C.Pen.Style := psSolid; - C.MoveTo(MX, 0); - C.LineTo(MX, H); - - C.Font.Color := TColor($004444FF); - C.Font.Size := 7; - C.Font.Name := 'Courier New'; - if MX + 4 + C.TextWidth(MarkerLbl) < W then - C.TextOut(MX + 4, 4, MarkerLbl) - else - C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl); -end; - -// Вспомогательная функция: BGRA32 top-to-bottom описание для TLazIntfImage. -// Заменяет Init_BPP32_B8G8R8A8_BIO_TTB которая не всегда доступна в старых Lazarus. -procedure InitRawDesc32(var Desc: TRawImageDescription; AWidth, AHeight: Integer); -begin - FillChar(Desc, SizeOf(Desc), 0); - Desc.Format := ricfRGBA; - Desc.Width := AWidth; - Desc.Height := AHeight; - Desc.Depth := 32; - Desc.BitOrder := riboBitsInOrder; - Desc.ByteOrder := riboLSBFirst; - Desc.LineOrder := riloTopToBottom; - Desc.BitsPerPixel:= 32; - Desc.LineEnd := rileDWordBoundary; - // BGRA: B=bits 0..7, G=8..15, R=16..23, A=24..31 - Desc.BlueShift := 0; Desc.BluePrec := 8; - Desc.GreenShift := 8; Desc.GreenPrec := 8; - Desc.RedShift := 16; Desc.RedPrec := 8; - Desc.AlphaShift := 24; Desc.AlphaPrec := 8; -end; - -function MixColorBGR(C1, C2: TColor; T: Double): TColor; -var - B1, G1, R1: Integer; - B2, G2, R2: Integer; - B, G, R: Integer; -begin - if T < 0.0 then T := 0.0 else - if T > 1.0 then T := 1.0; - - B1 := (C1 shr 16) and $FF; G1 := (C1 shr 8) and $FF; R1 := C1 and $FF; - B2 := (C2 shr 16) and $FF; G2 := (C2 shr 8) and $FF; R2 := C2 and $FF; - - B := Round(B1 + (B2 - B1) * T); - G := Round(G1 + (G2 - G1) * T); - R := Round(R1 + (R2 - R1) * T); - Result := TColor((B shl 16) or (G shl 8) or R); -end; - -procedure PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor); -var - Y: Integer; - T: Double; -begin - if (W <= 0) or (H <= 0) then Exit; - C.Pen.Style := psClear; - C.Brush.Style := bsSolid; - for Y := 0 to H - 1 do - begin - T := Y / Max(1, H - 1); - C.Brush.Color := MixColorBGR(TopColor, BottomColor, T); - C.FillRect(Rect(0, Y, W, Y + 1)); - end; - C.Pen.Style := psSolid; -end; - -procedure PaintHorizontalFade(C: TCanvas; X1, X2, H: Integer; LeftColor, RightColor: TColor); -var - X: Integer; - T: Double; -begin - if (H <= 0) or (X2 <= X1) then Exit; - C.Pen.Style := psClear; - C.Brush.Style := bsSolid; - for X := X1 to X2 - 1 do - begin - T := (X - X1) / Max(1, X2 - X1 - 1); - C.Brush.Color := MixColorBGR(LeftColor, RightColor, T); - C.FillRect(Rect(X, 0, X + 1, H)); - end; - C.Pen.Style := psSolid; -end; - -procedure TMainForm.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); -// ═══════════════════════════════════════════════════════════════════════ -// Попиксельный градиент через кэшированный FSpecGradImg. -// Старый подход: H/2 × MoveTo+LineTo = ~150-250 GDI calls = ~3-5ms -// Новый подход: записываем пиксели в FSpecGradImg + один BitBlt ≈ 1ms -// ═══════════════════════════════════════════════════════════════════════ -var - Row, Col: Integer; - YMin: Integer; - Alpha: Integer; - RowPtr: PByte; - BPPg: Integer; - RG, GG: Byte; - Desc: TRawImageDescription; -begin - if FSpectrumBitmap = nil then Exit; - if (W <= 0) or (H <= 0) then Exit; - - YMin := H; - for Col := 0 to W - 1 do - if SpPts[Col].Y < YMin then YMin := SpPts[Col].Y; - if YMin >= H then Exit; - - // Создаём/переиспользуем FSpecGradImg - if (FSpecGradImg = nil) or - (FSpecGradImg.Width <> W) or (FSpecGradImg.Height <> H) then - begin - FreeAndNil(FSpecGradImg); - FSpecGradImg := TLazIntfImage.Create(W, H); - InitRawDesc32(Desc, W, H); - FSpecGradImg.DataDescription := Desc; - FSpecGradImg.CreateData; - end; - - // Очищаем прозрачным - if FSpecGradImg.PixelData <> nil then - FillChar(FSpecGradImg.PixelData^, W * H * 4, 0); - - BPPg := FSpecGradImg.DataDescription.BitsPerPixel div 8; - if BPPg < 3 then BPPg := 4; - - for Row := YMin to H - 1 do - begin - Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200); - if Alpha <= 0 then Continue; - if Alpha > 200 then Alpha := 200; - GG := Byte(Alpha shr 1); // B-канал - RG := Byte(Alpha); // G-канал - - RowPtr := FSpecGradImg.GetDataLineStart(Row); - if RowPtr = nil then Continue; - - for Col := 0 to W - 1 do - begin - if SpPts[Col].Y <= Row then - begin - RowPtr[0] := GG; RowPtr[1] := RG; RowPtr[2] := 0; - if BPPg >= 4 then RowPtr[3] := Byte(Alpha); - end - else - begin - RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0; - if BPPg >= 4 then RowPtr[3] := 0; - end; - Inc(RowPtr, BPPg); - end; - end; - - // Выводим через кэшированный bitmap → BitBlt на спектрум-bitmap - FSpecGradBmp.LoadFromIntfImage(FSpecGradImg); - LCLIntf.BitBlt(FSpectrumBitmap.Canvas.Handle, 0, 0, W, H, - FSpecGradBmp.Canvas.Handle, 0, 0, $CC0020); -end; - -procedure TMainForm.DrawSpectrum; -var - C: TCanvas; - GC: TCanvas; // для рисования в FGridBitmap - i, Yp, W, H, GX: Integer; - DBmin, DBmax: Double; - dB: Double; - VfoX, X1, X2: Integer; - Lo_Hz, Hi_Hz, Half: Double; - AGCy, AGCHangY: Integer; - SrcF, Frac, dBv: Double; - S0, S1: Integer; - InvRange: Double; - LabelBandW: Integer; - SrcCount: Integer; -begin - if FSpectrumBitmap = nil then Exit; - W := FSpectrumBitmap.Width; - H := FSpectrumBitmap.Height; - if (W <= 0) or (H <= 0) then Exit; - - C := FSpectrumBitmap.Canvas; - DBmax := FSpecRefLevel; - DBmin := FSpecRefLevel - FSpecRange; - InvRange := 1.0 / (DBmax - DBmin); - - // ── 1. Фон+сетка из кэша ─────────────────────────────────────────────── - if (FGridBitmapW <> W) or (FGridBitmapH <> H) then - begin - FGridBitmapW := W; - FGridBitmapH := H; - FGridBitmap.SetSize(W, H); - GC := FGridBitmap.Canvas; - PaintVerticalGradient(GC, W, H, TColor($0020160B), TColor($006C4F1F)); - LabelBandW := 28; - GC.Pen.Style := psClear; - GC.Brush.Style := bsSolid; - GC.Brush.Color := TColor($00543F18); - GC.FillRect(Rect(0, 0, LabelBandW, H)); - GC.Pen.Style := psSolid; - GC.Font.Size := 7; - GC.Font.Name := 'Courier New'; - GC.Font.Color := TColor($00A8C9D9); - GC.Brush.Style := bsClear; - dB := DBmax - FSpecGridStep; - while dB >= DBmin do - begin - Yp := Round((DBmax - dB) * InvRange * H); - GC.Pen.Color := TColor($00685624); - GC.Pen.Width := 1; - GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp); - GC.TextOut(1, Yp - 9, Format('%4.0f', [dB])); - dB := dB - FSpecGridStep; - end; - for i := 0 to 8 do - begin - GX := ScaleX(i, 8, W); - GC.Pen.Color := TColor($00685624); - GC.MoveTo(GX, 0); GC.LineTo(GX, H); - end; - end; - LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020); - - // ── 2. Полоса фильтра — заливка ДО спектра ───────────────────────────── - Half := FFilterBW / 2; - // Используем частоту активного VFO (A или B) для позиции маркера и фильтра - if FActiveVfo = 0 then - VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * W) - else - VfoX := Round((FVfoB - FCenterFreq + FSpanHz / 2) / FSpanHz * W); - case FMode of - 0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end; - 1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end; - else begin Lo_Hz := -Half; Hi_Hz := Half; end; - end; - X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W))); - X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W))); - C.Brush.Color := TColor($006A4E22); - C.Brush.Style := bsSolid; - C.Pen.Style := psClear; - if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H)); - C.Pen.Style := psSolid; - - // ── 3. AGC линии ──────────────────────────────────────────────────────── - if FWDSPReady then - begin - AGCy := Round((DBmax - FDSPEngine.AGCThresh) * InvRange * H); - if (AGCy >= 0) and (AGCy < H) then - begin - C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash; - C.MoveTo(0, AGCy); C.LineTo(W, AGCy); - C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6; - C.TextOut(4, AGCy - 9, 'AGC T'); - end; - AGCHangY := Round((DBmax - FDSPEngine.AGCHangLevel) * InvRange * H); - if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then - begin - C.Pen.Color := TColor($00FFCC00); C.Pen.Width := 1; C.Pen.Style := psDot; - C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY); - C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); C.Font.Size := 6; - C.TextOut(4, AGCHangY + 2, 'AGC H'); - end; - end - else - begin - AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H); - if (AGCy >= 0) and (AGCy < H) then - begin - C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash; - C.MoveTo(0, AGCy); C.LineTo(W, AGCy); - C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6; - C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop])); - end; - end; - - // ── 4. Кривая спектра — кэшируем FSpPts без SetLength каждый кадр ────── - if FSpPtsLen <> W + 2 then - begin - SetLength(FSpPts, W + 2); - FSpPtsLen := W + 2; - end; - SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024); - for i := 0 to W - 1 do - begin - 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)))); - end; - FSpPts[W] := Point(W-1, H); - FSpPts[W+1] := Point(0, H); - - // ── 5. Градиент ───────────────────────────────────────────────────────── - C.Brush.Style := bsSolid; - C.Pen.Style := psClear; - DrawSpectrumGradient(FSpPts, W, H); - - // ── 6. Линия спектра ──────────────────────────────────────────────────── - C.Pen.Style := psSolid; - C.Pen.Color := TColor($0040FF80); - C.Pen.Width := 1; - C.Polyline(Slice(FSpPts, W)); // один вызов вместо W раз LineTo - - // ── 7. Края фильтра + VFO поверх спектра ──────────────────────────────── - C.Pen.Color := TColor($0040FFCC); C.Pen.Width := 1; C.Pen.Style := psSolid; - C.MoveTo(X1, 0); C.LineTo(X1, H); - C.MoveTo(X2, 0); C.LineTo(X2, H); - C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 2; - C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12); - C.Brush.Color := TColor($0000AAFF); C.Brush.Style := bsSolid; C.Pen.Width := 1; - C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]); - - if FMarkerActive then - DrawMarkerLine(C, W, H); -end; - -procedure TMainForm.DrawWaterfall; -// ═══════════════════════════════════════════════════════════════════════ -// Архитектура минимального CPU: -// FWfPixels[W*H] — весь водопад в памяти, строка 0 = верх (новейшая). -// Скролл = Move(FWfPixels[0]→FWfPixels[W], (H-1)*W*4) ~0.3ms -// Новая строка = инкрементный PLongWord по FWfPixels[0..W-1] ~0.1ms -// Вывод = TLazIntfImage.PixelData := FWfPixels + LoadFromIntfImage ~1ms -// Итого ~1.5ms vs старый IntfImage roundtrip ~5ms -// ═══════════════════════════════════════════════════════════════════════ -var - W, H, X: Integer; - dB, frac: Double; - WatSrcF: Double; - WatS0, WatS1, V: 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; - Desc: TRawImageDescription; - Row, Col: Integer; - RowPtr: PByte; - Src: PLongWord; - BPPi: Integer; - Pal: LongWord; - Hist: array[0..191] of Integer; - SrcCount: Integer; -const - 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); - - FillChar(Hist, SizeOf(Hist), 0); - HistMinDB := -170.0; - HistMaxDB := 22.0; - HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist); - for X := 0 to SrcCount - 1 do - begin - 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 - NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; - Break; - end; - end; - - CumCount := 0; - for HistIdx := 0 to High(Hist) do - begin - CumCount := CumCount + Hist[HistIdx]; - if CumCount >= HighTargetCount then - begin - SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; - Break; - end; - end; - - // ── AGC High/Low ──────────────────────────────────────────────────────── - if FWfAGCEnabled then - begin - 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; - 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); - - // ── Пересоздаём буфер при ресайзе ────────────────────────────────────── - if (FWfBitmapW <> W) or (FWfBitmapH <> H) then - begin - FWfBitmapW := W; FWfBitmapH := H; - SetLength(FWfPixels, W * H); - FillDWord(FWfPixels[0], W * H, 0); - FWfBitmap.SetSize(W, H); - // Сбрасываем кэш IntfImage — пересоздастся ниже с новым размером - FreeAndNil(FWfIntfImg); - end; - - // ── Скролл: Move строк вниз ───────────────────────────────────────────── - if H > 1 then - Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W); - - // ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ──────── - 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 > SrcCount - 2 then WatS0 := SrcCount - 2; - WatS1 := WatS0 + 1; - frac := WatSrcF - WatS0; - 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; - // Thetis default waterfall uses its enhanced color scheme, not a flat lookup table. - Pal := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh); - Px^ := Pal; - Inc(Px); - WatSrcF := WatSrcF + Step; - end; - - // ── Записываем FWfPixels в FWfBitmap через кэшированный TLazIntfImage ──── - // FWfIntfImg создаётся один раз на весь lifetime (или при ресайзе). - // Move(FWfPixels → IntfImg.PixelData) = один memcpy без GDI roundtrip. - // LoadFromIntfImage = один вызов platformAPI → ~1ms на Linux GTK2. - if FWfIntfImg = nil then - begin - FWfIntfImg := TLazIntfImage.Create(W, H); - InitRawDesc32(Desc, W, H); - FWfIntfImg.DataDescription := Desc; - FWfIntfImg.CreateData; - end; - BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8; - if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then - Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4) - else - begin - Src := @FWfPixels[0]; - for Row := 0 to H - 1 do - begin - RowPtr := FWfIntfImg.GetDataLineStart(Row); - if RowPtr = nil then begin Inc(Src, W); Continue; end; - for Col := 0 to W - 1 do - begin - RowPtr[0] := Byte(Src^); // B - RowPtr[1] := Byte(Src^ shr 8); // G - RowPtr[2] := Byte(Src^ shr 16); // R - if BPPi >= 4 then RowPtr[3] := $FF; - Inc(Src); Inc(RowPtr, BPPi); - end; - end; - end; - FWfBitmap.LoadFromIntfImage(FWfIntfImg); -end; - -// =========================================================================== -// Широкий S-метр — точная копия Thetis/PowerSDR стиля -// Структура (высота H): -// верх (H*35%): dBm шкала (-120..-20..0), тики, цифровое значение слева -// середина (H*35%): полоса сигнала (зелёный бар + широкий пик + белый маркер) -// низ (H*30%): S-шкала (S1..S9..+60), S-значение слева -// =========================================================================== -procedure TMainForm.DrawSMeterWide(ACanvas: TCanvas; R: TRect; - Value, Peak, MinVal: Double; - out ZX1, ZX2, ZY1, ZY2: Integer); -const - DB_MIN = -127.0; // левый край шкалы - DB_MAX = -13.0; // правый край шкалы (S9+60) - DB_S9 = -73.0; // граница S9 (зелёная/оранжевая) - DB_OVR = -43.0; // граница "красной" зоны (+30 over S9) - - // dBm верхняя шкала - DBM_MARKS: array[0..5] of Double = (-120,-100,-80,-60,-40,-20); - DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20'); - - // S-шкала нижняя: как в VfoOverlay - S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13); - S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60'); - - // Цвета (BGR в TColor) - CLR_SMETER_BG = TColor($00181818); // фон = CLR_PANEL - CLR_BAR_GREEN = TColor($00091F00); // тёмно-зелёный бар до S9 (RGB 0,31,9) - CLR_BAR_OVER = TColor($00091528); // тёмно-красный бар после S9 (RGB 40,21,9) — тот же тон что зелёный но красн. - CLR_PEAK_FILL = TColor($00796F75); // светло-серый заполнитель пика (RGB 117,111,121) - CLR_PEAK_MARKER = TColor($00F8F8FF); // белый пик-маркер (RGB 255,248,248) - CLR_BAR_ORANGE = TColor($000A1A28); // оранжевая зона (>S9) - CLR_BAR_RED = TColor($00000832); // красная зона (>+30) - CLR_TICK_GREEN = TColor($00708070); // тики зелёной зоны - CLR_TICK_WHITE = TColor($00E0E0DC); // тики белые (dBm шкала) - CLR_TICK_BLUE = TColor($0070A090); // тики S9+ зоны - CLR_LABEL_DBM = TColor($0096D8DC); // цифровое значение dBm (желтоватый) - CLR_LABEL_S = TColor($00F0F0F0); // S-значение — белый - CLR_SCALE_DBM = TColor($00D0DCDC); // метки dBm шкалы (бело-серые) - CLR_SCALE_S_GREEN = TColor($00E0E0E0); // метки S1..S9 — белые - CLR_SCALE_S_BLUE = TColor($0090D090); // метки +10..+60 — как S1..S9 оригинал (желт-зелён) - CLR_BORDER = TColor($00303028); // рамки - - LEFT_INFO = 62; // ширина левой информационной колонки (dBm + S-value) - TICK_LONG = 5; // длина длинного тика - TICK_MED = 3; // длина среднего тика - -var - W, H: Integer; - BX, BW: Integer; // начало и ширина зоны шкалы - Y_UPPER_MID: Integer; // середина верхней зоны (метки dBm) - Y_BAR_TOP: Integer; // верх полосы сигнала - Y_BAR_BOT: Integer; // низ полосы сигнала - Y_LOWER_MID: Integer; // середина нижней зоны (метки S) - BarEnd: Integer; // правый край текущего сигнала - PeakLeft: Integer; // левый край peak fill - PeakRight: Integer; // правый край peak fill (= белый маркер) - MinX: Integer; // позиция минимума - S9X, OvrX: Integer; // позиции границ зон - i, X, TW: Integer; - Lbl: string; - SNum: Integer; - Over: Integer; - - function DBtoX(dB: Double): Integer; inline; - begin - Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW); - if Result < BX then Result := BX; - if Result > BX + BW then Result := BX + BW; - end; - - procedure HLine(Y, X1, X2: Integer; C: TColor); - begin - ACanvas.Pen.Color := C; - ACanvas.MoveTo(X1, Y); ACanvas.LineTo(X2, Y); - end; - - procedure VLine(X, Y1, Y2: Integer; C: TColor); - begin - ACanvas.Pen.Color := C; - ACanvas.MoveTo(X, Y1); ACanvas.LineTo(X, Y2); - end; - - procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor); - begin - ACanvas.Brush.Color := C; - ACanvas.Brush.Style := bsSolid; - ACanvas.Pen.Style := psClear; - if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2)); - ACanvas.Pen.Style := psSolid; - end; - -begin - W := R.Right - R.Left; - H := R.Bottom - R.Top; - // Инициализируем out-параметры - ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0; - if (W < 80) or (H < 16) then Exit; - - // --- Геометрия --- - BX := R.Left + LEFT_INFO; - BW := W - LEFT_INFO - 4; - - Y_UPPER_MID := R.Top + H * 3 div 10; // центр верхней зоны меток - Y_BAR_TOP := R.Top + H * 36 div 100; // верх бара - Y_BAR_BOT := R.Top + H * 66 div 100; // низ бара - Y_LOWER_MID := R.Top + H * 80 div 100; // центр нижней зоны меток - - S9X := DBtoX(DB_S9); - OvrX := DBtoX(DB_OVR); - - // ================================================================ - // ФОН - // ================================================================ - ACanvas.Brush.Color := CLR_SMETER_BG; - ACanvas.Brush.Style := bsSolid; - ACanvas.Pen.Style := psClear; - ACanvas.FillRect(R); - ACanvas.Pen.Style := psSolid; - - // Лёгкий цветной фон зон шкалы в полосе бара - FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, TColor($00061006)); // тёмно-зелёный - FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, TColor($000A1006)); // тёмно-жёлтый - FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, TColor($00060A10)); // тёмно-красный - - // ================================================================ - // ПОЛОСА СИГНАЛА - // ================================================================ - BarEnd := DBtoX(Value); // текущий уровень - PeakLeft := DBtoX(MinVal); // левый край зоны = отслеживаемый минимум - PeakRight:= DBtoX(Peak); // правый край зоны = отслеживаемый пик - - // Заполняем out-параметры зоны для alpha-blend (после возврата) - ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight)); - ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight)); - ZY1 := Y_BAR_TOP + 1; - ZY2 := Y_BAR_BOT - 1; - - // Бар: зелёный до S9, красноватый после S9 — в том же тёмном стиле - if BarEnd > BX then - begin - if BarEnd <= S9X then - // Весь бар в зелёной зоне - FillBar(BX, Y_BAR_TOP + 1, BarEnd, Y_BAR_BOT - 1, CLR_BAR_GREEN) - else - begin - // Зелёная часть до S9 - FillBar(BX, Y_BAR_TOP + 1, S9X, Y_BAR_BOT - 1, CLR_BAR_GREEN); - // Красноватая часть после S9 - FillBar(S9X, Y_BAR_TOP + 1, BarEnd, Y_BAR_BOT - 1, CLR_BAR_OVER); - end; - end; - - // Белый маркер пика (2px) — правый край зоны - if (PeakRight > BX) and (PeakRight <= BX + BW) then - begin - ACanvas.Pen.Color := CLR_PEAK_MARKER; - ACanvas.Pen.Width := 2; - ACanvas.MoveTo(PeakRight, Y_BAR_TOP); - ACanvas.LineTo(PeakRight, Y_BAR_BOT); - ACanvas.Pen.Width := 1; - end; - - // Рамка полосы - ACanvas.Brush.Style := bsClear; - ACanvas.Pen.Color := CLR_BORDER; - ACanvas.Rectangle(BX, Y_BAR_TOP, BX + BW, Y_BAR_BOT); - - // ================================================================ - // ВЕРХНЯЯ ШКАЛА — dBm метки (-120..-100..-20..0) - // ================================================================ - ACanvas.Font.Name := 'Courier New'; - ACanvas.Font.Size := 6; - ACanvas.Font.Style := []; - ACanvas.Brush.Style:= bsClear; - - for i := 0 to High(DBM_MARKS) do - begin - X := DBtoX(DBM_MARKS[i]); - // Длинный тик вверх от полосы - VLine(X, Y_BAR_TOP - TICK_LONG, Y_BAR_TOP - 1, CLR_TICK_WHITE); - // Метка - Lbl := DBM_LABELS[i]; - TW := ACanvas.TextWidth(Lbl); - ACanvas.Font.Color := CLR_SCALE_DBM; - ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl); - end; - - // Мелкие тики каждые 10 dB между основными (каждые 5) - i := -125; - while i < -13 do - begin - X := DBtoX(i); - VLine(X, Y_BAR_TOP - TICK_MED, Y_BAR_TOP - 1, CLR_TICK_GREEN); - Inc(i, 5); - end; - - // ================================================================ - // НИЖНЯЯ ШКАЛА — S1..S9..+60 - // ================================================================ - for i := 0 to High(S_MARKS_DBM) do - begin - X := DBtoX(S_MARKS_DBM[i]); - // Тик вниз от полосы - if S_MARKS_DBM[i] >= DB_S9 then - VLine(X, Y_BAR_BOT + 1, Y_BAR_BOT + TICK_LONG, CLR_TICK_BLUE) - else - VLine(X, Y_BAR_BOT + 1, Y_BAR_BOT + TICK_LONG, CLR_TICK_GREEN); - // Метка - Lbl := S_MARKS_LBL[i]; - TW := ACanvas.TextWidth(Lbl); - if S_MARKS_DBM[i] >= DB_S9 then - ACanvas.Font.Color := CLR_SCALE_S_BLUE - else - ACanvas.Font.Color := CLR_SCALE_S_GREEN; - ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG + 1, Lbl); - end; - - // ================================================================ - // ЛЕВАЯ КОЛОНКА — цифровое значение - // ================================================================ - // dBm значение (крупно, вверху) - ACanvas.Font.Size := 8; - ACanvas.Font.Style := [fsBold]; - ACanvas.Font.Color := CLR_LABEL_DBM; - Lbl := Format('%6.1f', [Value]); - ACanvas.TextOut(R.Left + 1, R.Top + 1, Lbl); - - // 'dBm' мелко под значением - ACanvas.Font.Size := 6; - ACanvas.Font.Style := []; - ACanvas.Font.Color := TColor($00507070); - ACanvas.TextOut(R.Left + 1, Y_BAR_TOP - ACanvas.TextHeight('dBm') - 1, 'dBm'); - - // S-значение (внизу) - ACanvas.Font.Size := 8; - ACanvas.Font.Style := [fsBold]; - ACanvas.Font.Color := CLR_LABEL_S; - if Value >= -73.0 then - begin - Over := Round(Value - (-73.0)); - if Over < 5 then - Lbl := 'S9' - else - begin - Over := ((Over + 5) div 10) * 10; - if Over = 0 then Over := 10; - Lbl := Format('S9+%d', [Over]); - end; - end - else if Value <= -121.0 then - Lbl := 'S1' - else - begin - Lbl := 'S1'; - for SNum := 1 to 8 do - if Value >= (-121.0 + (SNum - 1) * 6.0) then - Lbl := 'S' + IntToStr(SNum); - end; - ACanvas.TextOut(R.Left + 1, Y_BAR_BOT + TICK_LONG + 1, Lbl); -end; - -// Полупрозрачная белая заливка зоны X1..X2, Y1..Y2. -// Вызывается ПОСЛЕ DrawSMeterWide — FSmBitmap уже содержит нарисованный фон. -// Используем TLazIntfImage для попиксельного alpha-blend. -procedure TMainForm.DrawSMeterZone(ACanvas: TCanvas; R: TRect; - X1, X2, Y1, Y2: Integer); -const - ALPHA = 16384; // ~25% осветления (от 0..65535) -var - Img: TLazIntfImage; - FC: TFPColor; - IX, IY: Integer; -begin - if (X2 <= X1) or (FSmBitmap = nil) then Exit; - // Получаем IntfImage из off-screen bitmap - Img := FSmBitmap.CreateIntfImage; - try - for IY := Y1 to Y2 - 1 do - for IX := X1 to X2 - 1 do - begin - FC := Img.Colors[IX, IY]; - FC.Red := Min(65535, FC.Red + ALPHA); - FC.Green := Min(65535, FC.Green + ALPHA); - FC.Blue := Min(65535, FC.Blue + ALPHA); - Img.Colors[IX, IY] := FC; - end; - // Записываем изменения обратно в bitmap - FSmBitmap.LoadFromIntfImage(Img); - finally - Img.Free; - end; -end; - -procedure TMainForm.PbSMeterRightPaint(Sender: TObject); -var - W, H: Integer; - ZX1, ZX2, ZY1, ZY2: Integer; -begin - W := PbSMeterRight.Width; - H := PbSMeterRight.Height; - if (W <= 0) or (H <= 0) then Exit; - - // Создаём/пересоздаём off-screen буфер - if (FSmBitmap = nil) or (FSmBitmap.Width <> W) or (FSmBitmap.Height <> H) then - begin - FreeAndNil(FSmBitmap); - FSmBitmap := TBitmap.Create; - FSmBitmap.SetSize(W, H); - end; - - // Рисуем основной S-метр в off-screen буфер - DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, W, H), - FLastSMeter, FSMeterPeak, FSMeterMin, - ZX1, ZX2, ZY1, ZY2); - - // Полупрозрачная белая заливка зоны Min..Peak поверх нарисованного - DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, W, H), ZX1, ZX2, ZY1, ZY2); - - // Выводим на экран одним блоком (без мерцания) - PbSMeterRight.Canvas.Draw(0, 0, FSmBitmap); -end; - - -procedure TMainForm.PbFwdPowerPaint(Sender: TObject); -begin - DrawBarMeter(PbFwdPower.Canvas, Rect(0,0,PbFwdPower.Width,PbFwdPower.Height), - FLastFwdW, 150, CLR_METER_ON); -end; - -procedure TMainForm.PbSWRPaint(Sender: TObject); -begin - DrawBarMeter(PbSWR.Canvas, Rect(0,0,PbSWR.Width,PbSWR.Height), - FLastSWR - 1.0, 4.0, CLR_AMBER); -end; - -procedure TMainForm.PbSpectrumPaint(Sender: TObject); -begin - if (FSpectrumBitmap.Width > 0) and (FSpectrumBitmap.Height > 0) then - PbSpectrum.Canvas.Draw(0, 0, FSpectrumBitmap) - else - begin - PbSpectrum.Canvas.Brush.Color := CLR_BG; - PbSpectrum.Canvas.FillRect(Rect(0,0,PbSpectrum.Width,PbSpectrum.Height)); - end; -end; - -// --------------------------------------------------------------------------- -// Ruler — полоса частотных меток между спектром и водопадом -// --------------------------------------------------------------------------- -procedure TMainForm.DrawRuler; -// ОПТИМИЗАЦИЯ: рисуем в offscreen FRulerBitmap, перерисовываем только когда -// изменился FCenterFreq. Экономим TextOut+LineTo на каждом тике таймера (20fps). -var - C: TCanvas; - W, H: Integer; - i, X: Integer; - FreqStart, FreqHz: Double; - VfoX: Integer; - Lbl: string; - TW: Integer; -begin - if PbRuler = nil then Exit; - W := PbRuler.Width; - H := PbRuler.Height; - if (W <= 0) or (H <= 0) then Exit; - - // Пропускаем перерисовку если ни частота центра, ни активный VFO не изменились - if (Abs(FCenterFreq - FRulerLastFreq) < 0.5) and - (Abs(ActiveVfoFreq - FRulerLastVfo) < 0.5) then Exit; - FRulerLastFreq := FCenterFreq; - FRulerLastVfo := ActiveVfoFreq; - - if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then - FRulerBitmap.SetSize(W, H); - - C := FRulerBitmap.Canvas; - PaintVerticalGradient(C, W, H, TColor($00372A12), TColor($00705A2A)); - C.Pen.Color := TColor($00685624); - C.Pen.Width := 1; - C.MoveTo(0, 0); C.LineTo(W, 0); - C.MoveTo(0, H-1); C.LineTo(W, H-1); - C.Font.Name := 'Courier New'; - C.Font.Size := 7; - C.Brush.Style := bsClear; - FreqStart := FCenterFreq - FSpanHz / 2; - for i := 0 to 8 do - begin - X := ScaleX(i, 8, W); - C.Pen.Color := TColor($00685624); - C.MoveTo(X, 0); C.LineTo(X, H div 2); - FreqHz := FreqStart + i * FSpanHz / 8; - Lbl := Format('%.3f', [FreqHz / 1e6]); - TW := C.TextWidth(Lbl); - C.Font.Color := TColor($00C8D8E2); - C.TextOut(X - TW div 2, H div 2 - 1, Lbl); - end; - VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W); - if (VfoX >= 0) and (VfoX < W) then - begin - C.Pen.Color := TColor($0000FF80); - C.Pen.Width := 2; - C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); - C.Pen.Width := 1; - end; -end; - -procedure TMainForm.PbRulerPaint(Sender: TObject); -begin - DrawRuler; // обновит FRulerBitmap только если FCenterFreq изменился - if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then - PbRuler.Canvas.Draw(0, 0, FRulerBitmap); -end; -procedure TMainForm.PbWaterfallPaint(Sender: TObject); -// Отображаем FWfBitmap — он уже содержит готовый кадр из DrawWaterfall. -// DrawWaterfall делает скролл через Move() в байтах IntfImage (~0.3ms), -// пишет новую строку, и один раз LoadFromIntfImage (~2ms). -// Здесь просто Canvas.Draw — данные уже в bitmap, GDI копирует за ~1ms. -var - W, H: Integer; -begin - W := PbWaterfall.Width; - H := PbWaterfall.Height; - if (W <= 0) or (H <= 0) then Exit; - - if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then - PbWaterfall.Canvas.Draw(0, 0, FWfBitmap) - else - begin - PbWaterfall.Canvas.Brush.Color := clBlack; - PbWaterfall.Canvas.FillRect(Rect(0, 0, W, H)); - end; - - if FMarkerActive then - DrawMarkerLine(PbWaterfall.Canvas, W, H); -end; // =========================================================================== // Timers @@ -3352,6 +2072,12 @@ begin FSMeterPeak := FSMeterPeak + PeakAlpha * (PeakTarget - FSMeterPeak); FSMeterMin := FSMeterMin + MinAlpha * (MinTarget - FSMeterMin); + FSpecView.LastSMeter := FLastSMeter; + FSpecView.SMeterPeak := FSMeterPeak; + FSpecView.SMeterMin := FSMeterMin; + FSpecView.LastFwdW := FLastFwdW; + FSpecView.LastSWR := FLastSWR; + if PbSMeterRight <> nil then PbSMeterRight.Invalidate; end; @@ -3373,6 +2099,7 @@ begin ResizeSpectrumPanels; ResizeSMeter; if FSpectrumWidth <= 0 then Exit; + SyncSpecViewFreq; // Проверка таймаута — трансивер не отвечает if FRunning and FNetwork.Running then @@ -3427,17 +2154,15 @@ begin // Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще. if FSpectrumDirty then begin - DrawSpectrum; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; - - if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or - (Abs(FVfoA - FRulerLastVfo) >= 0.5) then + if FSpecView.NeedsRulerRedraw then PbRuler.Invalidate; FSpectrumDirty := False; end; if FWaterfallDirty then begin - DrawWaterfall; + FSpecView.DrawWaterfall; PbWaterfall.Invalidate; FWaterfallDirty := False; end; @@ -3447,21 +2172,19 @@ begin // Не подключены — показываем чистый экран (без анимации) if not FNetwork.Connected then begin - // Очищаем до фона один раз при отключении - // (просто не вызываем Draw* — бitmaps остаются как были) - // Но при первом старте покажем пустой фон: - if FSpectrumBitmap.Width <> FSpectrumWidth then + // При первом старте показываем демо-спектр + if FSpecView.SpectrumBitmapWidth <> FSpectrumWidth then begin - FSpectrumBitmap.SetSize(FSpectrumWidth, FSpectrumHeight); - ResetSpectrumBuf; - DrawSpectrum; + FSpecView.SetSpectrumBitmapSize(FSpectrumWidth, FSpectrumHeight); + FSpecView.ResetSpectrumBuf; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; end; end else begin // Подключены, но не запущены — статичный спектр (шум) - DrawSpectrum; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; FSpectrumDirty := False; end; @@ -3471,13 +2194,12 @@ begin if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then begin if FSpectrumDirty then - DrawSpectrum; + FSpecView.DrawSpectrum; if FWaterfallDirty then - DrawWaterfall; + FSpecView.DrawWaterfall; if FSpectrumDirty then PbSpectrum.Invalidate; - if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or - (Abs(FVfoA - FRulerLastVfo) >= 0.5) then + if FSpecView.NeedsRulerRedraw then PbRuler.Invalidate; if FWaterfallDirty then PbWaterfall.Invalidate; @@ -3856,8 +2578,8 @@ begin FRXPacketCount := 0; FActiveDDC := 0; StatusBar1.Panels[3].Text := 'RX: waiting...'; - ResetSpectrumBuf; - DrawSpectrum; + FSpecView.ResetSpectrumBuf; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; FNetwork.Disconnect; BtnStartStop.Caption := 'START'; @@ -3996,8 +2718,12 @@ begin FWfManualHigh := G_Settings.WfManualHigh; FWfManualLow := G_Settings.WfManualLow; FWfAGCOffset := G_Settings.WfAGCOffset; - FWfHigh := FWfManualHigh; - FWfLow := FWfManualLow; + FSpecView.WfAGCEnabled := FWfAGCEnabled; + FSpecView.WfNFEnabled := FWfNFEnabled; + FSpecView.WfManualHigh := FWfManualHigh; + FSpecView.WfManualLow := FWfManualLow; + FSpecView.WfAGCOffset := FWfAGCOffset; + FSpecView.ResetWfAvgBuf; FShowSpectrum := G_Settings.ShowSpectrum; FShowWaterfall := G_Settings.ShowWaterfall; if G_Settings.DisplayFPS > 0 then @@ -4008,6 +2734,9 @@ begin FSpecRefLevel := G_Settings.SpecRefLevel; FSpecRange := G_Settings.SpecRange; if G_Settings.SpecGridStep > 0 then FSpecGridStep := G_Settings.SpecGridStep; + FSpecView.SpecRefLevel := FSpecRefLevel; + FSpecView.SpecRange := FSpecRange; + FSpecView.SpecGridStep := FSpecGridStep; InvalidateGridCache; if FWDSPReady then begin @@ -4093,7 +2822,8 @@ begin FillChar(DUCPkt, SizeOf(DUCPkt), 0); DUCPkt.NumDACs := 1; DUCPkt.SidetoneLevel := 50; - DUCPkt.SidetoneFreqLo := 600; + DUCPkt.SidetoneFreqHi := Hi(600); + DUCPkt.SidetoneFreqLo := Lo(600); DUCPkt.KeyerSpeed := 20; DUCPkt.KeyerWeight := 50; DUCPkt.DUC0RateHi := Hi(192); @@ -4230,13 +2960,11 @@ begin StyleButton(BtnBand[FCurrentBand], True); end; - // Инвалидируем кэш линейки - FRulerLastFreq := -1.0; - FRulerLastVfo := -1.0; - - DrawSpectrum; + FSpecView.InvalidateRulerCache; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; - DrawWaterfall; + FSpecView.DrawWaterfall; PbWaterfall.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; @@ -4286,8 +3014,9 @@ begin FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; - DrawSpectrum; + FSpecView.InvalidateRulerCache; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; @@ -4424,15 +3153,16 @@ begin // Перерисовываем спектр всегда (маркер и полоса двигаются) // Если идёт drag — не вызываем Draw напрямую, таймер подхватит + SyncSpecViewFreq; if FSpecDrag then FSpectrumDirty := True else begin - DrawSpectrum; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if Scrolled then begin - DrawWaterfall; + FSpecView.DrawWaterfall; PbWaterfall.Invalidate; end; end; @@ -4463,8 +3193,9 @@ begin // Включили CTUN: фиксируем текущее положение дисплея // FCenterFreq остаётся как есть — спектр не прыгает end; - DrawSpectrum; - DrawWaterfall; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; + FSpecView.DrawWaterfall; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; @@ -4510,8 +3241,9 @@ begin FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; - DrawSpectrum; + FSpecView.InvalidateRulerCache; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; @@ -4572,9 +3304,9 @@ begin if Button = mbLeft then begin // Левая кнопка: сбрасываем маркер если активен - if FMarkerActive then + if FSpecView.MarkerActive then begin - FMarkerActive := False; + FSpecView.MarkerActive := False; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; @@ -4590,16 +3322,16 @@ begin else if Button = mbRight then begin // Правая кнопка: toggle маркера - if FMarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FMarkerX) < 8) then + if FSpecView.MarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FSpecView.MarkerX) < 8) then begin // Клик рядом с текущим маркером — убираем - FMarkerActive := False; + FSpecView.MarkerActive := False; end else begin // Ставим маркер в новое место - FMarkerActive := True; - FMarkerX := Round(X / PbSpectrum.Width * 1000); + FSpecView.MarkerActive := True; + FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000); end; PbSpectrum.Invalidate; PbWaterfall.Invalidate; @@ -4612,9 +3344,9 @@ begin if FSpecDrag and (ssLeft in Shift) then DoSpectrumDrag(X, PbSpectrum.Width); // Маркер следует за курсором — обновление произойдёт в ближайшем тике таймера (50ms) - if FMarkerActive then + if FSpecView.MarkerActive then begin - FMarkerX := Round(X / PbSpectrum.Width * 1000); + FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000); FSpectrumDirty := True; end; end; @@ -4637,9 +3369,9 @@ procedure TMainForm.PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton; begin if Button = mbLeft then begin - if FMarkerActive then + if FSpecView.MarkerActive then begin - FMarkerActive := False; + FSpecView.MarkerActive := False; PbSpectrum.Invalidate; PbWaterfall.Invalidate; end; @@ -4654,12 +3386,12 @@ begin end else if Button = mbRight then begin - if FMarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FMarkerX) < 8) then - FMarkerActive := False + if FSpecView.MarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FSpecView.MarkerX) < 8) then + FSpecView.MarkerActive := False else begin - FMarkerActive := True; - FMarkerX := Round(X / PbWaterfall.Width * 1000); + FSpecView.MarkerActive := True; + FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000); end; PbSpectrum.Invalidate; PbWaterfall.Invalidate; @@ -4671,9 +3403,9 @@ procedure TMainForm.PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState; begin if FSpecDrag and (ssLeft in Shift) then DoSpectrumDrag(X, PbWaterfall.Width); - if FMarkerActive then + if FSpecView.MarkerActive then begin - FMarkerX := Round(X / PbWaterfall.Width * 1000); + FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000); FSpectrumDirty := True; end; end; @@ -4793,9 +3525,10 @@ begin StyleButton(BtnBand[FCurrentBand], True); end; - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; + FSpecView.InvalidateRulerCache; UpdateVfoDisplay; - DrawSpectrum; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; @@ -4814,8 +3547,9 @@ begin FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; - DrawSpectrum; PbSpectrum.Invalidate; + FSpecView.InvalidateRulerCache; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; FreqDispB.Frequency := Round(FVfoB); @@ -4957,16 +3691,14 @@ procedure TMainForm.BtnWfAGCClick(Sender: TObject); begin FWfAGCEnabled := not FWfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); - if not FWfAGCEnabled then - FWfHigh := FWfManualHigh; // сбрасываем к ручному уровню при выключении + FSpecView.WfAGCEnabled := FWfAGCEnabled; end; procedure TMainForm.BtnWfNFClick(Sender: TObject); begin FWfNFEnabled := not FWfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); - if not FWfNFEnabled then - FWfLow := FWfManualLow; + FSpecView.WfNFEnabled := FWfNFEnabled; end; procedure TMainForm.BtnHidePanelClick(Sender: TObject); @@ -5122,8 +3854,9 @@ begin FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; - DrawSpectrum; PbSpectrum.Invalidate; + FSpecView.InvalidateRulerCache; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; UpdateVfoDisplay; end; @@ -5311,6 +4044,7 @@ procedure TMainForm.SyncWebWfAGC; begin FWfAGCEnabled := FWebSyncBool; StyleButton(BtnWfAGC, FWfAGCEnabled); + FSpecView.WfAGCEnabled := FWfAGCEnabled; end; procedure TMainForm.WebOnWfNF(On_: Boolean); @@ -5324,6 +4058,7 @@ procedure TMainForm.SyncWebWfNF; begin FWfNFEnabled := FWebSyncBool; StyleButton(BtnWfNF, FWfNFEnabled); + FSpecView.WfNFEnabled := FWfNFEnabled; end; // ── Run ──────────────────────────────────────────────────────────────────── @@ -5470,8 +4205,9 @@ begin FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; - DrawSpectrum; PbSpectrum.Invalidate; + FSpecView.InvalidateRulerCache; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; end; UpdateVfoDisplay; @@ -5513,7 +4249,7 @@ begin end; if FNetwork.Connected and FNetwork.Running then FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True); - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; + FSpecView.InvalidateRulerCache; FSpectrumDirty := True; end; @@ -5681,7 +4417,7 @@ begin end; // Обновляем кэш диапазона FBandCache[FCurrentBand].AGCTop := FAGCTop; - DrawSpectrum; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; end; @@ -5710,13 +4446,11 @@ procedure TMainForm.OnSpectrumReady(const Pixels: array of Single; var i, N: Integer; begin - // Храним RAW данные от WDSP (всегда 1024 точки). - // Интерполяция на ширину экрана делается в DrawSpectrum/DrawWaterfall. - // Вызывается из DSP-потока — Single-запись атомарна. N := Min(Count, 1024); for i := 0 to N - 1 do - FSpectrumBuf[i] := Pixels[i]; + FSpectrumBuf[i] := Pixels[i]; // local copy for WebServer FSpectrumBufCount := N; + FSpecView.SetSpectrumData(Pixels, Count); FSpectrumDirty := True; end; @@ -5727,12 +4461,13 @@ var begin N := Min(Count, 1024); for i := 0 to N - 1 do - FWaterfallBuf[i] := Pixels[i]; + FWaterfallBuf[i] := Pixels[i]; // local copy for WebServer FWaterfallBufCount := N; Inc(FWaterfallFrameCounter); if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then begin FWaterfallFrameCounter := 0; + FSpecView.SetWaterfallData(Pixels, Count); FWaterfallDirty := True; end; end; @@ -5759,12 +4494,10 @@ begin FWfManualHigh := WfHigh; FWfManualLow := WfLow; FWfAGCOffset := WfAGCOffset; - if not FWfAGCEnabled then - begin - FWfHigh := FWfManualHigh; - FWfLow := FWfManualLow; - end; - ResetWfAvgBuf; + FSpecView.WfManualHigh := FWfManualHigh; + FSpecView.WfManualLow := FWfManualLow; + FSpecView.WfAGCOffset := FWfAGCOffset; + FSpecView.ResetWfAvgBuf; FSpectrumDirty := True; end; @@ -5773,6 +4506,9 @@ begin FSpecRefLevel := RefLevel; FSpecRange := Range; if GridStep > 0 then FSpecGridStep := GridStep; + FSpecView.SpecRefLevel := FSpecRefLevel; + FSpecView.SpecRange := FSpecRange; + FSpecView.SpecGridStep := FSpecGridStep; InvalidateGridCache; end; @@ -5832,6 +4568,7 @@ begin FWaterfallFrameInterval := 2; FWaterfallFrameCounter := 0; FWaterfallDirty := True; + FSpecView.WfFrameInterval := FWaterfallFrameInterval; if FWDSPReady then FDSPEngine.SetDisplayFPS(FPS); end; @@ -6108,8 +4845,9 @@ begin FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); end; - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; - DrawSpectrum; PbSpectrum.Invalidate; + FSpecView.InvalidateRulerCache; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; PbSpectrum.Invalidate; if PbRuler <> nil then PbRuler.Invalidate; UpdateVfoDisplay; end; diff --git a/SpectrumView.pas b/SpectrumView.pas new file mode 100644 index 0000000..b435d35 --- /dev/null +++ b/SpectrumView.pas @@ -0,0 +1,1141 @@ +unit SpectrumView; + +{ + SpectrumView.pas — рендеринг спектра, водопада и S-метра. + + TSpectrumView инкапсулирует всё рисование: спектр, водопад, линейка частот, + S-метр, индикаторы мощности/SWR. MainForm создаёт один экземпляр, передаёт + ссылки на TPaintBox после BuildUI, обновляет свойства при изменении состояния. + + Зависимости: нет обратной зависимости на MainForm (нет circular dep). +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, + IntfGraphics, FPImage, LCLIntf, LCLType, GraphType; + +const + SV_CLR_BG = TColor($00101010); + SV_CLR_BORDER = TColor($00303030); + SV_CLR_METER_ON = TColor($0000CC44); + SV_CLR_AMBER = TColor($0000AAFF); + +type + TSpectrumView = class + private + // ── Off-screen bitmaps ──────────────────────────────────────────────────── + FSpectrumBitmap: TBitmap; + FWaterfallBitmap: TBitmap; // размер-placeholder для DrawWaterfall + FWfBitmap: TBitmap; + FWfBitmapW: Integer; + FWfBitmapH: Integer; + FGridBitmap: TBitmap; + FGridBitmapW: Integer; + FGridBitmapH: Integer; + FRulerBitmap: TBitmap; + FSpecGradImg: TLazIntfImage; + FSpecGradBmp: TBitmap; + FWfIntfImg: TLazIntfImage; + FWfPixels: array of LongWord; + FSpPts: array of TPoint; + FSpPtsLen: Integer; + FSmBitmap: TBitmap; + FRulerLastFreq: Double; + FRulerLastVfo: Double; + + // ── Radio state ─────────────────────────────────────────────────────────── + FVfoA: Double; + FVfoB: Double; + FActiveVfo: Integer; + FCenterFreq: Double; + FSpanHz: Double; + FMode: Integer; + FFilterBW: Integer; + FAGCTop: Integer; + FAGCThresh: Double; + FAGCHangLevel: Double; + FWDSPReady: Boolean; + + // ── Waterfall settings ──────────────────────────────────────────────────── + FWfAGCEnabled: Boolean; + FWfNFEnabled: Boolean; + FWfManualHigh: Double; + FWfManualLow: Double; + FWfAGCOffset: Double; + FWfHigh: Double; + FWfLow: Double; + + // ── Display settings ────────────────────────────────────────────────────── + FSpecRefLevel: Double; + FSpecRange: Double; + FSpecGridStep: Double; + + // ── Marker ──────────────────────────────────────────────────────────────── + FMarkerActive: Boolean; + FMarkerX: Integer; + + // ── Meter values ────────────────────────────────────────────────────────── + FLastSMeter: Double; + FSMeterPeak: Double; + FSMeterMin: Double; + FLastFwdW: Double; + FLastSWR: Double; + + // ── Буферы данных (пишутся из DSP-потока) ──────────────────────────────── + FSpectrumBuf: array[0..1023] of Single; + FWaterfallBuf: array[0..1023] of Single; + FSpectrumBufCount: Integer; + FWaterfallBufCount: Integer; + FSpectrumDirty: Boolean; + FWaterfallDirty: Boolean; + FWfFrameCounter: Integer; + FWfFrameInterval: Integer; + + // ── Ссылки на TPaintBox (устанавливаются из MainForm после BuildUI) ─────── + FPbSpectrum: TPaintBox; + FPbWaterfall: TPaintBox; + FPbRuler: TPaintBox; + FPbSMeterRight: TPaintBox; + FPbFwdPower: TPaintBox; + FPbSWR: TPaintBox; + + // ── Приватные методы рендеринга ─────────────────────────────────────────── + function ActiveVfoFreq: Double; + function ScaleX(X, Total, Width: Integer): Integer; + procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); + procedure DrawMarkerLine(C: TCanvas; W, H: Integer); + + public + constructor Create; + destructor Destroy; override; + + // ── Состояние радио ─────────────────────────────────────────────────────── + property VfoA: Double read FVfoA write FVfoA; + property VfoB: Double read FVfoB write FVfoB; + property ActiveVfo: Integer read FActiveVfo write FActiveVfo; + property CenterFreq: Double read FCenterFreq write FCenterFreq; + property SpanHz: Double read FSpanHz write FSpanHz; + property Mode: Integer read FMode write FMode; + property FilterBW: Integer read FFilterBW write FFilterBW; + property AGCTop: Integer read FAGCTop write FAGCTop; + property AGCThresh: Double write FAGCThresh; + property AGCHangLevel: Double write FAGCHangLevel; + property WDSPReady: Boolean read FWDSPReady write FWDSPReady; + + // ── Водопад ─────────────────────────────────────────────────────────────── + property WfAGCEnabled: Boolean read FWfAGCEnabled write FWfAGCEnabled; + property WfNFEnabled: Boolean read FWfNFEnabled write FWfNFEnabled; + property WfManualHigh: Double read FWfManualHigh write FWfManualHigh; + property WfManualLow: Double read FWfManualLow write FWfManualLow; + property WfAGCOffset: Double read FWfAGCOffset write FWfAGCOffset; + + // ── Отображение ─────────────────────────────────────────────────────────── + property SpecRefLevel: Double read FSpecRefLevel write FSpecRefLevel; + property SpecRange: Double read FSpecRange write FSpecRange; + property SpecGridStep: Double read FSpecGridStep write FSpecGridStep; + + // ── Маркер ──────────────────────────────────────────────────────────────── + property MarkerActive: Boolean read FMarkerActive write FMarkerActive; + property MarkerX: Integer read FMarkerX write FMarkerX; + + // ── S-метр ──────────────────────────────────────────────────────────────── + property LastSMeter: Double read FLastSMeter write FLastSMeter; + property SMeterPeak: Double read FSMeterPeak write FSMeterPeak; + property SMeterMin: Double read FSMeterMin write FSMeterMin; + property LastFwdW: Double read FLastFwdW write FLastFwdW; + property LastSWR: Double read FLastSWR write FLastSWR; + + // ── Флаги обновления ────────────────────────────────────────────────────── + property SpectrumDirty: Boolean read FSpectrumDirty write FSpectrumDirty; + property WaterfallDirty: Boolean read FWaterfallDirty write FWaterfallDirty; + property WfFrameInterval: Integer read FWfFrameInterval write FWfFrameInterval; + + // ── Ссылки на PaintBox ──────────────────────────────────────────────────── + property PbSpectrum: TPaintBox write FPbSpectrum; + property PbWaterfall: TPaintBox write FPbWaterfall; + property PbRuler: TPaintBox write FPbRuler; + property PbSMeterRight: TPaintBox write FPbSMeterRight; + property PbFwdPower: TPaintBox write FPbFwdPower; + property PbSWR: TPaintBox write FPbSWR; + + // ── Данные от DSP (вызываются из DSP-потока) ────────────────────────────── + procedure SetSpectrumData(const Pixels: array of Single; Count: Integer); + procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); + + // ── Рендеринг ───────────────────────────────────────────────────────────── + procedure DrawSpectrum; + procedure DrawWaterfall; + procedure DrawRuler; + procedure DrawBarMeter(ACanvas: TCanvas; R: TRect; + Value, MaxVal: Double; BarColor: TColor); + procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect; + Value, Peak, MinVal: Double; + out ZX1, ZX2, ZY1, ZY2: Integer); + procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect; + X1, X2, Y1, Y2: Integer); + + // ── Управление размером bitmap ──────────────────────────────────────────── + procedure SetSpectrumBitmapSize(W, H: Integer); + procedure SetWaterfallBitmapSize(W, H: Integer); + procedure SetRulerSize(W, H: Integer); + function SpectrumBitmapWidth: Integer; + + // ── Paint-обработчики (назначаются в BuildUI) ───────────────────────────── + procedure PaintSpectrum(Sender: TObject); + procedure PaintWaterfall(Sender: TObject); + procedure PaintRuler(Sender: TObject); + procedure PaintSMeterRight(Sender: TObject); + procedure PaintFwdPower(Sender: TObject); + procedure PaintSWR(Sender: TObject); + + // ── Утилиты ─────────────────────────────────────────────────────────────── + procedure InvalidateGridCache; + procedure InvalidateRulerCache; + procedure ResetSpectrumBuf; + procedure ResetWfAvgBuf; + procedure FillDemoSpectrum; + function NeedsRulerRedraw: Boolean; + end; + +// Вспомогательные функции (публичные для возможного использования в MainForm) +function FormatFreqSV(Hz: Double): string; + +implementation + +// ════════════════════════════════════════════════════════════════════════════ +// Вспомогательные функции — цвет, градиент +// ════════════════════════════════════════════════════════════════════════════ + +function WaterfallColorThetis(Level: Integer): LongWord; +const + STOPS: array[0..5] of LongWord = ( + $000000, $0A1A48, $0088CC, $E0C020, $D84A12, $FFFFFF); +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 := ($FF shl 24) or (R shl 16) or (G shl 8) or B; +end; + +function WaterfallEnhancedColorThetis(ValueDB, LowDB, HighDB: Double): LongWord; +var + Overall, Local: Double; + R, G, B: Integer; +begin + if ValueDB <= LowDB then begin Result := $FF000000; Exit; end; + if ValueDB >= HighDB then begin Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; Exit; end; + Overall := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB); + if Overall < (2.0 / 9.0) then + begin Local := Overall / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end + else if Overall < (3.0 / 9.0) then + begin Local := (Overall - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end + else if Overall < (4.0 / 9.0) then + begin Local := (Overall - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end + else if Overall < (5.0 / 9.0) then + begin Local := (Overall - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end + else if Overall < (7.0 / 9.0) then + begin Local := (Overall - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end + else if Overall < (8.0 / 9.0) then + begin Local := (Overall - 7.0/9.0) / (1.0/9.0); R := 255; G := 0; B := Round(Local*255.0); end + else + begin + Local := (Overall - 8.0/9.0) / (1.0/9.0); + R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0); + G := Round(Local * 255.0 * 0.5); + B := 255; + end; + R := EnsureRange(R, 0, 255); + G := EnsureRange(G, 0, 255); + B := EnsureRange(B, 0, 255); + Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; +end; + +procedure InitRawDesc32(var Desc: TRawImageDescription; AWidth, AHeight: Integer); +begin + FillChar(Desc, SizeOf(Desc), 0); + Desc.Format := ricfRGBA; + Desc.Width := AWidth; + Desc.Height := AHeight; + Desc.Depth := 32; + Desc.BitOrder := riboBitsInOrder; + Desc.ByteOrder := riboLSBFirst; + Desc.LineOrder := riloTopToBottom; + Desc.BitsPerPixel := 32; + Desc.LineEnd := rileDWordBoundary; + Desc.BlueShift := 0; Desc.BluePrec := 8; + Desc.GreenShift := 8; Desc.GreenPrec := 8; + Desc.RedShift := 16; Desc.RedPrec := 8; + Desc.AlphaShift := 24; Desc.AlphaPrec := 8; +end; + +function MixColorBGR(C1, C2: TColor; T: Double): TColor; +var + B1, G1, R1, B2, G2, R2, B, G, R: Integer; +begin + if T < 0.0 then T := 0.0 else if T > 1.0 then T := 1.0; + B1 := (C1 shr 16) and $FF; G1 := (C1 shr 8) and $FF; R1 := C1 and $FF; + B2 := (C2 shr 16) and $FF; G2 := (C2 shr 8) and $FF; R2 := C2 and $FF; + B := Round(B1 + (B2-B1)*T); G := Round(G1 + (G2-G1)*T); R := Round(R1 + (R2-R1)*T); + Result := TColor((B shl 16) or (G shl 8) or R); +end; + +procedure PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor); +var Y: Integer; T: Double; +begin + if (W <= 0) or (H <= 0) then Exit; + C.Pen.Style := psClear; C.Brush.Style := bsSolid; + for Y := 0 to H - 1 do + begin + T := Y / Max(1, H - 1); + C.Brush.Color := MixColorBGR(TopColor, BottomColor, T); + C.FillRect(Rect(0, Y, W, Y + 1)); + end; + C.Pen.Style := psSolid; +end; + +function FormatFreqSV(Hz: Double): string; +var Mhz, KHz, Rest: Integer; +begin + Mhz := Trunc(Hz / 1000000); + KHz := Trunc((Hz - Mhz * 1000000) / 1000); + Rest := Trunc(Hz) mod 1000; + Result := Format('%3d.%3.3d.%3.3d', [Mhz, KHz, Rest]); +end; + +// ════════════════════════════════════════════════════════════════════════════ +// TSpectrumView +// ════════════════════════════════════════════════════════════════════════════ + +constructor TSpectrumView.Create; +begin + inherited Create; + FSpectrumBitmap := TBitmap.Create; + FWaterfallBitmap := TBitmap.Create; + FWfBitmap := TBitmap.Create; + FGridBitmap := TBitmap.Create; + FRulerBitmap := TBitmap.Create; + FSpecGradBmp := TBitmap.Create; + FSmBitmap := nil; + FSpecGradImg := nil; + FWfIntfImg := nil; + FWfBitmapW := 0; FWfBitmapH := 0; + FGridBitmapW := 0; FGridBitmapH := 0; + FSpPtsLen := 0; + FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; + // Водопад AGC defaults + FWfManualHigh := -80.0; + FWfManualLow := -130.0; + FWfAGCOffset := 0.0; + FWfHigh := -80.0; + FWfLow := -130.0; + FWfAGCEnabled := True; + FWfNFEnabled := False; + // Spectrum display defaults + FSpecRefLevel := -20.0; + FSpecRange := 110.0; + FSpecGridStep := 10.0; + FSpanHz := 192000; + FSpectrumBufCount := 1024; + FWaterfallBufCount := 1024; + FWfFrameInterval := 2; + FWfFrameCounter := 0; + FWaterfallDirty := True; + FLastSMeter := -130; FSMeterPeak := -130; FSMeterMin := -130; + FLastFwdW := 0; FLastSWR := 1; + ResetSpectrumBuf; +end; + +destructor TSpectrumView.Destroy; +begin + FSpectrumBitmap.Free; + FWaterfallBitmap.Free; + FWfBitmap.Free; + FGridBitmap.Free; + FRulerBitmap.Free; + FSpecGradBmp.Free; + FreeAndNil(FSpecGradImg); + FreeAndNil(FWfIntfImg); + FreeAndNil(FSmBitmap); + inherited; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// Приватные вспомогательные методы +// ──────────────────────────────────────────────────────────────────────────── + +function TSpectrumView.ActiveVfoFreq: Double; +begin + if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB; +end; + +function TSpectrumView.ScaleX(X, Total, Width: Integer): Integer; +begin + if Total = 0 then Result := 0 + else Result := Round(X / Total * Width); +end; + +procedure TSpectrumView.DrawMarkerLine(C: TCanvas; W, H: Integer); +var MX: Integer; MarkerFreq: Double; MarkerLbl: string; +begin + MX := Round(FMarkerX / 1000.0 * W); + if (MX < 0) or (MX >= W) then Exit; + MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz; + MarkerLbl := FormatFreqSV(Round(MarkerFreq)); + C.Pen.Color := TColor($004444FF); + C.Pen.Width := 1; C.Pen.Style := psSolid; + C.MoveTo(MX, 0); C.LineTo(MX, H); + C.Font.Color := TColor($004444FF); + C.Font.Size := 7; C.Font.Name := 'Courier New'; + if MX + 4 + C.TextWidth(MarkerLbl) < W then + C.TextOut(MX + 4, 4, MarkerLbl) + else + C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl); +end; + +procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); +var + Row, Col: Integer; YMin, Alpha: Integer; + RowPtr: PByte; BPPg: Integer; RG, GG: Byte; + Desc: TRawImageDescription; +begin + if FSpectrumBitmap = nil then Exit; + if (W <= 0) or (H <= 0) then Exit; + YMin := H; + for Col := 0 to W - 1 do + if SpPts[Col].Y < YMin then YMin := SpPts[Col].Y; + if YMin >= H then Exit; + if (FSpecGradImg = nil) or (FSpecGradImg.Width <> W) or (FSpecGradImg.Height <> H) then + begin + FreeAndNil(FSpecGradImg); + FSpecGradImg := TLazIntfImage.Create(W, H); + InitRawDesc32(Desc, W, H); + FSpecGradImg.DataDescription := Desc; + FSpecGradImg.CreateData; + end; + if FSpecGradImg.PixelData <> nil then + FillChar(FSpecGradImg.PixelData^, W * H * 4, 0); + BPPg := FSpecGradImg.DataDescription.BitsPerPixel div 8; + if BPPg < 3 then BPPg := 4; + for Row := YMin to H - 1 do + begin + Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200); + if Alpha <= 0 then Continue; + if Alpha > 200 then Alpha := 200; + GG := Byte(Alpha shr 1); RG := Byte(Alpha); + RowPtr := FSpecGradImg.GetDataLineStart(Row); + if RowPtr = nil then Continue; + for Col := 0 to W - 1 do + begin + if SpPts[Col].Y <= Row then + begin + RowPtr[0] := GG; RowPtr[1] := RG; RowPtr[2] := 0; + if BPPg >= 4 then RowPtr[3] := Byte(Alpha); + end else begin + RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0; + if BPPg >= 4 then RowPtr[3] := 0; + end; + Inc(RowPtr, BPPg); + end; + end; + FSpecGradBmp.LoadFromIntfImage(FSpecGradImg); + LCLIntf.BitBlt(FSpectrumBitmap.Canvas.Handle, 0, 0, W, H, + FSpecGradBmp.Canvas.Handle, 0, 0, $CC0020); +end; + +// ──────────────────────────────────────────────────────────────────────────── +// Публичные методы данных +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.SetSpectrumData(const Pixels: array of Single; Count: Integer); +var i, N: Integer; +begin + N := Min(Count, 1024); + for i := 0 to N - 1 do FSpectrumBuf[i] := Pixels[i]; + FSpectrumBufCount := N; + FSpectrumDirty := True; +end; + +procedure TSpectrumView.SetWaterfallData(const Pixels: array of Single; Count: Integer); +var i, N: Integer; +begin + N := Min(Count, 1024); + for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i]; + FWaterfallBufCount := N; + Inc(FWfFrameCounter); + if FWfFrameCounter >= Max(1, FWfFrameInterval) then + begin + FWfFrameCounter := 0; + FWaterfallDirty := True; + end; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// Размеры bitmap +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.SetSpectrumBitmapSize(W, H: Integer); +begin + if (W <= 0) or (H <= 0) then Exit; + FSpectrumBitmap.SetSize(W, H); + FSpectrumBitmap.Canvas.Brush.Color := SV_CLR_BG; + FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, W, H)); + FreeAndNil(FSpecGradImg); + InvalidateGridCache; + FSpPtsLen := 0; +end; + +procedure TSpectrumView.SetWaterfallBitmapSize(W, H: Integer); +begin + FWaterfallBitmap.SetSize(W, H); + FWfBitmapW := 0; FWfBitmapH := 0; + SetLength(FWfPixels, 0); + FreeAndNil(FWfIntfImg); +end; + +procedure TSpectrumView.SetRulerSize(W, H: Integer); +begin + if (W <= 0) or (H <= 0) then Exit; + FRulerBitmap.SetSize(W, H); + FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; +end; + +function TSpectrumView.SpectrumBitmapWidth: Integer; +begin + Result := FSpectrumBitmap.Width; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// Утилиты +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.InvalidateGridCache; +begin + FGridBitmapW := 0; FGridBitmapH := 0; + FSpectrumDirty := True; +end; + +procedure TSpectrumView.InvalidateRulerCache; +begin + FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; +end; + +procedure TSpectrumView.ResetSpectrumBuf; +var i: Integer; +begin + for i := 0 to 1023 do FSpectrumBuf[i] := -130.0; + for i := 0 to 1023 do FWaterfallBuf[i] := -130.0; + FWfHigh := FWfManualHigh; FWfLow := FWfManualLow; + FWaterfallDirty := True; +end; + +procedure TSpectrumView.ResetWfAvgBuf; +begin + FWfHigh := FWfManualHigh; FWfLow := FWfManualLow; + FWaterfallDirty := True; +end; + +procedure TSpectrumView.FillDemoSpectrum; +var i: Integer; FreqOff, Noise, Sig: Double; W: Integer; +begin + W := FSpectrumBitmap.Width; + if W <= 0 then W := 1024; + for i := 0 to W - 1 do + begin + if W > 1 then FreqOff := (i / (W - 1) - 0.5) * FSpanHz + else FreqOff := 0; + Noise := -110 + (Random - 0.5) * 6; + if Abs(FreqOff) < 2000 then Sig := -50 - Abs(FreqOff) / 200 + else Sig := -999; + FSpectrumBuf[i mod 1024] := Max(Noise, Sig); + end; +end; + +function TSpectrumView.NeedsRulerRedraw: Boolean; +begin + Result := (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or + (Abs(ActiveVfoFreq - FRulerLastVfo) >= 0.5); +end; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawSpectrum +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.DrawSpectrum; +var + C, GC: TCanvas; + i, Yp, W, H, GX: Integer; + DBmin, DBmax, dB: Double; + VfoX, X1, X2: Integer; + Lo_Hz, Hi_Hz, Half: Double; + AGCy, AGCHangY: Integer; + SrcF, Frac, dBv: Double; + S0, S1: Integer; + InvRange: Double; + LabelBandW, SrcCount: Integer; +begin + if FSpectrumBitmap = nil then Exit; + W := FSpectrumBitmap.Width; H := FSpectrumBitmap.Height; + if (W <= 0) or (H <= 0) then Exit; + C := FSpectrumBitmap.Canvas; + DBmax := FSpecRefLevel; + DBmin := FSpecRefLevel - FSpecRange; + InvRange := 1.0 / (DBmax - DBmin); + + // ── 1. Фон+сетка из кэша ────────────────────────────────────────────────── + if (FGridBitmapW <> W) or (FGridBitmapH <> H) then + begin + FGridBitmapW := W; FGridBitmapH := H; + FGridBitmap.SetSize(W, H); + GC := FGridBitmap.Canvas; + PaintVerticalGradient(GC, W, H, TColor($0020160B), TColor($006C4F1F)); + LabelBandW := 28; + GC.Pen.Style := psClear; GC.Brush.Style := bsSolid; + GC.Brush.Color := TColor($00543F18); + GC.FillRect(Rect(0, 0, LabelBandW, H)); + GC.Pen.Style := psSolid; + GC.Font.Size := 7; GC.Font.Name := 'Courier New'; + GC.Font.Color := TColor($00A8C9D9); + GC.Brush.Style := bsClear; + dB := DBmax - FSpecGridStep; + while dB >= DBmin do + begin + Yp := Round((DBmax - dB) * InvRange * H); + GC.Pen.Color := TColor($00685624); GC.Pen.Width := 1; + GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp); + GC.TextOut(1, Yp - 9, Format('%4.0f', [dB])); + dB := dB - FSpecGridStep; + end; + for i := 0 to 8 do + begin + GX := ScaleX(i, 8, W); + GC.Pen.Color := TColor($00685624); + GC.MoveTo(GX, 0); GC.LineTo(GX, H); + end; + end; + LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020); + + // ── 2. Полоса фильтра ───────────────────────────────────────────────────── + Half := FFilterBW / 2; + if FActiveVfo = 0 then + VfoX := Round((FVfoA - FCenterFreq + FSpanHz/2) / FSpanHz * W) + else + VfoX := Round((FVfoB - FCenterFreq + FSpanHz/2) / FSpanHz * W); + case FMode of + 0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end; + 1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end; + else begin Lo_Hz := -Half; Hi_Hz := Half; end; + end; + X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W))); + X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W))); + C.Brush.Color := TColor($006A4E22); C.Brush.Style := bsSolid; C.Pen.Style := psClear; + if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H)); + C.Pen.Style := psSolid; + + // ── 3. AGC линии ────────────────────────────────────────────────────────── + if FWDSPReady then + begin + AGCy := Round((DBmax - FAGCThresh) * InvRange * H); + if (AGCy >= 0) and (AGCy < H) then + begin + C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash; + C.MoveTo(0, AGCy); C.LineTo(W, AGCy); + C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6; + C.TextOut(4, AGCy - 9, 'AGC T'); + end; + AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H); + if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then + begin + C.Pen.Color := TColor($00FFCC00); C.Pen.Width := 1; C.Pen.Style := psDot; + C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY); + C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); C.Font.Size := 6; + C.TextOut(4, AGCHangY + 2, 'AGC H'); + end; + end else begin + AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H); + if (AGCy >= 0) and (AGCy < H) then + begin + C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash; + C.MoveTo(0, AGCy); C.LineTo(W, AGCy); + C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6; + C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop])); + end; + end; + + // ── 4. Кривая спектра ───────────────────────────────────────────────────── + if FSpPtsLen <> W + 2 then + begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end; + SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024); + for i := 0 to W - 1 do + begin + 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)))); + end; + FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H); + + // ── 5. Градиент ─────────────────────────────────────────────────────────── + C.Brush.Style := bsSolid; C.Pen.Style := psClear; + DrawSpectrumGradient(FSpPts, W, H); + + // ── 6. Линия спектра ────────────────────────────────────────────────────── + C.Pen.Style := psSolid; C.Pen.Color := TColor($0040FF80); C.Pen.Width := 1; + C.Polyline(Slice(FSpPts, W)); + + // ── 7. Края фильтра + VFO ───────────────────────────────────────────────── + C.Pen.Color := TColor($0040FFCC); C.Pen.Width := 1; C.Pen.Style := psSolid; + C.MoveTo(X1, 0); C.LineTo(X1, H); + C.MoveTo(X2, 0); C.LineTo(X2, H); + C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 2; + C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12); + C.Brush.Color := TColor($0000AAFF); C.Brush.Style := bsSolid; C.Pen.Width := 1; + C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]); + + if FMarkerActive then DrawMarkerLine(C, W, H); +end; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawWaterfall +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.DrawWaterfall; +var + W, H, X: Integer; + dB, frac, WatSrcF: Double; + WatS0, WatS1, V: Integer; + TargetLow, TargetHigh: Double; + HistMinDB, HistMaxDB, HistStepDB: Double; + NoiseFloorDB, SignalTopDB: Double; + CumCount, LowTargetCount, HighTargetCount, HistIdx: Integer; + WfHigh, WfLow, InvRange, Step: Double; + Px: PLongWord; + Desc: TRawImageDescription; + Row, Col: Integer; RowPtr: PByte; Src: PLongWord; BPPi: Integer; + Pal: LongWord; Hist: array[0..191] of Integer; SrcCount: Integer; +const + 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); + + FillChar(Hist, SizeOf(Hist), 0); + HistMinDB := -170.0; HistMaxDB := 22.0; + HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist); + for X := 0 to SrcCount - 1 do + begin + 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 NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end; + end; + CumCount := 0; + for HistIdx := 0 to High(Hist) do + begin + CumCount := CumCount + Hist[HistIdx]; + if CumCount >= HighTargetCount then + begin SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end; + end; + + if FWfAGCEnabled then + begin + 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; + 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); + + if (FWfBitmapW <> W) or (FWfBitmapH <> H) then + begin + FWfBitmapW := W; FWfBitmapH := H; + SetLength(FWfPixels, W * H); + FillDWord(FWfPixels[0], W * H, 0); + FWfBitmap.SetSize(W, H); + FreeAndNil(FWfIntfImg); + end; + + if H > 1 then Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W); + + 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 > SrcCount - 2 then WatS0 := SrcCount - 2; + WatS1 := WatS0 + 1; + frac := WatSrcF - WatS0; + 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; + Pal := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh); + Px^ := Pal; Inc(Px); + WatSrcF := WatSrcF + Step; + end; + + if FWfIntfImg = nil then + begin + FWfIntfImg := TLazIntfImage.Create(W, H); + InitRawDesc32(Desc, W, H); + FWfIntfImg.DataDescription := Desc; + FWfIntfImg.CreateData; + end; + BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8; + if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then + Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4) + else + begin + Src := @FWfPixels[0]; + for Row := 0 to H - 1 do + begin + RowPtr := FWfIntfImg.GetDataLineStart(Row); + if RowPtr = nil then begin Inc(Src, W); Continue; end; + for Col := 0 to W - 1 do + begin + RowPtr[0] := Byte(Src^); RowPtr[1] := Byte(Src^ shr 8); + RowPtr[2] := Byte(Src^ shr 16); + if BPPi >= 4 then RowPtr[3] := $FF; + Inc(Src); Inc(RowPtr, BPPi); + end; + end; + end; + FWfBitmap.LoadFromIntfImage(FWfIntfImg); +end; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawRuler +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.DrawRuler; +var + C: TCanvas; W, H, i, X: Integer; + FreqStart, FreqHz: Double; VfoX: Integer; + Lbl: string; TW: Integer; +begin + if FPbRuler = nil then Exit; + W := FPbRuler.Width; H := FPbRuler.Height; + if (W <= 0) or (H <= 0) then Exit; + if (Abs(FCenterFreq - FRulerLastFreq) < 0.5) and + (Abs(ActiveVfoFreq - FRulerLastVfo) < 0.5) then Exit; + FRulerLastFreq := FCenterFreq; FRulerLastVfo := ActiveVfoFreq; + if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then + FRulerBitmap.SetSize(W, H); + C := FRulerBitmap.Canvas; + PaintVerticalGradient(C, W, H, TColor($00372A12), TColor($00705A2A)); + C.Pen.Color := TColor($00685624); C.Pen.Width := 1; + C.MoveTo(0, 0); C.LineTo(W, 0); + C.MoveTo(0, H-1); C.LineTo(W, H-1); + C.Font.Name := 'Courier New'; C.Font.Size := 7; + C.Brush.Style := bsClear; + FreqStart := FCenterFreq - FSpanHz / 2; + for i := 0 to 8 do + begin + X := ScaleX(i, 8, W); + C.Pen.Color := TColor($00685624); + C.MoveTo(X, 0); C.LineTo(X, H div 2); + FreqHz := FreqStart + i * FSpanHz / 8; + Lbl := Format('%.3f', [FreqHz / 1e6]); + TW := C.TextWidth(Lbl); + C.Font.Color := TColor($00C8D8E2); + C.TextOut(X - TW div 2, H div 2 - 1, Lbl); + end; + VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W); + if (VfoX >= 0) and (VfoX < W) then + begin + C.Pen.Color := TColor($0000FF80); C.Pen.Width := 2; + C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1; + end; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawBarMeter, DrawSMeterWide, DrawSMeterZone +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.DrawBarMeter(ACanvas: TCanvas; R: TRect; + Value, MaxVal: Double; BarColor: TColor); +var Pct, BarW: Integer; +begin + ACanvas.Brush.Color := TColor($00080808); + ACanvas.FillRect(R); + if MaxVal > 0 then + Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2)) + else + Pct := 0; + BarW := Pct; + ACanvas.Brush.Color := BarColor; ACanvas.Pen.Color := BarColor; + ACanvas.FillRect(Rect(R.Left+1, R.Top+1, R.Left+1+BarW, R.Bottom-1)); + ACanvas.Brush.Style := bsClear; + ACanvas.Pen.Color := SV_CLR_BORDER; + ACanvas.Rectangle(R); +end; + +procedure TSpectrumView.DrawSMeterWide(ACanvas: TCanvas; R: TRect; + Value, Peak, MinVal: Double; out ZX1, ZX2, ZY1, ZY2: Integer); +const + DB_MIN = -127.0; DB_MAX = -13.0; DB_S9 = -73.0; DB_OVR = -43.0; + DBM_MARKS: array[0..5] of Double = (-120,-100,-80,-60,-40,-20); + DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20'); + S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13); + S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60'); + CLR_SMETER_BG = TColor($00181818); + CLR_BAR_GREEN = TColor($00091F00); + CLR_BAR_OVER = TColor($00091528); + CLR_PEAK_MARKER = TColor($00F8F8FF); + CLR_TICK_GREEN = TColor($00708070); + CLR_TICK_WHITE = TColor($00E0E0DC); + CLR_TICK_BLUE = TColor($0070A090); + CLR_LABEL_DBM = TColor($0096D8DC); + CLR_LABEL_S = TColor($00F0F0F0); + CLR_SCALE_DBM = TColor($00D0DCDC); + CLR_SCALE_S_GREEN = TColor($00E0E0E0); + CLR_SCALE_S_BLUE = TColor($0090D090); + CLR_BDR = TColor($00303028); + LEFT_INFO = 62; TICK_LONG = 5; TICK_MED = 3; +var + W, H, BX, BW: Integer; + Y_BAR_TOP, Y_BAR_BOT: Integer; + BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer; + i, X, TW: Integer; Lbl: string; SNum, Over: Integer; + + function DBtoX(dB: Double): Integer; inline; + begin + Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW); + if Result < BX then Result := BX; + if Result > BX + BW then Result := BX + BW; + end; + + procedure VLine(X2, Y1, Y2: Integer; C: TColor); + begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end; + + procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor); + begin + ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid; + ACanvas.Pen.Style := psClear; + if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2)); + ACanvas.Pen.Style := psSolid; + end; + +begin + W := R.Right - R.Left; H := R.Bottom - R.Top; + ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0; + if (W < 80) or (H < 16) then Exit; + BX := R.Left + LEFT_INFO; BW := W - LEFT_INFO - 4; + Y_BAR_TOP := R.Top + H * 36 div 100; + Y_BAR_BOT := R.Top + H * 66 div 100; + S9X := DBtoX(DB_S9); OvrX := DBtoX(DB_OVR); + + ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid; + ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid; + FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, TColor($00061006)); + FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, TColor($000A1006)); + FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, TColor($00060A10)); + + BarEnd := DBtoX(Value); + PeakLeft := DBtoX(MinVal); + PeakRight := DBtoX(Peak); + ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight)); + ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight)); + ZY1 := Y_BAR_TOP + 1; ZY2 := Y_BAR_BOT - 1; + + if BarEnd > BX then + begin + if BarEnd <= S9X then + FillBar(BX, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_GREEN) + else begin + FillBar(BX, Y_BAR_TOP+1, S9X, Y_BAR_BOT-1, CLR_BAR_GREEN); + FillBar(S9X, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_OVER); + end; + end; + if (PeakRight > BX) and (PeakRight <= BX + BW) then + begin + ACanvas.Pen.Color := CLR_PEAK_MARKER; ACanvas.Pen.Width := 2; + ACanvas.MoveTo(PeakRight, Y_BAR_TOP); ACanvas.LineTo(PeakRight, Y_BAR_BOT); + ACanvas.Pen.Width := 1; + end; + ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR; + ACanvas.Rectangle(BX, Y_BAR_TOP, BX + BW, Y_BAR_BOT); + + ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Size := 6; + ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear; + for i := 0 to High(DBM_MARKS) do + begin + X := DBtoX(DBM_MARKS[i]); + VLine(X, Y_BAR_TOP - TICK_LONG, Y_BAR_TOP - 1, CLR_TICK_WHITE); + Lbl := DBM_LABELS[i]; TW := ACanvas.TextWidth(Lbl); + ACanvas.Font.Color := CLR_SCALE_DBM; + ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl); + end; + i := -125; + while i < -13 do + begin + X := DBtoX(i); VLine(X, Y_BAR_TOP - TICK_MED, Y_BAR_TOP - 1, CLR_TICK_GREEN); + Inc(i, 5); + end; + for i := 0 to High(S_MARKS_DBM) do + begin + X := DBtoX(S_MARKS_DBM[i]); + if S_MARKS_DBM[i] >= DB_S9 then VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_BLUE) + else VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_GREEN); + Lbl := S_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl); + if S_MARKS_DBM[i] >= DB_S9 then ACanvas.Font.Color := CLR_SCALE_S_BLUE + else ACanvas.Font.Color := CLR_SCALE_S_GREEN; + ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG + 1, Lbl); + end; + + ACanvas.Font.Size := 8; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM; + Lbl := Format('%6.1f', [Value]); + ACanvas.TextOut(R.Left + 1, R.Top + 1, Lbl); + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; + ACanvas.Font.Color := TColor($00507070); + ACanvas.TextOut(R.Left + 1, Y_BAR_TOP - ACanvas.TextHeight('dBm') - 1, 'dBm'); + + ACanvas.Font.Size := 8; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S; + if Value >= -73.0 then + begin + Over := Round(Value - (-73.0)); + if Over < 5 then Lbl := 'S9' + else begin Over := ((Over + 5) div 10) * 10; if Over = 0 then Over := 10; Lbl := Format('S9+%d', [Over]); end; + end else if Value <= -121.0 then Lbl := 'S1' + else begin + Lbl := 'S1'; + for SNum := 1 to 8 do + if Value >= (-121.0 + (SNum - 1) * 6.0) then Lbl := 'S' + IntToStr(SNum); + end; + ACanvas.TextOut(R.Left + 1, Y_BAR_BOT + TICK_LONG + 1, Lbl); +end; + +procedure TSpectrumView.DrawSMeterZone(ACanvas: TCanvas; R: TRect; + X1, X2, Y1, Y2: Integer); +const ALPHA = 16384; +var Img: TLazIntfImage; FC: TFPColor; IX, IY: Integer; +begin + if (X2 <= X1) or (FSmBitmap = nil) then Exit; + Img := FSmBitmap.CreateIntfImage; + try + for IY := Y1 to Y2 - 1 do + for IX := X1 to X2 - 1 do + begin + FC := Img.Colors[IX, IY]; + FC.Red := Min(65535, FC.Red + ALPHA); + FC.Green := Min(65535, FC.Green + ALPHA); + FC.Blue := Min(65535, FC.Blue + ALPHA); + Img.Colors[IX, IY] := FC; + end; + FSmBitmap.LoadFromIntfImage(Img); + finally Img.Free; end; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// Paint-обработчики +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.PaintSpectrum(Sender: TObject); +var PB: TPaintBox; +begin + PB := TPaintBox(Sender); + if (FSpectrumBitmap.Width > 0) and (FSpectrumBitmap.Height > 0) then + PB.Canvas.Draw(0, 0, FSpectrumBitmap) + else begin + PB.Canvas.Brush.Color := SV_CLR_BG; + PB.Canvas.FillRect(Rect(0, 0, PB.Width, PB.Height)); + end; +end; + +procedure TSpectrumView.PaintWaterfall(Sender: TObject); +var PB: TPaintBox; W, H: Integer; +begin + PB := TPaintBox(Sender); + W := PB.Width; H := PB.Height; + if (W <= 0) or (H <= 0) then Exit; + if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then + PB.Canvas.Draw(0, 0, FWfBitmap) + else begin + PB.Canvas.Brush.Color := clBlack; + PB.Canvas.FillRect(Rect(0, 0, W, H)); + end; + if FMarkerActive then DrawMarkerLine(PB.Canvas, W, H); +end; + +procedure TSpectrumView.PaintRuler(Sender: TObject); +var PB: TPaintBox; +begin + PB := TPaintBox(Sender); + DrawRuler; + if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then + PB.Canvas.Draw(0, 0, FRulerBitmap); +end; + +procedure TSpectrumView.PaintSMeterRight(Sender: TObject); +var PB: TPaintBox; W, H, ZX1, ZX2, ZY1, ZY2: Integer; +begin + PB := TPaintBox(Sender); + W := PB.Width; H := PB.Height; + if (W <= 0) or (H <= 0) then Exit; + if (FSmBitmap = nil) or (FSmBitmap.Width <> W) or (FSmBitmap.Height <> H) then + begin + FreeAndNil(FSmBitmap); + FSmBitmap := TBitmap.Create; + FSmBitmap.SetSize(W, H); + end; + DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, W, H), + FLastSMeter, FSMeterPeak, FSMeterMin, + ZX1, ZX2, ZY1, ZY2); + DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, W, H), ZX1, ZX2, ZY1, ZY2); + PB.Canvas.Draw(0, 0, FSmBitmap); +end; + +procedure TSpectrumView.PaintFwdPower(Sender: TObject); +var PB: TPaintBox; +begin + PB := TPaintBox(Sender); + DrawBarMeter(PB.Canvas, Rect(0, 0, PB.Width, PB.Height), + FLastFwdW, 150, SV_CLR_METER_ON); +end; + +procedure TSpectrumView.PaintSWR(Sender: TObject); +var PB: TPaintBox; +begin + PB := TPaintBox(Sender); + DrawBarMeter(PB.Canvas, Rect(0, 0, PB.Width, PB.Height), + FLastSWR - 1.0, 4.0, SV_CLR_AMBER); +end; + +end. diff --git a/ewsdr.lpi b/ewsdr.lpi index ffb887e..8984683 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -151,6 +151,10 @@ + + + +