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 @@
+
+
+
+