mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix fft, waterfall
This commit is contained in:
+430
-108
@@ -26,7 +26,7 @@ uses
|
||||
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
|
||||
LCLIntf, LCLType, GraphType,
|
||||
HPSDRProtocol, HPSDRNetwork,
|
||||
WDSPEngine, AudioOutput, AudioInput,
|
||||
WDSP, WDSPEngine, AudioOutput, AudioInput,
|
||||
IntfGraphics, FPImage,
|
||||
Settings,
|
||||
WebServer;
|
||||
@@ -45,6 +45,7 @@ const
|
||||
CLR_METER_ON = TColor($0000CC44);
|
||||
CLR_METER_OVR= TColor($000000CC);
|
||||
CLR_SPECTRUM = TColor($0044FF44);
|
||||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||||
|
||||
BAND_COUNT = 11;
|
||||
MODE_COUNT = 8;
|
||||
@@ -229,11 +230,15 @@ type
|
||||
|
||||
// ---- Spectrum ----
|
||||
FSpectrumBuf: array[0..1023] of Single;
|
||||
FWfAvgBuf: array[0..1023] of Single;
|
||||
FWaterfallBuf: array[0..1023] of Single; // WDSP waterfall pixout=1 data
|
||||
FWaterfallBuf: array[0..1023] of Single; // текущая строка водопада после локального сглаживания
|
||||
FSpectrumBufCount: Integer;
|
||||
FWaterfallBufCount: Integer;
|
||||
FSpectrumBitmap: TBitmap;
|
||||
FWfAGCEnabled: Boolean;
|
||||
FWfNFEnabled: Boolean;
|
||||
FWfManualHigh: Double;
|
||||
FWfManualLow: Double;
|
||||
FWfAGCOffset: Double;
|
||||
FWfHigh: Double;
|
||||
FWfLow: Double;
|
||||
// Spectrum grid settings
|
||||
@@ -242,13 +247,16 @@ type
|
||||
FSpecGridStep: Double; // grid step in dB (e.g. 10)
|
||||
FWaterfallBitmap:TBitmap;
|
||||
FWaterfallTemp: TBitmap;
|
||||
FWaterfallDirty: Boolean;
|
||||
FWaterfallFrameInterval: Integer;
|
||||
FWaterfallFrameCounter: Integer;
|
||||
FSpectrumWidth: Integer;
|
||||
FSpectrumHeight: Integer;
|
||||
FWaterfallHeight:Integer;
|
||||
// ---- Visibility settings ----
|
||||
FShowSpectrum: Boolean;
|
||||
FShowWaterfall: Boolean;
|
||||
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30)
|
||||
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (1..100)
|
||||
// ---- Splitter между спектром и водопадом ----
|
||||
FSplitter: TPanel; // визуальная полоса-разделитель
|
||||
FSplitterDrag: Boolean; // True = идёт перетаскивание
|
||||
@@ -403,6 +411,7 @@ type
|
||||
procedure ResetSpectrumBuf;
|
||||
procedure ResizeSpectrumPanels;
|
||||
procedure RecreateDSPEngine(ASampleRate: Integer);
|
||||
function EnsureWDSPWisdom: Boolean;
|
||||
|
||||
// Network callbacks
|
||||
procedure OnDeviceFound(const Dev: THPSDRDevice);
|
||||
@@ -498,6 +507,8 @@ type
|
||||
procedure WebOnFreqB(Hz: Double);
|
||||
procedure WebOnActiveVfo(Idx: Integer);
|
||||
procedure WebOnCenter(Hz: Double);
|
||||
procedure WebOnMOX(On_: Boolean);
|
||||
procedure WebOnDrive(V: Integer);
|
||||
// Synchronize-обёртки (выполняются в UI-потоке)
|
||||
procedure SyncWebFreq;
|
||||
procedure SyncWebMode;
|
||||
@@ -519,6 +530,8 @@ type
|
||||
procedure SyncWebFreqB;
|
||||
procedure SyncWebActiveVfo;
|
||||
procedure SyncWebCenter;
|
||||
procedure SyncWebMOX;
|
||||
procedure SyncWebDrive;
|
||||
procedure BtnWfAGCClick(Sender: TObject);
|
||||
procedure BtnWfNFClick(Sender: TObject);
|
||||
procedure BtnHidePanelClick(Sender: TObject);
|
||||
@@ -555,10 +568,10 @@ type
|
||||
|
||||
public
|
||||
// Методы для SettingsForm — немедленное применение настроек
|
||||
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
|
||||
SpecAvgCount: Integer; SpecBackmult: Double);
|
||||
procedure ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
|
||||
WfBackmult: Double);
|
||||
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||||
SpecAvgTimeMS: Double);
|
||||
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||||
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
||||
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||||
@@ -575,6 +588,29 @@ type
|
||||
var Handled: Boolean);
|
||||
end;
|
||||
|
||||
TWisdomBuildThread = class(TThread)
|
||||
private
|
||||
FDirectory: string;
|
||||
FError: string;
|
||||
protected
|
||||
procedure Execute; override;
|
||||
public
|
||||
constructor Create(const ADirectory: string);
|
||||
property ErrorText: string read FError;
|
||||
end;
|
||||
|
||||
TWisdomProgressDialog = class(TForm)
|
||||
private
|
||||
FInfoLabel: TLabel;
|
||||
FProgress: TProgressBar;
|
||||
FTimer: TTimer;
|
||||
FThread: TWisdomBuildThread;
|
||||
FPhase: Integer;
|
||||
procedure TimerTick(Sender: TObject);
|
||||
public
|
||||
constructor Create(AOwner: TComponent; AThread: TWisdomBuildThread); reintroduce;
|
||||
end;
|
||||
|
||||
var
|
||||
MainForm: TMainForm;
|
||||
|
||||
@@ -584,6 +620,123 @@ uses SettingsForm;
|
||||
|
||||
{$R *.lfm}
|
||||
|
||||
function WaterfallColorThetis(Level: Integer): LongWord;
|
||||
const
|
||||
STOPS: array[0..5] of LongWord = (
|
||||
$000000, // black
|
||||
$0A1A48, // deep blue
|
||||
$0088CC, // cyan
|
||||
$E0C020, // yellow
|
||||
$D84A12, // orange-red
|
||||
$FFFFFF // white
|
||||
);
|
||||
var
|
||||
Seg, Base: Integer;
|
||||
T: Double;
|
||||
C0, C1: LongWord;
|
||||
R, G, B: 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 := (R shl 16) or (G shl 8) or B;
|
||||
end;
|
||||
|
||||
constructor TWisdomBuildThread.Create(const ADirectory: string);
|
||||
begin
|
||||
inherited Create(False);
|
||||
FreeOnTerminate := False;
|
||||
FDirectory := ADirectory;
|
||||
FError := '';
|
||||
end;
|
||||
|
||||
procedure TWisdomBuildThread.Execute;
|
||||
var
|
||||
DirA: AnsiString;
|
||||
begin
|
||||
try
|
||||
if not Assigned(@WDSPwisdom) then Exit;
|
||||
DirA := AnsiString(FDirectory);
|
||||
WDSPwisdom(PAnsiChar(DirA));
|
||||
except
|
||||
on E: Exception do
|
||||
FError := E.ClassName + ': ' + E.Message;
|
||||
end;
|
||||
end;
|
||||
|
||||
constructor TWisdomProgressDialog.Create(AOwner: TComponent;
|
||||
AThread: TWisdomBuildThread);
|
||||
begin
|
||||
inherited CreateNew(AOwner);
|
||||
FThread := AThread;
|
||||
FPhase := 0;
|
||||
|
||||
Caption := 'WDSP Wisdom';
|
||||
Width := 520;
|
||||
Height := 140;
|
||||
Position := poScreenCenter;
|
||||
BorderStyle := bsDialog;
|
||||
BorderIcons := [];
|
||||
Color := CLR_BG;
|
||||
Font.Name := 'Courier New';
|
||||
Font.Size := 9;
|
||||
Font.Color := CLR_TEXT;
|
||||
|
||||
FInfoLabel := TLabel.Create(Self);
|
||||
FInfoLabel.Parent := Self;
|
||||
FInfoLabel.SetBounds(16, 16, 480, 40);
|
||||
FInfoLabel.AutoSize := False;
|
||||
FInfoLabel.WordWrap := True;
|
||||
FInfoLabel.Font.Color := CLR_TEXT;
|
||||
FInfoLabel.Caption :=
|
||||
'Creating FFTW wisdom for WDSP. This is done once and may take a while on the first run.';
|
||||
|
||||
FProgress := TProgressBar.Create(Self);
|
||||
FProgress.Parent := Self;
|
||||
FProgress.SetBounds(16, 72, 480, 22);
|
||||
FProgress.Min := 0;
|
||||
FProgress.Max := 100;
|
||||
FProgress.Position := 0;
|
||||
|
||||
FTimer := TTimer.Create(Self);
|
||||
FTimer.Interval := 250;
|
||||
FTimer.OnTimer := TimerTick;
|
||||
FTimer.Enabled := True;
|
||||
end;
|
||||
|
||||
procedure TWisdomProgressDialog.TimerTick(Sender: TObject);
|
||||
var
|
||||
P: PAnsiChar;
|
||||
S: string;
|
||||
begin
|
||||
FPhase := (FPhase + 7) mod 101;
|
||||
FProgress.Position := FPhase;
|
||||
|
||||
if Assigned(@wisdom_get_status) then
|
||||
begin
|
||||
P := wisdom_get_status;
|
||||
if P <> nil then
|
||||
begin
|
||||
S := Trim(string(AnsiString(P)));
|
||||
if S <> '' then
|
||||
FInfoLabel.Caption := S;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Assigned(FThread) and FThread.Finished then
|
||||
begin
|
||||
FTimer.Enabled := False;
|
||||
ModalResult := mrOk;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Общая функция декодирования типа платы — используется везде
|
||||
// ===========================================================================
|
||||
@@ -713,26 +866,25 @@ begin
|
||||
Result.WindowType := FDSPEngine.WindowType;
|
||||
Result.SpecDetector := FDSPEngine.SpecDetector;
|
||||
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
|
||||
Result.SpecAvgCount := FDSPEngine.SpecAvgCount;
|
||||
Result.SpecBackmult := FDSPEngine.SpecBackmult;
|
||||
Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
|
||||
Result.WfDetector := FDSPEngine.WfDetector;
|
||||
Result.WfAvgMode := FDSPEngine.WfAvgMode;
|
||||
Result.WfAvgCount := FDSPEngine.WfAvgCount;
|
||||
Result.WfBackmult := FDSPEngine.WfBackmult;
|
||||
Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result.FFTSize := 4096;
|
||||
Result.WindowType := 5;
|
||||
Result.WindowType := 2;
|
||||
Result.SpecDetector := 0;
|
||||
Result.SpecAvgMode := 3;
|
||||
Result.SpecAvgCount := 2;
|
||||
Result.SpecBackmult := 0.45;
|
||||
Result.WfDetector := 2;
|
||||
Result.SpecAvgTimeMS := 30.0;
|
||||
Result.WfDetector := 0;
|
||||
Result.WfAvgMode := 3;
|
||||
Result.WfAvgCount := 2;
|
||||
Result.WfBackmult := 0.45;
|
||||
Result.WfAvgTimeMS := 120.0;
|
||||
end;
|
||||
Result.WfManualHigh := FWfManualHigh;
|
||||
Result.WfManualLow := FWfManualLow;
|
||||
Result.WfAGCOffset := FWfAGCOffset;
|
||||
Result.SpecRefLevel := FSpecRefLevel;
|
||||
Result.SpecRange := FSpecRange;
|
||||
Result.SpecGridStep := FSpecGridStep;
|
||||
@@ -763,9 +915,9 @@ begin
|
||||
// --- Кнопки диапазонов ---
|
||||
for i := 0 to BAND_COUNT - 1 do
|
||||
StyleButton(BtnBand[i], i = BandIdx);
|
||||
// Сбрасываем waterfall AGC/NF при смене диапазона
|
||||
FWfHigh := -50.0;
|
||||
FWfLow := -120.0;
|
||||
// Сбрасываем waterfall thresholds при смене диапазона
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
// Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон
|
||||
ResetWfAvgBuf;
|
||||
|
||||
@@ -859,12 +1011,17 @@ begin
|
||||
FSampleRate := 192000;
|
||||
FCurrentBand := 5; // 20m по умолчанию
|
||||
// Waterfall normalization
|
||||
FWfHigh := -50.0; // начальная верхняя граница dBm
|
||||
// WF avg buffer — инициализируем значением шумового пола
|
||||
// FWfAvgBuf инициализируется в ResetSpectrumBuf
|
||||
FWfLow := -120.0; // начальная нижняя граница dBm
|
||||
FWfAGCEnabled := False;
|
||||
FWfManualHigh := -80.0;
|
||||
FWfManualLow := -130.0;
|
||||
FWfAGCOffset := 0.0;
|
||||
FWfHigh := FWfManualHigh; // начальная верхняя граница dBm
|
||||
FWfLow := FWfManualLow; // начальная нижняя граница dBm
|
||||
FWfAGCEnabled := True;
|
||||
FWfNFEnabled := False;
|
||||
FSpectrumBufCount := 1024;
|
||||
FWaterfallBufCount := 1024;
|
||||
FWaterfallFrameInterval := 2;
|
||||
FWaterfallFrameCounter := 0;
|
||||
FDevConnected := False;
|
||||
// Spectrum grid settings defaults
|
||||
FSpecRefLevel := -20.0;
|
||||
@@ -920,6 +1077,8 @@ begin
|
||||
FWebServer.OnFreqB := WebOnFreqB;
|
||||
FWebServer.OnActiveVfo := WebOnActiveVfo;
|
||||
FWebServer.OnCenter := WebOnCenter;
|
||||
FWebServer.OnMOX := WebOnMOX;
|
||||
FWebServer.OnDrive := WebOnDrive;
|
||||
FWebServer.Start;
|
||||
FSMeterPeak := -130;
|
||||
FSMeterMin := -130;
|
||||
@@ -940,6 +1099,7 @@ begin
|
||||
FShowSpectrum := True;
|
||||
FShowWaterfall := True;
|
||||
FDisplayFPS := 20;
|
||||
FWaterfallDirty := True;
|
||||
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
|
||||
FSplitterDrag := False;
|
||||
|
||||
@@ -2020,21 +2180,20 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.ResetWfAvgBuf;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
|
||||
FWfHigh := -50.0;
|
||||
FWfLow := -120.0;
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ResetSpectrumBuf;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
|
||||
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
|
||||
for i := 0 to 1023 do FWaterfallBuf[i] := -120.0;
|
||||
FWfHigh := -50.0;
|
||||
FWfLow := -120.0;
|
||||
for i := 0 to 1023 do FWaterfallBuf[i] := -130.0;
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.InvalidateGridCache;
|
||||
@@ -2253,6 +2412,7 @@ var
|
||||
S0, S1: Integer;
|
||||
InvRange: Double;
|
||||
LabelBandW: Integer;
|
||||
SrcCount: Integer;
|
||||
begin
|
||||
if FSpectrumBitmap = nil then Exit;
|
||||
W := FSpectrumBitmap.Width;
|
||||
@@ -2359,11 +2519,12 @@ begin
|
||||
SetLength(FSpPts, W + 2);
|
||||
FSpPtsLen := W + 2;
|
||||
end;
|
||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
||||
for i := 0 to W - 1 do
|
||||
begin
|
||||
SrcF := i * 1023.0 / Max(1, W - 1);
|
||||
S0 := Min(Trunc(SrcF), 1023);
|
||||
S1 := Min(S0 + 1, 1023);
|
||||
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))));
|
||||
@@ -2409,10 +2570,17 @@ var
|
||||
dB, frac: Double;
|
||||
WatSrcF: Double;
|
||||
WatS0, WatS1, V: Integer;
|
||||
AvgDB: Double;
|
||||
PeakDB: Double;
|
||||
NFSum: Double;
|
||||
NFCount: Integer;
|
||||
TargetLow: Double;
|
||||
TargetHigh: Double;
|
||||
HistMinDB: Double;
|
||||
HistMaxDB: Double;
|
||||
HistStepDB: Double;
|
||||
NoiseFloorDB: Double;
|
||||
SignalTopDB: Double;
|
||||
CumCount: Integer;
|
||||
LowTargetCount: Integer;
|
||||
HighTargetCount: Integer;
|
||||
HistIdx: Integer;
|
||||
WfHigh, WfLow, InvRange: Double;
|
||||
Step: Double;
|
||||
Px: PLongWord;
|
||||
@@ -2422,53 +2590,78 @@ var
|
||||
Src: PLongWord;
|
||||
BPPi: Integer;
|
||||
Pal: LongWord;
|
||||
Hist: array[0..191] of Integer;
|
||||
SrcCount: Integer;
|
||||
const
|
||||
ALPHA_SPEC = 0.25;
|
||||
ALPHA_HIGH = 0.02;
|
||||
ALPHA_LOW = 0.005;
|
||||
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);
|
||||
|
||||
// ── Источник данных водопада ─────────────────────────────────────────────
|
||||
// При работе WDSP: FWaterfallBuf заполнен из pixout=1 (уже усреднённые WDSP данные)
|
||||
// Без WDSP (demo): делаем EMA FSpectrumBuf → FWfAvgBuf
|
||||
PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
|
||||
if FWDSPReady then
|
||||
FillChar(Hist, SizeOf(Hist), 0);
|
||||
HistMinDB := -170.0;
|
||||
HistMaxDB := 22.0;
|
||||
HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist);
|
||||
for X := 0 to SrcCount - 1 do
|
||||
begin
|
||||
// Используем WDSP waterfall данные напрямую
|
||||
for X := 0 to 1023 do
|
||||
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
|
||||
AvgDB := FWaterfallBuf[X];
|
||||
FWfAvgBuf[X] := AvgDB; // синхронизируем для AGC логики
|
||||
if AvgDB > PeakDB then PeakDB := AvgDB;
|
||||
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
|
||||
NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
|
||||
Break;
|
||||
end;
|
||||
end
|
||||
else
|
||||
end;
|
||||
|
||||
CumCount := 0;
|
||||
for HistIdx := 0 to High(Hist) do
|
||||
begin
|
||||
// Fallback: Pascal EMA от демо-спектра
|
||||
for X := 0 to 1023 do
|
||||
CumCount := CumCount + Hist[HistIdx];
|
||||
if CumCount >= HighTargetCount then
|
||||
begin
|
||||
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
|
||||
FWfAvgBuf[X] := AvgDB;
|
||||
if AvgDB > PeakDB then PeakDB := AvgDB;
|
||||
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
|
||||
SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ── AGC High/Low ────────────────────────────────────────────────────────
|
||||
if FWfAGCEnabled then
|
||||
begin
|
||||
if PeakDB > FWfHigh then FWfHigh := PeakDB + 5.0
|
||||
else FWfHigh := FWfHigh + ALPHA_HIGH * ((PeakDB + 5.0) - FWfHigh);
|
||||
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;
|
||||
if FWfNFEnabled and (NFCount > 10) then
|
||||
FWfLow := FWfLow + ALPHA_LOW * ((NFSum / NFCount - 5.0) - FWfLow);
|
||||
WfHigh := FWfHigh; WfLow := FWfLow;
|
||||
if WfHigh < WfLow + 15.0 then WfHigh := WfLow + 15.0;
|
||||
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);
|
||||
@@ -2489,21 +2682,21 @@ begin
|
||||
Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W);
|
||||
|
||||
// ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ────────
|
||||
Step := 1023.0 / Max(1, W - 1);
|
||||
Step := (SrcCount - 1.0) / Max(1, W - 1);
|
||||
WatSrcF := 0.0;
|
||||
Px := @FWfPixels[0];
|
||||
for X := 0 to W - 1 do
|
||||
begin
|
||||
WatS0 := Trunc(WatSrcF);
|
||||
if WatS0 > 1022 then WatS0 := 1022;
|
||||
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
||||
WatS1 := WatS0 + 1;
|
||||
frac := WatSrcF - WatS0;
|
||||
dB := FWfAvgBuf[WatS0] * (1.0 - frac) + FWfAvgBuf[WatS1] * frac;
|
||||
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
||||
V := Trunc((dB - WfLow) * InvRange);
|
||||
if V < 0 then V := 0;
|
||||
if V > 255 then V := 255;
|
||||
// Палитра задана как RGB, а в буфере нужен BGRA.
|
||||
// Меняем местами R/B и ставим alpha=255, чтобы совпадало с web/Thetis-градиентом.
|
||||
// WFALL_PALETTE задана в RGB-логике, а в буфере нужен BGRA.
|
||||
// Поэтому меняем R/B местами и добавляем alpha=255.
|
||||
Pal := WFALL_PALETTE[V];
|
||||
Px^ := ((Pal and $000000FF) shl 16) or
|
||||
(Pal and $0000FF00) or
|
||||
@@ -3084,7 +3277,6 @@ begin
|
||||
|
||||
if FWDSPReady then
|
||||
begin
|
||||
FDSPEngine.UpdateSpectrum;
|
||||
FLastSMeter := FDSPEngine.GetSMeterDBm;
|
||||
FDSPEngine.SetSpectrumWidth(FSpectrumWidth); // актуальная ширина для SetAGC/SetAGCTop
|
||||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||||
@@ -3096,22 +3288,24 @@ begin
|
||||
FVfoOverlay.UpdateVfo(FVfoA);
|
||||
PositionVfoOverlay;
|
||||
end;
|
||||
// Рисуем только при новых спектральных данных/изменении UI (FSpectrumDirty).
|
||||
// Это резко снижает CPU в холостом режиме.
|
||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||
if FSpectrumDirty then
|
||||
begin
|
||||
DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
|
||||
// Водопад рисуем на каждом новом спектральном кадре для плавности.
|
||||
DrawWaterfall;
|
||||
PbWaterfall.Invalidate;
|
||||
|
||||
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
|
||||
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
|
||||
PbRuler.Invalidate;
|
||||
FSpectrumDirty := False;
|
||||
end;
|
||||
if FWaterfallDirty then
|
||||
begin
|
||||
DrawWaterfall;
|
||||
PbWaterfall.Invalidate;
|
||||
FWaterfallDirty := False;
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -3139,15 +3333,20 @@ begin
|
||||
end;
|
||||
|
||||
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
|
||||
if FSpectrumDirty and (not FRunning) then
|
||||
if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then
|
||||
begin
|
||||
DrawSpectrum;
|
||||
DrawWaterfall;
|
||||
PbSpectrum.Invalidate;
|
||||
if FSpectrumDirty then
|
||||
DrawSpectrum;
|
||||
if FWaterfallDirty then
|
||||
DrawWaterfall;
|
||||
if FSpectrumDirty then
|
||||
PbSpectrum.Invalidate;
|
||||
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
|
||||
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
|
||||
PbRuler.Invalidate;
|
||||
PbWaterfall.Invalidate;
|
||||
if FWaterfallDirty then
|
||||
PbWaterfall.Invalidate;
|
||||
FWaterfallDirty := False;
|
||||
FSpectrumDirty := False;
|
||||
end;
|
||||
|
||||
@@ -3155,7 +3354,7 @@ begin
|
||||
if Assigned(FWebServer) then
|
||||
FWebServer.PushSpectrum(
|
||||
FSpectrumBuf, 1024,
|
||||
FWfAvgBuf,
|
||||
FWaterfallBuf,
|
||||
FLastSMeter,
|
||||
FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop,
|
||||
FSpanHz, FVolume,
|
||||
@@ -3165,7 +3364,8 @@ begin
|
||||
FRunning, FMuted, FCTun,
|
||||
BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0,
|
||||
FCenterFreq, FFilter,
|
||||
FVfoB, FActiveVfo);
|
||||
FVfoB, FActiveVfo,
|
||||
FTransmitting, TrkDrive.Position);
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
@@ -3412,6 +3612,67 @@ begin
|
||||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||||
end;
|
||||
|
||||
function TMainForm.EnsureWDSPWisdom: Boolean;
|
||||
var
|
||||
WisdomDir: string;
|
||||
WisdomPath: string;
|
||||
DirA: AnsiString;
|
||||
BuildThread: TWisdomBuildThread;
|
||||
Dlg: TWisdomProgressDialog;
|
||||
begin
|
||||
Result := True;
|
||||
|
||||
if not Assigned(@WDSPwisdom) then Exit;
|
||||
|
||||
WisdomDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
|
||||
WisdomPath := WisdomDir + WDSP_WISDOM_FILE;
|
||||
|
||||
if FileExists(WisdomPath) then
|
||||
begin
|
||||
// По рекомендации WDSP вызываем функцию на каждом старте:
|
||||
// если файл уже есть, библиотека просто импортирует wisdom.
|
||||
DirA := AnsiString(WisdomDir);
|
||||
WDSPwisdom(PAnsiChar(DirA));
|
||||
Exit;
|
||||
end;
|
||||
|
||||
BtnStartStop.Enabled := False;
|
||||
StatusBar1.Panels[3].Text := 'Creating WDSP wisdom...';
|
||||
|
||||
BuildThread := TWisdomBuildThread.Create(WisdomDir);
|
||||
try
|
||||
Dlg := TWisdomProgressDialog.Create(Self, BuildThread);
|
||||
try
|
||||
Dlg.ShowModal;
|
||||
finally
|
||||
Dlg.Free;
|
||||
end;
|
||||
|
||||
BuildThread.WaitFor;
|
||||
|
||||
if BuildThread.ErrorText <> '' then
|
||||
begin
|
||||
ShowMessage('Failed to create WDSP wisdom.' + LineEnding +
|
||||
BuildThread.ErrorText);
|
||||
Result := False;
|
||||
end
|
||||
else if not FileExists(WisdomPath) then
|
||||
begin
|
||||
ShowMessage('WDSP wisdom was not created.' + LineEnding +
|
||||
'Expected file: ' + WisdomPath);
|
||||
Result := False;
|
||||
end;
|
||||
finally
|
||||
BuildThread.Free;
|
||||
BtnStartStop.Enabled := True;
|
||||
end;
|
||||
|
||||
if Result then
|
||||
StatusBar1.Panels[3].Text := 'WDSP wisdom ready'
|
||||
else
|
||||
StatusBar1.Panels[3].Text := 'WDSP wisdom missing';
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
|
||||
begin
|
||||
// Открываем диалог выбора устройства
|
||||
@@ -3595,6 +3856,11 @@ begin
|
||||
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
|
||||
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
|
||||
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
|
||||
FWfManualHigh := G_Settings.WfManualHigh;
|
||||
FWfManualLow := G_Settings.WfManualLow;
|
||||
FWfAGCOffset := G_Settings.WfAGCOffset;
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
FShowSpectrum := G_Settings.ShowSpectrum;
|
||||
FShowWaterfall := G_Settings.ShowWaterfall;
|
||||
if G_Settings.DisplayFPS > 0 then
|
||||
@@ -3610,9 +3876,9 @@ begin
|
||||
begin
|
||||
FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
|
||||
FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode,
|
||||
G_Settings.SpecAvgCount, G_Settings.SpecBackmult);
|
||||
G_Settings.SpecAvgTimeMS);
|
||||
FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
|
||||
G_Settings.WfAvgCount, G_Settings.WfBackmult);
|
||||
G_Settings.WfAvgTimeMS);
|
||||
end;
|
||||
// Аудио устройство вывода
|
||||
FAudioOut.Close;
|
||||
@@ -4555,7 +4821,7 @@ begin
|
||||
FWfAGCEnabled := not FWfAGCEnabled;
|
||||
StyleButton(BtnWfAGC, FWfAGCEnabled);
|
||||
if not FWfAGCEnabled then
|
||||
FWfHigh := -50.0; // сбрасываем к умолчанию при выключении
|
||||
FWfHigh := FWfManualHigh; // сбрасываем к ручному уровню при выключении
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnWfNFClick(Sender: TObject);
|
||||
@@ -4563,7 +4829,7 @@ begin
|
||||
FWfNFEnabled := not FWfNFEnabled;
|
||||
StyleButton(BtnWfNF, FWfNFEnabled);
|
||||
if not FWfNFEnabled then
|
||||
FWfLow := -120.0;
|
||||
FWfLow := FWfManualLow;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnHidePanelClick(Sender: TObject);
|
||||
@@ -5114,11 +5380,41 @@ begin
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.WebOnMOX(On_: Boolean);
|
||||
begin
|
||||
FWebSyncBool := On_;
|
||||
FWebSyncM := SyncWebMOX;
|
||||
TThread.Synchronize(nil, FWebSyncM);
|
||||
end;
|
||||
|
||||
procedure TMainForm.SyncWebMOX;
|
||||
begin
|
||||
ApplyMOX(FWebSyncBool);
|
||||
end;
|
||||
|
||||
procedure TMainForm.WebOnDrive(V: Integer);
|
||||
begin
|
||||
FWebSyncInt := V;
|
||||
FWebSyncM := SyncWebDrive;
|
||||
TThread.Synchronize(nil, FWebSyncM);
|
||||
end;
|
||||
|
||||
procedure TMainForm.SyncWebDrive;
|
||||
begin
|
||||
// Установка Position вызывает TrkDriveChange через OnChange автоматически
|
||||
TrkDrive.Position := FWebSyncInt;
|
||||
end;
|
||||
|
||||
procedure TMainForm.TrkDriveChange(Sender: TObject);
|
||||
begin
|
||||
FDriveLevel := CalcDriveByte;
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
|
||||
if FNetwork.Connected and FNetwork.Running then
|
||||
begin
|
||||
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True);
|
||||
FNetwork.SendFullHP;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMainForm.CalcDriveByte: Byte;
|
||||
@@ -5283,6 +5579,7 @@ begin
|
||||
N := Min(Count, 1024);
|
||||
for i := 0 to N - 1 do
|
||||
FSpectrumBuf[i] := Pixels[i];
|
||||
FSpectrumBufCount := N;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
@@ -5291,31 +5588,47 @@ procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
|
||||
var
|
||||
i, N: Integer;
|
||||
begin
|
||||
// Заполняем буфер водопада из WDSP pixout=1.
|
||||
// Вызывается из DSP-потока — Single-запись атомарна.
|
||||
N := Min(Count, 1024);
|
||||
for i := 0 to N - 1 do
|
||||
FWaterfallBuf[i] := Pixels[i];
|
||||
FWaterfallBufCount := N;
|
||||
Inc(FWaterfallFrameCounter);
|
||||
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
|
||||
begin
|
||||
FWaterfallFrameCounter := 0;
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Settings apply methods (вызываются из TSettingsForm)
|
||||
// ===========================================================================
|
||||
|
||||
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
|
||||
SpecAvgCount: Integer; SpecBackmult: Double);
|
||||
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||||
SpecAvgTimeMS: Double);
|
||||
begin
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS);
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetFFTParams(FFTSize, WinType);
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgCount, SpecBackmult);
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
|
||||
WfBackmult: Double);
|
||||
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||||
begin
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgCount, WfBackmult);
|
||||
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS);
|
||||
FWfManualHigh := WfHigh;
|
||||
FWfManualLow := WfLow;
|
||||
FWfAGCOffset := WfAGCOffset;
|
||||
if not FWfAGCEnabled then
|
||||
begin
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
end;
|
||||
ResetWfAvgBuf;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||
@@ -5375,9 +5688,15 @@ end;
|
||||
procedure TMainForm.ApplyFPS(FPS: Integer);
|
||||
begin
|
||||
if FPS < 1 then FPS := 1;
|
||||
if FPS > 60 then FPS := 60;
|
||||
if FPS > 100 then FPS := 100;
|
||||
FDisplayFPS := FPS;
|
||||
FSpectrumTimer.Interval := 1000 div FPS;
|
||||
// Thetis по умолчанию обновляет водопад через один display frame.
|
||||
FWaterfallFrameInterval := 2;
|
||||
FWaterfallFrameCounter := 0;
|
||||
FWaterfallDirty := True;
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetDisplayFPS(FPS);
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnSettingsClick(Sender: TObject);
|
||||
@@ -5410,18 +5729,19 @@ begin
|
||||
FDSPEngine.WindowType,
|
||||
FDSPEngine.SpecDetector,
|
||||
FDSPEngine.SpecAvgMode,
|
||||
FDSPEngine.SpecAvgCount,
|
||||
FDSPEngine.SpecBackmult,
|
||||
FDSPEngine.SpecAvgTimeMS,
|
||||
FDSPEngine.WfDetector,
|
||||
FDSPEngine.WfAvgMode,
|
||||
FDSPEngine.WfAvgCount,
|
||||
FDSPEngine.WfBackmult,
|
||||
FDSPEngine.WfAvgTimeMS,
|
||||
FWfManualHigh,
|
||||
FWfManualLow,
|
||||
FWfAGCOffset,
|
||||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||||
FAudioOutDevName, FAudioInDevName)
|
||||
else
|
||||
SF.LoadValues(
|
||||
4096, 5, 0, 3, 2, 0.45,
|
||||
2, 3, 2, 0.45,
|
||||
4096, 2, 0, 3, 30.0,
|
||||
0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset,
|
||||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||||
FAudioOutDevName, FAudioInDevName);
|
||||
SF.Show;
|
||||
@@ -5442,6 +5762,8 @@ begin
|
||||
on E: Exception do
|
||||
StatusBar1.Panels[3].Text := 'Audio: ' + E.Message;
|
||||
end;
|
||||
|
||||
EnsureWDSPWisdom;
|
||||
end;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user