From d99c19ec9ab0753e9bde59e07485e0937f8b9f59 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 22 May 2026 11:16:55 +0300 Subject: [PATCH] Split SpectrumView into WaterfallView, SMeterView, RulerView modules TSpectrumView remains the public facade for MainForm; waterfall, S-meter, and ruler rendering are now in isolated units. Dead code removed. Co-Authored-By: Claude Sonnet 4.6 --- RulerView.pas | 248 +++++++++ SMeterView.pas | 507 ++++++++++++++++++ SpectrumView.pas | 1244 +++++++++------------------------------------ WaterfallView.pas | 445 ++++++++++++++++ ewsdr.lpi | 16 + 5 files changed, 1454 insertions(+), 1006 deletions(-) create mode 100644 RulerView.pas create mode 100644 SMeterView.pas create mode 100644 WaterfallView.pas diff --git a/RulerView.pas b/RulerView.pas new file mode 100644 index 0000000..bc32868 --- /dev/null +++ b/RulerView.pas @@ -0,0 +1,248 @@ +unit RulerView; + +{ + RulerView.pas — рендеринг линейки частот под спектром/водопадом. + TRulerView — изолированный класс, не зависит от SpectrumView. +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, + LCLIntf, LCLType, AppTheme; + +type + TRulerView = class + private + FRulerBitmap: TBitmap; + FRulerLastFreq: Double; + FRulerLastVfo: Double; + FRulerLastSpan: Double; + FVfoA: Double; + FVfoB: Double; + FActiveVfo: Integer; + FCenterFreq: Double; + FSpanHz: Double; + FFMGridStepHz: Double; + FTheme: TAppTheme; + FPbRuler: TPaintBox; + + function ActiveVfoFreq: Double; + function ScaleX(X, Total, Width: Integer): 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 FMGridStepHz: Double read FFMGridStepHz write FFMGridStepHz; + property PbRuler: TPaintBox write FPbRuler; + + procedure DrawRuler; + procedure PaintRuler(Sender: TObject); + procedure SetRulerSize(W, H: Integer); + procedure InvalidateRulerCache; + function NeedsRulerRedraw: Boolean; + procedure SetTheme(const T: TAppTheme); + end; + +implementation + +// ════════════════════════════════════════════════════════════════════════════ +// Вспомогательные функции (приватные копии) +// ════════════════════════════════════════════════════════════════════════════ + +function FormatFreqRuler(Hz: Double): string; +var Mhz: Int64; KHz, Rest: Integer; +begin + Mhz := Trunc(Hz / 1000000); + KHz := Trunc((Hz - Mhz * 1000000) / 1000); + Rest := Trunc(Hz) mod 1000; + Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]); +end; + +function MixColorBGRR(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 PaintVerticalGradientR(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 := MixColorBGRR(TopColor, BottomColor, T); + C.FillRect(Rect(0, Y, W, Y + 1)); + end; + C.Pen.Style := psSolid; +end; + +// ════════════════════════════════════════════════════════════════════════════ +// TRulerView +// ════════════════════════════════════════════════════════════════════════════ + +constructor TRulerView.Create; +begin + inherited Create; + FRulerBitmap := TBitmap.Create; + FRulerLastFreq := -1.0; + FRulerLastVfo := -1.0; + FRulerLastSpan := -1.0; + FTheme := DarkTheme; + FSpanHz := 192000; +end; + +destructor TRulerView.Destroy; +begin + FRulerBitmap.Free; + inherited; +end; + +function TRulerView.ActiveVfoFreq: Double; +begin + if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB; +end; + +function TRulerView.ScaleX(X, Total, Width: Integer): Integer; +begin + if Total = 0 then Result := 0 + else Result := Round(X / Total * Width); +end; + +procedure TRulerView.SetTheme(const T: TAppTheme); +begin + FTheme := T; + InvalidateRulerCache; +end; + +procedure TRulerView.SetRulerSize(W, H: Integer); +begin + if (W <= 0) or (H <= 0) then Exit; + FRulerBitmap.SetSize(W, H); + InvalidateRulerCache; +end; + +procedure TRulerView.InvalidateRulerCache; +begin + FRulerLastFreq := -1.0; + FRulerLastVfo := -1.0; + FRulerLastSpan := -1.0; +end; + +function TRulerView.NeedsRulerRedraw: Boolean; +begin + Result := (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or + (Abs(ActiveVfoFreq - FRulerLastVfo) >= 0.5) or + (Abs(FSpanHz - FRulerLastSpan) >= 1.0); +end; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawRuler +// ──────────────────────────────────────────────────────────────────────────── + +procedure TRulerView.DrawRuler; +var + C: TCanvas; W, H, i, X: Integer; + FreqStart, FreqHz, GridLine, pixPerStep: Double; VfoX: Integer; + Lbl: string; TW: Integer; + N, labelMult: 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) and + (Abs(FSpanHz - FRulerLastSpan) < 1.0) then Exit; + FRulerLastFreq := FCenterFreq; FRulerLastVfo := ActiveVfoFreq; FRulerLastSpan := FSpanHz; + if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then + FRulerBitmap.SetSize(W, H); + C := FRulerBitmap.Canvas; + PaintVerticalGradientR(C, W, H, FTheme.RulerGradTop, FTheme.RulerGradBot); + C.Pen.Color := FTheme.RulerBorder; 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; + if FFMGridStepHz > 0 then + begin + pixPerStep := W * FFMGridStepHz / FSpanHz; + if pixPerStep >= 1.0 then + labelMult := Max(1, Ceil((C.TextWidth('000.000') + 6) / pixPerStep)) + else + labelMult := MaxInt; + N := Ceil(FreqStart / FFMGridStepHz); + GridLine := N * FFMGridStepHz; + while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do + begin + X := Round((GridLine - FreqStart) / FSpanHz * W); + if (X >= 0) and (X < W) then + begin + C.Pen.Color := FTheme.RulerBorder; + if (N mod labelMult) = 0 then + begin + C.MoveTo(X, 0); C.LineTo(X, H div 2); + Lbl := Format('%.3f', [GridLine / 1e6]); + TW := C.TextWidth(Lbl); + C.Font.Color := FTheme.RulerText; + C.TextOut(X - TW div 2, H div 2 - 1, Lbl); + end else + begin + C.MoveTo(X, 0); C.LineTo(X, H div 4); + end; + end; + Inc(N); + GridLine := GridLine + FFMGridStepHz; + end; + end else + begin + for i := 0 to 8 do + begin + X := ScaleX(i, 8, W); + C.Pen.Color := FTheme.RulerBorder; + 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 := FTheme.RulerText; + C.TextOut(X - TW div 2, H div 2 - 1, Lbl); + end; + end; + VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W); + if (VfoX >= 0) and (VfoX < W) then + begin + C.Pen.Color := FTheme.RulerVfo; C.Pen.Width := 2; + C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1; + end; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// PaintRuler +// ──────────────────────────────────────────────────────────────────────────── + +procedure TRulerView.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; + +end. diff --git a/SMeterView.pas b/SMeterView.pas new file mode 100644 index 0000000..8bf1b63 --- /dev/null +++ b/SMeterView.pas @@ -0,0 +1,507 @@ +unit SMeterView; + +{ + SMeterView.pas — рендеринг S-метра, TX-метра и индикаторов мощности/SWR. + TSMeterView — изолированный класс без зависимостей на SpectrumView. +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, + IntfGraphics, FPImage, LCLType, GraphType, AppTheme; + +const + SM_CLR_METER_ON = TColor($0000CC44); + SM_CLR_AMBER = TColor($0000AAFF); + +type + TSMeterView = class + private + FSmBitmap: TBitmap; + FTheme: TAppTheme; + FLastSMeter: Double; + FSMeterPeak: Double; + FSMeterMin: Double; + FLastFwdW: Double; + FLastSWR: Double; + FPAMaxPower: Double; + FTransmitting: Boolean; + FPbSMeterRight: TPaintBox; + + 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 DrawTXMeter(ACanvas: TCanvas; R: TRect); + public + constructor Create; + destructor Destroy; override; + + 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 PAMaxPower: Double read FPAMaxPower write FPAMaxPower; + property Transmitting: Boolean read FTransmitting write FTransmitting; + property PbSMeterRight: TPaintBox write FPbSMeterRight; + + procedure PaintSMeterRight(Sender: TObject); + procedure SetTheme(const T: TAppTheme); + end; + +implementation + +// ════════════════════════════════════════════════════════════════════════════ +// TSMeterView +// ════════════════════════════════════════════════════════════════════════════ + +constructor TSMeterView.Create; +begin + inherited Create; + FSmBitmap := nil; + FTheme := DarkTheme; + FLastSMeter := -130; + FSMeterPeak := -130; + FSMeterMin := -130; + FLastFwdW := 0; + FLastSWR := 1; + FPAMaxPower := 100.0; + FTransmitting := False; +end; + +destructor TSMeterView.Destroy; +begin + FreeAndNil(FSmBitmap); + inherited; +end; + +procedure TSMeterView.SetTheme(const T: TAppTheme); +begin + FTheme := T; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawSMeterWide +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSMeterView.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'); + LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4; + TICK_LONG = 5; TICK_MED = 3; +var + CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor; + CLR_PEAK_MARKER, CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor; + CLR_LABEL_DBM, CLR_LABEL_S, CLR_SCALE_DBM: TColor; + CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor; + W, H, BX, BW, LeftInfo: Integer; + Y_BAR_TOP, Y_BAR_BOT: Integer; + BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer; + i, X, TW: Integer; Lbl: string; SNum, Over: Integer; + LblDbm, LblS: string; + TWDbm, LblZoneL, TH10, TH6, MaxTWDbm, MaxTWS, LabelNeed: 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 + CLR_SMETER_BG := FTheme.SMeterBG; + CLR_BAR_GREEN := FTheme.SMeterBarGreen; + CLR_BAR_OVER := FTheme.SMeterBarOver; + CLR_PEAK_MARKER := FTheme.SMeterPeakMarker; + CLR_TICK_GREEN := FTheme.SMeterTickGreen; + CLR_TICK_WHITE := FTheme.SMeterTickWhite; + CLR_TICK_BLUE := FTheme.SMeterTickBlue; + CLR_LABEL_DBM := FTheme.SMeterLabelDbm; + CLR_LABEL_S := FTheme.SMeterLabelS; + CLR_SCALE_DBM := FTheme.SMeterScaleDbm; + CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen; + CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue; + CLR_BDR := FTheme.SMeterBdr; + + 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; + + LblDbm := Format('%.1f', [Value]); + if Value >= -73.0 then + begin + Over := Round(Value - (-73.0)); + if Over < 5 then LblS := 'S9' + else begin Over := ((Over + 5) div 10) * 10; if Over = 0 then Over := 10; LblS := Format('S9+%d', [Over]); end; + end else if Value <= -121.0 then LblS := 'S1' + else begin + LblS := 'S1'; + for SNum := 1 to 8 do + if Value >= (-121.0 + (SNum - 1) * 6.0) then LblS := 'S' + IntToStr(SNum); + end; + + ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold]; + ACanvas.Font.Size := 10; TWDbm := ACanvas.TextWidth(LblDbm); + MaxTWDbm := ACanvas.TextWidth('-120.0'); + MaxTWS := ACanvas.TextWidth('S9+60'); + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; + LabelNeed := Max(MaxTWDbm + 2 + ACanvas.TextWidth('dBm '), MaxTWS) + LABEL_PAD * 2; + LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4); + BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD; + if BW < 10 then Exit; + LblZoneL := BX - LabelNeed; + + Y_BAR_TOP := R.Top + H * 44 div 100; + Y_BAR_BOT := R.Top + H * 58 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, CLR_BAR_GREEN); + FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, CLR_BAR_GREEN); + FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, CLR_BAR_OVER); + + 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 - (Y_BAR_BOT - Y_BAR_TOP)); + ACanvas.LineTo(PeakRight, Y_BAR_BOT + (Y_BAR_BOT - Y_BAR_TOP)); + ACanvas.Pen.Width := 1; + end; + + ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR; + ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT); + ACanvas.Pen.Color := FTheme.SMeterDivider; + ACanvas.MoveTo(BX, Y_BAR_TOP + 1); ACanvas.LineTo(BX, Y_BAR_BOT - 1); + + 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.Brush.Style := bsClear; + ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0'); + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0'); + ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM; + ACanvas.TextOut(LblZoneL + 4, Y_BAR_TOP - TH10 - 2, LblDbm); + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := CLR_TICK_BLUE; + ACanvas.TextOut(LblZoneL + 4 + TWDbm + 2, Y_BAR_TOP - TH6 - 4, 'dBm'); + ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S; + ACanvas.TextOut(LblZoneL + 4, Y_BAR_BOT + 2, LblS); +end; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawSMeterZone +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSMeterView.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; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawTXMeter +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSMeterView.DrawTXMeter(ACanvas: TCanvas; R: TRect); +const + SWR_HIGH_THRESH = 2.5; + SWR_MAX = 10.0; + LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4; + TICK_LONG = 5; TICK_MED = 3; + PWR_MARKS: array[0..4] of Double = (0.0, 0.25, 0.5, 0.75, 1.0); + SWR_MARKS_VAL: array[0..3] of Double = (1.0, 2.5, 5.0, 10.0); + SWR_MARKS_LBL: array[0..3] of string = ('1', '2.5', '5', '10'); +var + CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor; + CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor; + CLR_LABEL_DBM, CLR_LABEL_S: TColor; + CLR_SCALE_DBM, CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor; + CLR_SWR_HIGH: TColor; + W, H, BX, BW, LeftInfo: Integer; + Y_BAR_TOP, Y_BAR_BOT, Y_BAR_MID: Integer; + PY1, PY2, SY1, SY2: Integer; + PBarEnd, SBarEnd, S_WarnX: Integer; + LblPwr, LblSWR: string; + TWPwr, LblZoneL, TH10, TH6, MaxTWPwr, MaxTWSWR, LabelNeed: Integer; + i, X, TW: Integer; + Lbl: string; + SWRHigh: Boolean; + PwrPct: Double; + + function PwrToX(Pct: Double): Integer; inline; + begin + Result := BX + Round(Max(0.0, Min(1.0, Pct)) * BW); + if Result < BX then Result := BX; + if Result > BX + BW then Result := BX + BW; + end; + + function SWRToX(SWR: Double): Integer; inline; + begin + Result := BX + Round(Max(0.0, Min(1.0, (SWR - 1.0) / (SWR_MAX - 1.0))) * 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 + CLR_SMETER_BG := FTheme.SMeterBG; + CLR_BAR_GREEN := FTheme.SMeterBarGreen; + CLR_BAR_OVER := FTheme.SMeterBarOver; + CLR_TICK_GREEN := FTheme.SMeterTickGreen; + CLR_TICK_WHITE := FTheme.SMeterTickWhite; + CLR_TICK_BLUE := FTheme.SMeterTickBlue; + CLR_LABEL_DBM := FTheme.SMeterLabelDbm; + CLR_LABEL_S := FTheme.SMeterLabelS; + CLR_SCALE_DBM := FTheme.SMeterScaleDbm; + CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen; + CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue; + CLR_BDR := FTheme.SMeterBdr; + CLR_SWR_HIGH := TColor($000000CC); + + W := R.Right - R.Left; H := R.Bottom - R.Top; + if (W < 80) or (H < 16) then Exit; + + SWRHigh := FLastSWR > SWR_HIGH_THRESH; + LblPwr := Format('%.0fW', [FLastFwdW]); + LblSWR := Format('SWR:%.1f', [FLastSWR]); + + ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold]; + ACanvas.Font.Size := 10; + TWPwr := ACanvas.TextWidth(LblPwr); + MaxTWPwr := ACanvas.TextWidth(Format('%.0fW', [FPAMaxPower])); + MaxTWSWR := ACanvas.TextWidth('SWR:10.0'); + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; + LabelNeed := Max(MaxTWPwr, MaxTWSWR) + LABEL_PAD * 2; + LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4); + BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD; + if BW < 10 then Exit; + LblZoneL := BX - LabelNeed; + + Y_BAR_TOP := R.Top + H * 44 div 100; + Y_BAR_BOT := R.Top + H * 58 div 100; + Y_BAR_MID := (Y_BAR_TOP + Y_BAR_BOT) div 2; + + PY1 := Y_BAR_TOP; + PY2 := Y_BAR_MID - 1; + SY1 := Y_BAR_MID + 1; + SY2 := Y_BAR_BOT; + + S_WarnX := SWRToX(SWR_HIGH_THRESH); + + ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid; + ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid; + + FillBar(BX, PY1, BX + BW, PY2, CLR_BAR_GREEN); + FillBar(BX, SY1, S_WarnX, SY2, CLR_BAR_GREEN); + FillBar(S_WarnX, SY1, BX + BW, SY2, CLR_BAR_OVER); + + if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0; + PBarEnd := PwrToX(PwrPct); + FillBar(BX, PY1 + 1, PBarEnd, PY2 - 1, SM_CLR_METER_ON); + + SBarEnd := SWRToX(FLastSWR); + if not SWRHigh then + FillBar(BX, SY1 + 1, SBarEnd, SY2 - 1, SM_CLR_AMBER) + else + begin + FillBar(BX, SY1 + 1, Min(S_WarnX, SBarEnd), SY2 - 1, SM_CLR_AMBER); + if SBarEnd > S_WarnX then + FillBar(S_WarnX, SY1 + 1, SBarEnd, SY2 - 1, CLR_SWR_HIGH); + end; + + VLine(S_WarnX, SY1, SY2, FTheme.SMeterDivider); + + ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR; + ACanvas.Rectangle(LblZoneL, PY1, BX + BW, PY2); + if SWRHigh then ACanvas.Pen.Color := CLR_SWR_HIGH + else ACanvas.Pen.Color := CLR_BDR; + ACanvas.Rectangle(LblZoneL, SY1, BX + BW, SY2); + + ACanvas.Pen.Color := FTheme.SMeterDivider; + ACanvas.MoveTo(BX, PY1 + 1); ACanvas.LineTo(BX, PY2 - 1); + ACanvas.MoveTo(BX, SY1 + 1); ACanvas.LineTo(BX, SY2 - 1); + + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear; + + i := 5; + while i < 100 do + begin + if (i mod 25) <> 0 then + begin + X := PwrToX(i / 100.0); + VLine(X, PY1 - TICK_MED, PY1 - 1, CLR_TICK_GREEN); + end; + Inc(i, 5); + end; + + for i := 0 to 4 do + begin + X := PwrToX(PWR_MARKS[i]); + VLine(X, PY1 - TICK_LONG, PY1 - 1, CLR_TICK_WHITE); + Lbl := Format('%.0f', [PWR_MARKS[i] * FPAMaxPower]); + TW := ACanvas.TextWidth(Lbl); + ACanvas.Font.Color := CLR_SCALE_DBM; + ACanvas.TextOut(X - TW div 2, PY1 - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl); + end; + + for i := 2 to 9 do + begin + if i = 5 then Continue; + X := SWRToX(i); + if i >= 3 then VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_BLUE) + else VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_GREEN); + end; + + for i := 0 to 3 do + begin + X := SWRToX(SWR_MARKS_VAL[i]); + if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then + VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_BLUE) + else + VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_GREEN); + Lbl := SWR_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl); + if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then + begin + if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH + else ACanvas.Font.Color := CLR_SCALE_S_BLUE; + end else + ACanvas.Font.Color := CLR_SCALE_S_GREEN; + ACanvas.TextOut(X - TW div 2, SY2 + TICK_LONG + 1, Lbl); + end; + + ACanvas.Brush.Style := bsClear; + ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0'); + ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0'); + + ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM; + ACanvas.TextOut(LblZoneL + 4, PY1 - TH10 - 2, LblPwr); + + ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; + if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH + else ACanvas.Font.Color := CLR_LABEL_S; + ACanvas.TextOut(LblZoneL + 4, SY2 + 2, LblSWR); + + if SWRHigh then + begin + ACanvas.Font.Size := 6; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_SWR_HIGH; + ACanvas.TextOut(LblZoneL + 4 + TWPwr + 2, PY1 - TH6 - 4, 'SWR High'); + end; +end; + +// ──────────────────────────────────────────────────────────────────────────── +// PaintSMeterRight +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSMeterView.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; + if FTransmitting then + begin + DrawTXMeter(FSmBitmap.Canvas, Rect(0, 0, W, H)); + end else + begin + 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); + end; + PB.Canvas.Draw(0, 0, FSmBitmap); +end; + +end. diff --git a/SpectrumView.pas b/SpectrumView.pas index 01b3ebe..e54a291 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -1,13 +1,14 @@ unit SpectrumView; { - SpectrumView.pas — рендеринг спектра, водопада и S-метра. + SpectrumView.pas — рендеринг спектрограммы. - TSpectrumView инкапсулирует всё рисование: спектр, водопад, линейка частот, - S-метр, индикаторы мощности/SWR. MainForm создаёт один экземпляр, передаёт - ссылки на TPaintBox после BuildUI, обновляет свойства при изменении состояния. + TSpectrumView инкапсулирует рисование спектра и агрегирует дочерние + компоненты: TWaterfallView, TSMeterView, TRulerView. MainForm создаёт один + экземпляр TSpectrumView и работает с ним единообразно: свойства/методы + водопада, S-метра и линейки делегируются в соответствующие sub-view. - Зависимости: нет обратной зависимости на MainForm (нет circular dep). + Зависимости: нет обратной зависимости на MainForm. } {$IFDEF FPC} @@ -19,44 +20,30 @@ interface uses Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme, - AlertOverlay; + AlertOverlay, + WaterfallView, SMeterView, RulerView; const - SV_CLR_BG = TColor($00101010); - SV_CLR_BORDER = TColor($00303030); - SV_CLR_METER_ON = TColor($0000CC44); - SV_CLR_AMBER = TColor($0000AAFF); + SV_CLR_BG = TColor($00101010); 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; FFMGridLastCenter: Double; FFMGridLastStepHz: Double; FFMGridLastSpan: Double; - FRulerBitmap: TBitmap; FSpecGradImg: TLazIntfImage; FSpecGradBmp: TBitmap; - FWfIntfImg: TLazIntfImage; - FWfPixels: array of LongWord; FSpPts: array of TPoint; + FSpPtsLen: Integer; // ── Тема ───────────────────────────────────────────────────────────────── FTheme: TAppTheme; FLightTheme: Boolean; - FSpPtsLen: Integer; - FSmBitmap: TBitmap; - FRulerLastFreq: Double; - FRulerLastVfo: Double; - FRulerLastSpan: Double; - // ── Radio state ─────────────────────────────────────────────────────────── FVfoA: Double; FVfoB: Double; @@ -69,96 +56,96 @@ type 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; FFMGridStepHz: Double; - - // ── TX overlay (Thetis-style: при TX данные в FSpectrumBuf приходят - // от TX-анализатора @ FTXSpanHz Гц вокруг FTXFreq, и должны быть - // отрисованы в RX-координатах (FCenterFreq/FSpanHz), чтобы линейка - // частот совпадала с RX и сигнал стоял на правильной частоте даже - // при включённом CTUN. Если RX rate ≠ TX rate, видна только пересечение. + // ── TX overlay ──────────────────────────────────────────────────────────── FTXMode: Boolean; - FTXFreq: Double; // центр TX-сигнала (Hz) - FTXSpanHz: Double; // ширина TX-окна (192000 для Hermes/Saturn) - FTXVfoIndex: Integer; // 0=A, 1=B — какой VFO привязан к TX (для split) - // FTXOverlay — независимый от FTXMode флаг: «нарисовать TX-фильтр поверх - // данных». DUP-режим: FTXMode=False (данные RX), но FTXOverlay=True (TX-полоса - // красным сверху). Без DUP при TX оба флага True. Без TX оба False. + FTXFreq: Double; + FTXSpanHz: Double; + FTXVfoIndex: Integer; FTXOverlay: Boolean; - // ── ADC overload overlay ───────────────────────────────────────────────── FADCOverloadVisible: Boolean; - // ── Marker ──────────────────────────────────────────────────────────────── FMarkerActive: Boolean; FMarkerX: Integer; - - // ── Meter values ────────────────────────────────────────────────────────── - FLastSMeter: Double; - FSMeterPeak: Double; - FSMeterMin: Double; - FLastFwdW: Double; - FLastSWR: Double; - FPAMaxPower: Double; - FTransmitting: Boolean; - - // ── Буферы данных (пишутся из DSP-потока) ──────────────────────────────── + // ── Spectrum buffer ─────────────────────────────────────────────────────── 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; + // ── Sub-views ───────────────────────────────────────────────────────────── + FWaterfall: TWaterfallView; + FSMeter: TSMeterView; + FRuler: TRulerView; // ── Приватные методы рендеринга ─────────────────────────────────────────── function ActiveVfoFreq: Double; function ScaleX(X, Total, Width: Integer): Integer; procedure SetFMGridStepHz(V: Double); + procedure SetCenterFreq(V: Double); + procedure SetSpanHz(V: Double); + procedure SetVfoA(V: Double); + procedure SetVfoB(V: Double); + procedure SetActiveVfo(V: Integer); + procedure SetTXMode(V: Boolean); + procedure SetTXFreq(V: Double); + procedure SetTXSpanHz(V: Double); + procedure SetMarkerActive(V: Boolean); + procedure SetMarkerX(V: Integer); procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); procedure DrawMarkerLine(C: TCanvas; W, H: Integer); - // Альфа-blending вертикальной полосы поверх FSpectrumBitmap (pf32bit, BGRA). - // Используется для прозрачной заливки полосы фильтра разными цветами - // (RX-зелёный / TX-красный) поверх градиента сетки. procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte); procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer); - // Считает X1/X2 для полосы фильтра вокруг указанной частоты VFO, c учётом - // текущего режима (LSB/USB/DSB и пр.) и FFilterBW. procedure CalcFilterBandX(VfoFreq: Double; W: Integer; out X1, X2, VfoX: Integer); + function GetWaterfallDirty: Boolean; + procedure SetWaterfallDirty(V: Boolean); + // Waterfall sub-view delegating accessors + function GetWfAGCEnabled: Boolean; + procedure SetWfAGCEnabled(V: Boolean); + function GetWfNFEnabled: Boolean; + procedure SetWfNFEnabled(V: Boolean); + function GetWfManualHigh: Double; + procedure SetWfManualHigh(V: Double); + function GetWfManualLow: Double; + procedure SetWfManualLow(V: Double); + function GetWfAGCOffset: Double; + procedure SetWfAGCOffset(V: Double); + function GetWfFrameInterval: Integer; + procedure SetWfFrameInterval(V: Integer); + procedure SetPbWaterfall(V: TPaintBox); + // SMeter sub-view delegating accessors + function GetLastSMeter: Double; + procedure SetLastSMeter(V: Double); + function GetSMeterPeak: Double; + procedure SetSMeterPeak(V: Double); + function GetSMeterMin: Double; + procedure SetSMeterMin(V: Double); + function GetLastFwdW: Double; + procedure SetLastFwdW(V: Double); + function GetLastSWR: Double; + procedure SetLastSWR(V: Double); + function GetPAMaxPower: Double; + procedure SetPAMaxPower(V: Double); + function GetTransmitting: Boolean; + procedure SetTransmitting(V: Boolean); + procedure SetPbSMeterRight(V: TPaintBox); + // Ruler sub-view delegating accessor + procedure SetPbRuler(V: TPaintBox); 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 VfoA: Double read FVfoA write SetVfoA; + property VfoB: Double read FVfoB write SetVfoB; + property ActiveVfo: Integer read FActiveVfo write SetActiveVfo; + property CenterFreq: Double read FCenterFreq write SetCenterFreq; + property SpanHz: Double read FSpanHz write SetSpanHz; property Mode: Integer read FMode write FMode; property FilterBW: Integer read FFilterBW write FFilterBW; property AGCTop: Integer read FAGCTop write FAGCTop; @@ -167,11 +154,11 @@ type 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 WfAGCEnabled: Boolean read GetWfAGCEnabled write SetWfAGCEnabled; + property WfNFEnabled: Boolean read GetWfNFEnabled write SetWfNFEnabled; + property WfManualHigh: Double read GetWfManualHigh write SetWfManualHigh; + property WfManualLow: Double read GetWfManualLow write SetWfManualLow; + property WfAGCOffset: Double read GetWfAGCOffset write SetWfAGCOffset; // ── Отображение ─────────────────────────────────────────────────────────── property SpecRefLevel: Double read FSpecRefLevel write FSpecRefLevel; @@ -180,42 +167,38 @@ type property FMGridStepHz: Double read FFMGridStepHz write SetFMGridStepHz; // ── TX overlay ──────────────────────────────────────────────────────────── - property TXMode: Boolean read FTXMode write FTXMode; - property TXFreq: Double read FTXFreq write FTXFreq; - property TXSpanHz: Double read FTXSpanHz write FTXSpanHz; + property TXMode: Boolean read FTXMode write SetTXMode; + property TXFreq: Double read FTXFreq write SetTXFreq; + property TXSpanHz: Double read FTXSpanHz write SetTXSpanHz; property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex; property TXOverlay: Boolean read FTXOverlay write FTXOverlay; - // Test overlay for ADC overload; later this will be driven from HP Status. property ADCOverloadVisible: Boolean read FADCOverloadVisible write FADCOverloadVisible; // ── Маркер ──────────────────────────────────────────────────────────────── - property MarkerActive: Boolean read FMarkerActive write FMarkerActive; - property MarkerX: Integer read FMarkerX write FMarkerX; + property MarkerActive: Boolean read FMarkerActive write SetMarkerActive; + property MarkerX: Integer read FMarkerX write SetMarkerX; // ── 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 PAMaxPower: Double read FPAMaxPower write FPAMaxPower; - property Transmitting: Boolean read FTransmitting write FTransmitting; + property LastSMeter: Double read GetLastSMeter write SetLastSMeter; + property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak; + property SMeterMin: Double read GetSMeterMin write SetSMeterMin; + property LastFwdW: Double read GetLastFwdW write SetLastFwdW; + property LastSWR: Double read GetLastSWR write SetLastSWR; + property PAMaxPower: Double read GetPAMaxPower write SetPAMaxPower; + property Transmitting: Boolean read GetTransmitting write SetTransmitting; // ── Флаги обновления ────────────────────────────────────────────────────── property SpectrumDirty: Boolean read FSpectrumDirty write FSpectrumDirty; - property WaterfallDirty: Boolean read FWaterfallDirty write FWaterfallDirty; - property WfFrameInterval: Integer read FWfFrameInterval write FWfFrameInterval; + property WaterfallDirty: Boolean read GetWaterfallDirty write SetWaterfallDirty; + property WfFrameInterval: Integer read GetWfFrameInterval write SetWfFrameInterval; // ── Ссылки на 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; + property PbWaterfall: TPaintBox write SetPbWaterfall; + property PbRuler: TPaintBox write SetPbRuler; + property PbSMeterRight: TPaintBox write SetPbSMeterRight; - // ── Данные от DSP (вызываются из DSP-потока) ────────────────────────────── + // ── Данные от DSP ───────────────────────────────────────────────────────── procedure SetSpectrumData(const Pixels: array of Single; Count: Integer); procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); @@ -223,14 +206,6 @@ type 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); - procedure DrawTXMeter(ACanvas: TCanvas; R: TRect); // ── Управление размером bitmap ──────────────────────────────────────────── procedure SetSpectrumBitmapSize(W, H: Integer); @@ -238,17 +213,14 @@ type procedure SetRulerSize(W, H: Integer); function SpectrumBitmapWidth: Integer; - // ── Paint-обработчики (назначаются в BuildUI) ───────────────────────────── + // ── Paint-обработчики ──────────────────────────────────────────────────── 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 SetTheme(const T: TAppTheme); procedure InvalidateRulerCache; procedure ResetSpectrumBuf; @@ -257,93 +229,19 @@ type 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; +function FormatFreqSV(Hz: Double): string; +var Mhz: Int64; KHz, Rest: 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; - -// Цветовая схема водопада для светлой темы. -// Шум растворяется в фоне панели; сигналы: синий → cyan → зелёный → жёлтый → оранжевый → красный. -function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord; -const - NSTOPS = 8; - // panel steel blue cyan green yell orange red - SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255); - SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20); - SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20); -var - T: Double; - Seg: Integer; - R, G, B: Integer; -begin - if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end; - if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end; - T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1); - Seg := Min(NSTOPS - 2, Trunc(T)); - T := T - Seg; - R := EnsureRange(Round(SR[Seg] * (1.0 - T) + SR[Seg+1] * T), 0, 255); - G := EnsureRange(Round(SG[Seg] * (1.0 - T) + SG[Seg+1] * T), 0, 255); - B := EnsureRange(Round(SB[Seg] * (1.0 - T) + SB[Seg+1] * T), 0, 255); - Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; + Mhz := Trunc(Hz / 1000000); + KHz := Trunc((Hz - Mhz * 1000000) / 1000); + Rest := Trunc(Hz) mod 1000; + Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]); end; procedure InitRawDesc32(var Desc: TRawImageDescription; AWidth, AHeight: Integer); @@ -389,15 +287,6 @@ begin C.Pen.Style := psSolid; end; -function FormatFreqSV(Hz: Double): string; -var Mhz: Int64; KHz, Rest: Integer; -begin - Mhz := Trunc(Hz / 1000000); - KHz := Trunc((Hz - Mhz * 1000000) / 1000); - Rest := Trunc(Hz) mod 1000; - Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]); -end; - // ════════════════════════════════════════════════════════════════════════════ // TSpectrumView // ════════════════════════════════════════════════════════════════════════════ @@ -406,36 +295,20 @@ constructor TSpectrumView.Create; begin inherited Create; FSpectrumBitmap := TBitmap.Create; - // pf32bit нужен для попиксельного alpha-blend в BlendBand (TX/RX-полосы фильтра). FSpectrumBitmap.PixelFormat := pf32bit; - 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; FFMGridLastCenter := -1.0; FFMGridLastStepHz := -1.0; FFMGridLastSpan := -1.0; FFMGridStepHz := 0.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; - // TX overlay defaults — сетка совпадает с RX по умолчанию + // TX overlay defaults FTXMode := False; FTXFreq := 0.0; FTXSpanHz := 192000.0; @@ -443,32 +316,136 @@ begin FTXOverlay := False; FADCOverloadVisible := False; FSpectrumBufCount := 1024; - FWaterfallBufCount := 1024; - FWfFrameInterval := 2; - FWfFrameCounter := 0; - FWaterfallDirty := True; - FLastSMeter := -130; FSMeterPeak := -130; FSMeterMin := -130; - FLastFwdW := 0; FLastSWR := 1; - FPAMaxPower := 100.0; FTransmitting := False; + FSpectrumDirty := True; FLightTheme := False; FTheme := DarkTheme; + // Sub-views + FWaterfall := TWaterfallView.Create; + FSMeter := TSMeterView.Create; + FRuler := TRulerView.Create; ResetSpectrumBuf; end; destructor TSpectrumView.Destroy; begin FSpectrumBitmap.Free; - FWaterfallBitmap.Free; - FWfBitmap.Free; FGridBitmap.Free; - FRulerBitmap.Free; FSpecGradBmp.Free; FreeAndNil(FSpecGradImg); - FreeAndNil(FWfIntfImg); - FreeAndNil(FSmBitmap); + FWaterfall.Free; + FSMeter.Free; + FRuler.Free; inherited; end; +// ──────────────────────────────────────────────────────────────────────────── +// Setters with cascade to sub-views +// ──────────────────────────────────────────────────────────────────────────── + +procedure TSpectrumView.SetCenterFreq(V: Double); +begin + FCenterFreq := V; + FWaterfall.CenterFreq := V; + FRuler.CenterFreq := V; +end; + +procedure TSpectrumView.SetSpanHz(V: Double); +begin + FSpanHz := V; + FWaterfall.SpanHz := V; + FRuler.SpanHz := V; +end; + +procedure TSpectrumView.SetVfoA(V: Double); +begin + FVfoA := V; + FRuler.VfoA := V; +end; + +procedure TSpectrumView.SetVfoB(V: Double); +begin + FVfoB := V; + FRuler.VfoB := V; +end; + +procedure TSpectrumView.SetActiveVfo(V: Integer); +begin + FActiveVfo := V; + FRuler.ActiveVfo := V; +end; + +procedure TSpectrumView.SetTXMode(V: Boolean); +begin + FTXMode := V; + FWaterfall.TXMode := V; +end; + +procedure TSpectrumView.SetTXFreq(V: Double); +begin + FTXFreq := V; + FWaterfall.TXFreq := V; +end; + +procedure TSpectrumView.SetTXSpanHz(V: Double); +begin + FTXSpanHz := V; + FWaterfall.TXSpanHz := V; +end; + +procedure TSpectrumView.SetMarkerActive(V: Boolean); +begin + FMarkerActive := V; + FWaterfall.MarkerActive := V; +end; + +procedure TSpectrumView.SetMarkerX(V: Integer); +begin + FMarkerX := V; + FWaterfall.MarkerX := V; +end; + +function TSpectrumView.GetWaterfallDirty: Boolean; +begin + Result := FWaterfall.WaterfallDirty; +end; + +procedure TSpectrumView.SetWaterfallDirty(V: Boolean); +begin + FWaterfall.WaterfallDirty := V; +end; + +function TSpectrumView.GetWfAGCEnabled: Boolean; begin Result := FWaterfall.WfAGCEnabled; end; +procedure TSpectrumView.SetWfAGCEnabled(V: Boolean); begin FWaterfall.WfAGCEnabled := V; end; +function TSpectrumView.GetWfNFEnabled: Boolean; begin Result := FWaterfall.WfNFEnabled; end; +procedure TSpectrumView.SetWfNFEnabled(V: Boolean); begin FWaterfall.WfNFEnabled := V; end; +function TSpectrumView.GetWfManualHigh: Double; begin Result := FWaterfall.WfManualHigh; end; +procedure TSpectrumView.SetWfManualHigh(V: Double); begin FWaterfall.WfManualHigh := V; end; +function TSpectrumView.GetWfManualLow: Double; begin Result := FWaterfall.WfManualLow; end; +procedure TSpectrumView.SetWfManualLow(V: Double); begin FWaterfall.WfManualLow := V; end; +function TSpectrumView.GetWfAGCOffset: Double; begin Result := FWaterfall.WfAGCOffset; end; +procedure TSpectrumView.SetWfAGCOffset(V: Double); begin FWaterfall.WfAGCOffset := V; end; +function TSpectrumView.GetWfFrameInterval: Integer; begin Result := FWaterfall.WfFrameInterval; end; +procedure TSpectrumView.SetWfFrameInterval(V: Integer); begin FWaterfall.WfFrameInterval := V; end; +procedure TSpectrumView.SetPbWaterfall(V: TPaintBox); begin FWaterfall.PbWaterfall := V; end; + +function TSpectrumView.GetLastSMeter: Double; begin Result := FSMeter.LastSMeter; end; +procedure TSpectrumView.SetLastSMeter(V: Double); begin FSMeter.LastSMeter := V; end; +function TSpectrumView.GetSMeterPeak: Double; begin Result := FSMeter.SMeterPeak; end; +procedure TSpectrumView.SetSMeterPeak(V: Double); begin FSMeter.SMeterPeak := V; end; +function TSpectrumView.GetSMeterMin: Double; begin Result := FSMeter.SMeterMin; end; +procedure TSpectrumView.SetSMeterMin(V: Double); begin FSMeter.SMeterMin := V; end; +function TSpectrumView.GetLastFwdW: Double; begin Result := FSMeter.LastFwdW; end; +procedure TSpectrumView.SetLastFwdW(V: Double); begin FSMeter.LastFwdW := V; end; +function TSpectrumView.GetLastSWR: Double; begin Result := FSMeter.LastSWR; end; +procedure TSpectrumView.SetLastSWR(V: Double); begin FSMeter.LastSWR := V; end; +function TSpectrumView.GetPAMaxPower: Double; begin Result := FSMeter.PAMaxPower; end; +procedure TSpectrumView.SetPAMaxPower(V: Double); begin FSMeter.PAMaxPower := V; end; +function TSpectrumView.GetTransmitting: Boolean; begin Result := FSMeter.Transmitting; end; +procedure TSpectrumView.SetTransmitting(V: Boolean); begin FSMeter.Transmitting := V; end; +procedure TSpectrumView.SetPbSMeterRight(V: TPaintBox); begin FSMeter.PbSMeterRight := V; end; + +procedure TSpectrumView.SetPbRuler(V: TPaintBox); begin FRuler.PbRuler := V; end; + // ──────────────────────────────────────────────────────────────────────────── // Приватные вспомогательные методы // ──────────────────────────────────────────────────────────────────────────── @@ -489,8 +466,9 @@ begin if Abs(FFMGridStepHz - V) > 0.5 then begin FFMGridStepHz := V; + FRuler.FMGridStepHz := V; InvalidateGridCache; - InvalidateRulerCache; + FRuler.InvalidateRulerCache; end; end; @@ -513,10 +491,6 @@ begin end; procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte); -// Накладывает полупрозрачную вертикальную полосу [X1..X2) высотой H на -// FSpectrumBitmap. Канал A — сила оверлея 0..255 (255 = полностью непрозрачно). -// Формула: dst = src*(1-a) + color*a, BGRA-порядок (pf32bit Lazarus default). -// BeginUpdate(False) — нужен для прямого доступа к raw-пикселям через ScanLine. var Y, X, InvA: Integer; Row: PByte; @@ -540,7 +514,6 @@ begin Row[0] := Byte((Alpha * B + InvA * Row[0]) div 255); Row[1] := Byte((Alpha * G + InvA * Row[1]) div 255); Row[2] := Byte((Alpha * R + InvA * Row[2]) div 255); - // Row[3] оставляем — на BitBlt в TPaintBox не влияет. Inc(Row, 4); end; end; @@ -557,8 +530,6 @@ end; procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer; out X1, X2, VfoX: Integer); -// Возвращает X-координаты левого/правого края полосы фильтра вокруг VfoFreq -// и саму позицию VFO. Учитывает FMode (LSB/USB/DSB) и FFilterBW. var Lo_Hz, Hi_Hz, Half: Double; begin @@ -640,17 +611,8 @@ begin 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; + FWaterfall.SetWaterfallData(Pixels, Count); end; // ──────────────────────────────────────────────────────────────────────────── @@ -670,17 +632,12 @@ end; procedure TSpectrumView.SetWaterfallBitmapSize(W, H: Integer); begin - FWaterfallBitmap.SetSize(W, H); - FWfBitmapW := 0; FWfBitmapH := 0; - SetLength(FWfPixels, 0); - FreeAndNil(FWfIntfImg); + FWaterfall.SetWaterfallBitmapSize(W, H); 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; FRulerLastSpan := -1.0; + FRuler.SetRulerSize(W, H); end; function TSpectrumView.SpectrumBitmapWidth: Integer; @@ -701,31 +658,29 @@ end; procedure TSpectrumView.SetTheme(const T: TAppTheme); begin FTheme := T; - FLightTheme := T.BG > TColor($00808080); // светлая тема если фон светлее 50% - FreeAndNil(FSpecGradImg); // пересчитать градиент + FLightTheme := T.BG > TColor($00808080); + FreeAndNil(FSpecGradImg); InvalidateGridCache; - InvalidateRulerCache; - FWaterfallDirty := True; + FWaterfall.SetTheme(T); + FSMeter.SetTheme(T); + FRuler.SetTheme(T); end; procedure TSpectrumView.InvalidateRulerCache; begin - FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0; + FRuler.InvalidateRulerCache; 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; + for i := 0 to 1023 do FSpectrumBuf[i] := -130.0; + FWaterfall.ResetWfBuf; end; procedure TSpectrumView.ResetWfAvgBuf; begin - FWfHigh := FWfManualHigh; FWfLow := FWfManualLow; - FWaterfallDirty := True; + FWaterfall.ResetWfAvgBuf; end; procedure TSpectrumView.FillDemoSpectrum; @@ -746,9 +701,7 @@ end; function TSpectrumView.NeedsRulerRedraw: Boolean; begin - Result := (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or - (Abs(ActiveVfoFreq - FRulerLastVfo) >= 0.5) or - (Abs(FSpanHz - FRulerLastSpan) >= 1.0); + Result := FRuler.NeedsRulerRedraw; end; // ──────────────────────────────────────────────────────────────────────────── @@ -849,30 +802,20 @@ begin LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020); // ── 2. Полоса фильтра ───────────────────────────────────────────────────── - // Логика: - // Не TX → одна полоса на FActiveVfo, штатным цветом FTheme.SpecFilter (solid). - // TX, не split → одна полоса на FActiveVfo, alpha-blend красный. - // TX, split → две полосы: RX (FActiveVfo) — alpha-blend зелёный, - // TX (FTXVfoIndex) — alpha-blend красный. - // Edges + VFO-cursor рисуются позже на VfoX/X1/X2 — это RX-VFO-полоса - // (для split-режима TX-полоса рисуется без edges чтобы визуально отличалась). if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX) else CalcFilterBandX(FVfoB, W, X1, X2, VfoX); if FTXOverlay then begin if FTXVfoIndex <> FActiveVfo then begin - // Split TX: RX-полоса зелёная, TX-полоса (на другом VFO) красная. if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100); if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX) else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX); if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100); end else - // Не split: единственная полоса — TX на активном VFO, красная. if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100); end else begin - // Обычный приём — заливка штатным цветом без alpha. C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear; if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H)); C.Pen.Style := psSolid; @@ -914,18 +857,12 @@ begin SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024); if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then begin - // TX-режим: данные приходят @ FTXSpanHz вокруг FTXFreq (TX-анализатор — - // baseband), а рисуем в координатах RX (FCenterFreq/FSpanHz). Это даёт - // правильное положение TX-сигнала при CTUN ON и выравнивает сетку, когда - // RX rate ≠ TX rate (192k). for i := 0 to W - 1 do begin - // freq, соответствующая пикселю i в RX-координатах SrcF := FCenterFreq - FSpanHz * 0.5 + i * FSpanHz / Max(1, W - 1); - // смещение от центра TX-окна Frac := SrcF - FTXFreq; if (Frac < -FTXSpanHz * 0.5) or (Frac > FTXSpanHz * 0.5) then - dBv := -200.0 // вне диапазона TX-анализатора + dBv := -200.0 else begin SrcF := (Frac + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1); @@ -964,7 +901,6 @@ begin C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12); C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1; C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]); - // Split TX: рисуем края + cursor для TX-VFO красным, чтобы было видно где идёт передача if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then begin C.Pen.Color := TColor($002030E0); C.Pen.Width := 1; C.Pen.Style := psSolid; @@ -981,671 +917,17 @@ begin end; // ──────────────────────────────────────────────────────────────────────────── -// DrawWaterfall +// Делегирование к sub-views // ──────────────────────────────────────────────────────────────────────────── 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); - // TColor = $00BBGGRR → waterfall pixel = $FF_RR_GG_BB (swap R и B) - Pal := $FF000000 - or ((LongWord(FTheme.Panel) and $FF) shl 16) - or (LongWord(FTheme.Panel) and $FF00) - or ((LongWord(FTheme.Panel) shr 16) and $FF); - FillDWord(FWfPixels[0], W * H, Pal); - 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 - if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then - begin - // частота для пикселя X в RX-координатах → индекс в TX-буфере - dB := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq; - if (dB < -FTXSpanHz * 0.5) or (dB > FTXSpanHz * 0.5) then - begin - // Вне TX-окна: копируем пиксель из предыдущей строки (она лежит на - // расстоянии W в FWfPixels — Move уже сдвинул её ниже). Так старая - // RX-картинка продолжается без чёрной "дыры", пока TX рисует своё - // окно в центре. - if H > 1 then Px^ := FWfPixels[X + W] - else Px^ := 0; - Inc(Px); - WatSrcF := WatSrcF + Step; - Continue; - end; - frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1); - WatS0 := Trunc(frac); - if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2; - WatS1 := WatS0 + 1; - frac := frac - WatS0; - dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac; - end else - 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; - end; - V := Trunc((dB - WfLow) * InvRange); - if V < 0 then V := 0; if V > 255 then V := 255; - if FLightTheme then - Pal := WaterfallLightTheme(dB, WfLow, WfHigh) - else - 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); + FWaterfall.DrawWaterfall; end; -// ──────────────────────────────────────────────────────────────────────────── -// DrawRuler -// ──────────────────────────────────────────────────────────────────────────── - procedure TSpectrumView.DrawRuler; -var - C: TCanvas; W, H, i, X: Integer; - FreqStart, FreqHz, GridLine, pixPerStep: Double; VfoX: Integer; - Lbl: string; TW: Integer; - N, labelMult: 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) and - (Abs(FSpanHz - FRulerLastSpan) < 1.0) then Exit; - FRulerLastFreq := FCenterFreq; FRulerLastVfo := ActiveVfoFreq; FRulerLastSpan := FSpanHz; - if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then - FRulerBitmap.SetSize(W, H); - C := FRulerBitmap.Canvas; - PaintVerticalGradient(C, W, H, FTheme.RulerGradTop, FTheme.RulerGradBot); - C.Pen.Color := FTheme.RulerBorder; 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; - if FFMGridStepHz > 0 then - begin - pixPerStep := W * FFMGridStepHz / FSpanHz; - if pixPerStep >= 1.0 then - labelMult := Max(1, Ceil((C.TextWidth('000.000') + 6) / pixPerStep)) - else - labelMult := MaxInt; - N := Ceil(FreqStart / FFMGridStepHz); - GridLine := N * FFMGridStepHz; - while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do - begin - X := Round((GridLine - FreqStart) / FSpanHz * W); - if (X >= 0) and (X < W) then - begin - C.Pen.Color := FTheme.RulerBorder; - if (N mod labelMult) = 0 then - begin - C.MoveTo(X, 0); C.LineTo(X, H div 2); - Lbl := Format('%.3f', [GridLine / 1e6]); - TW := C.TextWidth(Lbl); - C.Font.Color := FTheme.RulerText; - C.TextOut(X - TW div 2, H div 2 - 1, Lbl); - end else - begin - C.MoveTo(X, 0); C.LineTo(X, H div 4); - end; - end; - Inc(N); - GridLine := GridLine + FFMGridStepHz; - end; - end else - begin - for i := 0 to 8 do - begin - X := ScaleX(i, 8, W); - C.Pen.Color := FTheme.RulerBorder; - 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 := FTheme.RulerText; - C.TextOut(X - TW div 2, H div 2 - 1, Lbl); - end; - end; - VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W); - if (VfoX >= 0) and (VfoX < W) then - begin - C.Pen.Color := FTheme.RulerVfo; 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 := FTheme.SMeterBG; - 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'); - LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4; - TICK_LONG = 5; TICK_MED = 3; -var - CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor; - CLR_PEAK_MARKER, CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor; - CLR_LABEL_DBM, CLR_LABEL_S, CLR_SCALE_DBM: TColor; - CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor; - W, H, BX, BW, LeftInfo: Integer; - Y_BAR_TOP, Y_BAR_BOT: Integer; - BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer; - i, X, TW: Integer; Lbl: string; SNum, Over: Integer; - LblDbm, LblS: string; - TWDbm, LblZoneL, TH10, TH6, MaxTWDbm, MaxTWS, LabelNeed: 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 - CLR_SMETER_BG := FTheme.SMeterBG; - CLR_BAR_GREEN := FTheme.SMeterBarGreen; - CLR_BAR_OVER := FTheme.SMeterBarOver; - CLR_PEAK_MARKER := FTheme.SMeterPeakMarker; - CLR_TICK_GREEN := FTheme.SMeterTickGreen; - CLR_TICK_WHITE := FTheme.SMeterTickWhite; - CLR_TICK_BLUE := FTheme.SMeterTickBlue; - CLR_LABEL_DBM := FTheme.SMeterLabelDbm; - CLR_LABEL_S := FTheme.SMeterLabelS; - CLR_SCALE_DBM := FTheme.SMeterScaleDbm; - CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen; - CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue; - CLR_BDR := FTheme.SMeterBdr; - - 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; - - // --- Вычисляем строки надписей заранее чтобы измерить ширину --- - LblDbm := Format('%.1f', [Value]); - if Value >= -73.0 then - begin - Over := Round(Value - (-73.0)); - if Over < 5 then LblS := 'S9' - else begin Over := ((Over + 5) div 10) * 10; if Over = 0 then Over := 10; LblS := Format('S9+%d', [Over]); end; - end else if Value <= -121.0 then LblS := 'S1' - else begin - LblS := 'S1'; - for SNum := 1 to 8 do - if Value >= (-121.0 + (SNum - 1) * 6.0) then LblS := 'S' + IntToStr(SNum); - end; - - // Измеряем ширину надписей - ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold]; - ACanvas.Font.Size := 10; TWDbm := ACanvas.TextWidth(LblDbm); - MaxTWDbm := ACanvas.TextWidth('-120.0'); - MaxTWS := ACanvas.TextWidth('S9+60'); - ACanvas.Font.Size := 6; ACanvas.Font.Style := []; - // Левая информационная зона зависит от реальных метрик шрифта. - LabelNeed := Max(MaxTWDbm + 2 + ACanvas.TextWidth('dBm '), MaxTWS) + LABEL_PAD * 2; - LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4); - BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD; - if BW < 10 then Exit; - LblZoneL := BX - LabelNeed; - - Y_BAR_TOP := R.Top + H * 44 div 100; - Y_BAR_BOT := R.Top + H * 58 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, CLR_BAR_GREEN); - FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, CLR_BAR_GREEN); - FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, CLR_BAR_OVER); - - 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 - (Y_BAR_BOT - Y_BAR_TOP)); - ACanvas.LineTo(PeakRight, Y_BAR_BOT + (Y_BAR_BOT - Y_BAR_TOP)); - ACanvas.Pen.Width := 1; - end; - - // Рамка: от левого края зоны надписей до правого края шкалы - ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR; - ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT); - // разделитель между зоной надписей и шкалой - ACanvas.Pen.Color := FTheme.SMeterDivider; - ACanvas.MoveTo(BX, Y_BAR_TOP + 1); ACanvas.LineTo(BX, Y_BAR_BOT - 1); - - // --- Шкальные тики и надписи --- - 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; - - // --- Надписи в левой зоне, левовыровнены к LblZoneL+4 --- - ACanvas.Brush.Style := bsClear; - ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0'); - ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0'); - // "-85.3" над "dBm", снизу прижаты к верхнему краю полосы - ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM; - ACanvas.TextOut(LblZoneL + 4, Y_BAR_TOP - TH10 - 2, LblDbm); - ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := CLR_TICK_BLUE; - ACanvas.TextOut(LblZoneL + 4 + TWDbm + 2, Y_BAR_TOP - TH6 - 4, 'dBm'); - // S-значение под полосой, левый край у LblZoneL+4 - ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S; - ACanvas.TextOut(LblZoneL + 4, Y_BAR_BOT + 2, LblS); -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; - -procedure TSpectrumView.DrawTXMeter(ACanvas: TCanvas; R: TRect); -// Визуально идентичен DrawSMeterWide: те же пропорции, тот же левый блок, -// те же тики — но центральная полоса разбита на две: мощность (верх) и КСВ (низ). -const - SWR_HIGH_THRESH = 2.5; - SWR_MAX = 10.0; - LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4; - TICK_LONG = 5; TICK_MED = 3; - PWR_MARKS: array[0..4] of Double = (0.0, 0.25, 0.5, 0.75, 1.0); - SWR_MARKS_VAL: array[0..3] of Double = (1.0, 2.5, 5.0, 10.0); - SWR_MARKS_LBL: array[0..3] of string = ('1', '2.5', '5', '10'); -var - CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor; - CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor; - CLR_LABEL_DBM, CLR_LABEL_S: TColor; - CLR_SCALE_DBM, CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor; - CLR_SWR_HIGH: TColor; - W, H, BX, BW, LeftInfo: Integer; - Y_BAR_TOP, Y_BAR_BOT, Y_BAR_MID: Integer; - PY1, PY2, SY1, SY2: Integer; - PBarEnd, SBarEnd, S_WarnX: Integer; - LblPwr, LblSWR: string; - TWPwr, LblZoneL, TH10, TH6, MaxTWPwr, MaxTWSWR, LabelNeed: Integer; - i, X, TW: Integer; - Lbl: string; - SWRHigh: Boolean; - PwrPct: Double; - - function PwrToX(Pct: Double): Integer; inline; - begin - Result := BX + Round(Max(0.0, Min(1.0, Pct)) * BW); - if Result < BX then Result := BX; - if Result > BX + BW then Result := BX + BW; - end; - - function SWRToX(SWR: Double): Integer; inline; - begin - Result := BX + Round(Max(0.0, Min(1.0, (SWR - 1.0) / (SWR_MAX - 1.0))) * 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 - CLR_SMETER_BG := FTheme.SMeterBG; - CLR_BAR_GREEN := FTheme.SMeterBarGreen; - CLR_BAR_OVER := FTheme.SMeterBarOver; - CLR_TICK_GREEN := FTheme.SMeterTickGreen; - CLR_TICK_WHITE := FTheme.SMeterTickWhite; - CLR_TICK_BLUE := FTheme.SMeterTickBlue; - CLR_LABEL_DBM := FTheme.SMeterLabelDbm; - CLR_LABEL_S := FTheme.SMeterLabelS; - CLR_SCALE_DBM := FTheme.SMeterScaleDbm; - CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen; - CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue; - CLR_BDR := FTheme.SMeterBdr; - CLR_SWR_HIGH := TColor($000000CC); - - W := R.Right - R.Left; H := R.Bottom - R.Top; - if (W < 80) or (H < 16) then Exit; - - SWRHigh := FLastSWR > SWR_HIGH_THRESH; - LblPwr := Format('%.0fW', [FLastFwdW]); - LblSWR := Format('SWR:%.1f', [FLastSWR]); - - // Левая информационная зона — такая же логика как в DrawSMeterWide - ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold]; - ACanvas.Font.Size := 10; - TWPwr := ACanvas.TextWidth(LblPwr); - MaxTWPwr := ACanvas.TextWidth(Format('%.0fW', [FPAMaxPower])); - MaxTWSWR := ACanvas.TextWidth('SWR:10.0'); - ACanvas.Font.Size := 6; ACanvas.Font.Style := []; - LabelNeed := Max(MaxTWPwr, MaxTWSWR) + LABEL_PAD * 2; - LeftInfo := Max(LEFT_INFO_MIN, LabelNeed + 4); - BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD; - if BW < 10 then Exit; - LblZoneL := BX - LabelNeed; - - // Те же вертикальные позиции бара что в DrawSMeterWide (44% / 58%) - Y_BAR_TOP := R.Top + H * 44 div 100; - Y_BAR_BOT := R.Top + H * 58 div 100; - Y_BAR_MID := (Y_BAR_TOP + Y_BAR_BOT) div 2; - - // Два суб-бара в той же зоне - PY1 := Y_BAR_TOP; - PY2 := Y_BAR_MID - 1; // power bar - SY1 := Y_BAR_MID + 1; - SY2 := Y_BAR_BOT; // SWR bar - - S_WarnX := SWRToX(SWR_HIGH_THRESH); - - // Фон - ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid; - ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid; - - // Фоновые зоны (как зоны зелёный/красный в S-метре) - FillBar(BX, PY1, BX + BW, PY2, CLR_BAR_GREEN); // power: весь зелёный фон - FillBar(BX, SY1, S_WarnX, SY2, CLR_BAR_GREEN); // SWR: OK-зона - FillBar(S_WarnX, SY1, BX + BW, SY2, CLR_BAR_OVER); // SWR: warn-зона - - // Активный заполненный бар мощности - if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0; - PBarEnd := PwrToX(PwrPct); - FillBar(BX, PY1 + 1, PBarEnd, PY2 - 1, SV_CLR_METER_ON); - - // Активный заполненный бар КСВ - SBarEnd := SWRToX(FLastSWR); - if not SWRHigh then - FillBar(BX, SY1 + 1, SBarEnd, SY2 - 1, SV_CLR_AMBER) - else - begin - FillBar(BX, SY1 + 1, Min(S_WarnX, SBarEnd), SY2 - 1, SV_CLR_AMBER); - if SBarEnd > S_WarnX then - FillBar(S_WarnX, SY1 + 1, SBarEnd, SY2 - 1, CLR_SWR_HIGH); - end; - - // Граница KСВ 2.5 - VLine(S_WarnX, SY1, SY2, FTheme.SMeterDivider); - - // Рамки обоих баров + левая зона надписей - ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR; - ACanvas.Rectangle(LblZoneL, PY1, BX + BW, PY2); - if SWRHigh then ACanvas.Pen.Color := CLR_SWR_HIGH - else ACanvas.Pen.Color := CLR_BDR; - ACanvas.Rectangle(LblZoneL, SY1, BX + BW, SY2); - - // Разделители между зоной надписей и шкалой - ACanvas.Pen.Color := FTheme.SMeterDivider; - ACanvas.MoveTo(BX, PY1 + 1); ACanvas.LineTo(BX, PY2 - 1); - ACanvas.MoveTo(BX, SY1 + 1); ACanvas.LineTo(BX, SY2 - 1); - - // --- Тики шкалы --- - ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear; - - // Мощность: промежуточные тики каждые 5% (не на главных метках кратных 25%) - i := 5; - while i < 100 do - begin - if (i mod 25) <> 0 then - begin - X := PwrToX(i / 100.0); - VLine(X, PY1 - TICK_MED, PY1 - 1, CLR_TICK_GREEN); - end; - Inc(i, 5); - end; - - // Мощность: главные тики с подписями (0%, 25%, 50%, 75%, 100%) - for i := 0 to 4 do - begin - X := PwrToX(PWR_MARKS[i]); - VLine(X, PY1 - TICK_LONG, PY1 - 1, CLR_TICK_WHITE); - Lbl := Format('%.0f', [PWR_MARKS[i] * FPAMaxPower]); - TW := ACanvas.TextWidth(Lbl); - ACanvas.Font.Color := CLR_SCALE_DBM; - ACanvas.TextOut(X - TW div 2, PY1 - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl); - end; - - // КСВ: промежуточные тики на целых значениях 2,3,4,6,7,8,9 - for i := 2 to 9 do - begin - if i = 5 then Continue; - X := SWRToX(i); - if i >= 3 then VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_BLUE) - else VLine(X, SY2 + 1, SY2 + TICK_MED, CLR_TICK_GREEN); - end; - - // КСВ: главные тики с подписями (1, 2.5, 5, 10) - for i := 0 to 3 do - begin - X := SWRToX(SWR_MARKS_VAL[i]); - if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then - VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_BLUE) - else - VLine(X, SY2 + 1, SY2 + TICK_LONG, CLR_TICK_GREEN); - Lbl := SWR_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl); - if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then - begin - if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH - else ACanvas.Font.Color := CLR_SCALE_S_BLUE; - end else - ACanvas.Font.Color := CLR_SCALE_S_GREEN; - ACanvas.TextOut(X - TW div 2, SY2 + TICK_LONG + 1, Lbl); - end; - - // --- Надписи в левой зоне (те же позиции что в DrawSMeterWide) --- - ACanvas.Brush.Style := bsClear; - ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0'); - ACanvas.Font.Size := 6; ACanvas.Font.Style := []; TH6 := ACanvas.TextHeight('0'); - - // Мощность — над верхним баром, как dBm в S-метре - ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM; - ACanvas.TextOut(LblZoneL + 4, PY1 - TH10 - 2, LblPwr); - - // КСВ — под нижним баром, как S-значение в S-метре - ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; - if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH - else ACanvas.Font.Color := CLR_LABEL_S; - ACanvas.TextOut(LblZoneL + 4, SY2 + 2, LblSWR); - - // "SWR High" рядом с надписью мощности (как "dBm" в S-метре) - if SWRHigh then - begin - ACanvas.Font.Size := 6; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_SWR_HIGH; - ACanvas.TextOut(LblZoneL + 4 + TWPwr + 2, PY1 - TH6 - 4, 'SWR High'); - end; + FRuler.DrawRuler; end; // ──────────────────────────────────────────────────────────────────────────── @@ -1665,68 +947,18 @@ begin 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 := FTheme.Panel; - PB.Canvas.FillRect(Rect(0, 0, W, H)); - end; - if FMarkerActive then DrawMarkerLine(PB.Canvas, W, H); + FWaterfall.PaintWaterfall(Sender); 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); + FRuler.PaintRuler(Sender); 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; - if FTransmitting then - begin - DrawTXMeter(FSmBitmap.Canvas, Rect(0, 0, W, H)); - end else - begin - 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); - end; - 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); + FSMeter.PaintSMeterRight(Sender); end; end. diff --git a/WaterfallView.pas b/WaterfallView.pas new file mode 100644 index 0000000..01e25be --- /dev/null +++ b/WaterfallView.pas @@ -0,0 +1,445 @@ +unit WaterfallView; + +{ + WaterfallView.pas — рендеринг водопада. + TWaterfallView — изолированный класс, не зависит от SpectrumView. +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, + IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme; + +type + TWaterfallView = class + private + FWaterfallBitmap: TBitmap; + FWfBitmap: TBitmap; + FWfBitmapW: Integer; + FWfBitmapH: Integer; + FWfIntfImg: TLazIntfImage; + FWfPixels: array of LongWord; + FWaterfallBuf: array[0..1023] of Single; + FWaterfallBufCount: Integer; + FWaterfallDirty: Boolean; + FWfFrameCounter: Integer; + FWfFrameInterval: Integer; + FWfAGCEnabled: Boolean; + FWfNFEnabled: Boolean; + FWfManualHigh: Double; + FWfManualLow: Double; + FWfAGCOffset: Double; + FWfHigh: Double; + FWfLow: Double; + FCenterFreq: Double; + FSpanHz: Double; + FTXMode: Boolean; + FTXFreq: Double; + FTXSpanHz: Double; + FMarkerActive: Boolean; + FMarkerX: Integer; + FTheme: TAppTheme; + FLightTheme: Boolean; + FPbWaterfall: TPaintBox; + + procedure DrawMarkerLine(C: TCanvas; W, H: Integer); + public + constructor Create; + destructor Destroy; override; + + property CenterFreq: Double read FCenterFreq write FCenterFreq; + property SpanHz: Double read FSpanHz write FSpanHz; + property TXMode: Boolean read FTXMode write FTXMode; + property TXFreq: Double read FTXFreq write FTXFreq; + property TXSpanHz: Double read FTXSpanHz write FTXSpanHz; + 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 WaterfallDirty: Boolean read FWaterfallDirty write FWaterfallDirty; + property WfFrameInterval: Integer read FWfFrameInterval write FWfFrameInterval; + property MarkerActive: Boolean read FMarkerActive write FMarkerActive; + property MarkerX: Integer read FMarkerX write FMarkerX; + property PbWaterfall: TPaintBox write FPbWaterfall; + + procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); + procedure DrawWaterfall; + procedure PaintWaterfall(Sender: TObject); + procedure SetWaterfallBitmapSize(W, H: Integer); + procedure ResetWfAvgBuf; + procedure ResetWfBuf; + procedure SetTheme(const T: TAppTheme); + end; + +implementation + +// ════════════════════════════════════════════════════════════════════════════ +// Вспомогательные функции +// ════════════════════════════════════════════════════════════════════════════ + +function FormatFreqWF(Hz: Double): string; +var Mhz: Int64; KHz, Rest: Integer; +begin + Mhz := Trunc(Hz / 1000000); + KHz := Trunc((Hz - Mhz * 1000000) / 1000); + Rest := Trunc(Hz) mod 1000; + Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]); +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; + +function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord; +const + NSTOPS = 8; + SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255); + SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20); + SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20); +var + T: Double; + Seg: Integer; + R, G, B: Integer; +begin + if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end; + if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end; + T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1); + Seg := Min(NSTOPS - 2, Trunc(T)); + T := T - Seg; + R := EnsureRange(Round(SR[Seg] * (1.0 - T) + SR[Seg+1] * T), 0, 255); + G := EnsureRange(Round(SG[Seg] * (1.0 - T) + SG[Seg+1] * T), 0, 255); + B := EnsureRange(Round(SB[Seg] * (1.0 - T) + SB[Seg+1] * T), 0, 255); + Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; +end; + +procedure InitRawDesc32WF(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; + +// ════════════════════════════════════════════════════════════════════════════ +// TWaterfallView +// ════════════════════════════════════════════════════════════════════════════ + +constructor TWaterfallView.Create; +begin + inherited Create; + FWaterfallBitmap := TBitmap.Create; + FWfBitmap := TBitmap.Create; + FWfIntfImg := nil; + FWfBitmapW := 0; + FWfBitmapH := 0; + FWfManualHigh := -80.0; + FWfManualLow := -130.0; + FWfAGCOffset := 0.0; + FWfHigh := -80.0; + FWfLow := -130.0; + FWfAGCEnabled := True; + FWfNFEnabled := False; + FWaterfallBufCount := 1024; + FWfFrameInterval := 2; + FWfFrameCounter := 0; + FWaterfallDirty := True; + FTheme := DarkTheme; + FLightTheme := False; + FSpanHz := 192000; + ResetWfBuf; +end; + +destructor TWaterfallView.Destroy; +begin + FWaterfallBitmap.Free; + FWfBitmap.Free; + FreeAndNil(FWfIntfImg); + inherited; +end; + +procedure TWaterfallView.SetTheme(const T: TAppTheme); +begin + FTheme := T; + FLightTheme := T.BG > TColor($00808080); + FWaterfallDirty := True; + // Сброс размера — чтобы следующий кадр перезаполнил фон новой темой + FWfBitmapW := 0; + FWfBitmapH := 0; + SetLength(FWfPixels, 0); + FreeAndNil(FWfIntfImg); +end; + +procedure TWaterfallView.ResetWfAvgBuf; +begin + FWfHigh := FWfManualHigh; + FWfLow := FWfManualLow; + FWaterfallDirty := True; +end; + +procedure TWaterfallView.ResetWfBuf; +var i: Integer; +begin + for i := 0 to 1023 do FWaterfallBuf[i] := -130.0; + FWfHigh := FWfManualHigh; + FWfLow := FWfManualLow; + FWaterfallDirty := True; +end; + +procedure TWaterfallView.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; + +procedure TWaterfallView.SetWaterfallBitmapSize(W, H: Integer); +begin + FWaterfallBitmap.SetSize(W, H); + FWfBitmapW := 0; + FWfBitmapH := 0; + SetLength(FWfPixels, 0); + FreeAndNil(FWfIntfImg); +end; + +procedure TWaterfallView.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 := FormatFreqWF(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; + +// ──────────────────────────────────────────────────────────────────────────── +// DrawWaterfall +// ──────────────────────────────────────────────────────────────────────────── + +procedure TWaterfallView.DrawWaterfall; +var + W, H, X: Integer; + dB, frac, WatSrcF: Double; + WatS0, WatS1: 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); + Pal := $FF000000 + or ((LongWord(FTheme.Panel) and $FF) shl 16) + or (LongWord(FTheme.Panel) and $FF00) + or ((LongWord(FTheme.Panel) shr 16) and $FF); + FillDWord(FWfPixels[0], W * H, Pal); + 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 + if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then + begin + dB := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq; + if (dB < -FTXSpanHz * 0.5) or (dB > FTXSpanHz * 0.5) then + begin + if H > 1 then Px^ := FWfPixels[X + W] + else Px^ := 0; + Inc(Px); + WatSrcF := WatSrcF + Step; + Continue; + end; + frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1); + WatS0 := Trunc(frac); + if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2; + WatS1 := WatS0 + 1; + frac := frac - WatS0; + dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac; + end else + 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; + end; + if FLightTheme then + Pal := WaterfallLightTheme(dB, WfLow, WfHigh) + else + Pal := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh); + Px^ := Pal; Inc(Px); + WatSrcF := WatSrcF + Step; + end; + + if FWfIntfImg = nil then + begin + FWfIntfImg := TLazIntfImage.Create(W, H); + InitRawDesc32WF(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; + +// ──────────────────────────────────────────────────────────────────────────── +// PaintWaterfall +// ──────────────────────────────────────────────────────────────────────────── + +procedure TWaterfallView.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 := FTheme.Panel; + PB.Canvas.FillRect(Rect(0, 0, W, H)); + end; + if FMarkerActive then DrawMarkerLine(PB.Canvas, W, H); +end; + +end. diff --git a/ewsdr.lpi b/ewsdr.lpi index dffb9d6..752b758 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -167,6 +167,22 @@ + + + + + + + + + + + + + + + +