unit WidebandView; { WidebandView.pas - raw ADC wideband spectrum pane. The network layer feeds 16-bit ADC samples collected from Protocol V4 wideband packets. This view windows the frame, runs an in-process real FFT and renders a compact Thetis-style wideband panadapter. CPU paint uses a bitmap; when MainForm creates a TOpenGLControl the same spectrum is rendered as GL primitives. } {$IFDEF FPC} {$MODE Delphi} {$ENDIF} interface uses Classes, SysUtils, Math, Graphics, Controls, ExtCtrls, OpenGLContext, GL, AppTheme; type TWidebandView = class private FBitmap: TBitmap; FRulerBitmap: TBitmap; FTheme: TAppTheme; FData: array of Single; FPoints: array of TPoint; FGLTex: GLuint; FDirty: Boolean; FSampleRateHz: Double; FRefLevel: Double; FRange: Double; FCalOffset: Double; FViewStartHz: Double; FViewEndHz: Double; FSourceStartHz: Double; FSourceEndHz: Double; FMarkerHz: Double; procedure EnsureBitmap(W, H: Integer); procedure ComputeSpectrum(const Samples: array of SmallInt; Count: Integer); procedure DrawCPU(W, H: Integer); procedure PaintCPU(C: TCanvas); procedure PaintGL(C: TOpenGLControl); procedure ColorToGL(AColor: TColor; out R, G, B: GLFloat); function BinToMHz(Bin, BinCount: Integer): Double; function FreqToX(FreqHz: Double; W: Integer): Integer; function GridStepHz(W: Integer): Double; function RulerGridStepHz(C: TCanvas; PlotW: Integer): Double; procedure PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor); procedure AlphaFillRect(const R: TRect; AColor: TColor; Alpha: Byte); procedure DrawHamBands(C: TCanvas; PlotW, H: Integer); procedure DrawHamBand(C: TCanvas; PlotW, H: Integer; F1, F2: Double; const Name: string; AColor: TColor); procedure DrawFrequencyMarker(C: TCanvas; PlotW, H: Integer); procedure UploadBitmapToGL; public constructor Create; destructor Destroy; override; procedure SetTheme(const T: TAppTheme); procedure SetSampleRateHz(AHz: Double); function SetFrequencyView(ViewStartHz, ViewEndHz, SourceStartHz, SourceEndHz: Double): Boolean; function SetMarkerHz(AHz: Double): Boolean; function TryPixelToFrequency(PixelX, PanelWidth: Integer; out FreqHz: Double): Boolean; procedure SetSamples(const Samples: array of SmallInt; Count: Integer); procedure SetBitmapSize(W, H: Integer); procedure SetRulerSize(W, H: Integer); procedure Draw; procedure DrawRuler; procedure Paint(Sender: TObject); procedure PaintRuler(Sender: TObject); property Dirty: Boolean read FDirty write FDirty; end; implementation const WB_DB_SCALE_W = 34; constructor TWidebandView.Create; begin inherited Create; FBitmap := TBitmap.Create; FBitmap.PixelFormat := pf32bit; FRulerBitmap := TBitmap.Create; FRulerBitmap.PixelFormat := pf32bit; FTheme := DarkTheme; FSampleRateHz := 122880000.0; FRefLevel := -50.0; FRange := 120.0; FCalOffset := -40.1; FViewStartHz := 0.0; FViewEndHz := FSampleRateHz * 0.5; FSourceStartHz := 0.0; FSourceEndHz := FSampleRateHz * 0.5; FMarkerHz := 0.0; FGLTex := 0; FDirty := True; end; destructor TWidebandView.Destroy; begin if FGLTex <> 0 then glDeleteTextures(1, @FGLTex); FRulerBitmap.Free; FBitmap.Free; inherited Destroy; end; procedure TWidebandView.SetTheme(const T: TAppTheme); begin FTheme := T; FDirty := True; end; procedure TWidebandView.SetSampleRateHz(AHz: Double); begin if AHz > 0 then begin FSampleRateHz := AHz; SetFrequencyView(0.0, FSampleRateHz * 0.5, 0.0, FSampleRateHz * 0.5); end; end; function TWidebandView.SetFrequencyView(ViewStartHz, ViewEndHz, SourceStartHz, SourceEndHz: Double): Boolean; var Nyq: Double; T: Double; begin Result := False; Nyq := FSampleRateHz * 0.5; if Nyq <= 0.0 then Nyq := 61440000.0; if ViewEndHz <= ViewStartHz then begin ViewStartHz := 0.0; ViewEndHz := Nyq; end; if SourceEndHz < SourceStartHz then begin T := SourceStartHz; SourceStartHz := SourceEndHz; SourceEndHz := T; end; if (Abs(FViewStartHz - ViewStartHz) < 0.5) and (Abs(FViewEndHz - ViewEndHz) < 0.5) and (Abs(FSourceStartHz - SourceStartHz) < 0.5) and (Abs(FSourceEndHz - SourceEndHz) < 0.5) then Exit; FViewStartHz := ViewStartHz; FViewEndHz := ViewEndHz; FSourceStartHz := SourceStartHz; FSourceEndHz := SourceEndHz; FDirty := True; Result := True; end; function TWidebandView.SetMarkerHz(AHz: Double): Boolean; begin Result := False; if Abs(FMarkerHz - AHz) < 0.5 then Exit; FMarkerHz := AHz; Result := True; end; function TWidebandView.TryPixelToFrequency(PixelX, PanelWidth: Integer; out FreqHz: Double): Boolean; var PlotW: Integer; begin Result := False; FreqHz := 0.0; PlotW := Max(1, PanelWidth - WB_DB_SCALE_W); if (PanelWidth <= 0) or (PixelX < 0) or (PixelX >= PlotW) then Exit; if FViewEndHz <= FViewStartHz then Exit; FreqHz := FViewStartHz + PixelX / Max(1, PlotW - 1) * (FViewEndHz - FViewStartHz); Result := True; end; procedure TWidebandView.EnsureBitmap(W, H: Integer); begin W := Max(1, W); H := Max(1, H); if (FBitmap.Width <> W) or (FBitmap.Height <> H) then FBitmap.SetSize(W, H); end; procedure TWidebandView.SetBitmapSize(W, H: Integer); begin EnsureBitmap(W, H); FDirty := True; end; procedure TWidebandView.SetRulerSize(W, H: Integer); begin W := Max(1, W); H := Max(1, H); if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then FRulerBitmap.SetSize(W, H); end; procedure TWidebandView.ComputeSpectrum(const Samples: array of SmallInt; Count: Integer); var N, Half, I, J, K, M, Step: Integer; Wr, Wi, Ur, Ui, Tr, Ti, Ang, Re, Im, Mag, Win, WinSum: Double; RealBuf, ImagBuf: array of Double; begin if Count < 256 then Exit; N := 1; while (N shl 1 <= Count) and (N shl 1 <= 16384) do N := N shl 1; Half := N div 2; SetLength(RealBuf, N); SetLength(ImagBuf, N); WinSum := 0.0; for I := 0 to N - 1 do begin Win := 0.35875 - 0.48829 * Cos(2 * Pi * I / (N - 1)) + 0.14128 * Cos(4 * Pi * I / (N - 1)) - 0.01168 * Cos(6 * Pi * I / (N - 1)); WinSum := WinSum + Win; RealBuf[I] := Samples[I] / 32768.0 * Win; ImagBuf[I] := 0.0; end; if WinSum <= 0.0 then WinSum := N; J := 0; for I := 1 to N - 2 do begin K := N shr 1; while J >= K do begin Dec(J, K); K := K shr 1; end; Inc(J, K); if I < J then begin Re := RealBuf[I]; RealBuf[I] := RealBuf[J]; RealBuf[J] := Re; Im := ImagBuf[I]; ImagBuf[I] := ImagBuf[J]; ImagBuf[J] := Im; end; end; M := 2; while M <= N do begin Ang := -2 * Pi / M; Wr := Cos(Ang); Wi := Sin(Ang); Step := M div 2; K := 0; while K < N do begin Ur := 1.0; Ui := 0.0; for J := 0 to Step - 1 do begin I := K + J; Tr := Ur * RealBuf[I + Step] - Ui * ImagBuf[I + Step]; Ti := Ur * ImagBuf[I + Step] + Ui * RealBuf[I + Step]; RealBuf[I + Step] := RealBuf[I] - Tr; ImagBuf[I + Step] := ImagBuf[I] - Ti; RealBuf[I] := RealBuf[I] + Tr; ImagBuf[I] := ImagBuf[I] + Ti; Re := Ur * Wr - Ui * Wi; Ui := Ur * Wi + Ui * Wr; Ur := Re; end; Inc(K, M); end; M := M shl 1; end; SetLength(FData, Half); for I := 0 to Half - 1 do begin Mag := Sqr(RealBuf[I]) + Sqr(ImagBuf[I]); // Convert FFT bin amplitude to dBFS, then apply Thetis-style display // calibration offset. The previous unnormalised FFT power depended on N // and could not line up with the dB scale. FData[I] := 20.0 * Log10((2.0 * Sqrt(Mag) / WinSum) + 1.0E-20) + FCalOffset; end; end; procedure TWidebandView.SetSamples(const Samples: array of SmallInt; Count: Integer); begin ComputeSpectrum(Samples, Count); FDirty := True; end; function TWidebandView.BinToMHz(Bin, BinCount: Integer): Double; begin if BinCount <= 1 then Result := 0 else Result := (Bin / (BinCount - 1)) * (FSampleRateHz * 0.5) / 1000000.0; end; function TWidebandView.FreqToX(FreqHz: Double; W: Integer): Integer; var SpanHz: Double; begin SpanHz := FViewEndHz - FViewStartHz; if SpanHz <= 0.0 then SpanHz := Max(1.0, FSampleRateHz * 0.5); Result := Round((FreqHz - FViewStartHz) / SpanHz * Max(1, W - 1)); end; function TWidebandView.GridStepHz(W: Integer): Double; var PixPerMHz, SpanMHz: Double; begin SpanMHz := Max(0.001, (FViewEndHz - FViewStartHz) / 1000000.0); PixPerMHz := W / SpanMHz; if PixPerMHz >= 44.0 then Result := 500000.0 else if PixPerMHz >= 18.0 then Result := 1000000.0 else if PixPerMHz >= 10.0 then Result := 2000000.0 else if PixPerMHz >= 5.0 then Result := 5000000.0 else Result := 10000000.0; end; function TWidebandView.RulerGridStepHz(C: TCanvas; PlotW: Integer): Double; var BaseStep, PixPerStep: Double; LabelMult: Integer; begin BaseStep := GridStepHz(PlotW); PixPerStep := PlotW * BaseStep / Max(1.0, FViewEndHz - FViewStartHz); if PixPerStep >= 1.0 then LabelMult := Max(1, Ceil((C.TextWidth('000.0') + 8) / PixPerStep)) else LabelMult := MaxInt; Result := BaseStep * LabelMult; end; procedure TWidebandView.PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor); var Y, B1, G1, R1, B2, G2, R2, B, G, R: Integer; T: Double; begin if (W <= 0) or (H <= 0) then Exit; B1 := (TopColor shr 16) and $FF; G1 := (TopColor shr 8) and $FF; R1 := TopColor and $FF; B2 := (BottomColor shr 16) and $FF; G2 := (BottomColor shr 8) and $FF; R2 := BottomColor and $FF; C.Pen.Style := psClear; C.Brush.Style := bsSolid; for Y := 0 to H - 1 do begin T := Y / Max(1, H - 1); B := Round(B1 + (B2 - B1) * T); G := Round(G1 + (G2 - G1) * T); R := Round(R1 + (R2 - R1) * T); C.Brush.Color := TColor((B shl 16) or (G shl 8) or R); C.FillRect(Rect(0, Y, W, Y + 1)); end; C.Pen.Style := psSolid; end; procedure TWidebandView.AlphaFillRect(const R: TRect; AColor: TColor; Alpha: Byte); var X, Y: Integer; Row: PByte; BR, BG, BB: Integer; SR, SG, SB: Integer; RR: TRect; begin if Alpha = 0 then Exit; RR := Rect( EnsureRange(R.Left, 0, FBitmap.Width), EnsureRange(R.Top, 0, FBitmap.Height), EnsureRange(R.Right, 0, FBitmap.Width), EnsureRange(R.Bottom, 0, FBitmap.Height)); if (RR.Right <= RR.Left) or (RR.Bottom <= RR.Top) then Exit; SR := AColor and $FF; SG := (AColor shr 8) and $FF; SB := (AColor shr 16) and $FF; FBitmap.BeginUpdate(False); try for Y := RR.Top to RR.Bottom - 1 do begin Row := PByte(FBitmap.ScanLine[Y]); Inc(Row, RR.Left * 4); for X := RR.Left to RR.Right - 1 do begin BB := Row[0]; BG := Row[1]; BR := Row[2]; Row[0] := Byte((SB * Alpha + BB * (255 - Alpha)) div 255); Row[1] := Byte((SG * Alpha + BG * (255 - Alpha)) div 255); Row[2] := Byte((SR * Alpha + BR * (255 - Alpha)) div 255); Inc(Row, 4); end; end; finally FBitmap.EndUpdate(False); end; end; procedure TWidebandView.DrawHamBand(C: TCanvas; PlotW, H: Integer; F1, F2: Double; const Name: string; AColor: TColor); var X1, X2, LabelX, LabelY, TW, TH: Integer; R: TRect; begin X1 := FreqToX(F1, PlotW); X2 := FreqToX(F2, PlotW); if (X2 <= 0) or (X1 >= PlotW) then Exit; X1 := EnsureRange(X1, 0, PlotW - 1); X2 := EnsureRange(X2, 0, PlotW - 1); if X2 <= X1 then Exit; R := Rect(X1, 0, X2 + 1, H); AlphaFillRect(R, AColor, 86); AlphaFillRect(Rect(X1, 0, X2 + 1, Min(H, 15)), AColor, 118); C.Font.Name := 'Sans'; C.Font.Size := 7; TW := C.TextWidth(Name); TH := C.TextHeight(Name); if X2 - X1 >= 2 then begin LabelX := EnsureRange(((X1 + X2) div 2) - TW div 2, 2, Max(2, PlotW - TW - 5)); LabelY := Max(1, Min(H - TH - 1, 2)); C.Font.Color := TColor($00EAF7D7); C.TextOut(LabelX, LabelY, Name); end; end; procedure TWidebandView.DrawHamBands(C: TCanvas; PlotW, H: Integer); begin DrawHamBand(C, PlotW, H, 1810000, 2000000, '160m', TColor($003B8F5A)); DrawHamBand(C, PlotW, H, 3500000, 3800000, '80m', TColor($004D8F3B)); DrawHamBand(C, PlotW, H, 5258500, 5403500, '60m', TColor($00688F3B)); DrawHamBand(C, PlotW, H, 7000000, 7300000, '40m', TColor($00808D34)); DrawHamBand(C, PlotW, H, 10100000, 10150000, '30m', TColor($008F7834)); DrawHamBand(C, PlotW, H, 14000000, 14350000, '20m', TColor($008F5D34)); DrawHamBand(C, PlotW, H, 18068000, 18168000, '17m', TColor($008F4934)); DrawHamBand(C, PlotW, H, 21000000, 21450000, '15m', TColor($008C3E58)); DrawHamBand(C, PlotW, H, 24890000, 24990000, '12m', TColor($007A3E8C)); DrawHamBand(C, PlotW, H, 28000000, 29700000, '10m', TColor($005B4B9A)); DrawHamBand(C, PlotW, H, 50000000, 51990000, '6m', TColor($003C729A)); end; procedure TWidebandView.DrawFrequencyMarker(C: TCanvas; PlotW, H: Integer); var X: Integer; begin if (FMarkerHz < FViewStartHz) or (FMarkerHz > FViewEndHz) then Exit; X := EnsureRange(FreqToX(FMarkerHz, PlotW), 0, PlotW - 1); C.Pen.Width := 1; C.Pen.Color := TColor($006DA6FF); C.MoveTo(Max(0, X - 2), 0); C.LineTo(Min(PlotW - 1, X + 2), 0); C.Pen.Color := TColor($00FFF3C0); C.MoveTo(X, 0); C.LineTo(X, H); end; procedure TWidebandView.DrawCPU(W, H: Integer); var C: TCanvas; X, Y, Bin, N, TopH, PlotW, ScaleX: Integer; DB, DBMin, InvRange, StepHz, FreqHz, SrcHz, Nyq: Double; begin EnsureBitmap(W, H); C := FBitmap.Canvas; C.Brush.Color := FTheme.BG; C.FillRect(0, 0, W, H); PlotW := Max(1, W - WB_DB_SCALE_W); ScaleX := PlotW; DBMin := FRefLevel - FRange; InvRange := 1.0 / Max(1.0, FRange); C.Pen.Color := FTheme.SpecGrid; C.Font.Color := FTheme.TextDim; C.Font.Size := 8; StepHz := RulerGridStepHz(C, PlotW); FreqHz := Ceil(FViewStartHz / StepHz) * StepHz; while FreqHz <= FViewEndHz + 0.5 do begin X := FreqToX(FreqHz, PlotW); C.Line(X, 0, X, H); FreqHz := FreqHz + StepHz; end; TopH := Max(1, H - 1); DB := FRefLevel - 20.0; while DB >= DBMin do begin Y := Round((FRefLevel - DB) * InvRange * TopH); C.Line(0, Y, W, Y); DB := DB - 20.0; end; C.Pen.Style := psClear; C.Brush.Style := bsSolid; C.Brush.Color := FTheme.SpecLabelBand; C.FillRect(Rect(ScaleX, 0, W, H)); C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecGrid; C.MoveTo(ScaleX, 0); C.LineTo(ScaleX, H); C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Font.Color := FTheme.SpecLabelText; C.Brush.Style := bsClear; DB := FRefLevel - 20.0; while DB >= DBMin do begin Y := Round((FRefLevel - DB) * InvRange * TopH); C.TextOut(ScaleX + 2, Y - 9, Format('%4.0f', [DB])); DB := DB - 20.0; end; N := Length(FData); if N > 1 then begin SetLength(FPoints, PlotW); Nyq := FSampleRateHz * 0.5; if Nyq <= 0.0 then Nyq := 61440000.0; for X := 0 to PlotW - 1 do begin SrcHz := FSourceStartHz + X / Max(1, PlotW - 1) * (FSourceEndHz - FSourceStartHz); if (SrcHz < 0.0) or (SrcHz > Nyq) then DB := -200.0 else begin Bin := EnsureRange(Round(SrcHz / Nyq * (N - 1)), 0, N - 1); DB := FData[Bin]; end; Y := Round((FRefLevel - DB) * InvRange * TopH); Y := EnsureRange(Y, 1, TopH); FPoints[X] := Point(X, Y); end; C.Pen.Color := TColor($0040FF80); C.Polyline(FPoints); end; DrawHamBands(C, PlotW, H); C.Font.Color := FTheme.Text; C.TextOut(6, 4, 'Wideband'); FDirty := False; end; procedure TWidebandView.DrawRuler; var C: TCanvas; W, H, X, TW, PlotW: Integer; StepHz, FreqHz: Double; Lbl: string; begin W := FRulerBitmap.Width; H := FRulerBitmap.Height; if (W <= 0) or (H <= 0) then Exit; 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; C.Font.Color := FTheme.RulerText; PlotW := Max(1, W - WB_DB_SCALE_W); StepHz := RulerGridStepHz(C, PlotW); FreqHz := Ceil(FViewStartHz / StepHz) * StepHz; while FreqHz <= FViewEndHz + 0.5 do begin X := FreqToX(FreqHz, PlotW); C.Pen.Color := FTheme.RulerBorder; C.MoveTo(X, 0); C.LineTo(X, H div 2); if FreqHz >= 100000000.0 then Lbl := FormatFloat('0.###', FreqHz / 1000000.0) else Lbl := FormatFloat('0.#', FreqHz / 1000000.0); TW := C.TextWidth(Lbl); C.TextOut(EnsureRange(X - TW div 2, 2, Max(2, PlotW - TW - 2)), H div 2 - 1, Lbl); FreqHz := FreqHz + StepHz; end; C.Pen.Color := FTheme.RulerBorder; C.MoveTo(PlotW, 0); C.LineTo(PlotW, H); C.Brush.Style := bsSolid; C.Brush.Color := FTheme.SpecLabelBand; C.FillRect(Rect(PlotW + 1, 1, W, H - 1)); C.Brush.Style := bsClear; C.Font.Color := FTheme.TextDim; Lbl := 'MHz'; C.TextOut(Max(2, PlotW - C.TextWidth(Lbl) - 4), H div 2 - 1, Lbl); end; procedure TWidebandView.Draw; begin DrawCPU(FBitmap.Width, FBitmap.Height); end; procedure TWidebandView.PaintCPU(C: TCanvas); begin if FDirty then DrawCPU(FBitmap.Width, FBitmap.Height); C.Draw(0, 0, FBitmap); DrawFrequencyMarker(C, Max(1, FBitmap.Width - WB_DB_SCALE_W), FBitmap.Height); end; procedure TWidebandView.ColorToGL(AColor: TColor; out R, G, B: GLFloat); begin R := (AColor and $FF) / 255.0; G := ((AColor shr 8) and $FF) / 255.0; B := ((AColor shr 16) and $FF) / 255.0; end; procedure TWidebandView.UploadBitmapToGL; var X, Y, I: Integer; Src: PByte; Buf: array of Byte; begin if (FBitmap.Width <= 0) or (FBitmap.Height <= 0) then Exit; if FGLTex = 0 then glGenTextures(1, @FGLTex); SetLength(Buf, FBitmap.Width * FBitmap.Height * 4); FBitmap.BeginUpdate(False); try I := 0; for Y := 0 to FBitmap.Height - 1 do begin Src := PByte(FBitmap.ScanLine[Y]); for X := 0 to FBitmap.Width - 1 do begin Buf[I] := Src[2]; Buf[I + 1] := Src[1]; Buf[I + 2] := Src[0]; Buf[I + 3] := $FF; Inc(Src, 4); Inc(I, 4); end; end; finally FBitmap.EndUpdate(False); end; glBindTexture(GL_TEXTURE_2D, FGLTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, FBitmap.Width, FBitmap.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, @Buf[0]); end; procedure TWidebandView.PaintGL(C: TOpenGLControl); var W, H, PlotW, X: Integer; NeedUpload: Boolean; begin if C = nil then Exit; W := Max(1, C.Width); H := Max(1, C.Height); if not C.MakeCurrent then Exit; NeedUpload := False; if (FBitmap.Width <> W) or (FBitmap.Height <> H) then begin SetBitmapSize(W, H); NeedUpload := True; end; if FDirty then begin DrawCPU(W, H); NeedUpload := True; end; if FGLTex = 0 then NeedUpload := True; if NeedUpload then UploadBitmapToGL; glViewport(0, 0, W, H); glMatrixMode(GL_PROJECTION); glLoadIdentity; glOrtho(0, W, 0, H, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity; glDisable(GL_DEPTH_TEST); glClearColor(0, 0, 0, 1.0); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, FGLTex); glColor3f(1, 1, 1); glBegin(GL_QUADS); glTexCoord2f(0, 1); glVertex2f(0, 0); glTexCoord2f(1, 1); glVertex2f(W, 0); glTexCoord2f(1, 0); glVertex2f(W, H); glTexCoord2f(0, 0); glVertex2f(0, H); glEnd; glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); if (FMarkerHz >= FViewStartHz) and (FMarkerHz <= FViewEndHz) then begin PlotW := Max(1, W - WB_DB_SCALE_W); X := EnsureRange(FreqToX(FMarkerHz, PlotW), 0, PlotW - 1); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColor4f(1.0, 0.64, 0.36, 0.32); glBegin(GL_QUADS); glVertex2f(Max(0, X - 2), 0); glVertex2f(Min(PlotW - 1, X + 2), 0); glVertex2f(Min(PlotW - 1, X + 2), H); glVertex2f(Max(0, X - 2), H); glEnd; glColor4f(1.0, 0.95, 0.75, 0.90); glBegin(GL_LINES); glVertex2f(X, 0); glVertex2f(X, H); glEnd; glDisable(GL_BLEND); end; C.SwapBuffers; FDirty := False; end; procedure TWidebandView.Paint(Sender: TObject); begin if Sender is TOpenGLControl then PaintGL(TOpenGLControl(Sender)) else if Sender is TPaintBox then PaintCPU(TPaintBox(Sender).Canvas); end; procedure TWidebandView.PaintRuler(Sender: TObject); var PB: TPaintBox; begin if not (Sender is TPaintBox) then Exit; PB := TPaintBox(Sender); if (FRulerBitmap.Width <> PB.Width) or (FRulerBitmap.Height <> PB.Height) then SetRulerSize(PB.Width, PB.Height); DrawRuler; if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then PB.Canvas.Draw(0, 0, FRulerBitmap); end; end.