mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
Builds on the beacon-lock baseline: keeps the proven display-FFT control
path (hardware-LO trim folded into the active transverter LOError), and
hardens it. The narrowband NCO/FLL scaffold (BeaconLock.pas) is removed —
it was never in the control path.
- RadioController:
* Persist live: each correction folds straight into the active xvtr
LOError (visible in Settings, survives restart as LNB calibration);
disk writes throttled. FBeaconLockHz is now only a session indicator.
* Robust tracking vs nearby signals: narrow ±2 kHz gate while locked
(±6 kHz only for acquisition), slew-rate outlier rejection (a sudden
centroid jump = interferer, not slow LNB drift → ignored), and lobe
shape validation in MeasureBeaconFFT (width + power symmetry) so a CW
carrier / one-sided neighbour isn't mistaken for the BPSK beacon.
Prominence measured over the in-window noise floor (gate-width robust).
* Fast convergence: feed-forward (predict post-retune position so the
window follows the beacon through the jump) + near-deadbeat correction
on the manual click → one click locks instead of repeated tapping.
* Single central BPSK beacon (10489.750); dead CoarseBeaconOffset and
unused throttle counters removed; stale NCO/FLL/4 Hz comments fixed.
- WDSPEngine: drop the per-sample TBeaconTracker RX-IQ tap (tracker gone).
- MainForm: BEACON is now a plain button, shown only on Pluto in a
transverter. Lock status (off / click / search / LOCK + correction +
prominence) moves to status-bar field 7 in place of PLL on Pluto;
openHPSDR keeps PLL there.
- SettingsForm/StatusBar: widen LO Error field to ±10 MHz (QO-100 LNBs
drift hundreds of kHz) and widen status field 7 for the beacon text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1046 lines
45 KiB
ObjectPascal
1046 lines
45 KiB
ObjectPascal
unit SpectrumView;
|
|
|
|
{
|
|
SpectrumView.pas — рендеринг спектрограммы.
|
|
|
|
TSpectrumView инкапсулирует рисование спектра и агрегирует дочерние
|
|
компоненты: TWaterfallView, TSMeterView, TRulerView. MainForm создаёт один
|
|
экземпляр TSpectrumView и работает с ним единообразно: свойства/методы
|
|
водопада, S-метра и линейки делегируются в соответствующие sub-view.
|
|
|
|
Зависимости: нет обратной зависимости на MainForm.
|
|
}
|
|
|
|
{$IFDEF FPC}
|
|
{$MODE Delphi}
|
|
{$ENDIF}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
|
|
AppTheme,
|
|
AlertOverlay, SampleRateOverlay, VfoOverlay,
|
|
WaterfallView, SMeterView, RulerView;
|
|
|
|
const
|
|
SV_CLR_BG = TColor($00101010);
|
|
|
|
type
|
|
TSpectrumView = class
|
|
protected
|
|
// ── Off-screen bitmaps ────────────────────────────────────────────────────
|
|
FSpectrumBitmap: TBitmap;
|
|
FGridBitmap: TBitmap;
|
|
FGridBitmapW: Integer;
|
|
FGridBitmapH: Integer;
|
|
FFMGridLastCenter: Double;
|
|
FFMGridLastStepHz: Double;
|
|
FFMGridLastSpan: Double;
|
|
FSpPts: array of TPoint;
|
|
FSpPtsLen: Integer;
|
|
FFillSpectrum: Boolean;
|
|
// ── Тема ─────────────────────────────────────────────────────────────────
|
|
FTheme: TAppTheme;
|
|
FLightTheme: Boolean;
|
|
// ── Radio state ───────────────────────────────────────────────────────────
|
|
FVfoA: Double;
|
|
FVfoB: Double;
|
|
FActiveVfo: Integer;
|
|
FCenterFreq: Double;
|
|
FSpanHz: Double;
|
|
FMode: Integer;
|
|
FFilterBW: Integer;
|
|
FAGCTop: Integer;
|
|
FAGCThresh: Double;
|
|
FAGCHangLevel: Double;
|
|
FWDSPReady: Boolean;
|
|
// ── Display settings ──────────────────────────────────────────────────────
|
|
FSpecRefLevel: Double;
|
|
FSpecRange: Double;
|
|
FSpecGridStep: Double;
|
|
FFMGridStepHz: Double;
|
|
// ── TX overlay ────────────────────────────────────────────────────────────
|
|
FTXMode: Boolean;
|
|
FTXFreq: Double;
|
|
FTXSpanHz: Double;
|
|
FTXVfoIndex: Integer;
|
|
FTXOverlay: Boolean;
|
|
// ── ADC overload overlay ─────────────────────────────────────────────────
|
|
FADCOverloadVisible: Boolean;
|
|
FSampleRateOverlay: TSampleRateOverlay;
|
|
FVfoOverlay: TVfoOverlay;
|
|
// ── Marker ────────────────────────────────────────────────────────────────
|
|
FMarkerActive: Boolean;
|
|
FMarkerX: Integer;
|
|
// ── QO-100 beacon markers (диагностика лока) ──────────────────────────────
|
|
FBeaconMarkActive: Boolean;
|
|
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
|
|
FBeaconTrkFreq: Double; // отслеживаемый центроид маяка сейчас
|
|
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
|
FSpectrumBuf: array[0..1023] of Single;
|
|
FSpectrumBufCount: Integer;
|
|
FSpectrumDirty: Boolean;
|
|
// ── 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 SetFillSpectrum(V: Boolean);
|
|
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
|
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
|
procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
|
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
|
procedure CopyGridToSpectrum(W, H: Integer);
|
|
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
|
|
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: TControl);
|
|
// 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 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;
|
|
property AGCThresh: Double write FAGCThresh;
|
|
property AGCHangLevel: Double write FAGCHangLevel;
|
|
property WDSPReady: Boolean read FWDSPReady write FWDSPReady;
|
|
|
|
// ── Водопад ───────────────────────────────────────────────────────────────
|
|
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;
|
|
property SpecRange: Double read FSpecRange write FSpecRange;
|
|
property SpecGridStep: Double read FSpecGridStep write FSpecGridStep;
|
|
property FMGridStepHz: Double read FFMGridStepHz write SetFMGridStepHz;
|
|
|
|
// ── TX overlay ────────────────────────────────────────────────────────────
|
|
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;
|
|
|
|
property ADCOverloadVisible: Boolean read FADCOverloadVisible write FADCOverloadVisible;
|
|
|
|
// ── Маркер ────────────────────────────────────────────────────────────────
|
|
property MarkerActive: Boolean read FMarkerActive write SetMarkerActive;
|
|
property MarkerX: Integer read FMarkerX write SetMarkerX;
|
|
|
|
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
|
|
// Спектр перерисовывается каждый кадр, поэтому отдельный invalidate не нужен.
|
|
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
|
|
|
// ── S-метр ────────────────────────────────────────────────────────────────
|
|
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 FillSpectrum: Boolean read FFillSpectrum write SetFillSpectrum;
|
|
property WaterfallDirty: Boolean read GetWaterfallDirty write SetWaterfallDirty;
|
|
property WfFrameInterval: Integer read GetWfFrameInterval write SetWfFrameInterval;
|
|
|
|
// ── Ссылки на PaintBox ────────────────────────────────────────────────────
|
|
property PbWaterfall: TControl write SetPbWaterfall;
|
|
property PbRuler: TPaintBox write SetPbRuler;
|
|
property PbSMeterRight: TPaintBox write SetPbSMeterRight;
|
|
property SampleRateOverlay: TSampleRateOverlay read FSampleRateOverlay write FSampleRateOverlay;
|
|
property VfoOverlay: TVfoOverlay read FVfoOverlay write FVfoOverlay;
|
|
|
|
// ── Данные от DSP ─────────────────────────────────────────────────────────
|
|
procedure SetSpectrumData(const Pixels: array of Single; Count: Integer); virtual;
|
|
procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); virtual;
|
|
|
|
// ── Рендеринг ─────────────────────────────────────────────────────────────
|
|
procedure DrawSpectrum; virtual;
|
|
procedure DrawWaterfall; virtual;
|
|
procedure DrawRuler; virtual;
|
|
|
|
// ── Управление размером bitmap ────────────────────────────────────────────
|
|
procedure SetSpectrumBitmapSize(W, H: Integer); virtual;
|
|
procedure SetWaterfallBitmapSize(W, H: Integer); virtual;
|
|
procedure SetRulerSize(W, H: Integer); virtual;
|
|
function SpectrumBitmapWidth: Integer; virtual;
|
|
|
|
// ── Paint-обработчики ────────────────────────────────────────────────────
|
|
procedure PaintSpectrum(Sender: TObject); virtual;
|
|
procedure PaintWaterfall(Sender: TObject); virtual;
|
|
procedure PaintRuler(Sender: TObject); virtual;
|
|
procedure PaintSMeterRight(Sender: TObject); virtual;
|
|
|
|
// ── Утилиты ───────────────────────────────────────────────────────────────
|
|
procedure InvalidateGridCache; virtual;
|
|
procedure InvalidateOverlayCache; virtual;
|
|
procedure SetTheme(const T: TAppTheme); virtual;
|
|
procedure InvalidateRulerCache; virtual;
|
|
procedure ResetSpectrumBuf; virtual;
|
|
procedure ResetWfAvgBuf; virtual;
|
|
procedure FillDemoSpectrum; virtual;
|
|
function NeedsRulerRedraw: Boolean; virtual;
|
|
end;
|
|
|
|
implementation
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
// Вспомогательные функции
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
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;
|
|
|
|
|
|
function MixColorBGR(C1, C2: TColor; T: Double): TColor;
|
|
var
|
|
B1, G1, R1, B2, G2, R2, B, G, R: Integer;
|
|
begin
|
|
if T < 0.0 then T := 0.0 else if T > 1.0 then T := 1.0;
|
|
B1 := (C1 shr 16) and $FF; G1 := (C1 shr 8) and $FF; R1 := C1 and $FF;
|
|
B2 := (C2 shr 16) and $FF; G2 := (C2 shr 8) and $FF; R2 := C2 and $FF;
|
|
B := Round(B1 + (B2-B1)*T); G := Round(G1 + (G2-G1)*T); R := Round(R1 + (R2-R1)*T);
|
|
Result := TColor((B shl 16) or (G shl 8) or R);
|
|
end;
|
|
|
|
procedure PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor);
|
|
var Y: Integer; T: Double;
|
|
begin
|
|
if (W <= 0) or (H <= 0) then Exit;
|
|
C.Pen.Style := psClear; C.Brush.Style := bsSolid;
|
|
for Y := 0 to H - 1 do
|
|
begin
|
|
T := Y / Max(1, H - 1);
|
|
C.Brush.Color := MixColorBGR(TopColor, BottomColor, T);
|
|
C.FillRect(Rect(0, Y, W, Y + 1));
|
|
end;
|
|
C.Pen.Style := psSolid;
|
|
end;
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
// TSpectrumView
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
constructor TSpectrumView.Create;
|
|
begin
|
|
inherited Create;
|
|
FSpectrumBitmap := TBitmap.Create;
|
|
FSpectrumBitmap.PixelFormat := pf32bit;
|
|
FGridBitmap := TBitmap.Create;
|
|
FGridBitmap.PixelFormat := pf32bit;
|
|
FGridBitmapW := 0; FGridBitmapH := 0;
|
|
FFMGridLastCenter := -1.0; FFMGridLastStepHz := -1.0; FFMGridLastSpan := -1.0;
|
|
FFMGridStepHz := 0.0;
|
|
FSpPtsLen := 0;
|
|
// Spectrum display defaults
|
|
FSpecRefLevel := -20.0;
|
|
FSpecRange := 110.0;
|
|
FSpecGridStep := 10.0;
|
|
FSpanHz := 192000;
|
|
// TX overlay defaults
|
|
FTXMode := False;
|
|
FTXFreq := 0.0;
|
|
FTXSpanHz := 192000.0;
|
|
FTXVfoIndex := 0;
|
|
FTXOverlay := False;
|
|
FADCOverloadVisible := False;
|
|
FSpectrumBufCount := 1024;
|
|
FSpectrumDirty := True;
|
|
FLightTheme := False;
|
|
FFillSpectrum := True;
|
|
FTheme := DarkTheme;
|
|
// Sub-views
|
|
FWaterfall := TWaterfallView.Create;
|
|
FSMeter := TSMeterView.Create;
|
|
FRuler := TRulerView.Create;
|
|
ResetSpectrumBuf;
|
|
end;
|
|
|
|
destructor TSpectrumView.Destroy;
|
|
begin
|
|
FSpectrumBitmap.Free;
|
|
FGridBitmap.Free;
|
|
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;
|
|
|
|
procedure TSpectrumView.SetFillSpectrum(V: Boolean);
|
|
begin
|
|
if FFillSpectrum = V then Exit;
|
|
FFillSpectrum := V;
|
|
FSpectrumDirty := True;
|
|
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: TControl); 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;
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Приватные вспомогательные методы
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
function TSpectrumView.ActiveVfoFreq: Double;
|
|
begin
|
|
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
|
|
end;
|
|
|
|
function TSpectrumView.ScaleX(X, Total, Width: Integer): Integer;
|
|
begin
|
|
if Total = 0 then Result := 0
|
|
else Result := Round(X / Total * Width);
|
|
end;
|
|
|
|
procedure TSpectrumView.SetFMGridStepHz(V: Double);
|
|
begin
|
|
if Abs(FFMGridStepHz - V) > 0.5 then
|
|
begin
|
|
FFMGridStepHz := V;
|
|
FRuler.FMGridStepHz := V;
|
|
InvalidateGridCache;
|
|
FRuler.InvalidateRulerCache;
|
|
end;
|
|
end;
|
|
|
|
procedure TSpectrumView.DrawMarkerLine(C: TCanvas; W, H: Integer);
|
|
var MX: Integer; MarkerFreq: Double; MarkerLbl: string;
|
|
begin
|
|
MX := Round(FMarkerX / 1000.0 * W);
|
|
if (MX < 0) or (MX >= W) then Exit;
|
|
MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz;
|
|
MarkerLbl := FormatFreqSV(Round(MarkerFreq));
|
|
C.Pen.Color := TColor($004444FF);
|
|
C.Pen.Width := 1; C.Pen.Style := psSolid;
|
|
C.MoveTo(MX, 0); C.LineTo(MX, H);
|
|
C.Font.Color := TColor($004444FF);
|
|
C.Font.Size := 7; C.Font.Name := 'Courier New';
|
|
if MX + 4 + C.TextWidth(MarkerLbl) < W then
|
|
C.TextOut(MX + 4, 4, MarkerLbl)
|
|
else
|
|
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
|
|
end;
|
|
|
|
procedure TSpectrumView.SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
|
begin
|
|
FBeaconMarkActive := Active;
|
|
FBeaconRefFreq := RefHz;
|
|
FBeaconTrkFreq := TrkHz;
|
|
end;
|
|
|
|
procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
|
// Две вертикали: опорная частота маяка (зелёная пунктир) и отслеживаемый
|
|
// центроид (оранжевая сплошная). Расхождение видно глазом → понятно, сел ли
|
|
// контур на пик маяка или на шум.
|
|
procedure Vline(FreqHz: Double; Col: TColor; Style: TPenStyle);
|
|
var x: Integer;
|
|
begin
|
|
if FSpanHz <= 0 then Exit;
|
|
x := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
|
if (x < 0) or (x >= W) then Exit;
|
|
C.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
|
|
C.MoveTo(x, 0); C.LineTo(x, H);
|
|
end;
|
|
begin
|
|
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
|
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
|
|
C.Pen.Style := psSolid;
|
|
end;
|
|
|
|
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
|
var
|
|
Y, X, InvA: Integer;
|
|
Row: PByte;
|
|
W: Integer;
|
|
begin
|
|
if (FSpectrumBitmap = nil) or (Alpha = 0) then Exit;
|
|
W := FSpectrumBitmap.Width;
|
|
if X1 < 0 then X1 := 0;
|
|
if X2 > W then X2 := W;
|
|
if X2 <= X1 then Exit;
|
|
InvA := 255 - Alpha;
|
|
FSpectrumBitmap.BeginUpdate(False);
|
|
try
|
|
for Y := 0 to H - 1 do
|
|
begin
|
|
Row := PByte(FSpectrumBitmap.ScanLine[Y]);
|
|
if Row = nil then Continue;
|
|
Inc(Row, X1 * 4);
|
|
for X := X1 to X2 - 1 do
|
|
begin
|
|
{$IFDEF DARWIN}
|
|
Row[1] := Byte((Alpha * R + InvA * Row[1]) div 255);
|
|
Row[2] := Byte((Alpha * G + InvA * Row[2]) div 255);
|
|
Row[3] := Byte((Alpha * B + InvA * Row[3]) div 255);
|
|
{$ELSE}
|
|
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);
|
|
{$ENDIF}
|
|
Inc(Row, 4);
|
|
end;
|
|
end;
|
|
finally
|
|
FSpectrumBitmap.EndUpdate(False);
|
|
end;
|
|
end;
|
|
|
|
procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer);
|
|
var
|
|
Y: Integer;
|
|
Src, Dst: PByte;
|
|
begin
|
|
if (FSpectrumBitmap = nil) or (FGridBitmap = nil) then Exit;
|
|
if (W <= 0) or (H <= 0) then Exit;
|
|
{$IFDEF DARWIN}
|
|
// On macOS/Cocoa, Canvas writes into a CGBitmapContext; ScanLine reads the
|
|
// raw backing buffer which is not synced until BeginUpdate is called on the
|
|
// source bitmap. Canvas.Draw goes through CoreGraphics and sees the live
|
|
// CGContext, so use it instead of the direct ScanLine copy.
|
|
FSpectrumBitmap.Canvas.Draw(0, 0, FGridBitmap);
|
|
{$ELSE}
|
|
FSpectrumBitmap.BeginUpdate(False);
|
|
try
|
|
for Y := 0 to H - 1 do
|
|
begin
|
|
Src := PByte(FGridBitmap.ScanLine[Y]);
|
|
Dst := PByte(FSpectrumBitmap.ScanLine[Y]);
|
|
if (Src <> nil) and (Dst <> nil) then
|
|
Move(Src^, Dst^, W * 4);
|
|
end;
|
|
finally
|
|
FSpectrumBitmap.EndUpdate(False);
|
|
end;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
procedure TSpectrumView.DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
|
|
begin
|
|
if not FADCOverloadVisible then Exit;
|
|
DrawAlertOverlay(FSpectrumBitmap, C, W, H, 'ADC OVERLOAD', 'Input clipping detected');
|
|
end;
|
|
|
|
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
|
out X1, X2, VfoX: Integer);
|
|
var
|
|
Lo_Hz, Hi_Hz, Half: Double;
|
|
begin
|
|
Half := FFilterBW / 2;
|
|
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
|
case FMode of
|
|
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
|
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
|
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
|
end;
|
|
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
|
X2 := Max(0, Min(W - 1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
|
end;
|
|
|
|
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
|
var
|
|
Row, Col, Alpha, YMin, Off: Integer;
|
|
RowPtr: PByte;
|
|
GradB, GradG, GradR: Byte;
|
|
begin
|
|
if FSpectrumBitmap = nil then Exit;
|
|
if (W <= 0) or (H <= 0) then Exit;
|
|
YMin := H;
|
|
for Col := 0 to W - 1 do
|
|
if SpPts[Col].Y < YMin then YMin := SpPts[Col].Y;
|
|
if YMin >= H then Exit;
|
|
|
|
FSpectrumBitmap.BeginUpdate(False);
|
|
for Row := YMin to H - 1 do
|
|
begin
|
|
Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200);
|
|
if Alpha <= 0 then Continue;
|
|
if Alpha > 200 then Alpha := 200;
|
|
GradB := Byte(FTheme.SpecGradB * Alpha div 200);
|
|
GradG := Byte(FTheme.SpecGradG * Alpha div 200);
|
|
GradR := Byte(FTheme.SpecGradR * Alpha div 200);
|
|
RowPtr := PByte(FSpectrumBitmap.ScanLine[Row]);
|
|
for Col := 0 to W - 1 do
|
|
if SpPts[Col].Y <= Row then
|
|
begin
|
|
Off := Col * 4;
|
|
{$IFDEF DARWIN}
|
|
RowPtr[Off+1] := (GradR * Alpha + RowPtr[Off+1] * (200 - Alpha)) div 200;
|
|
RowPtr[Off+2] := (GradG * Alpha + RowPtr[Off+2] * (200 - Alpha)) div 200;
|
|
RowPtr[Off+3] := (GradB * Alpha + RowPtr[Off+3] * (200 - Alpha)) div 200;
|
|
{$ELSE}
|
|
RowPtr[Off] := (GradB * Alpha + RowPtr[Off] * (200 - Alpha)) div 200;
|
|
RowPtr[Off+1] := (GradG * Alpha + RowPtr[Off+1] * (200 - Alpha)) div 200;
|
|
RowPtr[Off+2] := (GradR * Alpha + RowPtr[Off+2] * (200 - Alpha)) div 200;
|
|
{$ENDIF}
|
|
end;
|
|
end;
|
|
FSpectrumBitmap.EndUpdate(False);
|
|
end;
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Публичные методы данных
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
procedure TSpectrumView.SetSpectrumData(const Pixels: array of Single; Count: Integer);
|
|
var i, N: Integer;
|
|
begin
|
|
N := Min(Count, 1024);
|
|
for i := 0 to N - 1 do FSpectrumBuf[i] := Pixels[i];
|
|
FSpectrumBufCount := N;
|
|
FSpectrumDirty := True;
|
|
end;
|
|
|
|
procedure TSpectrumView.SetWaterfallData(const Pixels: array of Single; Count: Integer);
|
|
begin
|
|
FWaterfall.SetWaterfallData(Pixels, Count);
|
|
end;
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Размеры bitmap
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
procedure TSpectrumView.SetSpectrumBitmapSize(W, H: Integer);
|
|
begin
|
|
if (W <= 0) or (H <= 0) then Exit;
|
|
FSpectrumBitmap.SetSize(W, H);
|
|
FSpectrumBitmap.Canvas.Brush.Color := FTheme.BG;
|
|
FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, W, H));
|
|
InvalidateGridCache;
|
|
FSpPtsLen := 0;
|
|
end;
|
|
|
|
procedure TSpectrumView.SetWaterfallBitmapSize(W, H: Integer);
|
|
begin
|
|
FWaterfall.SetWaterfallBitmapSize(W, H);
|
|
end;
|
|
|
|
procedure TSpectrumView.SetRulerSize(W, H: Integer);
|
|
begin
|
|
FRuler.SetRulerSize(W, H);
|
|
end;
|
|
|
|
function TSpectrumView.SpectrumBitmapWidth: Integer;
|
|
begin
|
|
Result := FSpectrumBitmap.Width;
|
|
end;
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Утилиты
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
procedure TSpectrumView.InvalidateGridCache;
|
|
begin
|
|
FGridBitmapW := 0; FGridBitmapH := 0;
|
|
FSpectrumDirty := True;
|
|
end;
|
|
|
|
procedure TSpectrumView.InvalidateOverlayCache;
|
|
begin
|
|
FSpectrumDirty := True;
|
|
end;
|
|
|
|
procedure TSpectrumView.SetTheme(const T: TAppTheme);
|
|
begin
|
|
FTheme := T;
|
|
FLightTheme := T.BG > TColor($00808080);
|
|
InvalidateGridCache;
|
|
FWaterfall.SetTheme(T);
|
|
FSMeter.SetTheme(T);
|
|
FRuler.SetTheme(T);
|
|
end;
|
|
|
|
procedure TSpectrumView.InvalidateRulerCache;
|
|
begin
|
|
FRuler.InvalidateRulerCache;
|
|
end;
|
|
|
|
procedure TSpectrumView.ResetSpectrumBuf;
|
|
var i: Integer;
|
|
begin
|
|
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
|
|
FWaterfall.ResetWfBuf;
|
|
end;
|
|
|
|
procedure TSpectrumView.ResetWfAvgBuf;
|
|
begin
|
|
FWaterfall.ResetWfAvgBuf;
|
|
end;
|
|
|
|
procedure TSpectrumView.FillDemoSpectrum;
|
|
var i: Integer; FreqOff, Noise, Sig: Double; W: Integer;
|
|
begin
|
|
W := FSpectrumBitmap.Width;
|
|
if W <= 0 then W := 1024;
|
|
for i := 0 to W - 1 do
|
|
begin
|
|
if W > 1 then FreqOff := (i / (W - 1) - 0.5) * FSpanHz
|
|
else FreqOff := 0;
|
|
Noise := -110 + (Random - 0.5) * 6;
|
|
if Abs(FreqOff) < 2000 then Sig := -50 - Abs(FreqOff) / 200
|
|
else Sig := -999;
|
|
FSpectrumBuf[i mod 1024] := Max(Noise, Sig);
|
|
end;
|
|
end;
|
|
|
|
function TSpectrumView.NeedsRulerRedraw: Boolean;
|
|
begin
|
|
Result := FRuler.NeedsRulerRedraw;
|
|
end;
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// DrawSpectrum
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
procedure TSpectrumView.DrawSpectrum;
|
|
var
|
|
C, GC: TCanvas;
|
|
i, Yp, W, H, GX: Integer;
|
|
{$IFNDEF DARWIN}
|
|
RowLW: PLongWord;
|
|
{$ENDIF}
|
|
DBmin, DBmax, dB: Double;
|
|
VfoX, X1, X2: Integer;
|
|
TXVfoX, TXX1, TXX2: Integer;
|
|
AGCy, AGCHangY: Integer;
|
|
SrcF, Frac, dBv: Double;
|
|
S0, S1: Integer;
|
|
InvRange: Double;
|
|
LabelBandW, SrcCount: Integer;
|
|
GridFreqS, GridLine, pixPerStep: Double;
|
|
N, gridMult: Integer;
|
|
begin
|
|
if FSpectrumBitmap = nil then Exit;
|
|
W := FSpectrumBitmap.Width; H := FSpectrumBitmap.Height;
|
|
if (W <= 0) or (H <= 0) then Exit;
|
|
C := FSpectrumBitmap.Canvas;
|
|
DBmax := FSpecRefLevel;
|
|
DBmin := FSpecRefLevel - FSpecRange;
|
|
InvRange := 1.0 / (DBmax - DBmin);
|
|
|
|
// ── 1. Фон+сетка из кэша ──────────────────────────────────────────────────
|
|
if (FGridBitmapW <> W) or (FGridBitmapH <> H) or
|
|
((FFMGridStepHz > 0) and
|
|
((Abs(FCenterFreq - FFMGridLastCenter) > 0.5) or
|
|
(FFMGridStepHz <> FFMGridLastStepHz) or
|
|
(Abs(FSpanHz - FFMGridLastSpan) > 1.0))) then
|
|
begin
|
|
FGridBitmapW := W; FGridBitmapH := H;
|
|
FFMGridLastCenter := FCenterFreq;
|
|
FFMGridLastStepHz := FFMGridStepHz;
|
|
FFMGridLastSpan := FSpanHz;
|
|
FGridBitmap.SetSize(W, H);
|
|
GC := FGridBitmap.Canvas;
|
|
PaintVerticalGradient(GC, W, H, FTheme.SpecGradTop, FTheme.SpecGradBot);
|
|
LabelBandW := 28;
|
|
GC.Pen.Style := psClear; GC.Brush.Style := bsSolid;
|
|
GC.Brush.Color := FTheme.SpecLabelBand;
|
|
GC.FillRect(Rect(0, 0, LabelBandW, H));
|
|
GC.Pen.Style := psSolid;
|
|
GC.Font.Size := 7; GC.Font.Name := 'Courier New';
|
|
GC.Font.Color := FTheme.SpecLabelText;
|
|
GC.Brush.Style := bsClear;
|
|
dB := DBmax - FSpecGridStep;
|
|
while dB >= DBmin do
|
|
begin
|
|
Yp := Round((DBmax - dB) * InvRange * H);
|
|
GC.Pen.Color := FTheme.SpecGrid; GC.Pen.Width := 1;
|
|
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
|
|
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
|
|
dB := dB - FSpecGridStep;
|
|
end;
|
|
if FFMGridStepHz > 0 then
|
|
begin
|
|
GridFreqS := FCenterFreq - FSpanHz / 2;
|
|
pixPerStep := W * FFMGridStepHz / FSpanHz;
|
|
if pixPerStep >= 1.0 then
|
|
gridMult := Max(1, Ceil(4.0 / pixPerStep))
|
|
else
|
|
gridMult := 0;
|
|
if gridMult > 0 then
|
|
begin
|
|
N := Ceil(GridFreqS / FFMGridStepHz);
|
|
GridLine := N * FFMGridStepHz;
|
|
while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do
|
|
begin
|
|
if (N mod gridMult) = 0 then
|
|
begin
|
|
GX := Round((GridLine - GridFreqS) / FSpanHz * W);
|
|
if (GX >= 0) and (GX < W) then
|
|
begin
|
|
GC.Pen.Color := FTheme.SpecGrid;
|
|
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
|
|
end;
|
|
end;
|
|
Inc(N);
|
|
GridLine := GridLine + FFMGridStepHz;
|
|
end;
|
|
end;
|
|
end else
|
|
begin
|
|
for i := 0 to 8 do
|
|
begin
|
|
GX := ScaleX(i, 8, W);
|
|
GC.Pen.Color := FTheme.SpecGrid;
|
|
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
|
|
end;
|
|
end;
|
|
end;
|
|
CopyGridToSpectrum(W, H);
|
|
|
|
// ── 2. Полоса фильтра ─────────────────────────────────────────────────────
|
|
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
|
|
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
|
|
if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100);
|
|
end else
|
|
begin
|
|
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;
|
|
end;
|
|
|
|
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
|
if FWDSPReady then
|
|
begin
|
|
AGCy := Round((DBmax - FAGCThresh) * InvRange * H);
|
|
if (AGCy >= 0) and (AGCy < H) then
|
|
begin
|
|
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
|
|
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
|
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
|
|
C.TextOut(4, AGCy - 9, 'AGC T');
|
|
end;
|
|
AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H);
|
|
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
|
|
begin
|
|
C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot;
|
|
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
|
|
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6;
|
|
C.TextOut(4, AGCHangY + 2, 'AGC H');
|
|
end;
|
|
end else begin
|
|
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
|
|
if (AGCy >= 0) and (AGCy < H) then
|
|
begin
|
|
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
|
|
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
|
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
|
|
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
|
|
end;
|
|
end;
|
|
|
|
// ── 4. Кривая спектра ─────────────────────────────────────────────────────
|
|
if FSpPtsLen <> W + 2 then
|
|
begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end;
|
|
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
|
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
|
|
begin
|
|
for i := 0 to W - 1 do
|
|
begin
|
|
SrcF := FCenterFreq - FSpanHz * 0.5 + i * FSpanHz / Max(1, W - 1);
|
|
Frac := SrcF - FTXFreq;
|
|
if (Frac < -FTXSpanHz * 0.5) or (Frac > FTXSpanHz * 0.5) then
|
|
dBv := -200.0
|
|
else
|
|
begin
|
|
SrcF := (Frac + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
|
|
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
|
S1 := Min(S0 + 1, SrcCount - 1);
|
|
Frac := SrcF - S0;
|
|
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
|
end;
|
|
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
|
end;
|
|
end else
|
|
for i := 0 to W - 1 do
|
|
begin
|
|
SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
|
|
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
|
S1 := Min(S0 + 1, SrcCount - 1);
|
|
Frac := SrcF - S0;
|
|
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
|
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
|
end;
|
|
FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H);
|
|
|
|
// ── 5. Градиент (заливка под кривой) ──────────────────────────────────────
|
|
if FFillSpectrum then
|
|
begin
|
|
C.Brush.Style := bsSolid; C.Pen.Style := psClear;
|
|
DrawSpectrumGradient(FSpPts, W, H);
|
|
end;
|
|
|
|
// ── 6. Линия спектра ──────────────────────────────────────────────────────
|
|
C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecLine; C.Pen.Width := 1;
|
|
C.Polyline(Slice(FSpPts, W));
|
|
|
|
// ── 7. Края фильтра + VFO ─────────────────────────────────────────────────
|
|
C.Pen.Color := FTheme.SpecFilterEdge; C.Pen.Width := 1; C.Pen.Style := psSolid;
|
|
C.MoveTo(X1, 0); C.LineTo(X1, H);
|
|
C.MoveTo(X2, 0); C.LineTo(X2, H);
|
|
C.Pen.Color := FTheme.SpecVfoCursor; C.Pen.Width := 2;
|
|
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)]);
|
|
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
|
|
begin
|
|
C.Pen.Color := TColor($002030E0); C.Pen.Width := 1; C.Pen.Style := psSolid;
|
|
C.MoveTo(TXX1, 0); C.LineTo(TXX1, H);
|
|
C.MoveTo(TXX2, 0); C.LineTo(TXX2, H);
|
|
C.Pen.Width := 2;
|
|
C.MoveTo(TXVfoX, 0); C.LineTo(TXVfoX, H - 12);
|
|
C.Brush.Color := TColor($002030E0); C.Brush.Style := bsSolid; C.Pen.Width := 1;
|
|
C.Polygon([Point(TXVfoX-5,0), Point(TXVfoX+5,0), Point(TXVfoX,8)]);
|
|
end;
|
|
|
|
if FMarkerActive then DrawMarkerLine(C, W, H);
|
|
if FBeaconMarkActive then DrawBeaconMarkers(C, W, H);
|
|
DrawADCOverloadOverlay(C, W, H);
|
|
if Assigned(FSampleRateOverlay) then
|
|
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
|
if Assigned(FVfoOverlay) then
|
|
FVfoOverlay.DrawOverlay(FSpectrumBitmap, W, H);
|
|
|
|
// Восстановление непрозрачности. На Cocoa проверено, что проход не нужен
|
|
// (Canvas-операции сохраняют альфу $FF), поэтому там его не делаем.
|
|
// На Qt6/win32 бэкенд другой и сглаженные края могут занулять альфу —
|
|
// оставляем быстрый dword-OR до проверки на этих платформах.
|
|
{$IFNDEF DARWIN}
|
|
FSpectrumBitmap.BeginUpdate(False);
|
|
for i := 0 to H - 1 do
|
|
begin
|
|
RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]);
|
|
if RowLW = nil then Continue;
|
|
for Yp := 0 to W - 1 do
|
|
begin
|
|
RowLW^ := RowLW^ or $FF000000; // BGRA: альфа в байте 3
|
|
Inc(RowLW);
|
|
end;
|
|
end;
|
|
FSpectrumBitmap.EndUpdate(False);
|
|
{$ENDIF}
|
|
end;
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Делегирование к sub-views
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
procedure TSpectrumView.DrawWaterfall;
|
|
begin
|
|
FWaterfall.DrawWaterfall;
|
|
end;
|
|
|
|
procedure TSpectrumView.DrawRuler;
|
|
begin
|
|
FRuler.DrawRuler;
|
|
end;
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Paint-обработчики
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
procedure TSpectrumView.PaintSpectrum(Sender: TObject);
|
|
var PB: TPaintBox;
|
|
begin
|
|
PB := TPaintBox(Sender);
|
|
PB.Canvas.Draw(0, 0, FSpectrumBitmap);
|
|
end;
|
|
|
|
procedure TSpectrumView.PaintWaterfall(Sender: TObject);
|
|
begin
|
|
FWaterfall.PaintWaterfall(Sender);
|
|
end;
|
|
|
|
procedure TSpectrumView.PaintRuler(Sender: TObject);
|
|
begin
|
|
FRuler.PaintRuler(Sender);
|
|
end;
|
|
|
|
procedure TSpectrumView.PaintSMeterRight(Sender: TObject);
|
|
begin
|
|
FSMeter.PaintSMeterRight(Sender);
|
|
end;
|
|
|
|
end.
|