Use WDSP analyzer for wideband

This commit is contained in:
2026-05-26 17:25:07 +03:00
parent e0d49c7bfa
commit f61290ad3d
+130 -87
View File
@@ -4,10 +4,10 @@ unit WidebandView;
WidebandView.pas - raw ADC wideband spectrum pane. WidebandView.pas - raw ADC wideband spectrum pane.
The network layer feeds 16-bit ADC samples collected from Protocol V4 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 wideband packets. This view feeds them to a dedicated WDSP analyzer and
and renders a compact Thetis-style wideband panadapter. CPU paint uses a renders a compact Thetis-style wideband panadapter. CPU paint uses a bitmap;
bitmap; when MainForm creates a TOpenGLControl the same spectrum is rendered when MainForm creates a TOpenGLControl the same spectrum is rendered as GL
as GL primitives. primitives.
} }
{$IFDEF FPC} {$IFDEF FPC}
@@ -18,7 +18,7 @@ interface
uses uses
Classes, SysUtils, Math, Graphics, Controls, ExtCtrls, Classes, SysUtils, Math, Graphics, Controls, ExtCtrls,
OpenGLContext, GL, AppTheme; OpenGLContext, GL, AppTheme, WDSP;
type type
TWidebandView = class TWidebandView = class
@@ -39,13 +39,22 @@ type
FSourceStartHz: Double; FSourceStartHz: Double;
FSourceEndHz: Double; FSourceEndHz: Double;
FMarkerHz: Double; FMarkerHz: Double;
FWDSPAnalyzerOpen: Boolean;
FWDSPAnalyzerConfigured: Boolean;
FWDSPAnalyzerID: Integer;
FWDSPFlp: array[0..0] of Integer;
FWDSPIn: array of Double;
FWDSPPixels: array of Single;
procedure EnsureBitmap(W, H: Integer); procedure EnsureBitmap(W, H: Integer);
procedure ComputeSpectrum(const Samples: array of SmallInt; Count: Integer); function EnsureWDSPAnalyzer: Boolean;
function ComputeSpectrumWDSP(const Samples: array of SmallInt;
Count: Integer): Boolean;
procedure CloseWDSPAnalyzer;
procedure DrawCPU(W, H: Integer); procedure DrawCPU(W, H: Integer);
procedure PaintCPU(C: TCanvas); procedure PaintCPU(C: TCanvas);
procedure PaintGL(C: TOpenGLControl); procedure PaintGL(C: TOpenGLControl);
procedure ColorToGL(AColor: TColor; out R, G, B: GLFloat); procedure ColorToGL(AColor: TColor; out R, G, B: GLFloat);
function BinToMHz(Bin, BinCount: Integer): Double; function SourceFreqToBin(FreqHz: Double; BinCount: Integer): Integer;
function FreqToX(FreqHz: Double; W: Integer): Integer; function FreqToX(FreqHz: Double; W: Integer): Integer;
function GridStepHz(W: Integer): Double; function GridStepHz(W: Integer): Double;
function RulerGridStepHz(C: TCanvas; PlotW: Integer): Double; function RulerGridStepHz(C: TCanvas; PlotW: Integer): Double;
@@ -80,6 +89,10 @@ implementation
const const
WB_DB_SCALE_W = 34; WB_DB_SCALE_W = 34;
WB_WDSP_ID = 32;
WB_WDSP_FFT = 16384;
WB_WDSP_BLOCK = 512;
WB_WDSP_PIXELS = 4096;
constructor TWidebandView.Create; constructor TWidebandView.Create;
begin begin
@@ -98,12 +111,19 @@ begin
FSourceStartHz := 0.0; FSourceStartHz := 0.0;
FSourceEndHz := FSampleRateHz * 0.5; FSourceEndHz := FSampleRateHz * 0.5;
FMarkerHz := 0.0; FMarkerHz := 0.0;
FWDSPAnalyzerOpen := False;
FWDSPAnalyzerConfigured := False;
FWDSPAnalyzerID := WB_WDSP_ID;
FWDSPFlp[0] := 0;
SetLength(FWDSPIn, WB_WDSP_BLOCK * 2);
SetLength(FWDSPPixels, WB_WDSP_PIXELS);
FGLTex := 0; FGLTex := 0;
FDirty := True; FDirty := True;
end; end;
destructor TWidebandView.Destroy; destructor TWidebandView.Destroy;
begin begin
CloseWDSPAnalyzer;
if FGLTex <> 0 then if FGLTex <> 0 then
glDeleteTextures(1, @FGLTex); glDeleteTextures(1, @FGLTex);
FRulerBitmap.Free; FRulerBitmap.Free;
@@ -122,6 +142,7 @@ begin
if AHz > 0 then if AHz > 0 then
begin begin
FSampleRateHz := AHz; FSampleRateHz := AHz;
FWDSPAnalyzerConfigured := False;
SetFrequencyView(0.0, FSampleRateHz * 0.5, 0.0, FSampleRateHz * 0.5); SetFrequencyView(0.0, FSampleRateHz * 0.5, 0.0, FSampleRateHz * 0.5);
end; end;
end; end;
@@ -203,102 +224,111 @@ begin
FRulerBitmap.SetSize(W, H); FRulerBitmap.SetSize(W, H);
end; end;
procedure TWidebandView.ComputeSpectrum(const Samples: array of SmallInt; procedure TWidebandView.CloseWDSPAnalyzer;
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 begin
if Count < 256 then Exit; if FWDSPAnalyzerOpen and Assigned(@DestroyAnalyzer) then
N := 1; DestroyAnalyzer(FWDSPAnalyzerID);
while (N shl 1 <= Count) and (N shl 1 <= 16384) do FWDSPAnalyzerOpen := False;
N := N shl 1; FWDSPAnalyzerConfigured := False;
Half := N div 2; end;
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; function TWidebandView.EnsureWDSPAnalyzer: Boolean;
for I := 1 to N - 2 do var
Success: Integer;
SampleRate: Integer;
AvBackmult: Double;
begin
Result := False;
if (not Assigned(@XCreateAnalyzer)) or (not Assigned(@SetAnalyzer)) or
(not Assigned(@Spectrum0)) or (not Assigned(@GetPixels)) then Exit;
if not FWDSPAnalyzerOpen then
begin begin
K := N shr 1; Success := -1;
while J >= K do XCreateAnalyzer(FWDSPAnalyzerID, @Success, WB_WDSP_FFT, 1, 1, nil);
begin if Success <> 0 then Exit;
Dec(J, K); FWDSPAnalyzerOpen := True;
K := K shr 1; FWDSPAnalyzerConfigured := False;
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; end;
M := 2; if not FWDSPAnalyzerConfigured then
while M <= N do
begin begin
Ang := -2 * Pi / M; SampleRate := Max(1, Round(FSampleRateHz));
Wr := Cos(Ang); AvBackmult := Exp(-1.0 / (15.0 * 0.120)); // Thetis wideband default.
Wi := Sin(Ang); SetAnalyzer(
Step := M div 2; FWDSPAnalyzerID,
K := 0; 2,
while K < N do 1,
begin 1, // Feed like piHPSDR: interleaved I/Q via Spectrum0.
Ur := 1.0; @FWDSPFlp[0],
Ui := 0.0; WB_WDSP_FFT,
for J := 0 to Step - 1 do WB_WDSP_BLOCK,
begin 6, // Thetis wideband default window.
I := K + J; 14.0,
Tr := Ur * RealBuf[I + Step] - Ui * ImagBuf[I + Step]; 0, // no overlap for discontinuous WB packets.
Ti := Ur * ImagBuf[I + Step] + Ui * RealBuf[I + Step]; 0,
RealBuf[I + Step] := RealBuf[I] - Tr; 0.0,
ImagBuf[I + Step] := ImagBuf[I] - Ti; 0.0,
RealBuf[I] := RealBuf[I] + Tr; WB_WDSP_PIXELS,
ImagBuf[I] := ImagBuf[I] + Ti; 1,
Re := Ur * Wr - Ui * Wi; 0,
Ui := Ur * Wi + Ui * Wr; 0.0,
Ur := Re; 0.0,
end; 2 * WB_WDSP_FFT);
Inc(K, M); SetDisplayAverageMode(FWDSPAnalyzerID, 0, AVERAGE_MODE_LOG_RECURSIVE);
end; SetDisplayAvBackmult(FWDSPAnalyzerID, 0, AvBackmult);
M := M shl 1; SetDisplaySampleRate(FWDSPAnalyzerID, SampleRate);
ResetPixelBuffers(FWDSPAnalyzerID);
FWDSPAnalyzerConfigured := True;
end; end;
SetLength(FData, Half); Result := True;
for I := 0 to Half - 1 do end;
function TWidebandView.ComputeSpectrumWDSP(const Samples: array of SmallInt;
Count: Integer): Boolean;
var
Offset, I, Flag: Integer;
MaxPix: Single;
begin
Result := False;
if Count < WB_WDSP_BLOCK then Exit;
if not EnsureWDSPAnalyzer then Exit;
Offset := 0;
while Offset + WB_WDSP_BLOCK <= Count do
begin begin
Mag := Sqr(RealBuf[I]) + Sqr(ImagBuf[I]); for I := 0 to WB_WDSP_BLOCK - 1 do
// Convert FFT bin amplitude to dBFS, then apply Thetis-style display begin
// calibration offset. The previous unnormalised FFT power depended on N FWDSPIn[I * 2] := Samples[Offset + I] / 32768.0;
// and could not line up with the dB scale. FWDSPIn[I * 2 + 1] := 0.0;
FData[I] := 20.0 * Log10((2.0 * Sqrt(Mag) / WinSum) + 1.0E-20) + FCalOffset;
end; end;
Spectrum0(1, FWDSPAnalyzerID, 0, 0, @FWDSPIn[0]);
Inc(Offset, WB_WDSP_BLOCK);
end;
Flag := 0;
GetPixels(FWDSPAnalyzerID, 0, @FWDSPPixels[0], @Flag);
if Flag = 0 then Exit;
MaxPix := -1.0E30;
for I := 0 to WB_WDSP_PIXELS - 1 do
if FWDSPPixels[I] > MaxPix then
MaxPix := FWDSPPixels[I];
if MaxPix < -250.0 then Exit;
SetLength(FData, WB_WDSP_PIXELS);
for I := 0 to WB_WDSP_PIXELS - 1 do
FData[I] := FWDSPPixels[I] + FCalOffset;
Result := True;
end; end;
procedure TWidebandView.SetSamples(const Samples: array of SmallInt; Count: Integer); procedure TWidebandView.SetSamples(const Samples: array of SmallInt; Count: Integer);
begin begin
ComputeSpectrum(Samples, Count); if ComputeSpectrumWDSP(Samples, Count) then
FDirty := True; FDirty := True;
end; 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; function TWidebandView.FreqToX(FreqHz: Double; W: Integer): Integer;
var var
SpanHz: Double; SpanHz: Double;
@@ -308,6 +338,19 @@ begin
Result := Round((FreqHz - FViewStartHz) / SpanHz * Max(1, W - 1)); Result := Round((FreqHz - FViewStartHz) / SpanHz * Max(1, W - 1));
end; end;
function TWidebandView.SourceFreqToBin(FreqHz: Double; BinCount: Integer): Integer;
var
Fs: Double;
begin
if BinCount <= 1 then Exit(0);
Fs := FSampleRateHz;
if Fs <= 0.0 then Fs := 122880000.0;
// We feed WDSP through Spectrum0 as a complex stream with Q=0. GetPixels is
// then laid out as -Fs/2..+Fs/2, so raw ADC frequencies 0..Fs/2 live in the
// right half of the pixel array. Do not map 0..Nyquist across the whole array.
Result := EnsureRange(Round((0.5 + FreqHz / Fs) * (BinCount - 1)), 0, BinCount - 1);
end;
function TWidebandView.GridStepHz(W: Integer): Double; function TWidebandView.GridStepHz(W: Integer): Double;
var var
PixPerMHz, SpanMHz: Double; PixPerMHz, SpanMHz: Double;
@@ -536,7 +579,7 @@ begin
DB := -200.0 DB := -200.0
else else
begin begin
Bin := EnsureRange(Round(SrcHz / Nyq * (N - 1)), 0, N - 1); Bin := SourceFreqToBin(SrcHz, N);
DB := FData[Bin]; DB := FData[Bin];
end; end;
Y := Round((FRefLevel - DB) * InvRange * TopH); Y := Round((FRefLevel - DB) * InvRange * TopH);