mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Use WDSP analyzer for wideband
This commit is contained in:
+131
-88
@@ -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,100 +224,109 @@ 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;
|
||||||
|
Spectrum0(1, FWDSPAnalyzerID, 0, 0, @FWDSPIn[0]);
|
||||||
|
Inc(Offset, WB_WDSP_BLOCK);
|
||||||
end;
|
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;
|
|
||||||
|
|
||||||
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;
|
end;
|
||||||
|
|
||||||
function TWidebandView.FreqToX(FreqHz: Double; W: Integer): Integer;
|
function TWidebandView.FreqToX(FreqHz: Double; W: Integer): Integer;
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user