fix fft, waterfall

This commit is contained in:
2026-04-22 22:09:42 +03:00
parent ae532a33cd
commit 77d380acfd
7 changed files with 1016 additions and 396 deletions
View File
+427 -105
View File
@@ -26,7 +26,7 @@ uses
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types, StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType, LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork, HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput, AudioInput, WDSP, WDSPEngine, AudioOutput, AudioInput,
IntfGraphics, FPImage, IntfGraphics, FPImage,
Settings, Settings,
WebServer; WebServer;
@@ -45,6 +45,7 @@ const
CLR_METER_ON = TColor($0000CC44); CLR_METER_ON = TColor($0000CC44);
CLR_METER_OVR= TColor($000000CC); CLR_METER_OVR= TColor($000000CC);
CLR_SPECTRUM = TColor($0044FF44); CLR_SPECTRUM = TColor($0044FF44);
WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 11; BAND_COUNT = 11;
MODE_COUNT = 8; MODE_COUNT = 8;
@@ -229,11 +230,15 @@ type
// ---- Spectrum ---- // ---- Spectrum ----
FSpectrumBuf: array[0..1023] of Single; FSpectrumBuf: array[0..1023] of Single;
FWfAvgBuf: array[0..1023] of Single; FWaterfallBuf: array[0..1023] of Single; // текущая строка водопада после локального сглаживания
FWaterfallBuf: array[0..1023] of Single; // WDSP waterfall pixout=1 data FSpectrumBufCount: Integer;
FWaterfallBufCount: Integer;
FSpectrumBitmap: TBitmap; FSpectrumBitmap: TBitmap;
FWfAGCEnabled: Boolean; FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean; FWfNFEnabled: Boolean;
FWfManualHigh: Double;
FWfManualLow: Double;
FWfAGCOffset: Double;
FWfHigh: Double; FWfHigh: Double;
FWfLow: Double; FWfLow: Double;
// Spectrum grid settings // Spectrum grid settings
@@ -242,13 +247,16 @@ type
FSpecGridStep: Double; // grid step in dB (e.g. 10) FSpecGridStep: Double; // grid step in dB (e.g. 10)
FWaterfallBitmap:TBitmap; FWaterfallBitmap:TBitmap;
FWaterfallTemp: TBitmap; FWaterfallTemp: TBitmap;
FWaterfallDirty: Boolean;
FWaterfallFrameInterval: Integer;
FWaterfallFrameCounter: Integer;
FSpectrumWidth: Integer; FSpectrumWidth: Integer;
FSpectrumHeight: Integer; FSpectrumHeight: Integer;
FWaterfallHeight:Integer; FWaterfallHeight:Integer;
// ---- Visibility settings ---- // ---- Visibility settings ----
FShowSpectrum: Boolean; FShowSpectrum: Boolean;
FShowWaterfall: Boolean; FShowWaterfall: Boolean;
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30) FDisplayFPS: Integer; // таймер спектра: кадров в секунду (1..100)
// ---- Splitter между спектром и водопадом ---- // ---- Splitter между спектром и водопадом ----
FSplitter: TPanel; // визуальная полоса-разделитель FSplitter: TPanel; // визуальная полоса-разделитель
FSplitterDrag: Boolean; // True = идёт перетаскивание FSplitterDrag: Boolean; // True = идёт перетаскивание
@@ -403,6 +411,7 @@ type
procedure ResetSpectrumBuf; procedure ResetSpectrumBuf;
procedure ResizeSpectrumPanels; procedure ResizeSpectrumPanels;
procedure RecreateDSPEngine(ASampleRate: Integer); procedure RecreateDSPEngine(ASampleRate: Integer);
function EnsureWDSPWisdom: Boolean;
// Network callbacks // Network callbacks
procedure OnDeviceFound(const Dev: THPSDRDevice); procedure OnDeviceFound(const Dev: THPSDRDevice);
@@ -498,6 +507,8 @@ type
procedure WebOnFreqB(Hz: Double); procedure WebOnFreqB(Hz: Double);
procedure WebOnActiveVfo(Idx: Integer); procedure WebOnActiveVfo(Idx: Integer);
procedure WebOnCenter(Hz: Double); procedure WebOnCenter(Hz: Double);
procedure WebOnMOX(On_: Boolean);
procedure WebOnDrive(V: Integer);
// Synchronize-обёртки (выполняются в UI-потоке) // Synchronize-обёртки (выполняются в UI-потоке)
procedure SyncWebFreq; procedure SyncWebFreq;
procedure SyncWebMode; procedure SyncWebMode;
@@ -519,6 +530,8 @@ type
procedure SyncWebFreqB; procedure SyncWebFreqB;
procedure SyncWebActiveVfo; procedure SyncWebActiveVfo;
procedure SyncWebCenter; procedure SyncWebCenter;
procedure SyncWebMOX;
procedure SyncWebDrive;
procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfAGCClick(Sender: TObject);
procedure BtnWfNFClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject);
procedure BtnHidePanelClick(Sender: TObject); procedure BtnHidePanelClick(Sender: TObject);
@@ -555,10 +568,10 @@ type
public public
// Методы для SettingsForm — немедленное применение настроек // Методы для SettingsForm — немедленное применение настроек
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode, procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgCount: Integer; SpecBackmult: Double); SpecAvgTimeMS: Double);
procedure ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer; procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
WfBackmult: Double); WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
procedure ApplyGridParams(RefLevel, Range, GridStep: Double); procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string); procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string); procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
@@ -575,6 +588,29 @@ type
var Handled: Boolean); var Handled: Boolean);
end; 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 var
MainForm: TMainForm; MainForm: TMainForm;
@@ -584,6 +620,123 @@ uses SettingsForm;
{$R *.lfm} {$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.WindowType := FDSPEngine.WindowType;
Result.SpecDetector := FDSPEngine.SpecDetector; Result.SpecDetector := FDSPEngine.SpecDetector;
Result.SpecAvgMode := FDSPEngine.SpecAvgMode; Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
Result.SpecAvgCount := FDSPEngine.SpecAvgCount; Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
Result.SpecBackmult := FDSPEngine.SpecBackmult;
Result.WfDetector := FDSPEngine.WfDetector; Result.WfDetector := FDSPEngine.WfDetector;
Result.WfAvgMode := FDSPEngine.WfAvgMode; Result.WfAvgMode := FDSPEngine.WfAvgMode;
Result.WfAvgCount := FDSPEngine.WfAvgCount; Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
Result.WfBackmult := FDSPEngine.WfBackmult;
end end
else else
begin begin
Result.FFTSize := 4096; Result.FFTSize := 4096;
Result.WindowType := 5; Result.WindowType := 2;
Result.SpecDetector := 0; Result.SpecDetector := 0;
Result.SpecAvgMode := 3; Result.SpecAvgMode := 3;
Result.SpecAvgCount := 2; Result.SpecAvgTimeMS := 30.0;
Result.SpecBackmult := 0.45; Result.WfDetector := 0;
Result.WfDetector := 2;
Result.WfAvgMode := 3; Result.WfAvgMode := 3;
Result.WfAvgCount := 2; Result.WfAvgTimeMS := 120.0;
Result.WfBackmult := 0.45;
end; end;
Result.WfManualHigh := FWfManualHigh;
Result.WfManualLow := FWfManualLow;
Result.WfAGCOffset := FWfAGCOffset;
Result.SpecRefLevel := FSpecRefLevel; Result.SpecRefLevel := FSpecRefLevel;
Result.SpecRange := FSpecRange; Result.SpecRange := FSpecRange;
Result.SpecGridStep := FSpecGridStep; Result.SpecGridStep := FSpecGridStep;
@@ -763,9 +915,9 @@ begin
// --- Кнопки диапазонов --- // --- Кнопки диапазонов ---
for i := 0 to BAND_COUNT - 1 do for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], i = BandIdx); StyleButton(BtnBand[i], i = BandIdx);
// Сбрасываем waterfall AGC/NF при смене диапазона // Сбрасываем waterfall thresholds при смене диапазона
FWfHigh := -50.0; FWfHigh := FWfManualHigh;
FWfLow := -120.0; FWfLow := FWfManualLow;
// Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон // Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон
ResetWfAvgBuf; ResetWfAvgBuf;
@@ -859,12 +1011,17 @@ begin
FSampleRate := 192000; FSampleRate := 192000;
FCurrentBand := 5; // 20m по умолчанию FCurrentBand := 5; // 20m по умолчанию
// Waterfall normalization // Waterfall normalization
FWfHigh := -50.0; // начальная верхняя граница dBm FWfManualHigh := -80.0;
// WF avg buffer — инициализируем значением шумового пола FWfManualLow := -130.0;
// FWfAvgBuf инициализируется в ResetSpectrumBuf FWfAGCOffset := 0.0;
FWfLow := -120.0; // начальная нижняя граница dBm FWfHigh := FWfManualHigh; // начальная верхняя граница dBm
FWfAGCEnabled := False; FWfLow := FWfManualLow; // начальная нижняя граница dBm
FWfAGCEnabled := True;
FWfNFEnabled := False; FWfNFEnabled := False;
FSpectrumBufCount := 1024;
FWaterfallBufCount := 1024;
FWaterfallFrameInterval := 2;
FWaterfallFrameCounter := 0;
FDevConnected := False; FDevConnected := False;
// Spectrum grid settings defaults // Spectrum grid settings defaults
FSpecRefLevel := -20.0; FSpecRefLevel := -20.0;
@@ -920,6 +1077,8 @@ begin
FWebServer.OnFreqB := WebOnFreqB; FWebServer.OnFreqB := WebOnFreqB;
FWebServer.OnActiveVfo := WebOnActiveVfo; FWebServer.OnActiveVfo := WebOnActiveVfo;
FWebServer.OnCenter := WebOnCenter; FWebServer.OnCenter := WebOnCenter;
FWebServer.OnMOX := WebOnMOX;
FWebServer.OnDrive := WebOnDrive;
FWebServer.Start; FWebServer.Start;
FSMeterPeak := -130; FSMeterPeak := -130;
FSMeterMin := -130; FSMeterMin := -130;
@@ -940,6 +1099,7 @@ begin
FShowSpectrum := True; FShowSpectrum := True;
FShowWaterfall := True; FShowWaterfall := True;
FDisplayFPS := 20; FDisplayFPS := 20;
FWaterfallDirty := True;
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
FSplitterDrag := False; FSplitterDrag := False;
@@ -2020,21 +2180,20 @@ begin
end; end;
procedure TMainForm.ResetWfAvgBuf; procedure TMainForm.ResetWfAvgBuf;
var i: Integer;
begin begin
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0; FWfHigh := FWfManualHigh;
FWfHigh := -50.0; FWfLow := FWfManualLow;
FWfLow := -120.0; FWaterfallDirty := True;
end; end;
procedure TMainForm.ResetSpectrumBuf; procedure TMainForm.ResetSpectrumBuf;
var i: Integer; var i: Integer;
begin begin
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0; 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] := -130.0;
for i := 0 to 1023 do FWaterfallBuf[i] := -120.0; FWfHigh := FWfManualHigh;
FWfHigh := -50.0; FWfLow := FWfManualLow;
FWfLow := -120.0; FWaterfallDirty := True;
end; end;
procedure TMainForm.InvalidateGridCache; procedure TMainForm.InvalidateGridCache;
@@ -2253,6 +2412,7 @@ var
S0, S1: Integer; S0, S1: Integer;
InvRange: Double; InvRange: Double;
LabelBandW: Integer; LabelBandW: Integer;
SrcCount: Integer;
begin begin
if FSpectrumBitmap = nil then Exit; if FSpectrumBitmap = nil then Exit;
W := FSpectrumBitmap.Width; W := FSpectrumBitmap.Width;
@@ -2359,11 +2519,12 @@ begin
SetLength(FSpPts, W + 2); SetLength(FSpPts, W + 2);
FSpPtsLen := W + 2; FSpPtsLen := W + 2;
end; end;
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
for i := 0 to W - 1 do for i := 0 to W - 1 do
begin begin
SrcF := i * 1023.0 / Max(1, W - 1); SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
S0 := Min(Trunc(SrcF), 1023); S0 := Min(Trunc(SrcF), SrcCount - 1);
S1 := Min(S0 + 1, 1023); S1 := Min(S0 + 1, SrcCount - 1);
Frac := SrcF - S0; Frac := SrcF - S0;
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac; dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H)))); FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
@@ -2409,10 +2570,17 @@ var
dB, frac: Double; dB, frac: Double;
WatSrcF: Double; WatSrcF: Double;
WatS0, WatS1, V: Integer; WatS0, WatS1, V: Integer;
AvgDB: Double; TargetLow: Double;
PeakDB: Double; TargetHigh: Double;
NFSum: Double; HistMinDB: Double;
NFCount: Integer; HistMaxDB: Double;
HistStepDB: Double;
NoiseFloorDB: Double;
SignalTopDB: Double;
CumCount: Integer;
LowTargetCount: Integer;
HighTargetCount: Integer;
HistIdx: Integer;
WfHigh, WfLow, InvRange: Double; WfHigh, WfLow, InvRange: Double;
Step: Double; Step: Double;
Px: PLongWord; Px: PLongWord;
@@ -2422,53 +2590,78 @@ var
Src: PLongWord; Src: PLongWord;
BPPi: Integer; BPPi: Integer;
Pal: LongWord; Pal: LongWord;
Hist: array[0..191] of Integer;
SrcCount: Integer;
const const
ALPHA_SPEC = 0.25; ALPHA_HIGH = 0.10;
ALPHA_HIGH = 0.02; ALPHA_LOW = 0.08;
ALPHA_LOW = 0.005; WF_AUTO_OFFSET = -4.0;
WF_MIN_RANGE = 48.0;
WF_MAX_RANGE = 62.0;
begin begin
if FWaterfallBitmap = nil then Exit; if FWaterfallBitmap = nil then Exit;
W := FWaterfallBitmap.Width; W := FWaterfallBitmap.Width;
H := FWaterfallBitmap.Height; H := FWaterfallBitmap.Height;
if (W <= 0) or (H <= 0) then Exit; if (W <= 0) or (H <= 0) then Exit;
SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
// ── Источник данных водопада ───────────────────────────────────────────── FillChar(Hist, SizeOf(Hist), 0);
// При работе WDSP: FWaterfallBuf заполнен из pixout=1 (уже усреднённые WDSP данные) HistMinDB := -170.0;
// Без WDSP (demo): делаем EMA FSpectrumBuf → FWfAvgBuf HistMaxDB := 22.0;
PeakDB := -200.0; NFSum := 0.0; NFCount := 0; HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist);
if FWDSPReady then for X := 0 to SrcCount - 1 do
begin begin
// Используем WDSP waterfall данные напрямую HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist));
for X := 0 to 1023 do Inc(Hist[HistIdx]);
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;
end; end;
end
else LowTargetCount := Round(SrcCount * 0.30);
HighTargetCount := Round(SrcCount * 0.98);
CumCount := 0;
NoiseFloorDB := FWfLow;
SignalTopDB := FWfHigh;
for HistIdx := 0 to High(Hist) do
begin begin
// Fallback: Pascal EMA от демо-спектра CumCount := CumCount + Hist[HistIdx];
for X := 0 to 1023 do if CumCount >= LowTargetCount then
begin begin
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC; NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
FWfAvgBuf[X] := AvgDB; Break;
if AvgDB > PeakDB then PeakDB := AvgDB; end;
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end; end;
CumCount := 0;
for HistIdx := 0 to High(Hist) do
begin
CumCount := CumCount + Hist[HistIdx];
if CumCount >= HighTargetCount then
begin
SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
Break;
end; end;
end; end;
// ── AGC High/Low ──────────────────────────────────────────────────────── // ── AGC High/Low ────────────────────────────────────────────────────────
if FWfAGCEnabled then if FWfAGCEnabled then
begin begin
if PeakDB > FWfHigh then FWfHigh := PeakDB + 5.0 TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset;
else FWfHigh := FWfHigh + ALPHA_HIGH * ((PeakDB + 5.0) - FWfHigh); 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; end;
if FWfNFEnabled and (NFCount > 10) then WfHigh := FWfHigh;
FWfLow := FWfLow + ALPHA_LOW * ((NFSum / NFCount - 5.0) - FWfLow); WfLow := FWfLow;
WfHigh := FWfHigh; WfLow := FWfLow; if not FWfAGCEnabled then
if WfHigh < WfLow + 15.0 then WfHigh := WfLow + 15.0; 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 WfHigh > 0.0 then WfHigh := 0.0;
if WfLow < -160 then WfLow := -160; if WfLow < -160 then WfLow := -160;
InvRange := 255.0 / (WfHigh - WfLow); InvRange := 255.0 / (WfHigh - WfLow);
@@ -2489,21 +2682,21 @@ begin
Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W); Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W);
// ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ──────── // ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ────────
Step := 1023.0 / Max(1, W - 1); Step := (SrcCount - 1.0) / Max(1, W - 1);
WatSrcF := 0.0; WatSrcF := 0.0;
Px := @FWfPixels[0]; Px := @FWfPixels[0];
for X := 0 to W - 1 do for X := 0 to W - 1 do
begin begin
WatS0 := Trunc(WatSrcF); WatS0 := Trunc(WatSrcF);
if WatS0 > 1022 then WatS0 := 1022; if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
WatS1 := WatS0 + 1; WatS1 := WatS0 + 1;
frac := WatSrcF - WatS0; 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); V := Trunc((dB - WfLow) * InvRange);
if V < 0 then V := 0; if V < 0 then V := 0;
if V > 255 then V := 255; if V > 255 then V := 255;
// Палитра задана как RGB, а в буфере нужен BGRA. // WFALL_PALETTE задана в RGB-логике, а в буфере нужен BGRA.
// Меняем местами R/B и ставим alpha=255, чтобы совпадало с web/Thetis-градиентом. // Поэтому меняем R/B местами и добавляем alpha=255.
Pal := WFALL_PALETTE[V]; Pal := WFALL_PALETTE[V];
Px^ := ((Pal and $000000FF) shl 16) or Px^ := ((Pal and $000000FF) shl 16) or
(Pal and $0000FF00) or (Pal and $0000FF00) or
@@ -3084,7 +3277,6 @@ begin
if FWDSPReady then if FWDSPReady then
begin begin
FDSPEngine.UpdateSpectrum;
FLastSMeter := FDSPEngine.GetSMeterDBm; FLastSMeter := FDSPEngine.GetSMeterDBm;
FDSPEngine.SetSpectrumWidth(FSpectrumWidth); // актуальная ширина для SetAGC/SetAGCTop FDSPEngine.SetSpectrumWidth(FSpectrumWidth); // актуальная ширина для SetAGC/SetAGCTop
FDSPEngine.UpdateAGCLines(FSpectrumWidth); FDSPEngine.UpdateAGCLines(FSpectrumWidth);
@@ -3096,22 +3288,24 @@ begin
FVfoOverlay.UpdateVfo(FVfoA); FVfoOverlay.UpdateVfo(FVfoA);
PositionVfoOverlay; PositionVfoOverlay;
end; end;
// Рисуем только при новых спектральных данных/изменении UI (FSpectrumDirty). // Панорама и водопад теперь рисуются независимо по факту новых кадров.
// Это резко снижает CPU в холостом режиме. // Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
if FSpectrumDirty then if FSpectrumDirty then
begin begin
DrawSpectrum; DrawSpectrum;
PbSpectrum.Invalidate; PbSpectrum.Invalidate;
// Водопад рисуем на каждом новом спектральном кадре для плавности.
DrawWaterfall;
PbWaterfall.Invalidate;
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then (Abs(FVfoA - FRulerLastVfo) >= 0.5) then
PbRuler.Invalidate; PbRuler.Invalidate;
FSpectrumDirty := False; FSpectrumDirty := False;
end; end;
if FWaterfallDirty then
begin
DrawWaterfall;
PbWaterfall.Invalidate;
FWaterfallDirty := False;
end;
end end
else else
begin begin
@@ -3139,15 +3333,20 @@ begin
end; end;
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем. // Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
if FSpectrumDirty and (not FRunning) then if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then
begin begin
if FSpectrumDirty then
DrawSpectrum; DrawSpectrum;
if FWaterfallDirty then
DrawWaterfall; DrawWaterfall;
if FSpectrumDirty then
PbSpectrum.Invalidate; PbSpectrum.Invalidate;
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then (Abs(FVfoA - FRulerLastVfo) >= 0.5) then
PbRuler.Invalidate; PbRuler.Invalidate;
if FWaterfallDirty then
PbWaterfall.Invalidate; PbWaterfall.Invalidate;
FWaterfallDirty := False;
FSpectrumDirty := False; FSpectrumDirty := False;
end; end;
@@ -3155,7 +3354,7 @@ begin
if Assigned(FWebServer) then if Assigned(FWebServer) then
FWebServer.PushSpectrum( FWebServer.PushSpectrum(
FSpectrumBuf, 1024, FSpectrumBuf, 1024,
FWfAvgBuf, FWaterfallBuf,
FLastSMeter, FLastSMeter,
FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop, FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop,
FSpanHz, FVolume, FSpanHz, FVolume,
@@ -3165,7 +3364,8 @@ begin
FRunning, FMuted, FCTun, FRunning, FMuted, FCTun,
BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0,
FCenterFreq, FFilter, FCenterFreq, FFilter,
FVfoB, FActiveVfo); FVfoB, FActiveVfo,
FTransmitting, TrkDrive.Position);
end; end;
// =========================================================================== // ===========================================================================
@@ -3412,6 +3612,67 @@ begin
FDSPEngine.OnTXIQ := OnTXIQReady; FDSPEngine.OnTXIQ := OnTXIQReady;
end; 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); procedure TMainForm.BtnDiscoverClick(Sender: TObject);
begin begin
// Открываем диалог выбора устройства // Открываем диалог выбора устройства
@@ -3595,6 +3856,11 @@ begin
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton; BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); 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; FShowSpectrum := G_Settings.ShowSpectrum;
FShowWaterfall := G_Settings.ShowWaterfall; FShowWaterfall := G_Settings.ShowWaterfall;
if G_Settings.DisplayFPS > 0 then if G_Settings.DisplayFPS > 0 then
@@ -3610,9 +3876,9 @@ begin
begin begin
FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType); FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode, 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, FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
G_Settings.WfAvgCount, G_Settings.WfBackmult); G_Settings.WfAvgTimeMS);
end; end;
// Аудио устройство вывода // Аудио устройство вывода
FAudioOut.Close; FAudioOut.Close;
@@ -4555,7 +4821,7 @@ begin
FWfAGCEnabled := not FWfAGCEnabled; FWfAGCEnabled := not FWfAGCEnabled;
StyleButton(BtnWfAGC, FWfAGCEnabled); StyleButton(BtnWfAGC, FWfAGCEnabled);
if not FWfAGCEnabled then if not FWfAGCEnabled then
FWfHigh := -50.0; // сбрасываем к умолчанию при выключении FWfHigh := FWfManualHigh; // сбрасываем к ручному уровню при выключении
end; end;
procedure TMainForm.BtnWfNFClick(Sender: TObject); procedure TMainForm.BtnWfNFClick(Sender: TObject);
@@ -4563,7 +4829,7 @@ begin
FWfNFEnabled := not FWfNFEnabled; FWfNFEnabled := not FWfNFEnabled;
StyleButton(BtnWfNF, FWfNFEnabled); StyleButton(BtnWfNF, FWfNFEnabled);
if not FWfNFEnabled then if not FWfNFEnabled then
FWfLow := -120.0; FWfLow := FWfManualLow;
end; end;
procedure TMainForm.BtnHidePanelClick(Sender: TObject); procedure TMainForm.BtnHidePanelClick(Sender: TObject);
@@ -5114,11 +5380,41 @@ begin
FSpectrumDirty := True; FSpectrumDirty := True;
end; 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); procedure TMainForm.TrkDriveChange(Sender: TObject);
begin begin
FDriveLevel := CalcDriveByte; FDriveLevel := CalcDriveByte;
if FWDSPReady then if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0); 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; end;
function TMainForm.CalcDriveByte: Byte; function TMainForm.CalcDriveByte: Byte;
@@ -5283,6 +5579,7 @@ begin
N := Min(Count, 1024); N := Min(Count, 1024);
for i := 0 to N - 1 do for i := 0 to N - 1 do
FSpectrumBuf[i] := Pixels[i]; FSpectrumBuf[i] := Pixels[i];
FSpectrumBufCount := N;
FSpectrumDirty := True; FSpectrumDirty := True;
end; end;
@@ -5291,31 +5588,47 @@ procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
var var
i, N: Integer; i, N: Integer;
begin begin
// Заполняем буфер водопада из WDSP pixout=1.
// Вызывается из DSP-потока — Single-запись атомарна.
N := Min(Count, 1024); N := Min(Count, 1024);
for i := 0 to N - 1 do for i := 0 to N - 1 do
FWaterfallBuf[i] := Pixels[i]; FWaterfallBuf[i] := Pixels[i];
FWaterfallBufCount := N;
Inc(FWaterfallFrameCounter);
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
begin
FWaterfallFrameCounter := 0;
FWaterfallDirty := True;
end;
end; end;
// =========================================================================== // ===========================================================================
// Settings apply methods (вызываются из TSettingsForm) // Settings apply methods (вызываются из TSettingsForm)
// =========================================================================== // ===========================================================================
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode, procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgCount: Integer; SpecBackmult: Double); SpecAvgTimeMS: Double);
begin begin
if FWDSPReady then
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS);
if FWDSPReady then if FWDSPReady then
FDSPEngine.SetFFTParams(FFTSize, WinType); FDSPEngine.SetFFTParams(FFTSize, WinType);
if FWDSPReady then FSpectrumDirty := True;
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgCount, SpecBackmult);
end; end;
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer; procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
WfBackmult: Double); WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
begin begin
if FWDSPReady then 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; end;
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double); procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
@@ -5375,9 +5688,15 @@ end;
procedure TMainForm.ApplyFPS(FPS: Integer); procedure TMainForm.ApplyFPS(FPS: Integer);
begin begin
if FPS < 1 then FPS := 1; if FPS < 1 then FPS := 1;
if FPS > 60 then FPS := 60; if FPS > 100 then FPS := 100;
FDisplayFPS := FPS; FDisplayFPS := FPS;
FSpectrumTimer.Interval := 1000 div FPS; FSpectrumTimer.Interval := 1000 div FPS;
// Thetis по умолчанию обновляет водопад через один display frame.
FWaterfallFrameInterval := 2;
FWaterfallFrameCounter := 0;
FWaterfallDirty := True;
if FWDSPReady then
FDSPEngine.SetDisplayFPS(FPS);
end; end;
procedure TMainForm.BtnSettingsClick(Sender: TObject); procedure TMainForm.BtnSettingsClick(Sender: TObject);
@@ -5410,18 +5729,19 @@ begin
FDSPEngine.WindowType, FDSPEngine.WindowType,
FDSPEngine.SpecDetector, FDSPEngine.SpecDetector,
FDSPEngine.SpecAvgMode, FDSPEngine.SpecAvgMode,
FDSPEngine.SpecAvgCount, FDSPEngine.SpecAvgTimeMS,
FDSPEngine.SpecBackmult,
FDSPEngine.WfDetector, FDSPEngine.WfDetector,
FDSPEngine.WfAvgMode, FDSPEngine.WfAvgMode,
FDSPEngine.WfAvgCount, FDSPEngine.WfAvgTimeMS,
FDSPEngine.WfBackmult, FWfManualHigh,
FWfManualLow,
FWfAGCOffset,
FSpecRefLevel, FSpecRange, FSpecGridStep, FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName) FAudioOutDevName, FAudioInDevName)
else else
SF.LoadValues( SF.LoadValues(
4096, 5, 0, 3, 2, 0.45, 4096, 2, 0, 3, 30.0,
2, 3, 2, 0.45, 0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset,
FSpecRefLevel, FSpecRange, FSpecGridStep, FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName); FAudioOutDevName, FAudioInDevName);
SF.Show; SF.Show;
@@ -5442,6 +5762,8 @@ begin
on E: Exception do on E: Exception do
StatusBar1.Panels[3].Text := 'Audio: ' + E.Message; StatusBar1.Panels[3].Text := 'Audio: ' + E.Message;
end; end;
EnsureWDSPWisdom;
end; end;
+65 -23
View File
@@ -6,7 +6,7 @@ unit Settings;
} }
{$mode objfpc}{$H+} {$mode objfpc}{$H+}
interface interface
uses SysUtils, Classes, fpJSON, jsonparser, jsonscanner; uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner;
const const
SETTINGS_FILE = 'hpsdr_settings.json'; SETTINGS_FILE = 'hpsdr_settings.json';
@@ -53,12 +53,13 @@ type
WindowType: Integer; // 0=Rect,1=BH4,2=Hann,3=FlatTop,4=Hamming,5=Kaiser,6=BH7 WindowType: Integer; // 0=Rect,1=BH4,2=Hann,3=FlatTop,4=Hamming,5=Kaiser,6=BH7
SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
SpecAvgCount: Integer; // frames to average SpecAvgTimeMS: Double; // averaging time in milliseconds
SpecBackmult: Double; // 0.0..1.0 recursive averaging multiplier
WfDetector: Integer; // waterfall detector WfDetector: Integer; // waterfall detector
WfAvgMode: Integer; // waterfall average mode WfAvgMode: Integer; // waterfall average mode
WfAvgCount: Integer; // waterfall average count WfAvgTimeMS: Double; // waterfall averaging time in milliseconds
WfBackmult: Double; // waterfall backmult WfManualHigh: Double; // manual waterfall high threshold
WfManualLow: Double; // manual waterfall low threshold
WfAGCOffset: Double; // AGC offset in dB
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20 SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range) SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
SpecGridStep: Double; // grid step in dB, e.g. 10 SpecGridStep: Double; // grid step in dB, e.g. 10
@@ -70,7 +71,7 @@ type
ShowSpectrum: Boolean; // True = draw spectrum on main form ShowSpectrum: Boolean; // True = draw spectrum on main form
ShowWaterfall: Boolean; // True = draw waterfall on main form ShowWaterfall: Boolean; // True = draw waterfall on main form
// --- Display FPS --- // --- Display FPS ---
DisplayFPS: Integer; // spectrum/waterfall timer fps (5..30) DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
// --- PA (Power Amplifier) settings --- // --- PA (Power Amplifier) settings ---
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200) PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0 PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
@@ -117,6 +118,26 @@ type
implementation implementation
function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer;
Backmult, DefaultMS: Double): Double;
var
FPSv: Integer;
begin
if AvgMode <= 0 then
Exit(0.0);
FPSv := EnsureRange(FPS, 1, 100);
if (Backmult > 0.0) and (Backmult < 1.0) then
Result := -1000.0 / (FPSv * Ln(Backmult))
else
Result := (1000.0 * Max(1, AvgCount)) / FPSv;
if IsNan(Result) or IsInfinite(Result) or (Result <= 0.0) then
Result := DefaultMS;
Result := EnsureRange(Result, 10.0, 5000.0);
end;
class function TSettingsManager.MacToStr(const MAC: array of Byte): string; class function TSettingsManager.MacToStr(const MAC: array of Byte): string;
begin begin
Result := Format('%02X:%02X:%02X:%02X:%02X:%02X', Result := Format('%02X:%02X:%02X:%02X:%02X:%02X',
@@ -144,20 +165,23 @@ begin
G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0; G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0;
G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False; G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False;
G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5; G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5;
G.WfAGCEnabled := True;
G.WfNFEnabled := False;
G.SampleRate := 192000; G.SampleRate := 192000;
G.PAMaxPower := 100.0; G.PAMaxPower := 100.0;
for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0; for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0;
// Display defaults // Display defaults
G.FFTSize := 4096; G.FFTSize := 4096;
G.WindowType := 5; // Kaiser G.WindowType := 2; // Hann
G.SpecDetector := 0; // Peak G.SpecDetector := 0; // Peak
G.SpecAvgMode := 3; // Log Recursive G.SpecAvgMode := 3; // Log Recursive
G.SpecAvgCount := 2; G.SpecAvgTimeMS := 30.0;
G.SpecBackmult := 0.45; G.WfDetector := 0; // Peak
G.WfDetector := 2; // Average
G.WfAvgMode := 3; // Log Recursive G.WfAvgMode := 3; // Log Recursive
G.WfAvgCount := 2; G.WfAvgTimeMS := 120.0;
G.WfBackmult := 0.45; G.WfManualHigh := -80.0;
G.WfManualLow := -130.0;
G.WfAGCOffset := 0.0;
G.SpecRefLevel := -20.0; G.SpecRefLevel := -20.0;
G.SpecRange := 110.0; G.SpecRange := 110.0;
G.SpecGridStep := 10.0; G.SpecGridStep := 10.0;
@@ -307,7 +331,7 @@ begin
G.ANFEnabled := JB(GObj,'anf_enabled',False); G.ANFEnabled := JB(GObj,'anf_enabled',False);
G.AGCSlope := JI(GObj,'agc_slope',0); G.AGCSlope := JI(GObj,'agc_slope',0);
G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100); G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100);
G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',False); G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',True);
G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False); G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False);
G.LastBand := JI(GObj,'last_band',5); G.LastBand := JI(GObj,'last_band',5);
G.SampleRate := JI(GObj,'sample_rate',192000); G.SampleRate := JI(GObj,'sample_rate',192000);
@@ -315,13 +339,30 @@ begin
G.FFTSize := JI(GObj,'fft_size',4096); G.FFTSize := JI(GObj,'fft_size',4096);
G.WindowType := JI(GObj,'win_type',5); G.WindowType := JI(GObj,'win_type',5);
G.SpecDetector := JI(GObj,'spec_detector',0); G.SpecDetector := JI(GObj,'spec_detector',0);
G.SpecAvgMode := JI(GObj,'spec_avg_mode',3); G.SpecAvgMode := JI(GObj,'spec_avg_mode',0);
G.SpecAvgCount := JI(GObj,'spec_avg_count',2); if GObj.Find('spec_avg_time_ms') <> nil then
G.SpecBackmult := JD(GObj,'spec_backmult',0.45); G.SpecAvgTimeMS := JD(GObj,'spec_avg_time_ms',120.0)
else
G.SpecAvgTimeMS := LegacyAverageTimeMS(
JI(GObj,'display_fps',20),
G.SpecAvgMode,
JI(GObj,'spec_avg_count',1),
JD(GObj,'spec_backmult',0.0),
120.0);
G.WfDetector := JI(GObj,'wf_detector',2); G.WfDetector := JI(GObj,'wf_detector',2);
G.WfAvgMode := JI(GObj,'wf_avg_mode',3); G.WfAvgMode := JI(GObj,'wf_avg_mode',1);
G.WfAvgCount := JI(GObj,'wf_avg_count',2); if GObj.Find('wf_avg_time_ms') <> nil then
G.WfBackmult := JD(GObj,'wf_backmult',0.45); G.WfAvgTimeMS := JD(GObj,'wf_avg_time_ms',120.0)
else
G.WfAvgTimeMS := LegacyAverageTimeMS(
JI(GObj,'display_fps',20),
G.WfAvgMode,
JI(GObj,'wf_avg_count',2),
JD(GObj,'wf_backmult',0.25),
120.0);
G.WfManualHigh := JD(GObj,'wf_manual_high',-80.0);
G.WfManualLow := JD(GObj,'wf_manual_low',-130.0);
G.WfAGCOffset := JD(GObj,'wf_agc_offset',0.0);
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0); G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
G.SpecRange := JD(GObj,'spec_range',110.0); G.SpecRange := JD(GObj,'spec_range',110.0);
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0); G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
@@ -376,12 +417,13 @@ begin
JW(O,'win_type',G.WindowType); JW(O,'win_type',G.WindowType);
JW(O,'spec_detector',G.SpecDetector); JW(O,'spec_detector',G.SpecDetector);
JW(O,'spec_avg_mode',G.SpecAvgMode); JW(O,'spec_avg_mode',G.SpecAvgMode);
JW(O,'spec_avg_count',G.SpecAvgCount); JW(O,'spec_avg_time_ms',G.SpecAvgTimeMS);
JW(O,'spec_backmult',G.SpecBackmult);
JW(O,'wf_detector',G.WfDetector); JW(O,'wf_detector',G.WfDetector);
JW(O,'wf_avg_mode',G.WfAvgMode); JW(O,'wf_avg_mode',G.WfAvgMode);
JW(O,'wf_avg_count',G.WfAvgCount); JW(O,'wf_avg_time_ms',G.WfAvgTimeMS);
JW(O,'wf_backmult',G.WfBackmult); JW(O,'wf_manual_high',G.WfManualHigh);
JW(O,'wf_manual_low',G.WfManualLow);
JW(O,'wf_agc_offset',G.WfAGCOffset);
JW(O,'spec_ref_level',G.SpecRefLevel); JW(O,'spec_ref_level',G.SpecRefLevel);
JW(O,'spec_range',G.SpecRange); JW(O,'spec_range',G.SpecRange);
JW(O,'spec_grid_step',G.SpecGridStep); JW(O,'spec_grid_step',G.SpecGridStep);
+143 -169
View File
@@ -35,9 +35,9 @@ type
const BandCal: array of Double) of object; const BandCal: array of Double) of object;
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector, TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
SpecAvgMode, SpecAvgCount: Integer; SpecBackmult: Double) of object; SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer; TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
WfBackmult: Double) of object; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object; TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object; TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object; TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
@@ -79,13 +79,14 @@ type
// Spectrum group // Spectrum group
FCmbSpecDet: TComboBox; FCmbSpecDet: TComboBox;
FCmbSpecAvg: TComboBox; FCmbSpecAvg: TComboBox;
FSpinSpecCount: TSpinEdit; FSpecAvgTime: TFloatSpinEdit;
FEdSpecBackmult: TEdit;
// Waterfall group // Waterfall group
FCmbWfDet: TComboBox; FCmbWfDet: TComboBox;
FCmbWfAvg: TComboBox; FCmbWfAvg: TComboBox;
FSpinWfCount: TSpinEdit; FWfAvgTime: TFloatSpinEdit;
FEdWfBackmult: TEdit; FWfHigh: TFloatSpinEdit;
FWfLow: TFloatSpinEdit;
FWfAGCOffset: TFloatSpinEdit;
// Grid group // Grid group
FEdRefLevel: TEdit; FEdRefLevel: TEdit;
FEdRange: TEdit; FEdRange: TEdit;
@@ -136,12 +137,11 @@ type
procedure OnWindowChange(Sender: TObject); procedure OnWindowChange(Sender: TObject);
procedure OnSpecDetChange(Sender: TObject); procedure OnSpecDetChange(Sender: TObject);
procedure OnSpecAvgModeChange(Sender: TObject); procedure OnSpecAvgModeChange(Sender: TObject);
procedure OnSpecCountChange(Sender: TObject); procedure OnSpecAvgTimeChange(Sender: TObject);
procedure OnSpecBackmultChange(Sender: TObject);
procedure OnWfDetChange(Sender: TObject); procedure OnWfDetChange(Sender: TObject);
procedure OnWfAvgModeChange(Sender: TObject); procedure OnWfAvgModeChange(Sender: TObject);
procedure OnWfCountChange(Sender: TObject); procedure OnWfAvgTimeChange(Sender: TObject);
procedure OnWfBackmultChange(Sender: TObject); procedure OnWfThresholdChange(Sender: TObject);
procedure OnRefLevelChange(Sender: TObject); procedure OnRefLevelChange(Sender: TObject);
procedure OnRangeChange(Sender: TObject); procedure OnRangeChange(Sender: TObject);
procedure OnGridStepChange(Sender: TObject); procedure OnGridStepChange(Sender: TObject);
@@ -157,8 +157,11 @@ type
procedure FireWaterfallChange; procedure FireWaterfallChange;
procedure FireGridChange; procedure FireGridChange;
function GetSpecBackmult: Double; function GetSpecAvgTimeMS: Double;
function GetWfBackmult: Double; function GetWfAvgTimeMS: Double;
function GetWfHigh: Double;
function GetWfLow: Double;
function GetWfAGCOffset: Double;
function GetRefLevel: Double; function GetRefLevel: Double;
function GetRange: Double; function GetRange: Double;
function GetGridStep: Double; function GetGridStep: Double;
@@ -169,10 +172,10 @@ type
// Заполнить элементы управления текущими значениями (без firing events) // Заполнить элементы управления текущими значениями (без firing events)
procedure LoadValues( procedure LoadValues(
FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer; FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecBackmult: Double; SpecAvgTimeMS: Double;
WfDet, WfAvgMode, WfAvgCount: Integer; WfDet, WfAvgMode: Integer;
WfBackmult: Double; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
RefLevel, Range, GridStep: Double; RefLevel, Range, GridStep: Double;
const AudioOutDevName: string; const AudioOutDevName: string;
const AudioInDevName: string); const AudioInDevName: string);
@@ -208,9 +211,8 @@ const
CLR_ACCENT = TColor($0040FF80); CLR_ACCENT = TColor($0040FF80);
BTN_H = 24; BTN_H = 24;
// FFT sizes (index → value) FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144);
FFT_SIZES: array[0..5] of Integer = (1024, 2048, 4096, 8192, 16384, 32768); FFT_NAMES: array[0..6] of string = ('4096','8192','16384','32768','65536','131072','262144');
FFT_NAMES: array[0..5] of string = ('1024','2048','4096','8192','16384','32768');
WINDOW_NAMES: array[0..6] of string = ( WINDOW_NAMES: array[0..6] of string = (
'Rectangular', '4-term Blackman-Harris', 'Hann', 'Rectangular', '4-term Blackman-Harris', 'Hann',
@@ -219,17 +221,19 @@ const
DETECTOR_NAMES: array[0..4] of string = ( DETECTOR_NAMES: array[0..4] of string = (
'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS'); 'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS');
AVGMODE_NAMES: array[0..4] of string = ( AVGMODE_NAMES: array[0..3] of string = (
'Peak Hold', 'None', 'Recursive', 'Time Window', 'Log Recursive'); 'None', 'Recursive', 'Time Window', 'Log Recursive');
// Note: AVERAGE_PEAK_HOLD=-1, NONE=0, RECURSIVE=1, TIME_WINDOW=2, LOG_RECURSIVE=3 AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
// Index → WDSP value mapping: 0→-1, 1→0, 2→1, 3→2, 4→3 WF_AVGMODE_NAMES: array[0..3] of string = (
AVGMODE_VALUES: array[0..4] of Integer = (-1, 0, 1, 2, 3); 'None', 'Recursive', 'Time Window', 'Log Recursive');
WF_AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0); GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0);
GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB'); GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB');
FPS_VALUES: array[0..5] of Integer = (5, 10, 15, 20, 25, 30); FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100);
FPS_NAMES: array[0..5] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps'); FPS_NAMES: array[0..12] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps',
'40 fps','50 fps','60 fps','75 fps','80 fps','90 fps','100 fps');
PA_BAND_NAMES: array[0..10] of string = PA_BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'); ('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
@@ -240,8 +244,8 @@ constructor TSettingsForm.Create(AOwner: TComponent);
begin begin
inherited CreateNew(AOwner); inherited CreateNew(AOwner);
Caption := 'Settings'; Caption := 'Settings';
Width := 700; Width := 760;
Height := 560; Height := 640;
Position := poScreenCenter; Position := poScreenCenter;
BorderStyle := bsSizeable; BorderStyle := bsSizeable;
Color := CLR_BG; Color := CLR_BG;
@@ -457,31 +461,20 @@ begin
end; end;
procedure TSettingsForm.BuildRX1Tab; procedure TSettingsForm.BuildRX1Tab;
// Layout (alTop stack in FTabDispRX1):
// 8px top padding
// FFT group — full width
// 12px separator
// PnlMid — Spectrum (alLeft) | 10px gap | Waterfall (alClient), same height
// 12px separator
// Spectrum Grid group — full width
// hint (alBottom)
//
// Row step: STEP = BTN_H + 10 = 34px. Inner padding: R1 = 10.
const const
R1 = 10; // top padding inside groups R1 = 12;
LX = 10; // left margin inside group LX = 12;
LW = 88; // label column width LW = 112;
CX = LX + LW + 4; // combo/edit column start (= 102) CX = LX + LW + 8;
STEP = BTN_H + 10; // row step (= 34) STEP = 34;
SEP = 12; // gap between groups SEP = 12;
COLW = 328; // left column panel width GH_FFT = 100;
COLGAP = 10; // horizontal gap between columns GH_SPEC = 136;
GH2 = 100; // GroupBox height for 2-row groups GH_WF = 170;
GH3 = 136; // GroupBox height for 3-row groups GH_GRID = 100;
var var
PnlFFT, PnlMid, PnlL, PnlSepH, PnlR: TPanel; PnlFFT, PnlSpec, PnlWF: TPanel;
Grp: TGroupBox; Grp: TGroupBox;
Lbl: TLabel;
i: Integer; i: Integer;
function MkGrp(AParent: TWinControl; const Cap: string; AH: Integer): TGroupBox; function MkGrp(AParent: TWinControl; const Cap: string; AH: Integer): TGroupBox;
@@ -508,28 +501,17 @@ var
S.Height := AH; S.Height := AH;
end; end;
function MkEdit(AParent: TWinControl; AX, AY, AW: Integer; function MkFloat(AParent: TWinControl; AX, AY, AW: Integer; AValue: Double;
AOnExit: TNotifyEvent): TEdit; AMin, AMax, AInc: Double; AOnChange: TNotifyEvent): TFloatSpinEdit;
begin begin
Result := TEdit.Create(Self); Result := TFloatSpinEdit.Create(Self);
Result.Parent := AParent; Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H); Result.SetBounds(AX, AY, AW, BTN_H);
Result.Color := CLR_PANEL; Result.Value := AValue;
Result.Font.Color := CLR_TEXT; Result.MinValue := AMin;
Result.Font.Name := 'Courier New'; Result.MaxValue := AMax;
Result.Font.Size := 8; Result.Increment := AInc;
Result.OnExit := AOnExit; Result.DecimalPlaces := 1;
end;
function MkSpin(AParent: TWinControl; AX, AY, AW: Integer;
AOnChange: TNotifyEvent): TSpinEdit;
begin
Result := TSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.MinValue := 1;
Result.MaxValue := 256;
Result.Value := 2;
Result.Color := CLR_PANEL; Result.Color := CLR_PANEL;
Result.Font.Color := CLR_TEXT; Result.Font.Color := CLR_TEXT;
Result.Font.Name := 'Courier New'; Result.Font.Name := 'Courier New';
@@ -538,24 +520,15 @@ var
end; end;
begin begin
// Hint at bottom (added first so alBottom is honoured by later alTop panels)
Lbl := MakeLbl(FTabDispRX1,
'Backmult: higher value = slower decay (range 0.0 .. 1.0)', LX, 0, 0);
Lbl.Align := alBottom;
Lbl.Height := 22;
Lbl.Font.Color := CLR_TEXTDIM;
// ── Top padding ───────────────────────────────────────────────────────────
MkSep(FTabDispRX1, 8); MkSep(FTabDispRX1, 8);
// ── FFT (full width, alTop) ───────────────────────────────────────────────
PnlFFT := TPanel.Create(Self); PnlFFT := TPanel.Create(Self);
PnlFFT.Parent := FTabDispRX1; PnlFFT.Parent := FTabDispRX1;
PnlFFT.BevelOuter := bvNone; PnlFFT.BevelOuter := bvNone;
PnlFFT.Color := CLR_BG; PnlFFT.Color := CLR_BG;
PnlFFT.Align := alTop; PnlFFT.Align := alTop;
PnlFFT.Height := GH2; PnlFFT.Height := GH_FFT;
Grp := MkGrp(PnlFFT, 'FFT', GH2); Grp := MkGrp(PnlFFT, 'FFT / Analyzer', GH_FFT);
MakeLbl(Grp, 'FFT Size:', LX, R1+4, LW); MakeLbl(Grp, 'FFT Size:', LX, R1+4, LW);
FCmbFFTSize := MakeCombo(Grp, CX, R1, 130, OnFFTSizeChange); FCmbFFTSize := MakeCombo(Grp, CX, R1, 130, OnFFTSizeChange);
for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]); for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]);
@@ -563,75 +536,70 @@ begin
FCmbWindow := MakeCombo(Grp, CX, R1+STEP, 250, OnWindowChange); FCmbWindow := MakeCombo(Grp, CX, R1+STEP, 250, OnWindowChange);
for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]); for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]);
// ── Separator ─────────────────────────────────────────────────────────────
MkSep(FTabDispRX1, SEP); MkSep(FTabDispRX1, SEP);
// ── Middle row: Spectrum (left) | Waterfall (right) at the same level ───── PnlSpec := TPanel.Create(Self);
PnlMid := TPanel.Create(Self); PnlSpec.Parent := FTabDispRX1;
PnlMid.Parent := FTabDispRX1; PnlSpec.BevelOuter := bvNone;
PnlMid.BevelOuter := bvNone; PnlSpec.Color := CLR_BG;
PnlMid.Color := CLR_BG; PnlSpec.Align := alTop;
PnlMid.Align := alTop; PnlSpec.Height := GH_SPEC;
PnlMid.Height := GH3; Grp := MkGrp(PnlSpec, 'Spectrum', GH_SPEC);
PnlL := TPanel.Create(Self);
PnlL.Parent := PnlMid;
PnlL.BevelOuter := bvNone;
PnlL.Color := CLR_BG;
PnlL.Align := alLeft;
PnlL.Width := COLW;
PnlSepH := TPanel.Create(Self);
PnlSepH.Parent := PnlMid;
PnlSepH.BevelOuter := bvNone;
PnlSepH.Color := CLR_BG;
PnlSepH.Align := alLeft;
PnlSepH.Width := COLGAP;
PnlR := TPanel.Create(Self);
PnlR.Parent := PnlMid;
PnlR.BevelOuter := bvNone;
PnlR.Color := CLR_BG;
PnlR.Align := alClient;
// ── Spectrum (left column) ────────────────────────────────────────────────
Grp := MkGrp(PnlL, 'Spectrum', GH3);
MakeLbl(Grp, 'Detector:', LX, R1+4, LW); MakeLbl(Grp, 'Detector:', LX, R1+4, LW);
FCmbSpecDet := MakeCombo(Grp, CX, R1, 172, OnSpecDetChange); FCmbSpecDet := MakeCombo(Grp, CX, R1, 172, OnSpecDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]); for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW);
FCmbSpecAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnSpecAvgModeChange); FCmbSpecAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnSpecAvgModeChange);
for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]); for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW); MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW);
FSpinSpecCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnSpecCountChange); FSpecAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);
MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64);
FEdSpecBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnSpecBackmultChange);
FEdSpecBackmult.Text := '0.45';
// ── Waterfall (right column) ────────────────────────────────────────────── MkSep(FTabDispRX1, SEP);
Grp := MkGrp(PnlR, 'Waterfall', GH3);
PnlWF := TPanel.Create(Self);
PnlWF.Parent := FTabDispRX1;
PnlWF.BevelOuter := bvNone;
PnlWF.Color := CLR_BG;
PnlWF.Align := alTop;
PnlWF.Height := GH_WF;
Grp := MkGrp(PnlWF, 'Waterfall', GH_WF);
MakeLbl(Grp, 'Detector:', LX, R1+4, LW); MakeLbl(Grp, 'Detector:', LX, R1+4, LW);
FCmbWfDet := MakeCombo(Grp, CX, R1, 172, OnWfDetChange); FCmbWfDet := MakeCombo(Grp, CX, R1, 172, OnWfDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]); for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW);
FCmbWfAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnWfAvgModeChange); FCmbWfAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnWfAvgModeChange);
for i := 0 to High(AVGMODE_NAMES) do FCmbWfAvg.Items.Add(AVGMODE_NAMES[i]); for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW); MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW);
FSpinWfCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnWfCountChange); FWfAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange);
MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64); MakeLbl(Grp, 'High (dBm):', LX, R1+3*STEP+4, LW);
FEdWfBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnWfBackmultChange); FWfHigh := MkFloat(Grp, CX, R1+3*STEP, 92, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange);
FEdWfBackmult.Text := '0.45'; MakeLbl(Grp, 'Low (dBm):', CX+120, R1+3*STEP+4, 72);
FWfLow := MkFloat(Grp, CX+196, R1+3*STEP, 92, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange);
MakeLbl(Grp, 'AGC Offset:', LX, R1+4*STEP+4, LW);
FWfAGCOffset := MkFloat(Grp, CX, R1+4*STEP, 92, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange);
// ── Separator ─────────────────────────────────────────────────────────────
MkSep(FTabDispRX1, SEP); MkSep(FTabDispRX1, SEP);
// ── Spectrum Grid (full width, alTop) ───────────────────────────────────── Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH_GRID);
Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH2);
MakeLbl(Grp, 'Ref Level (dBm):', LX, R1+4, 116); MakeLbl(Grp, 'Ref Level (dBm):', LX, R1+4, 116);
FEdRefLevel := MkEdit(Grp, LX+120, R1, 80, OnRefLevelChange); FEdRefLevel := TEdit.Create(Self);
FEdRefLevel.Parent := Grp;
FEdRefLevel.SetBounds(LX+120, R1, 80, BTN_H);
FEdRefLevel.Color := CLR_PANEL;
FEdRefLevel.Font.Color := CLR_TEXT;
FEdRefLevel.Font.Name := 'Courier New';
FEdRefLevel.Font.Size := 8;
FEdRefLevel.OnExit := OnRefLevelChange;
FEdRefLevel.Text := '-20'; FEdRefLevel.Text := '-20';
MakeLbl(Grp, 'Range (dB):', LX, R1+STEP+4, LW); MakeLbl(Grp, 'Range (dB):', LX, R1+STEP+4, LW);
FEdRange := MkEdit(Grp, CX, R1+STEP, 80, OnRangeChange); FEdRange := TEdit.Create(Self);
FEdRange.Parent := Grp;
FEdRange.SetBounds(CX, R1+STEP, 80, BTN_H);
FEdRange.Color := CLR_PANEL;
FEdRange.Font.Color := CLR_TEXT;
FEdRange.Font.Name := 'Courier New';
FEdRange.Font.Size := 8;
FEdRange.OnExit := OnRangeChange;
FEdRange.Text := '110'; FEdRange.Text := '110';
MakeLbl(Grp, 'Grid Step:', CX+90, R1+STEP+4, 72); MakeLbl(Grp, 'Grid Step:', CX+90, R1+STEP+4, 72);
FCmbGridStep := MakeCombo(Grp, CX+90+76, R1+STEP, 80, OnGridStepChange); FCmbGridStep := MakeCombo(Grp, CX+90+76, R1+STEP, 80, OnGridStepChange);
@@ -656,10 +624,10 @@ end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TSettingsForm.LoadValues( procedure TSettingsForm.LoadValues(
FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer; FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecBackmult: Double; SpecAvgTimeMS: Double;
WfDet, WfAvgMode, WfAvgCount: Integer; WfDet, WfAvgMode: Integer;
WfBackmult: Double; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
RefLevel, Range, GridStep: Double; RefLevel, Range, GridStep: Double;
const AudioOutDevName: string; const AudioOutDevName: string;
const AudioInDevName: string); const AudioInDevName: string);
@@ -669,7 +637,7 @@ begin
FLoading := True; FLoading := True;
try try
// FFT Size // FFT Size
Idx := 2; // default 4096 Idx := 0; // default 4096
for i := 0 to High(FFT_SIZES) do for i := 0 to High(FFT_SIZES) do
if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end; if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end;
FCmbFFTSize.ItemIndex := Idx; FCmbFFTSize.ItemIndex := Idx;
@@ -678,7 +646,7 @@ begin
if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then
FCmbWindow.ItemIndex := WinType FCmbWindow.ItemIndex := WinType
else else
FCmbWindow.ItemIndex := 5; // Kaiser FCmbWindow.ItemIndex := 2; // Hann
// Spectrum Detector // Spectrum Detector
if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then
@@ -687,13 +655,11 @@ begin
FCmbSpecDet.ItemIndex := 0; FCmbSpecDet.ItemIndex := 0;
// Spectrum Avg Mode (WDSP value → combo index) // Spectrum Avg Mode (WDSP value → combo index)
Idx := 2; // default Recursive Idx := 0; // default None
for i := 0 to High(AVGMODE_VALUES) do for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end; if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end;
FCmbSpecAvg.ItemIndex := Idx; FCmbSpecAvg.ItemIndex := Idx;
FSpecAvgTime.Value := EnsureRange(SpecAvgTimeMS, 0.0, 5000.0);
FSpinSpecCount.Value := EnsureRange(SpecAvgCount, 1, 256);
FEdSpecBackmult.Text := FormatFloat('0.##', SpecBackmult);
// Waterfall Detector // Waterfall Detector
if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then
@@ -702,13 +668,14 @@ begin
FCmbWfDet.ItemIndex := 2; // Average FCmbWfDet.ItemIndex := 2; // Average
// Waterfall Avg Mode // Waterfall Avg Mode
Idx := 4; // default Log Recursive Idx := 1; // default Recursive
for i := 0 to High(AVGMODE_VALUES) do for i := 0 to High(WF_AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end; if WF_AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end;
FCmbWfAvg.ItemIndex := Idx; FCmbWfAvg.ItemIndex := Idx;
FWfAvgTime.Value := EnsureRange(WfAvgTimeMS, 0.0, 5000.0);
FSpinWfCount.Value := EnsureRange(WfAvgCount, 1, 256); FWfHigh.Value := EnsureRange(WfHigh, -160.0, 20.0);
FEdWfBackmult.Text := FormatFloat('0.##', WfBackmult); FWfLow.Value := EnsureRange(WfLow, -180.0, 0.0);
FWfAGCOffset.Value := EnsureRange(WfAGCOffset, -40.0, 40.0);
// Grid // Grid
FEdRefLevel.Text := FormatFloat('0.#', RefLevel); FEdRefLevel.Text := FormatFloat('0.#', RefLevel);
@@ -817,20 +784,29 @@ begin
Result := 4096; Result := 4096;
end; end;
function TSettingsForm.GetSpecBackmult: Double; function TSettingsForm.GetSpecAvgTimeMS: Double;
var V: Double; Err: Integer;
begin begin
Val(FEdSpecBackmult.Text, V, Err); Result := EnsureRange(FSpecAvgTime.Value, 0.0, 5000.0);
if Err <> 0 then V := 0.45;
Result := EnsureRange(V, 0.0, 1.0);
end; end;
function TSettingsForm.GetWfBackmult: Double; function TSettingsForm.GetWfAvgTimeMS: Double;
var V: Double; Err: Integer;
begin begin
Val(FEdWfBackmult.Text, V, Err); Result := EnsureRange(FWfAvgTime.Value, 0.0, 5000.0);
if Err <> 0 then V := 0.45; end;
Result := EnsureRange(V, 0.0, 1.0);
function TSettingsForm.GetWfHigh: Double;
begin
Result := EnsureRange(FWfHigh.Value, -160.0, 20.0);
end;
function TSettingsForm.GetWfLow: Double;
begin
Result := EnsureRange(FWfLow.Value, -180.0, 0.0);
end;
function TSettingsForm.GetWfAGCOffset: Double;
begin
Result := EnsureRange(FWfAGCOffset.Value, -40.0, 40.0);
end; end;
function TSettingsForm.GetRefLevel: Double; function TSettingsForm.GetRefLevel: Double;
@@ -880,8 +856,7 @@ begin
FCmbWindow.ItemIndex, FCmbWindow.ItemIndex,
FCmbSpecDet.ItemIndex, FCmbSpecDet.ItemIndex,
AvgVal, AvgVal,
FSpinSpecCount.Value, GetSpecAvgTimeMS);
GetSpecBackmult);
end; end;
procedure TSettingsForm.FireWaterfallChange; procedure TSettingsForm.FireWaterfallChange;
@@ -892,15 +867,17 @@ begin
if FLoading then Exit; if FLoading then Exit;
if not Assigned(FOnWaterfallChange) then Exit; if not Assigned(FOnWaterfallChange) then Exit;
AvgIdx := FCmbWfAvg.ItemIndex; AvgIdx := FCmbWfAvg.ItemIndex;
if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then if (AvgIdx >= 0) and (AvgIdx <= High(WF_AVGMODE_VALUES)) then
AvgVal := AVGMODE_VALUES[AvgIdx] AvgVal := WF_AVGMODE_VALUES[AvgIdx]
else else
AvgVal := 3; AvgVal := 1;
FOnWaterfallChange( FOnWaterfallChange(
FCmbWfDet.ItemIndex, FCmbWfDet.ItemIndex,
AvgVal, AvgVal,
FSpinWfCount.Value, GetWfAvgTimeMS,
GetWfBackmult); GetWfHigh,
GetWfLow,
GetWfAGCOffset);
end; end;
procedure TSettingsForm.FireGridChange; procedure TSettingsForm.FireGridChange;
@@ -926,10 +903,7 @@ begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject); procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject);
begin FireDisplayChange; end; begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecCountChange(Sender: TObject); procedure TSettingsForm.OnSpecAvgTimeChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecBackmultChange(Sender: TObject);
begin FireDisplayChange; end; begin FireDisplayChange; end;
procedure TSettingsForm.OnWfDetChange(Sender: TObject); procedure TSettingsForm.OnWfDetChange(Sender: TObject);
@@ -938,10 +912,10 @@ begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject); procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject);
begin FireWaterfallChange; end; begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfCountChange(Sender: TObject); procedure TSettingsForm.OnWfAvgTimeChange(Sender: TObject);
begin FireWaterfallChange; end; begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfBackmultChange(Sender: TObject); procedure TSettingsForm.OnWfThresholdChange(Sender: TObject);
begin FireWaterfallChange; end; begin FireWaterfallChange; end;
procedure TSettingsForm.OnRefLevelChange(Sender: TObject); procedure TSettingsForm.OnRefLevelChange(Sender: TObject);
+324 -77
View File
@@ -98,6 +98,7 @@ const
DISP_ID = 0; // ID для XCreateAnalyzer DISP_ID = 0; // ID для XCreateAnalyzer
DSP_BUFSIZE = 1024; DSP_BUFSIZE = 1024;
SPECTRUM_PIXELS = 1024; SPECTRUM_PIXELS = 1024;
DISPLAY_BLOCK_SIZE = 64;
// Очередь IQ пакетов между сетевым и DSP потоком // Очередь IQ пакетов между сетевым и DSP потоком
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски) IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
@@ -150,11 +151,15 @@ type
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает) // Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSpecBuf: array of Double; FSpecBuf: array of Double;
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
FDispBuf: array of Double;
FDispPos: Integer;
// Аудио выходные буферы — аллоцируем один раз // Аудио выходные буферы — аллоцируем один раз
FOutL: array of Single; FOutL: array of Single;
FOutR: array of Single; FOutR: array of Single;
// DSP поток + очередь пакетов // DSP поток + очередь пакетов
FDSPThread: TThread; FDSPThread: TThread;
FDisplayThread:TThread;
FQueue: array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem; FQueue: array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem;
FQueueHead: Integer; // пишет сетевой поток FQueueHead: Integer; // пишет сетевой поток
FQueueTail: Integer; // читает DSP поток FQueueTail: Integer; // читает DSP поток
@@ -168,14 +173,15 @@ type
// Настройки анализатора // Настройки анализатора
FFFTSize: Integer; FFFTSize: Integer;
FWindowType: Integer; FWindowType: Integer;
FDisplayFPS: Integer;
FEffectiveDisplayFPS: Double;
FDisplayPixelCount: Integer;
FSpecDetector: Integer; FSpecDetector: Integer;
FSpecAvgMode: Integer; FSpecAvgMode: Integer;
FSpecAvgCount: Integer; FSpecAvgTimeMS: Double;
FSpecBackmult: Double;
FWfDetector: Integer; FWfDetector: Integer;
FWfAvgMode: Integer; FWfAvgMode: Integer;
FWfAvgCount: Integer; FWfAvgTimeMS: Double;
FWfBackmult: Double;
FOnAudio: TOnAudioReady; FOnAudio: TOnAudioReady;
FOnSpectrum: TOnSpectrumReady; FOnSpectrum: TOnSpectrumReady;
@@ -213,11 +219,20 @@ type
function ModeToWDSP(Mode: Integer): Integer; function ModeToWDSP(Mode: Integer): Integer;
procedure ApplyDefaultFilter; procedure ApplyDefaultFilter;
procedure FeedDisplaySample(const I, Q: Double);
procedure ProcessRXBlock; procedure ProcessRXBlock;
procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer; procedure OpenAnalyzer;
procedure CloseAnalyzer; procedure CloseAnalyzer;
procedure ApplyAnalyzerSettings; procedure ApplyAnalyzerSettings;
function CalcDisplayFFTSize: Integer;
procedure CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
out EffectiveFPS: Double);
function NormalizeFFTSize(FFTSize: Integer): Integer;
procedure NormalizeDisplayParams(var Detector, AvgMode: Integer;
var AvgTimeMS: Double; ForWaterfall: Boolean);
procedure AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
out AvgCount: Integer; out Backmult: Double);
procedure ApplyNRState; procedure ApplyNRState;
procedure ApplyNBState; procedure ApplyNBState;
procedure ApplySNBState; procedure ApplySNBState;
@@ -274,8 +289,9 @@ type
// Настройки анализатора (применяются немедленно если открыт) // Настройки анализатора (применяются немедленно если открыт)
procedure SetFFTParams(FFTSize, WinType: Integer); procedure SetFFTParams(FFTSize, WinType: Integer);
procedure SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double); procedure SetDisplayFPS(FPS: Integer);
procedure SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double); procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
// Spectrum — вызывать из таймера (~20 fps) // Spectrum — вызывать из таймера (~20 fps)
procedure UpdateSpectrum; procedure UpdateSpectrum;
@@ -304,12 +320,10 @@ type
property WindowType: Integer read FWindowType; property WindowType: Integer read FWindowType;
property SpecDetector: Integer read FSpecDetector; property SpecDetector: Integer read FSpecDetector;
property SpecAvgMode: Integer read FSpecAvgMode; property SpecAvgMode: Integer read FSpecAvgMode;
property SpecAvgCount: Integer read FSpecAvgCount; property SpecAvgTimeMS: Double read FSpecAvgTimeMS;
property SpecBackmult: Double read FSpecBackmult;
property WfDetector: Integer read FWfDetector; property WfDetector: Integer read FWfDetector;
property WfAvgMode: Integer read FWfAvgMode; property WfAvgMode: Integer read FWfAvgMode;
property WfAvgCount: Integer read FWfAvgCount; property WfAvgTimeMS: Double read FWfAvgTimeMS;
property WfBackmult: Double read FWfBackmult;
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio; property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall; property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
@@ -325,6 +339,15 @@ implementation
// Аналог tx_thread в piHPSDR/transmitter.c // Аналог tx_thread в piHPSDR/transmitter.c
// =========================================================================== // ===========================================================================
type type
TDisplayThread = class(TThread)
private
FEngine: TWDSPEngine;
protected
procedure Execute; override;
public
constructor Create(AEngine: TWDSPEngine);
end;
TTXDSPThread = class(TThread) TTXDSPThread = class(TThread)
private private
FEngine: TWDSPEngine; FEngine: TWDSPEngine;
@@ -334,6 +357,48 @@ type
constructor Create(AEngine: TWDSPEngine); constructor Create(AEngine: TWDSPEngine);
end; end;
function UsePanNormOneHz(Detector: Integer): Integer;
begin
// В Thetis нормализация к 1 Гц включается только для panadapter
// и только для detector modes 2/3/4.
if Detector in [2, 3, 4] then
Result := 1
else
Result := 0;
end;
constructor TDisplayThread.Create(AEngine: TWDSPEngine);
begin
FEngine := AEngine;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TDisplayThread.Execute;
var
NextTick: QWord;
NowTick: QWord;
DelayMS: Integer;
begin
NextTick := GetTickCount64;
while not Terminated do
begin
if FEngine.FInitialized and FEngine.FAnalyzerOpen then
FEngine.UpdateSpectrum;
Inc(NextTick, QWord(Max(1, Round(1000.0 / Max(1, FEngine.FDisplayFPS)))));
NowTick := GetTickCount64;
if NextTick > NowTick then
begin
DelayMS := Integer(NextTick - NowTick);
if DelayMS > 0 then
Sleep(DelayMS);
end
else
NextTick := NowTick;
end;
end;
constructor TTXDSPThread.Create(AEngine: TWDSPEngine); constructor TTXDSPThread.Create(AEngine: TWDSPEngine);
begin begin
FEngine := AEngine; FEngine := AEngine;
@@ -342,24 +407,44 @@ begin
end; end;
procedure TTXDSPThread.Execute; procedure TTXDSPThread.Execute;
// Аналог tx_thread в piHPSDR/transmitter.c:
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
var var
Avail: Integer; Avail: Integer;
PeriodMs: Integer;
begin begin
// 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс
PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate);
while not Terminated do while not Terminated do
begin begin
RTLEventWaitFor(FEngine.FTXMicSem, 50); // Ждём сигнала от mic-данных ИЛИ истечения периода —
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
if Terminated then Break; if Terminated then Break;
if not FEngine.FTXActive then Continue; if not FEngine.FTXActive then Continue;
// Обрабатываем все накопившиеся блоки // Обрабатываем накопившиеся блоки с реальными mic-данными
repeat repeat
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1); and (TX_MIC_RING - 1);
if Avail >= FEngine.FAudioBufSize then if (Avail >= FEngine.FAudioBufSize) and not Terminated then
FEngine.ProcessTXBlock FEngine.ProcessTXBlock
else else
Break; Break;
until Terminated; until False;
if Terminated then Break;
// Если mic-данных не было — всё равно обрабатываем один блок тишины.
// Аналог piHPSDR: если mic_ring пуст, tx_add_mic_sample получает 0.0.
// Это держит DUC IQ поток и обновляет спектр/водопад TX-сигналом.
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1);
if Avail < FEngine.FAudioBufSize then
FEngine.ProcessTXBlock;
end; end;
end; end;
@@ -467,15 +552,16 @@ begin
FRXAccPos := 0; FRXAccPos := 0;
// Analyzer defaults // Analyzer defaults
FFFTSize := 4096; FFFTSize := 4096;
FWindowType := 5; // Kaiser FWindowType := 2; // Hann
FDisplayFPS := 20;
FEffectiveDisplayFPS := 20.0;
FDisplayPixelCount := SPECTRUM_PIXELS;
FSpecDetector := 0; // Peak FSpecDetector := 0; // Peak
FSpecAvgMode := 3; // Log Recursive FSpecAvgMode := 3; // Log Recursive
FSpecAvgCount := 2; FSpecAvgTimeMS := 30.0;
FSpecBackmult := 0.45; FWfDetector := 0; // Peak
FWfDetector := 2; // Average
FWfAvgMode := 3; // Log Recursive FWfAvgMode := 3; // Log Recursive
FWfAvgCount := 2; FWfAvgTimeMS := 120.0;
FWfBackmult := 0.45;
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2) SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2) SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
@@ -484,6 +570,8 @@ begin
SetLength(FRXAccI, FBufSize); SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize); SetLength(FRXAccQ, FBufSize);
SetLength(FSpecBuf, FBufSize * 2); SetLength(FSpecBuf, FBufSize * 2);
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
FDispPos := 0;
SetLength(FOutL, FAudioBufSize); SetLength(FOutL, FAudioBufSize);
SetLength(FOutR, FAudioBufSize); SetLength(FOutR, FAudioBufSize);
@@ -493,6 +581,7 @@ begin
FDSPRunning := False; FDSPRunning := False;
FQueueSem := RTLEventCreate; FQueueSem := RTLEventCreate;
FDSPThread := nil; FDSPThread := nil;
FDisplayThread := nil;
// TX инициализация // TX инициализация
FTXMicHead := 0; FTXMicHead := 0;
@@ -523,47 +612,75 @@ begin
if FAnalyzerOpen then Exit; if FAnalyzerOpen then Exit;
Success := -1; Success := -1;
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil); XCreateAnalyzer(DISP_ID, @Success, 262144, 1, 1, nil);
if Success <> 0 then Exit; if Success <> 0 then Exit;
FFlp[0] := 0; FFlp[0] := 0;
ApplyAnalyzerSettings; ApplyAnalyzerSettings;
FAnalyzerOpen := True; FAnalyzerOpen := True;
if FDisplayThread = nil then
FDisplayThread := TDisplayThread.Create(Self);
end; end;
procedure TWDSPEngine.ApplyAnalyzerSettings; procedure TWDSPEngine.ApplyAnalyzerSettings;
// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров // Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
var var
AnalyzerFFT: Integer;
AvgCount: Integer;
Backmult: Double;
WaterfallAvgCount: Integer;
WaterfallBackmult: Double;
MaxW: Integer; MaxW: Integer;
Overlap: Integer;
const
KEEP_TIME_SEC = 0.10;
var
DisplaySampleRate: Integer;
DisplayPixels: Integer;
begin begin
// MaxW = FFTSize — минимально допустимо (piHPSDR approach) // FFT Size пользователя управляет именно display-analyzer, как в Thetis,
MaxW := FFFTSize; // но визуальная нормализация должна идти к ширине видимого окна, как в piHPSDR.
AnalyzerFFT := CalcDisplayFFTSize;
FDispPos := 0;
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
FDisplayPixelCount := DisplayPixels;
DisplaySampleRate := FSampleRate;
// n_pixout=2: pixout=0 — спектр, pixout=1 водопад // Раздельные выходы display: pixout=0 панорама, pixout=1 водопад.
SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], FFFTSize, FAudioBufSize, FWindowType, 14.0, SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE, FWindowType, 14.0,
0, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW); Overlap, 0, 0.0, 0.0, DisplayPixels, 1, 0, 0.0, 0.0, MaxW);
// Спектр (pixout=0) AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WaterfallAvgCount, WaterfallBackmult);
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector); SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode); SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount); SetDisplayNumAverage(DISP_ID, 0, AvgCount);
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult); SetDisplayAvBackmult(DISP_ID, 0, Backmult);
SetDisplayNormOneHz(DISP_ID, 0, 0); SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
// Водопад (pixout=1)
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector); SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode); SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount); SetDisplayNumAverage(DISP_ID, 1, WaterfallAvgCount);
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult); SetDisplayAvBackmult(DISP_ID, 1, WaterfallBackmult);
SetDisplayNormOneHz(DISP_ID, 1, 0); SetDisplayNormOneHz(DISP_ID, 1, 0);
SetDisplaySampleRate(DISP_ID, FSampleRate); // В Thetis сюда идёт реальный sample rate приёмника.
SetDisplaySampleRate(DISP_ID, DisplaySampleRate);
end; end;
procedure TWDSPEngine.CloseAnalyzer; procedure TWDSPEngine.CloseAnalyzer;
begin begin
if not FAnalyzerOpen then Exit; if not FAnalyzerOpen then Exit;
if Assigned(FDisplayThread) then
begin
FDisplayThread.Terminate;
FDisplayThread.WaitFor;
FreeAndNil(FDisplayThread);
end;
DestroyAnalyzer(DISP_ID); DestroyAnalyzer(DISP_ID);
FAnalyzerOpen := False; FAnalyzerOpen := False;
end; end;
@@ -719,6 +836,8 @@ begin
SetLength(FSpecBuf, FBufSize * 2); SetLength(FSpecBuf, FBufSize * 2);
SetLength(FRXAccI, FBufSize); SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize); SetLength(FRXAccQ, FBufSize);
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
FDispPos := 0;
// Переинициализируем WDSP с новым rate // Переинициализируем WDSP с новым rate
Open; Open;
@@ -771,6 +890,8 @@ begin
if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000); if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000); if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);
FeedDisplaySample(IR * SCALE, QR * SCALE);
FRXAccI[FRXAccPos] := IR * SCALE; FRXAccI[FRXAccPos] := IR * SCALE;
FRXAccQ[FRXAccPos] := QR * SCALE; FRXAccQ[FRXAccPos] := QR * SCALE;
Inc(FRXAccPos); Inc(FRXAccPos);
@@ -780,6 +901,22 @@ begin
end; end;
end; end;
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
begin
if not FAnalyzerOpen then Exit;
if FTXActive then Exit;
FDispBuf[FDispPos * 2] := I;
FDispBuf[FDispPos * 2 + 1] := Q;
Inc(FDispPos);
if FDispPos >= DISPLAY_BLOCK_SIZE then
begin
Spectrum0(1, DISP_ID, 0, 0, @FDispBuf[0]);
FDispPos := 0;
end;
end;
procedure TWDSPEngine.ProcessRXBlock; procedure TWDSPEngine.ProcessRXBlock;
var var
i: Integer; i: Integer;
@@ -797,21 +934,10 @@ begin
FRXIn[i * 2 + 1] := FRXAccQ[i]; FRXIn[i * 2 + 1] := FRXAccQ[i];
end; end;
// Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn
if FAnalyzerOpen then
begin
Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double));
end;
// DSP обработка // DSP обработка
Err := 0; Err := 0;
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
// Подаём RX IQ в анализатор только когда не передаём.
// Во время TX анализатор кормит ProcessTXBlock (TX сигнал на той же частоте).
if FAnalyzerOpen and not FTXActive then
Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]);
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter. // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй // GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь! // вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
@@ -933,7 +1059,11 @@ end;
procedure TWDSPEngine.SetSpectrumWidth(W: Integer); procedure TWDSPEngine.SetSpectrumWidth(W: Integer);
begin begin
if W > 0 then FLastSpectrumW := W; if W <= 0 then Exit;
if FLastSpectrumW = W then Exit;
FLastSpectrumW := W;
if FAnalyzerOpen then
ApplyAnalyzerSettings;
end; end;
procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer); procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer);
@@ -1104,6 +1234,7 @@ end;
procedure TWDSPEngine.SetTXRun(Run: Boolean); procedure TWDSPEngine.SetTXRun(Run: Boolean);
begin begin
FTXActive := Run; FTXActive := Run;
FDispPos := 0;
if Run then if Run then
begin begin
@@ -1135,13 +1266,21 @@ end;
procedure TWDSPEngine.ProcessTXBlock; procedure TWDSPEngine.ProcessTXBlock;
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов // Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback // через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback.
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
// если mic ring buffer содержит достаточно данных — берём их,
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
var var
i, tail: Integer; i, tail: Integer;
Err: Integer; Err: Integer;
Avail: Integer;
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
if Avail >= FAudioBufSize then
begin
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn // Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
tail := FTXMicTail; tail := FTXMicTail;
for i := 0 to FAudioBufSize - 1 do for i := 0 to FAudioBufSize - 1 do
@@ -1151,6 +1290,11 @@ begin
tail := (tail + 1) and (TX_MIC_RING - 1); tail := (tail + 1) and (TX_MIC_RING - 1);
end; end;
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread) FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
end else
begin
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end;
Err := 0; Err := 0;
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err); fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
@@ -1205,30 +1349,29 @@ procedure TWDSPEngine.UpdateSpectrum;
var var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single; PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer; Flag: Integer;
i: Integer; i, N: Integer;
begin begin
if not FInitialized or not FAnalyzerOpen then Exit; if not FInitialized or not FAnalyzerOpen then Exit;
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
// Спектр (pixout=0)
Flag := 0; Flag := 0;
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag); GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
if Flag <> 0 then if Flag <> 0 then
begin begin
for i := 0 to SPECTRUM_PIXELS - 1 do for i := 0 to N - 1 do
FSpectrumPixels[i] := PixBuf[i]; FSpectrumPixels[i] := PixBuf[i];
if Assigned(FOnSpectrum) then if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS); FOnSpectrum(FSpectrumPixels, N);
end; end;
// Водопад (pixout=1)
Flag := 0; Flag := 0;
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag); GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
if Flag <> 0 then if Flag <> 0 then
begin begin
for i := 0 to SPECTRUM_PIXELS - 1 do for i := 0 to N - 1 do
FWaterfallPixels[i] := PixBuf[i]; FWaterfallPixels[i] := PixBuf[i];
if Assigned(FOnWaterfall) then if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS); FOnWaterfall(FWaterfallPixels, N);
end; end;
end; end;
@@ -1254,46 +1397,150 @@ end;
// Настройки анализатора (применяются немедленно) // Настройки анализатора (применяются немедленно)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer); function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
begin begin
// Ограничения: FFT size должен быть >= bf_sz (FAudioBufSize = 1024) // Thetis-style display FFT range.
// и степенью двойки, не превышать m_size=16384 (из XCreateAnalyzer) if FFTSize < 4096 then FFTSize := 4096;
if FFTSize < FAudioBufSize then FFTSize := FAudioBufSize; Result := 4096;
if FFTSize > 16384 then FFTSize := 16384; while Result < FFTSize do
Result := Result shl 1;
if Result > 262144 then
Result := 262144;
end;
function TWDSPEngine.CalcDisplayFFTSize: Integer;
begin
// FFT Size теперь снова управляет display-analyzer как в Thetis,
// но не влияет на аудио DSP цепочку.
Result := FFFTSize;
end;
procedure TWDSPEngine.CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
out EffectiveFPS: Double);
var
TargetFPS: Double;
begin
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
// Thetis/piHPSDR-style timing:
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
EffectiveFPS := TargetFPS;
end;
procedure TWDSPEngine.NormalizeDisplayParams(var Detector, AvgMode: Integer;
var AvgTimeMS: Double; ForWaterfall: Boolean);
begin
Detector := EnsureRange(Detector, 0, 4);
if ForWaterfall then
begin
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
if AvgMode < 0 then AvgMode := 1;
end
else
if AvgMode < -1 then AvgMode := -1;
if AvgMode > 3 then AvgMode := 3;
if AvgMode <= 0 then
AvgTimeMS := 0.0
else
AvgTimeMS := EnsureRange(AvgTimeMS, 10.0, 5000.0);
end;
procedure TWDSPEngine.AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
out AvgCount: Integer; out Backmult: Double);
var
TimeSec: Double;
Frames: Double;
FPSv: Double;
begin
if AvgMode <= 0 then
begin
AvgCount := 1;
Backmult := 0.0;
Exit;
end;
TimeSec := EnsureRange(AvgTimeMS, 10.0, 5000.0) * 0.001;
FPSv := Max(1.0, FEffectiveDisplayFPS);
Frames := FPSv * TimeSec;
AvgCount := EnsureRange(Round(Frames), 2, 60);
Backmult := Exp(-1.0 / Max(1E-6, FPSv * TimeSec));
Backmult := EnsureRange(Backmult, 0.0, 0.9999);
end;
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
var
NewFFT: Integer;
begin
NewFFT := NormalizeFFTSize(FFTSize);
if WinType < 0 then WinType := 0; if WinType < 0 then WinType := 0;
if WinType > 6 then WinType := 6; if WinType > 6 then WinType := 6;
FFFTSize := FFTSize;
if (FFFTSize = NewFFT) and (FWindowType = WinType) then Exit;
FFFTSize := NewFFT;
FWindowType := WinType; FWindowType := WinType;
if FAnalyzerOpen then if FAnalyzerOpen then
ApplyAnalyzerSettings; ApplyAnalyzerSettings;
end; end;
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; procedure TWDSPEngine.SetDisplayFPS(FPS: Integer);
Backmult: Double);
begin begin
FSpecDetector := Detector; FPS := EnsureRange(FPS, 1, 100);
FSpecAvgMode := AvgMode; if FDisplayFPS = FPS then Exit;
FSpecAvgCount := AvgCount; FDisplayFPS := FPS;
FSpecBackmult := Backmult; if FAnalyzerOpen then
if not FAnalyzerOpen then Exit; ApplyAnalyzerSettings;
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount);
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult);
end; end;
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode: Integer;
Backmult: Double); AvgTimeMS: Double);
var
AvgCount: Integer;
Backmult: Double;
begin begin
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);
if (FSpecDetector = Detector) and
(FSpecAvgMode = AvgMode) and
(Abs(FSpecAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;
FSpecDetector := Detector;
FSpecAvgMode := AvgMode;
FSpecAvgTimeMS := AvgTimeMS;
if not FAnalyzerOpen then Exit;
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(DISP_ID, 0, AvgCount);
SetDisplayAvBackmult(DISP_ID, 0, Backmult);
SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
end;
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
AvgTimeMS: Double);
var
AvgCount: Integer;
Backmult: Double;
begin
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);
if (FWfDetector = Detector) and
(FWfAvgMode = AvgMode) and
(Abs(FWfAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;
FWfDetector := Detector; FWfDetector := Detector;
FWfAvgMode := AvgMode; FWfAvgMode := AvgMode;
FWfAvgCount := AvgCount; FWfAvgTimeMS := AvgTimeMS;
FWfBackmult := Backmult;
if not FAnalyzerOpen then Exit; if not FAnalyzerOpen then Exit;
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector); SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode); SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount); SetDisplayNumAverage(DISP_ID, 1, AvgCount);
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult); SetDisplayAvBackmult(DISP_ID, 1, Backmult);
SetDisplayNormOneHz(DISP_ID, 1, 0);
end; end;
end. end.
+9 -3
View File
@@ -56,6 +56,7 @@ begin
'button:hover{background:linear-gradient(180deg,#4c4c4c,#383838)}' + 'button:hover{background:linear-gradient(180deg,#4c4c4c,#383838)}' +
'button.on{background:linear-gradient(180deg,#1e4d1e,#163216);border-color:#3d7a3d;color:#7fff7f}' + 'button.on{background:linear-gradient(180deg,#1e4d1e,#163216);border-color:#3d7a3d;color:#7fff7f}' +
'button.rx-tx{background:linear-gradient(180deg,#4d1e1e,#321616);border-color:#7a3d3d;color:#ff8080}' + 'button.rx-tx{background:linear-gradient(180deg,#4d1e1e,#321616);border-color:#7a3d3d;color:#ff8080}' +
'button.mox-on{background:linear-gradient(180deg,#6d1e1e,#4d0a0a);border-color:#cc3333;color:#ffaaaa;font-weight:bold}' +
'select{background:#222;color:#bbb;border:1px solid #484848;border-radius:3px;padding:0 4px;font-size:11px;height:22px;cursor:pointer}' + 'select{background:#222;color:#bbb;border:1px solid #484848;border-radius:3px;padding:0 4px;font-size:11px;height:22px;cursor:pointer}' +
'.sl-g{display:flex;align-items:center;gap:3px}' + '.sl-g{display:flex;align-items:center;gap:3px}' +
'.sl-lb{font-size:10px;color:#777;white-space:nowrap}' + '.sl-lb{font-size:10px;color:#777;white-space:nowrap}' +
@@ -110,6 +111,7 @@ begin
'<button id="toneBtn">TONE</button>' + '<button id="toneBtn">TONE</button>' +
'<span class="sl-sep">|</span>' + '<span class="sl-sep">|</span>' +
'<div class="sl-g"><span class="sl-lb">TX</span><input type="range" id="txSl" min="0" max="100" value="50"><span class="sl-val" id="txVal">50</span></div>' + '<div class="sl-g"><span class="sl-lb">TX</span><input type="range" id="txSl" min="0" max="100" value="50"><span class="sl-val" id="txVal">50</span></div>' +
'<button id="moxBtn">MOX</button>' +
'<span class="sl-sep">|</span>' + '<span class="sl-sep">|</span>' +
'<div class="tsep"></div>' + '<div class="tsep"></div>' +
'<select id="agcSel">' + '<select id="agcSel">' +
@@ -206,7 +208,9 @@ begin
'q("volSl").oninput=function(e){ws2({cmd:"volume",v:+e.target.value});var v=q("volVal");if(v)v.textContent=e.target.value;};' + 'q("volSl").oninput=function(e){ws2({cmd:"volume",v:+e.target.value});var v=q("volVal");if(v)v.textContent=e.target.value;};' +
'q("rfSl").oninput=function(e){ws2({cmd:"agctop",db:+e.target.value});var v=q("rfVal");if(v)v.textContent=e.target.value+"dB";};' + 'q("rfSl").oninput=function(e){ws2({cmd:"agctop",db:+e.target.value});var v=q("rfVal");if(v)v.textContent=e.target.value+"dB";};' +
'q("txSl").oninput=function(e){var v=q("txVal");if(v)v.textContent=e.target.value;};' + 'q("txSl").oninput=function(e){var v=+e.target.value;var lbl=q("txVal");if(lbl)lbl.textContent=v;pend("drive");ws2({cmd:"drive",v:v});};' +
'function updMoxBtn(tx){var b=q("moxBtn");if(!b)return;b.textContent=tx?"TX":"MOX";b.classList.toggle("mox-on",!!tx);}' +
'q("moxBtn").onclick=function(){audioKick();cur.transmitting=!cur.transmitting;pend("mox");updMoxBtn(cur.transmitting);cmd("set_mox",cur.transmitting);};' +
// DSP кнопки — cur обновляется локально сразу, не ждём ответа сервера // DSP кнопки — cur обновляется локально сразу, не ждём ответа сервера
'q("nrBtn").onclick=function(){cur.nr_mode=((cur.nr_mode|0)+1)%5;cur.nr=cur.nr_mode!==0;updNrBtn();pend("nr");ws2({cmd:"set_nr_mode",mode:cur.nr_mode});};' + 'q("nrBtn").onclick=function(){cur.nr_mode=((cur.nr_mode|0)+1)%5;cur.nr=cur.nr_mode!==0;updNrBtn();pend("nr");ws2({cmd:"set_nr_mode",mode:cur.nr_mode});};' +
@@ -351,7 +355,7 @@ begin
'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' + 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' +
// Маппинг: server JSON field → pend key // Маппинг: server JSON field → pend key
'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc"};' + 'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive"};' +
'function applyState(msg){' + 'function applyState(msg){' +
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' + 'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' +
@@ -376,11 +380,13 @@ begin
'if(!isPending("nr"))updNrBtn();if(!isPending("nb"))updNbBtn();' + 'if(!isPending("nr"))updNrBtn();if(!isPending("nb"))updNbBtn();' +
'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' + 'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' +
'if(!isPending("run"))updRunBtn(!!cur.running);' + 'if(!isPending("run"))updRunBtn(!!cur.running);' +
'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' +
'if(!isPending("drive")){const ts=q("txSl");if(ts&&!ts.matches(":active")){ts.value=cur.drive||0;var tv=q("txVal");if(tv)tv.textContent=cur.drive||0;}}' +
'if(!isPending("mode")){syncModGrp(cur.mode||0);}' + 'if(!isPending("mode")){syncModGrp(cur.mode||0);}' +
'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' + 'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' +
'paintSM(Number(cur.smeter_dbm||-130));' + 'paintSM(Number(cur.smeter_dbm||-130));' +
'const mn=cur.mode_name||(MODE_N[cur.mode||0]||"?");' + 'const mn=cur.mode_name||(MODE_N[cur.mode||0]||"?");' +
'stEl.textContent=(cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")+" | "+mn+" | "+((activeVfoHz()/1e6).toFixed(3))+" MHz";}' + 'stEl.textContent=(cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")+" | "+(cur.transmitting?"TX":"RX")+" | "+mn+" | "+((activeVfoHz()/1e6).toFixed(3))+" MHz";}' +
'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' + 'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' +
'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===0)return[-bw,-100];if(m===1)return[100,bw];return[-bw/2,bw/2];}' + 'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===0)return[-bw,-100];if(m===1)return[100,bw];return[-bw/2,bw/2];}' +
+33 -4
View File
@@ -124,6 +124,8 @@ type
TWebCmdFreqB = procedure(Hz: Double) of object; TWebCmdFreqB = procedure(Hz: Double) of object;
TWebCmdActiveVfo = procedure(Idx: Integer) of object; TWebCmdActiveVfo = procedure(Idx: Integer) of object;
TWebCmdCenter = procedure(Hz: Double) of object; TWebCmdCenter = procedure(Hz: Double) of object;
TWebCmdMOX = procedure(On_: Boolean) of object;
TWebCmdDrive = procedure(V: Integer) of object;
// ── Главный класс сервера ───────────────────────────────────────────────── // ── Главный класс сервера ─────────────────────────────────────────────────
TWebServer = class TWebServer = class
@@ -204,6 +206,10 @@ type
FOnFreqB: TWebCmdFreqB; FOnFreqB: TWebCmdFreqB;
FOnActiveVfo: TWebCmdActiveVfo; FOnActiveVfo: TWebCmdActiveVfo;
FOnCenter: TWebCmdCenter; FOnCenter: TWebCmdCenter;
FOnMOX: TWebCmdMOX;
FOnDrive: TWebCmdDrive;
FTransmitting: Boolean;
FDriveLevel: Integer;
FWebClientActive: Boolean; FWebClientActive: Boolean;
@@ -246,7 +252,8 @@ type
TrxRunning, Muted, Ctun: Boolean; TrxRunning, Muted, Ctun: Boolean;
NRMode, NBMode: Integer; SNB, ANF: Boolean; NRMode, NBMode: Integer; SNB, ANF: Boolean;
CenterHz: Double; FilterIdx: Integer; CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer); VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer);
property WebClientActive: Boolean read FWebClientActive; property WebClientActive: Boolean read FWebClientActive;
@@ -270,6 +277,8 @@ type
property OnFreqB: TWebCmdFreqB read FOnFreqB write FOnFreqB; property OnFreqB: TWebCmdFreqB read FOnFreqB write FOnFreqB;
property OnActiveVfo: TWebCmdActiveVfo read FOnActiveVfo write FOnActiveVfo; property OnActiveVfo: TWebCmdActiveVfo read FOnActiveVfo write FOnActiveVfo;
property OnCenter: TWebCmdCenter read FOnCenter write FOnCenter; property OnCenter: TWebCmdCenter read FOnCenter write FOnCenter;
property OnMOX: TWebCmdMOX read FOnMOX write FOnMOX;
property OnDrive: TWebCmdDrive read FOnDrive write FOnDrive;
end; end;
implementation implementation
@@ -981,6 +990,20 @@ begin
HzF := JsonGetFloat(Json, 'hz', FCenterHz); HzF := JsonGetFloat(Json, 'hz', FCenterHz);
FStateLock.Enter; FCenterHz := HzF; FStateLock.Leave; FStateLock.Enter; FCenterHz := HzF; FStateLock.Leave;
if Assigned(FOnCenter) then FOnCenter(HzF); if Assigned(FOnCenter) then FOnCenter(HzF);
end
else if Cmd = 'set_mox' then
begin
On_ := JsonGetBool(Json, 'on', FTransmitting);
FStateLock.Enter; FTransmitting := On_; FStateLock.Leave;
if Assigned(FOnMOX) then FOnMOX(On_);
end
else if Cmd = 'drive' then
begin
V := JsonGetInt(Json, 'v', FDriveLevel);
if V < 0 then V := 0;
if V > 100 then V := 100;
FStateLock.Enter; FDriveLevel := V; FStateLock.Leave;
if Assigned(FOnDrive) then FOnDrive(V);
end; end;
end; end;
@@ -1077,7 +1100,8 @@ begin
'"span_hz":%.0f,"center_hz":%.0f,"volume":%d,' + '"span_hz":%.0f,"center_hz":%.0f,"volume":%d,' +
'"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' + '"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' +
'"running":%s,"mute":%s,"ctun":%s,' + '"running":%s,"mute":%s,"ctun":%s,' +
'"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s}', '"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s,' +
'"transmitting":%s,"drive":%d}',
[FFreq, FVfoB, FActiveVfo, [FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8], FMode, MODE_N[FMode mod 8],
FFilterIdx, FFilterBW, FFilterIdx, FFilterBW,
@@ -1095,7 +1119,9 @@ begin
BoolToStr(FNBMode <> 0, 'true', 'false'), BoolToStr(FNBMode <> 0, 'true', 'false'),
BoolToStr(FSNB, 'true', 'false'), BoolToStr(FSNB, 'true', 'false'),
BoolToStr(FANF, 'true', 'false'), BoolToStr(FANF, 'true', 'false'),
BoolToStr(FConnected, 'true', 'false') BoolToStr(FConnected, 'true', 'false'),
BoolToStr(FTransmitting, 'true', 'false'),
FDriveLevel
]); ]);
finally finally
FStateLock.Leave; FStateLock.Leave;
@@ -1160,7 +1186,8 @@ procedure TWebServer.PushSpectrum(
TrxRunning, Muted, Ctun: Boolean; TrxRunning, Muted, Ctun: Boolean;
NRMode, NBMode: Integer; SNB, ANF: Boolean; NRMode, NBMode: Integer; SNB, ANF: Boolean;
CenterHz: Double; FilterIdx: Integer; CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer); VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer);
var N, i: Integer; var N, i: Integer;
begin begin
if not FRunning then Exit; if not FRunning then Exit;
@@ -1192,6 +1219,8 @@ begin
FFilterIdx := FilterIdx; FFilterIdx := FilterIdx;
FVfoB := VfoB; FVfoB := VfoB;
FActiveVfo := ActiveVfo; FActiveVfo := ActiveVfo;
FTransmitting := Transmitting;
FDriveLevel := DriveLevel;
finally finally
FStateLock.Leave; FStateLock.Leave;
end; end;