mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix fft, waterfall
This commit is contained in:
+430
-108
@@ -26,7 +26,7 @@ uses
|
||||
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
|
||||
LCLIntf, LCLType, GraphType,
|
||||
HPSDRProtocol, HPSDRNetwork,
|
||||
WDSPEngine, AudioOutput, AudioInput,
|
||||
WDSP, WDSPEngine, AudioOutput, AudioInput,
|
||||
IntfGraphics, FPImage,
|
||||
Settings,
|
||||
WebServer;
|
||||
@@ -45,6 +45,7 @@ const
|
||||
CLR_METER_ON = TColor($0000CC44);
|
||||
CLR_METER_OVR= TColor($000000CC);
|
||||
CLR_SPECTRUM = TColor($0044FF44);
|
||||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||||
|
||||
BAND_COUNT = 11;
|
||||
MODE_COUNT = 8;
|
||||
@@ -229,11 +230,15 @@ type
|
||||
|
||||
// ---- Spectrum ----
|
||||
FSpectrumBuf: array[0..1023] of Single;
|
||||
FWfAvgBuf: array[0..1023] of Single;
|
||||
FWaterfallBuf: array[0..1023] of Single; // WDSP waterfall pixout=1 data
|
||||
FWaterfallBuf: array[0..1023] of Single; // текущая строка водопада после локального сглаживания
|
||||
FSpectrumBufCount: Integer;
|
||||
FWaterfallBufCount: Integer;
|
||||
FSpectrumBitmap: TBitmap;
|
||||
FWfAGCEnabled: Boolean;
|
||||
FWfNFEnabled: Boolean;
|
||||
FWfManualHigh: Double;
|
||||
FWfManualLow: Double;
|
||||
FWfAGCOffset: Double;
|
||||
FWfHigh: Double;
|
||||
FWfLow: Double;
|
||||
// Spectrum grid settings
|
||||
@@ -242,13 +247,16 @@ type
|
||||
FSpecGridStep: Double; // grid step in dB (e.g. 10)
|
||||
FWaterfallBitmap:TBitmap;
|
||||
FWaterfallTemp: TBitmap;
|
||||
FWaterfallDirty: Boolean;
|
||||
FWaterfallFrameInterval: Integer;
|
||||
FWaterfallFrameCounter: Integer;
|
||||
FSpectrumWidth: Integer;
|
||||
FSpectrumHeight: Integer;
|
||||
FWaterfallHeight:Integer;
|
||||
// ---- Visibility settings ----
|
||||
FShowSpectrum: Boolean;
|
||||
FShowWaterfall: Boolean;
|
||||
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30)
|
||||
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (1..100)
|
||||
// ---- Splitter между спектром и водопадом ----
|
||||
FSplitter: TPanel; // визуальная полоса-разделитель
|
||||
FSplitterDrag: Boolean; // True = идёт перетаскивание
|
||||
@@ -403,6 +411,7 @@ type
|
||||
procedure ResetSpectrumBuf;
|
||||
procedure ResizeSpectrumPanels;
|
||||
procedure RecreateDSPEngine(ASampleRate: Integer);
|
||||
function EnsureWDSPWisdom: Boolean;
|
||||
|
||||
// Network callbacks
|
||||
procedure OnDeviceFound(const Dev: THPSDRDevice);
|
||||
@@ -498,6 +507,8 @@ type
|
||||
procedure WebOnFreqB(Hz: Double);
|
||||
procedure WebOnActiveVfo(Idx: Integer);
|
||||
procedure WebOnCenter(Hz: Double);
|
||||
procedure WebOnMOX(On_: Boolean);
|
||||
procedure WebOnDrive(V: Integer);
|
||||
// Synchronize-обёртки (выполняются в UI-потоке)
|
||||
procedure SyncWebFreq;
|
||||
procedure SyncWebMode;
|
||||
@@ -519,6 +530,8 @@ type
|
||||
procedure SyncWebFreqB;
|
||||
procedure SyncWebActiveVfo;
|
||||
procedure SyncWebCenter;
|
||||
procedure SyncWebMOX;
|
||||
procedure SyncWebDrive;
|
||||
procedure BtnWfAGCClick(Sender: TObject);
|
||||
procedure BtnWfNFClick(Sender: TObject);
|
||||
procedure BtnHidePanelClick(Sender: TObject);
|
||||
@@ -555,10 +568,10 @@ type
|
||||
|
||||
public
|
||||
// Методы для SettingsForm — немедленное применение настроек
|
||||
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
|
||||
SpecAvgCount: Integer; SpecBackmult: Double);
|
||||
procedure ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
|
||||
WfBackmult: Double);
|
||||
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||||
SpecAvgTimeMS: Double);
|
||||
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||||
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
||||
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||||
@@ -575,6 +588,29 @@ type
|
||||
var Handled: Boolean);
|
||||
end;
|
||||
|
||||
TWisdomBuildThread = class(TThread)
|
||||
private
|
||||
FDirectory: string;
|
||||
FError: string;
|
||||
protected
|
||||
procedure Execute; override;
|
||||
public
|
||||
constructor Create(const ADirectory: string);
|
||||
property ErrorText: string read FError;
|
||||
end;
|
||||
|
||||
TWisdomProgressDialog = class(TForm)
|
||||
private
|
||||
FInfoLabel: TLabel;
|
||||
FProgress: TProgressBar;
|
||||
FTimer: TTimer;
|
||||
FThread: TWisdomBuildThread;
|
||||
FPhase: Integer;
|
||||
procedure TimerTick(Sender: TObject);
|
||||
public
|
||||
constructor Create(AOwner: TComponent; AThread: TWisdomBuildThread); reintroduce;
|
||||
end;
|
||||
|
||||
var
|
||||
MainForm: TMainForm;
|
||||
|
||||
@@ -584,6 +620,123 @@ uses SettingsForm;
|
||||
|
||||
{$R *.lfm}
|
||||
|
||||
function WaterfallColorThetis(Level: Integer): LongWord;
|
||||
const
|
||||
STOPS: array[0..5] of LongWord = (
|
||||
$000000, // black
|
||||
$0A1A48, // deep blue
|
||||
$0088CC, // cyan
|
||||
$E0C020, // yellow
|
||||
$D84A12, // orange-red
|
||||
$FFFFFF // white
|
||||
);
|
||||
var
|
||||
Seg, Base: Integer;
|
||||
T: Double;
|
||||
C0, C1: LongWord;
|
||||
R, G, B: Integer;
|
||||
begin
|
||||
Level := EnsureRange(Level, 0, 255);
|
||||
Seg := Min(4, Level div 51);
|
||||
Base := Seg * 51;
|
||||
T := (Level - Base) / 51.0;
|
||||
C0 := STOPS[Seg];
|
||||
C1 := STOPS[Seg + 1];
|
||||
|
||||
R := Round(((C0 shr 16) and $FF) * (1.0 - T) + ((C1 shr 16) and $FF) * T);
|
||||
G := Round(((C0 shr 8) and $FF) * (1.0 - T) + ((C1 shr 8) and $FF) * T);
|
||||
B := Round((C0 and $FF) * (1.0 - T) + (C1 and $FF) * T);
|
||||
Result := (R shl 16) or (G shl 8) or B;
|
||||
end;
|
||||
|
||||
constructor TWisdomBuildThread.Create(const ADirectory: string);
|
||||
begin
|
||||
inherited Create(False);
|
||||
FreeOnTerminate := False;
|
||||
FDirectory := ADirectory;
|
||||
FError := '';
|
||||
end;
|
||||
|
||||
procedure TWisdomBuildThread.Execute;
|
||||
var
|
||||
DirA: AnsiString;
|
||||
begin
|
||||
try
|
||||
if not Assigned(@WDSPwisdom) then Exit;
|
||||
DirA := AnsiString(FDirectory);
|
||||
WDSPwisdom(PAnsiChar(DirA));
|
||||
except
|
||||
on E: Exception do
|
||||
FError := E.ClassName + ': ' + E.Message;
|
||||
end;
|
||||
end;
|
||||
|
||||
constructor TWisdomProgressDialog.Create(AOwner: TComponent;
|
||||
AThread: TWisdomBuildThread);
|
||||
begin
|
||||
inherited CreateNew(AOwner);
|
||||
FThread := AThread;
|
||||
FPhase := 0;
|
||||
|
||||
Caption := 'WDSP Wisdom';
|
||||
Width := 520;
|
||||
Height := 140;
|
||||
Position := poScreenCenter;
|
||||
BorderStyle := bsDialog;
|
||||
BorderIcons := [];
|
||||
Color := CLR_BG;
|
||||
Font.Name := 'Courier New';
|
||||
Font.Size := 9;
|
||||
Font.Color := CLR_TEXT;
|
||||
|
||||
FInfoLabel := TLabel.Create(Self);
|
||||
FInfoLabel.Parent := Self;
|
||||
FInfoLabel.SetBounds(16, 16, 480, 40);
|
||||
FInfoLabel.AutoSize := False;
|
||||
FInfoLabel.WordWrap := True;
|
||||
FInfoLabel.Font.Color := CLR_TEXT;
|
||||
FInfoLabel.Caption :=
|
||||
'Creating FFTW wisdom for WDSP. This is done once and may take a while on the first run.';
|
||||
|
||||
FProgress := TProgressBar.Create(Self);
|
||||
FProgress.Parent := Self;
|
||||
FProgress.SetBounds(16, 72, 480, 22);
|
||||
FProgress.Min := 0;
|
||||
FProgress.Max := 100;
|
||||
FProgress.Position := 0;
|
||||
|
||||
FTimer := TTimer.Create(Self);
|
||||
FTimer.Interval := 250;
|
||||
FTimer.OnTimer := TimerTick;
|
||||
FTimer.Enabled := True;
|
||||
end;
|
||||
|
||||
procedure TWisdomProgressDialog.TimerTick(Sender: TObject);
|
||||
var
|
||||
P: PAnsiChar;
|
||||
S: string;
|
||||
begin
|
||||
FPhase := (FPhase + 7) mod 101;
|
||||
FProgress.Position := FPhase;
|
||||
|
||||
if Assigned(@wisdom_get_status) then
|
||||
begin
|
||||
P := wisdom_get_status;
|
||||
if P <> nil then
|
||||
begin
|
||||
S := Trim(string(AnsiString(P)));
|
||||
if S <> '' then
|
||||
FInfoLabel.Caption := S;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Assigned(FThread) and FThread.Finished then
|
||||
begin
|
||||
FTimer.Enabled := False;
|
||||
ModalResult := mrOk;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Общая функция декодирования типа платы — используется везде
|
||||
// ===========================================================================
|
||||
@@ -713,26 +866,25 @@ begin
|
||||
Result.WindowType := FDSPEngine.WindowType;
|
||||
Result.SpecDetector := FDSPEngine.SpecDetector;
|
||||
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
|
||||
Result.SpecAvgCount := FDSPEngine.SpecAvgCount;
|
||||
Result.SpecBackmult := FDSPEngine.SpecBackmult;
|
||||
Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
|
||||
Result.WfDetector := FDSPEngine.WfDetector;
|
||||
Result.WfAvgMode := FDSPEngine.WfAvgMode;
|
||||
Result.WfAvgCount := FDSPEngine.WfAvgCount;
|
||||
Result.WfBackmult := FDSPEngine.WfBackmult;
|
||||
Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result.FFTSize := 4096;
|
||||
Result.WindowType := 5;
|
||||
Result.WindowType := 2;
|
||||
Result.SpecDetector := 0;
|
||||
Result.SpecAvgMode := 3;
|
||||
Result.SpecAvgCount := 2;
|
||||
Result.SpecBackmult := 0.45;
|
||||
Result.WfDetector := 2;
|
||||
Result.SpecAvgTimeMS := 30.0;
|
||||
Result.WfDetector := 0;
|
||||
Result.WfAvgMode := 3;
|
||||
Result.WfAvgCount := 2;
|
||||
Result.WfBackmult := 0.45;
|
||||
Result.WfAvgTimeMS := 120.0;
|
||||
end;
|
||||
Result.WfManualHigh := FWfManualHigh;
|
||||
Result.WfManualLow := FWfManualLow;
|
||||
Result.WfAGCOffset := FWfAGCOffset;
|
||||
Result.SpecRefLevel := FSpecRefLevel;
|
||||
Result.SpecRange := FSpecRange;
|
||||
Result.SpecGridStep := FSpecGridStep;
|
||||
@@ -763,9 +915,9 @@ begin
|
||||
// --- Кнопки диапазонов ---
|
||||
for i := 0 to BAND_COUNT - 1 do
|
||||
StyleButton(BtnBand[i], i = BandIdx);
|
||||
// Сбрасываем waterfall AGC/NF при смене диапазона
|
||||
FWfHigh := -50.0;
|
||||
FWfLow := -120.0;
|
||||
// Сбрасываем waterfall thresholds при смене диапазона
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
// Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон
|
||||
ResetWfAvgBuf;
|
||||
|
||||
@@ -859,12 +1011,17 @@ begin
|
||||
FSampleRate := 192000;
|
||||
FCurrentBand := 5; // 20m по умолчанию
|
||||
// Waterfall normalization
|
||||
FWfHigh := -50.0; // начальная верхняя граница dBm
|
||||
// WF avg buffer — инициализируем значением шумового пола
|
||||
// FWfAvgBuf инициализируется в ResetSpectrumBuf
|
||||
FWfLow := -120.0; // начальная нижняя граница dBm
|
||||
FWfAGCEnabled := False;
|
||||
FWfManualHigh := -80.0;
|
||||
FWfManualLow := -130.0;
|
||||
FWfAGCOffset := 0.0;
|
||||
FWfHigh := FWfManualHigh; // начальная верхняя граница dBm
|
||||
FWfLow := FWfManualLow; // начальная нижняя граница dBm
|
||||
FWfAGCEnabled := True;
|
||||
FWfNFEnabled := False;
|
||||
FSpectrumBufCount := 1024;
|
||||
FWaterfallBufCount := 1024;
|
||||
FWaterfallFrameInterval := 2;
|
||||
FWaterfallFrameCounter := 0;
|
||||
FDevConnected := False;
|
||||
// Spectrum grid settings defaults
|
||||
FSpecRefLevel := -20.0;
|
||||
@@ -920,6 +1077,8 @@ begin
|
||||
FWebServer.OnFreqB := WebOnFreqB;
|
||||
FWebServer.OnActiveVfo := WebOnActiveVfo;
|
||||
FWebServer.OnCenter := WebOnCenter;
|
||||
FWebServer.OnMOX := WebOnMOX;
|
||||
FWebServer.OnDrive := WebOnDrive;
|
||||
FWebServer.Start;
|
||||
FSMeterPeak := -130;
|
||||
FSMeterMin := -130;
|
||||
@@ -940,6 +1099,7 @@ begin
|
||||
FShowSpectrum := True;
|
||||
FShowWaterfall := True;
|
||||
FDisplayFPS := 20;
|
||||
FWaterfallDirty := True;
|
||||
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
|
||||
FSplitterDrag := False;
|
||||
|
||||
@@ -2020,21 +2180,20 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.ResetWfAvgBuf;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
|
||||
FWfHigh := -50.0;
|
||||
FWfLow := -120.0;
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ResetSpectrumBuf;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
|
||||
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
|
||||
for i := 0 to 1023 do FWaterfallBuf[i] := -120.0;
|
||||
FWfHigh := -50.0;
|
||||
FWfLow := -120.0;
|
||||
for i := 0 to 1023 do FWaterfallBuf[i] := -130.0;
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.InvalidateGridCache;
|
||||
@@ -2253,6 +2412,7 @@ var
|
||||
S0, S1: Integer;
|
||||
InvRange: Double;
|
||||
LabelBandW: Integer;
|
||||
SrcCount: Integer;
|
||||
begin
|
||||
if FSpectrumBitmap = nil then Exit;
|
||||
W := FSpectrumBitmap.Width;
|
||||
@@ -2359,11 +2519,12 @@ begin
|
||||
SetLength(FSpPts, W + 2);
|
||||
FSpPtsLen := W + 2;
|
||||
end;
|
||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
||||
for i := 0 to W - 1 do
|
||||
begin
|
||||
SrcF := i * 1023.0 / Max(1, W - 1);
|
||||
S0 := Min(Trunc(SrcF), 1023);
|
||||
S1 := Min(S0 + 1, 1023);
|
||||
SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
|
||||
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
||||
S1 := Min(S0 + 1, SrcCount - 1);
|
||||
Frac := SrcF - S0;
|
||||
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
||||
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
||||
@@ -2409,10 +2570,17 @@ var
|
||||
dB, frac: Double;
|
||||
WatSrcF: Double;
|
||||
WatS0, WatS1, V: Integer;
|
||||
AvgDB: Double;
|
||||
PeakDB: Double;
|
||||
NFSum: Double;
|
||||
NFCount: Integer;
|
||||
TargetLow: Double;
|
||||
TargetHigh: Double;
|
||||
HistMinDB: Double;
|
||||
HistMaxDB: Double;
|
||||
HistStepDB: Double;
|
||||
NoiseFloorDB: Double;
|
||||
SignalTopDB: Double;
|
||||
CumCount: Integer;
|
||||
LowTargetCount: Integer;
|
||||
HighTargetCount: Integer;
|
||||
HistIdx: Integer;
|
||||
WfHigh, WfLow, InvRange: Double;
|
||||
Step: Double;
|
||||
Px: PLongWord;
|
||||
@@ -2422,53 +2590,78 @@ var
|
||||
Src: PLongWord;
|
||||
BPPi: Integer;
|
||||
Pal: LongWord;
|
||||
Hist: array[0..191] of Integer;
|
||||
SrcCount: Integer;
|
||||
const
|
||||
ALPHA_SPEC = 0.25;
|
||||
ALPHA_HIGH = 0.02;
|
||||
ALPHA_LOW = 0.005;
|
||||
ALPHA_HIGH = 0.10;
|
||||
ALPHA_LOW = 0.08;
|
||||
WF_AUTO_OFFSET = -4.0;
|
||||
WF_MIN_RANGE = 48.0;
|
||||
WF_MAX_RANGE = 62.0;
|
||||
begin
|
||||
if FWaterfallBitmap = nil then Exit;
|
||||
W := FWaterfallBitmap.Width;
|
||||
H := FWaterfallBitmap.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
|
||||
|
||||
// ── Источник данных водопада ─────────────────────────────────────────────
|
||||
// При работе WDSP: FWaterfallBuf заполнен из pixout=1 (уже усреднённые WDSP данные)
|
||||
// Без WDSP (demo): делаем EMA FSpectrumBuf → FWfAvgBuf
|
||||
PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
|
||||
if FWDSPReady then
|
||||
FillChar(Hist, SizeOf(Hist), 0);
|
||||
HistMinDB := -170.0;
|
||||
HistMaxDB := 22.0;
|
||||
HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist);
|
||||
for X := 0 to SrcCount - 1 do
|
||||
begin
|
||||
// Используем WDSP waterfall данные напрямую
|
||||
for X := 0 to 1023 do
|
||||
HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist));
|
||||
Inc(Hist[HistIdx]);
|
||||
end;
|
||||
|
||||
LowTargetCount := Round(SrcCount * 0.30);
|
||||
HighTargetCount := Round(SrcCount * 0.98);
|
||||
CumCount := 0;
|
||||
NoiseFloorDB := FWfLow;
|
||||
SignalTopDB := FWfHigh;
|
||||
for HistIdx := 0 to High(Hist) do
|
||||
begin
|
||||
CumCount := CumCount + Hist[HistIdx];
|
||||
if CumCount >= LowTargetCount then
|
||||
begin
|
||||
AvgDB := FWaterfallBuf[X];
|
||||
FWfAvgBuf[X] := AvgDB; // синхронизируем для AGC логики
|
||||
if AvgDB > PeakDB then PeakDB := AvgDB;
|
||||
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
|
||||
NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
|
||||
Break;
|
||||
end;
|
||||
end
|
||||
else
|
||||
end;
|
||||
|
||||
CumCount := 0;
|
||||
for HistIdx := 0 to High(Hist) do
|
||||
begin
|
||||
// Fallback: Pascal EMA от демо-спектра
|
||||
for X := 0 to 1023 do
|
||||
CumCount := CumCount + Hist[HistIdx];
|
||||
if CumCount >= HighTargetCount then
|
||||
begin
|
||||
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
|
||||
FWfAvgBuf[X] := AvgDB;
|
||||
if AvgDB > PeakDB then PeakDB := AvgDB;
|
||||
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
|
||||
SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ── AGC High/Low ────────────────────────────────────────────────────────
|
||||
if FWfAGCEnabled then
|
||||
begin
|
||||
if PeakDB > FWfHigh then FWfHigh := PeakDB + 5.0
|
||||
else FWfHigh := FWfHigh + ALPHA_HIGH * ((PeakDB + 5.0) - FWfHigh);
|
||||
TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset;
|
||||
if FWfNFEnabled then
|
||||
TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0)
|
||||
else
|
||||
TargetHigh := TargetLow + 52.0;
|
||||
if TargetHigh > TargetLow + WF_MAX_RANGE then
|
||||
TargetHigh := TargetLow + WF_MAX_RANGE;
|
||||
FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow);
|
||||
FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh);
|
||||
end;
|
||||
if FWfNFEnabled and (NFCount > 10) then
|
||||
FWfLow := FWfLow + ALPHA_LOW * ((NFSum / NFCount - 5.0) - FWfLow);
|
||||
WfHigh := FWfHigh; WfLow := FWfLow;
|
||||
if WfHigh < WfLow + 15.0 then WfHigh := WfLow + 15.0;
|
||||
WfHigh := FWfHigh;
|
||||
WfLow := FWfLow;
|
||||
if not FWfAGCEnabled then
|
||||
begin
|
||||
WfHigh := FWfManualHigh;
|
||||
WfLow := FWfManualLow;
|
||||
end;
|
||||
if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0;
|
||||
if WfHigh > 0.0 then WfHigh := 0.0;
|
||||
if WfLow < -160 then WfLow := -160;
|
||||
InvRange := 255.0 / (WfHigh - WfLow);
|
||||
@@ -2489,21 +2682,21 @@ begin
|
||||
Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W);
|
||||
|
||||
// ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ────────
|
||||
Step := 1023.0 / Max(1, W - 1);
|
||||
Step := (SrcCount - 1.0) / Max(1, W - 1);
|
||||
WatSrcF := 0.0;
|
||||
Px := @FWfPixels[0];
|
||||
for X := 0 to W - 1 do
|
||||
begin
|
||||
WatS0 := Trunc(WatSrcF);
|
||||
if WatS0 > 1022 then WatS0 := 1022;
|
||||
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
||||
WatS1 := WatS0 + 1;
|
||||
frac := WatSrcF - WatS0;
|
||||
dB := FWfAvgBuf[WatS0] * (1.0 - frac) + FWfAvgBuf[WatS1] * frac;
|
||||
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
||||
V := Trunc((dB - WfLow) * InvRange);
|
||||
if V < 0 then V := 0;
|
||||
if V > 255 then V := 255;
|
||||
// Палитра задана как RGB, а в буфере нужен BGRA.
|
||||
// Меняем местами R/B и ставим alpha=255, чтобы совпадало с web/Thetis-градиентом.
|
||||
// WFALL_PALETTE задана в RGB-логике, а в буфере нужен BGRA.
|
||||
// Поэтому меняем R/B местами и добавляем alpha=255.
|
||||
Pal := WFALL_PALETTE[V];
|
||||
Px^ := ((Pal and $000000FF) shl 16) or
|
||||
(Pal and $0000FF00) or
|
||||
@@ -3084,7 +3277,6 @@ begin
|
||||
|
||||
if FWDSPReady then
|
||||
begin
|
||||
FDSPEngine.UpdateSpectrum;
|
||||
FLastSMeter := FDSPEngine.GetSMeterDBm;
|
||||
FDSPEngine.SetSpectrumWidth(FSpectrumWidth); // актуальная ширина для SetAGC/SetAGCTop
|
||||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||||
@@ -3096,22 +3288,24 @@ begin
|
||||
FVfoOverlay.UpdateVfo(FVfoA);
|
||||
PositionVfoOverlay;
|
||||
end;
|
||||
// Рисуем только при новых спектральных данных/изменении UI (FSpectrumDirty).
|
||||
// Это резко снижает CPU в холостом режиме.
|
||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||
if FSpectrumDirty then
|
||||
begin
|
||||
DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
|
||||
// Водопад рисуем на каждом новом спектральном кадре для плавности.
|
||||
DrawWaterfall;
|
||||
PbWaterfall.Invalidate;
|
||||
|
||||
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
|
||||
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
|
||||
PbRuler.Invalidate;
|
||||
FSpectrumDirty := False;
|
||||
end;
|
||||
if FWaterfallDirty then
|
||||
begin
|
||||
DrawWaterfall;
|
||||
PbWaterfall.Invalidate;
|
||||
FWaterfallDirty := False;
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -3139,15 +3333,20 @@ begin
|
||||
end;
|
||||
|
||||
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
|
||||
if FSpectrumDirty and (not FRunning) then
|
||||
if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then
|
||||
begin
|
||||
DrawSpectrum;
|
||||
DrawWaterfall;
|
||||
PbSpectrum.Invalidate;
|
||||
if FSpectrumDirty then
|
||||
DrawSpectrum;
|
||||
if FWaterfallDirty then
|
||||
DrawWaterfall;
|
||||
if FSpectrumDirty then
|
||||
PbSpectrum.Invalidate;
|
||||
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
|
||||
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
|
||||
PbRuler.Invalidate;
|
||||
PbWaterfall.Invalidate;
|
||||
if FWaterfallDirty then
|
||||
PbWaterfall.Invalidate;
|
||||
FWaterfallDirty := False;
|
||||
FSpectrumDirty := False;
|
||||
end;
|
||||
|
||||
@@ -3155,7 +3354,7 @@ begin
|
||||
if Assigned(FWebServer) then
|
||||
FWebServer.PushSpectrum(
|
||||
FSpectrumBuf, 1024,
|
||||
FWfAvgBuf,
|
||||
FWaterfallBuf,
|
||||
FLastSMeter,
|
||||
FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop,
|
||||
FSpanHz, FVolume,
|
||||
@@ -3165,7 +3364,8 @@ begin
|
||||
FRunning, FMuted, FCTun,
|
||||
BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0,
|
||||
FCenterFreq, FFilter,
|
||||
FVfoB, FActiveVfo);
|
||||
FVfoB, FActiveVfo,
|
||||
FTransmitting, TrkDrive.Position);
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
@@ -3412,6 +3612,67 @@ begin
|
||||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||||
end;
|
||||
|
||||
function TMainForm.EnsureWDSPWisdom: Boolean;
|
||||
var
|
||||
WisdomDir: string;
|
||||
WisdomPath: string;
|
||||
DirA: AnsiString;
|
||||
BuildThread: TWisdomBuildThread;
|
||||
Dlg: TWisdomProgressDialog;
|
||||
begin
|
||||
Result := True;
|
||||
|
||||
if not Assigned(@WDSPwisdom) then Exit;
|
||||
|
||||
WisdomDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
|
||||
WisdomPath := WisdomDir + WDSP_WISDOM_FILE;
|
||||
|
||||
if FileExists(WisdomPath) then
|
||||
begin
|
||||
// По рекомендации WDSP вызываем функцию на каждом старте:
|
||||
// если файл уже есть, библиотека просто импортирует wisdom.
|
||||
DirA := AnsiString(WisdomDir);
|
||||
WDSPwisdom(PAnsiChar(DirA));
|
||||
Exit;
|
||||
end;
|
||||
|
||||
BtnStartStop.Enabled := False;
|
||||
StatusBar1.Panels[3].Text := 'Creating WDSP wisdom...';
|
||||
|
||||
BuildThread := TWisdomBuildThread.Create(WisdomDir);
|
||||
try
|
||||
Dlg := TWisdomProgressDialog.Create(Self, BuildThread);
|
||||
try
|
||||
Dlg.ShowModal;
|
||||
finally
|
||||
Dlg.Free;
|
||||
end;
|
||||
|
||||
BuildThread.WaitFor;
|
||||
|
||||
if BuildThread.ErrorText <> '' then
|
||||
begin
|
||||
ShowMessage('Failed to create WDSP wisdom.' + LineEnding +
|
||||
BuildThread.ErrorText);
|
||||
Result := False;
|
||||
end
|
||||
else if not FileExists(WisdomPath) then
|
||||
begin
|
||||
ShowMessage('WDSP wisdom was not created.' + LineEnding +
|
||||
'Expected file: ' + WisdomPath);
|
||||
Result := False;
|
||||
end;
|
||||
finally
|
||||
BuildThread.Free;
|
||||
BtnStartStop.Enabled := True;
|
||||
end;
|
||||
|
||||
if Result then
|
||||
StatusBar1.Panels[3].Text := 'WDSP wisdom ready'
|
||||
else
|
||||
StatusBar1.Panels[3].Text := 'WDSP wisdom missing';
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
|
||||
begin
|
||||
// Открываем диалог выбора устройства
|
||||
@@ -3595,6 +3856,11 @@ begin
|
||||
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
|
||||
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
|
||||
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
|
||||
FWfManualHigh := G_Settings.WfManualHigh;
|
||||
FWfManualLow := G_Settings.WfManualLow;
|
||||
FWfAGCOffset := G_Settings.WfAGCOffset;
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
FShowSpectrum := G_Settings.ShowSpectrum;
|
||||
FShowWaterfall := G_Settings.ShowWaterfall;
|
||||
if G_Settings.DisplayFPS > 0 then
|
||||
@@ -3610,9 +3876,9 @@ begin
|
||||
begin
|
||||
FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
|
||||
FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode,
|
||||
G_Settings.SpecAvgCount, G_Settings.SpecBackmult);
|
||||
G_Settings.SpecAvgTimeMS);
|
||||
FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
|
||||
G_Settings.WfAvgCount, G_Settings.WfBackmult);
|
||||
G_Settings.WfAvgTimeMS);
|
||||
end;
|
||||
// Аудио устройство вывода
|
||||
FAudioOut.Close;
|
||||
@@ -4555,7 +4821,7 @@ begin
|
||||
FWfAGCEnabled := not FWfAGCEnabled;
|
||||
StyleButton(BtnWfAGC, FWfAGCEnabled);
|
||||
if not FWfAGCEnabled then
|
||||
FWfHigh := -50.0; // сбрасываем к умолчанию при выключении
|
||||
FWfHigh := FWfManualHigh; // сбрасываем к ручному уровню при выключении
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnWfNFClick(Sender: TObject);
|
||||
@@ -4563,7 +4829,7 @@ begin
|
||||
FWfNFEnabled := not FWfNFEnabled;
|
||||
StyleButton(BtnWfNF, FWfNFEnabled);
|
||||
if not FWfNFEnabled then
|
||||
FWfLow := -120.0;
|
||||
FWfLow := FWfManualLow;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnHidePanelClick(Sender: TObject);
|
||||
@@ -5114,11 +5380,41 @@ begin
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.WebOnMOX(On_: Boolean);
|
||||
begin
|
||||
FWebSyncBool := On_;
|
||||
FWebSyncM := SyncWebMOX;
|
||||
TThread.Synchronize(nil, FWebSyncM);
|
||||
end;
|
||||
|
||||
procedure TMainForm.SyncWebMOX;
|
||||
begin
|
||||
ApplyMOX(FWebSyncBool);
|
||||
end;
|
||||
|
||||
procedure TMainForm.WebOnDrive(V: Integer);
|
||||
begin
|
||||
FWebSyncInt := V;
|
||||
FWebSyncM := SyncWebDrive;
|
||||
TThread.Synchronize(nil, FWebSyncM);
|
||||
end;
|
||||
|
||||
procedure TMainForm.SyncWebDrive;
|
||||
begin
|
||||
// Установка Position вызывает TrkDriveChange через OnChange автоматически
|
||||
TrkDrive.Position := FWebSyncInt;
|
||||
end;
|
||||
|
||||
procedure TMainForm.TrkDriveChange(Sender: TObject);
|
||||
begin
|
||||
FDriveLevel := CalcDriveByte;
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
|
||||
if FNetwork.Connected and FNetwork.Running then
|
||||
begin
|
||||
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True);
|
||||
FNetwork.SendFullHP;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMainForm.CalcDriveByte: Byte;
|
||||
@@ -5283,6 +5579,7 @@ begin
|
||||
N := Min(Count, 1024);
|
||||
for i := 0 to N - 1 do
|
||||
FSpectrumBuf[i] := Pixels[i];
|
||||
FSpectrumBufCount := N;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
@@ -5291,31 +5588,47 @@ procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
|
||||
var
|
||||
i, N: Integer;
|
||||
begin
|
||||
// Заполняем буфер водопада из WDSP pixout=1.
|
||||
// Вызывается из DSP-потока — Single-запись атомарна.
|
||||
N := Min(Count, 1024);
|
||||
for i := 0 to N - 1 do
|
||||
FWaterfallBuf[i] := Pixels[i];
|
||||
FWaterfallBufCount := N;
|
||||
Inc(FWaterfallFrameCounter);
|
||||
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
|
||||
begin
|
||||
FWaterfallFrameCounter := 0;
|
||||
FWaterfallDirty := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Settings apply methods (вызываются из TSettingsForm)
|
||||
// ===========================================================================
|
||||
|
||||
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
|
||||
SpecAvgCount: Integer; SpecBackmult: Double);
|
||||
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||||
SpecAvgTimeMS: Double);
|
||||
begin
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS);
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetFFTParams(FFTSize, WinType);
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgCount, SpecBackmult);
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
|
||||
WfBackmult: Double);
|
||||
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||||
begin
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgCount, WfBackmult);
|
||||
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS);
|
||||
FWfManualHigh := WfHigh;
|
||||
FWfManualLow := WfLow;
|
||||
FWfAGCOffset := WfAGCOffset;
|
||||
if not FWfAGCEnabled then
|
||||
begin
|
||||
FWfHigh := FWfManualHigh;
|
||||
FWfLow := FWfManualLow;
|
||||
end;
|
||||
ResetWfAvgBuf;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||
@@ -5375,9 +5688,15 @@ end;
|
||||
procedure TMainForm.ApplyFPS(FPS: Integer);
|
||||
begin
|
||||
if FPS < 1 then FPS := 1;
|
||||
if FPS > 60 then FPS := 60;
|
||||
if FPS > 100 then FPS := 100;
|
||||
FDisplayFPS := FPS;
|
||||
FSpectrumTimer.Interval := 1000 div FPS;
|
||||
// Thetis по умолчанию обновляет водопад через один display frame.
|
||||
FWaterfallFrameInterval := 2;
|
||||
FWaterfallFrameCounter := 0;
|
||||
FWaterfallDirty := True;
|
||||
if FWDSPReady then
|
||||
FDSPEngine.SetDisplayFPS(FPS);
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnSettingsClick(Sender: TObject);
|
||||
@@ -5410,18 +5729,19 @@ begin
|
||||
FDSPEngine.WindowType,
|
||||
FDSPEngine.SpecDetector,
|
||||
FDSPEngine.SpecAvgMode,
|
||||
FDSPEngine.SpecAvgCount,
|
||||
FDSPEngine.SpecBackmult,
|
||||
FDSPEngine.SpecAvgTimeMS,
|
||||
FDSPEngine.WfDetector,
|
||||
FDSPEngine.WfAvgMode,
|
||||
FDSPEngine.WfAvgCount,
|
||||
FDSPEngine.WfBackmult,
|
||||
FDSPEngine.WfAvgTimeMS,
|
||||
FWfManualHigh,
|
||||
FWfManualLow,
|
||||
FWfAGCOffset,
|
||||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||||
FAudioOutDevName, FAudioInDevName)
|
||||
else
|
||||
SF.LoadValues(
|
||||
4096, 5, 0, 3, 2, 0.45,
|
||||
2, 3, 2, 0.45,
|
||||
4096, 2, 0, 3, 30.0,
|
||||
0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset,
|
||||
FSpecRefLevel, FSpecRange, FSpecGridStep,
|
||||
FAudioOutDevName, FAudioInDevName);
|
||||
SF.Show;
|
||||
@@ -5442,6 +5762,8 @@ begin
|
||||
on E: Exception do
|
||||
StatusBar1.Panels[3].Text := 'Audio: ' + E.Message;
|
||||
end;
|
||||
|
||||
EnsureWDSPWisdom;
|
||||
end;
|
||||
|
||||
|
||||
|
||||
+65
-23
@@ -6,7 +6,7 @@ unit Settings;
|
||||
}
|
||||
{$mode objfpc}{$H+}
|
||||
interface
|
||||
uses SysUtils, Classes, fpJSON, jsonparser, jsonscanner;
|
||||
uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner;
|
||||
|
||||
const
|
||||
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
|
||||
SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
|
||||
SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
|
||||
SpecAvgCount: Integer; // frames to average
|
||||
SpecBackmult: Double; // 0.0..1.0 recursive averaging multiplier
|
||||
SpecAvgTimeMS: Double; // averaging time in milliseconds
|
||||
WfDetector: Integer; // waterfall detector
|
||||
WfAvgMode: Integer; // waterfall average mode
|
||||
WfAvgCount: Integer; // waterfall average count
|
||||
WfBackmult: Double; // waterfall backmult
|
||||
WfAvgTimeMS: Double; // waterfall averaging time in milliseconds
|
||||
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
|
||||
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
||||
SpecGridStep: Double; // grid step in dB, e.g. 10
|
||||
@@ -70,7 +71,7 @@ type
|
||||
ShowSpectrum: Boolean; // True = draw spectrum on main form
|
||||
ShowWaterfall: Boolean; // True = draw waterfall on main form
|
||||
// --- Display FPS ---
|
||||
DisplayFPS: Integer; // spectrum/waterfall timer fps (5..30)
|
||||
DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
|
||||
// --- PA (Power Amplifier) settings ---
|
||||
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
|
||||
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
|
||||
@@ -117,6 +118,26 @@ type
|
||||
|
||||
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;
|
||||
begin
|
||||
Result := Format('%02X:%02X:%02X:%02X:%02X:%02X',
|
||||
@@ -144,20 +165,23 @@ begin
|
||||
G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0;
|
||||
G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False;
|
||||
G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5;
|
||||
G.WfAGCEnabled := True;
|
||||
G.WfNFEnabled := False;
|
||||
G.SampleRate := 192000;
|
||||
G.PAMaxPower := 100.0;
|
||||
for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0;
|
||||
// Display defaults
|
||||
G.FFTSize := 4096;
|
||||
G.WindowType := 5; // Kaiser
|
||||
G.WindowType := 2; // Hann
|
||||
G.SpecDetector := 0; // Peak
|
||||
G.SpecAvgMode := 3; // Log Recursive
|
||||
G.SpecAvgCount := 2;
|
||||
G.SpecBackmult := 0.45;
|
||||
G.WfDetector := 2; // Average
|
||||
G.SpecAvgTimeMS := 30.0;
|
||||
G.WfDetector := 0; // Peak
|
||||
G.WfAvgMode := 3; // Log Recursive
|
||||
G.WfAvgCount := 2;
|
||||
G.WfBackmult := 0.45;
|
||||
G.WfAvgTimeMS := 120.0;
|
||||
G.WfManualHigh := -80.0;
|
||||
G.WfManualLow := -130.0;
|
||||
G.WfAGCOffset := 0.0;
|
||||
G.SpecRefLevel := -20.0;
|
||||
G.SpecRange := 110.0;
|
||||
G.SpecGridStep := 10.0;
|
||||
@@ -307,7 +331,7 @@ begin
|
||||
G.ANFEnabled := JB(GObj,'anf_enabled',False);
|
||||
G.AGCSlope := JI(GObj,'agc_slope',0);
|
||||
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.LastBand := JI(GObj,'last_band',5);
|
||||
G.SampleRate := JI(GObj,'sample_rate',192000);
|
||||
@@ -315,13 +339,30 @@ begin
|
||||
G.FFTSize := JI(GObj,'fft_size',4096);
|
||||
G.WindowType := JI(GObj,'win_type',5);
|
||||
G.SpecDetector := JI(GObj,'spec_detector',0);
|
||||
G.SpecAvgMode := JI(GObj,'spec_avg_mode',3);
|
||||
G.SpecAvgCount := JI(GObj,'spec_avg_count',2);
|
||||
G.SpecBackmult := JD(GObj,'spec_backmult',0.45);
|
||||
G.SpecAvgMode := JI(GObj,'spec_avg_mode',0);
|
||||
if GObj.Find('spec_avg_time_ms') <> nil then
|
||||
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.WfAvgMode := JI(GObj,'wf_avg_mode',3);
|
||||
G.WfAvgCount := JI(GObj,'wf_avg_count',2);
|
||||
G.WfBackmult := JD(GObj,'wf_backmult',0.45);
|
||||
G.WfAvgMode := JI(GObj,'wf_avg_mode',1);
|
||||
if GObj.Find('wf_avg_time_ms') <> nil then
|
||||
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.SpecRange := JD(GObj,'spec_range',110.0);
|
||||
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
|
||||
@@ -376,12 +417,13 @@ begin
|
||||
JW(O,'win_type',G.WindowType);
|
||||
JW(O,'spec_detector',G.SpecDetector);
|
||||
JW(O,'spec_avg_mode',G.SpecAvgMode);
|
||||
JW(O,'spec_avg_count',G.SpecAvgCount);
|
||||
JW(O,'spec_backmult',G.SpecBackmult);
|
||||
JW(O,'spec_avg_time_ms',G.SpecAvgTimeMS);
|
||||
JW(O,'wf_detector',G.WfDetector);
|
||||
JW(O,'wf_avg_mode',G.WfAvgMode);
|
||||
JW(O,'wf_avg_count',G.WfAvgCount);
|
||||
JW(O,'wf_backmult',G.WfBackmult);
|
||||
JW(O,'wf_avg_time_ms',G.WfAvgTimeMS);
|
||||
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_range',G.SpecRange);
|
||||
JW(O,'spec_grid_step',G.SpecGridStep);
|
||||
|
||||
+143
-169
@@ -35,9 +35,9 @@ type
|
||||
const BandCal: array of Double) of object;
|
||||
|
||||
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
|
||||
SpecAvgMode, SpecAvgCount: Integer; SpecBackmult: Double) of object;
|
||||
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer;
|
||||
WfBackmult: Double) of object;
|
||||
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
|
||||
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
|
||||
TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
|
||||
TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
||||
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
|
||||
@@ -79,13 +79,14 @@ type
|
||||
// Spectrum group
|
||||
FCmbSpecDet: TComboBox;
|
||||
FCmbSpecAvg: TComboBox;
|
||||
FSpinSpecCount: TSpinEdit;
|
||||
FEdSpecBackmult: TEdit;
|
||||
FSpecAvgTime: TFloatSpinEdit;
|
||||
// Waterfall group
|
||||
FCmbWfDet: TComboBox;
|
||||
FCmbWfAvg: TComboBox;
|
||||
FSpinWfCount: TSpinEdit;
|
||||
FEdWfBackmult: TEdit;
|
||||
FWfAvgTime: TFloatSpinEdit;
|
||||
FWfHigh: TFloatSpinEdit;
|
||||
FWfLow: TFloatSpinEdit;
|
||||
FWfAGCOffset: TFloatSpinEdit;
|
||||
// Grid group
|
||||
FEdRefLevel: TEdit;
|
||||
FEdRange: TEdit;
|
||||
@@ -136,12 +137,11 @@ type
|
||||
procedure OnWindowChange(Sender: TObject);
|
||||
procedure OnSpecDetChange(Sender: TObject);
|
||||
procedure OnSpecAvgModeChange(Sender: TObject);
|
||||
procedure OnSpecCountChange(Sender: TObject);
|
||||
procedure OnSpecBackmultChange(Sender: TObject);
|
||||
procedure OnSpecAvgTimeChange(Sender: TObject);
|
||||
procedure OnWfDetChange(Sender: TObject);
|
||||
procedure OnWfAvgModeChange(Sender: TObject);
|
||||
procedure OnWfCountChange(Sender: TObject);
|
||||
procedure OnWfBackmultChange(Sender: TObject);
|
||||
procedure OnWfAvgTimeChange(Sender: TObject);
|
||||
procedure OnWfThresholdChange(Sender: TObject);
|
||||
procedure OnRefLevelChange(Sender: TObject);
|
||||
procedure OnRangeChange(Sender: TObject);
|
||||
procedure OnGridStepChange(Sender: TObject);
|
||||
@@ -157,8 +157,11 @@ type
|
||||
procedure FireWaterfallChange;
|
||||
procedure FireGridChange;
|
||||
|
||||
function GetSpecBackmult: Double;
|
||||
function GetWfBackmult: Double;
|
||||
function GetSpecAvgTimeMS: Double;
|
||||
function GetWfAvgTimeMS: Double;
|
||||
function GetWfHigh: Double;
|
||||
function GetWfLow: Double;
|
||||
function GetWfAGCOffset: Double;
|
||||
function GetRefLevel: Double;
|
||||
function GetRange: Double;
|
||||
function GetGridStep: Double;
|
||||
@@ -169,10 +172,10 @@ type
|
||||
|
||||
// Заполнить элементы управления текущими значениями (без firing events)
|
||||
procedure LoadValues(
|
||||
FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer;
|
||||
SpecBackmult: Double;
|
||||
WfDet, WfAvgMode, WfAvgCount: Integer;
|
||||
WfBackmult: Double;
|
||||
FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||||
SpecAvgTimeMS: Double;
|
||||
WfDet, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
|
||||
RefLevel, Range, GridStep: Double;
|
||||
const AudioOutDevName: string;
|
||||
const AudioInDevName: string);
|
||||
@@ -208,9 +211,8 @@ const
|
||||
CLR_ACCENT = TColor($0040FF80);
|
||||
BTN_H = 24;
|
||||
|
||||
// FFT sizes (index → value)
|
||||
FFT_SIZES: array[0..5] of Integer = (1024, 2048, 4096, 8192, 16384, 32768);
|
||||
FFT_NAMES: array[0..5] of string = ('1024','2048','4096','8192','16384','32768');
|
||||
FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144);
|
||||
FFT_NAMES: array[0..6] of string = ('4096','8192','16384','32768','65536','131072','262144');
|
||||
|
||||
WINDOW_NAMES: array[0..6] of string = (
|
||||
'Rectangular', '4-term Blackman-Harris', 'Hann',
|
||||
@@ -219,17 +221,19 @@ const
|
||||
DETECTOR_NAMES: array[0..4] of string = (
|
||||
'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS');
|
||||
|
||||
AVGMODE_NAMES: array[0..4] of string = (
|
||||
'Peak Hold', 'None', 'Recursive', 'Time Window', 'Log Recursive');
|
||||
// Note: AVERAGE_PEAK_HOLD=-1, NONE=0, RECURSIVE=1, TIME_WINDOW=2, LOG_RECURSIVE=3
|
||||
// Index → WDSP value mapping: 0→-1, 1→0, 2→1, 3→2, 4→3
|
||||
AVGMODE_VALUES: array[0..4] of Integer = (-1, 0, 1, 2, 3);
|
||||
AVGMODE_NAMES: array[0..3] of string = (
|
||||
'None', 'Recursive', 'Time Window', 'Log Recursive');
|
||||
AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
|
||||
WF_AVGMODE_NAMES: array[0..3] of string = (
|
||||
'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_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_NAMES: array[0..5] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps');
|
||||
FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100);
|
||||
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 =
|
||||
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
|
||||
@@ -240,8 +244,8 @@ constructor TSettingsForm.Create(AOwner: TComponent);
|
||||
begin
|
||||
inherited CreateNew(AOwner);
|
||||
Caption := 'Settings';
|
||||
Width := 700;
|
||||
Height := 560;
|
||||
Width := 760;
|
||||
Height := 640;
|
||||
Position := poScreenCenter;
|
||||
BorderStyle := bsSizeable;
|
||||
Color := CLR_BG;
|
||||
@@ -457,31 +461,20 @@ begin
|
||||
end;
|
||||
|
||||
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
|
||||
R1 = 10; // top padding inside groups
|
||||
LX = 10; // left margin inside group
|
||||
LW = 88; // label column width
|
||||
CX = LX + LW + 4; // combo/edit column start (= 102)
|
||||
STEP = BTN_H + 10; // row step (= 34)
|
||||
SEP = 12; // gap between groups
|
||||
COLW = 328; // left column panel width
|
||||
COLGAP = 10; // horizontal gap between columns
|
||||
GH2 = 100; // GroupBox height for 2-row groups
|
||||
GH3 = 136; // GroupBox height for 3-row groups
|
||||
R1 = 12;
|
||||
LX = 12;
|
||||
LW = 112;
|
||||
CX = LX + LW + 8;
|
||||
STEP = 34;
|
||||
SEP = 12;
|
||||
GH_FFT = 100;
|
||||
GH_SPEC = 136;
|
||||
GH_WF = 170;
|
||||
GH_GRID = 100;
|
||||
var
|
||||
PnlFFT, PnlMid, PnlL, PnlSepH, PnlR: TPanel;
|
||||
PnlFFT, PnlSpec, PnlWF: TPanel;
|
||||
Grp: TGroupBox;
|
||||
Lbl: TLabel;
|
||||
i: Integer;
|
||||
|
||||
function MkGrp(AParent: TWinControl; const Cap: string; AH: Integer): TGroupBox;
|
||||
@@ -508,28 +501,17 @@ var
|
||||
S.Height := AH;
|
||||
end;
|
||||
|
||||
function MkEdit(AParent: TWinControl; AX, AY, AW: Integer;
|
||||
AOnExit: TNotifyEvent): TEdit;
|
||||
function MkFloat(AParent: TWinControl; AX, AY, AW: Integer; AValue: Double;
|
||||
AMin, AMax, AInc: Double; AOnChange: TNotifyEvent): TFloatSpinEdit;
|
||||
begin
|
||||
Result := TEdit.Create(Self);
|
||||
Result := TFloatSpinEdit.Create(Self);
|
||||
Result.Parent := AParent;
|
||||
Result.SetBounds(AX, AY, AW, BTN_H);
|
||||
Result.Color := CLR_PANEL;
|
||||
Result.Font.Color := CLR_TEXT;
|
||||
Result.Font.Name := 'Courier New';
|
||||
Result.Font.Size := 8;
|
||||
Result.OnExit := AOnExit;
|
||||
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.Value := AValue;
|
||||
Result.MinValue := AMin;
|
||||
Result.MaxValue := AMax;
|
||||
Result.Increment := AInc;
|
||||
Result.DecimalPlaces := 1;
|
||||
Result.Color := CLR_PANEL;
|
||||
Result.Font.Color := CLR_TEXT;
|
||||
Result.Font.Name := 'Courier New';
|
||||
@@ -538,24 +520,15 @@ var
|
||||
end;
|
||||
|
||||
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);
|
||||
|
||||
// ── FFT (full width, alTop) ───────────────────────────────────────────────
|
||||
PnlFFT := TPanel.Create(Self);
|
||||
PnlFFT.Parent := FTabDispRX1;
|
||||
PnlFFT.BevelOuter := bvNone;
|
||||
PnlFFT.Color := CLR_BG;
|
||||
PnlFFT.Align := alTop;
|
||||
PnlFFT.Height := GH2;
|
||||
Grp := MkGrp(PnlFFT, 'FFT', GH2);
|
||||
PnlFFT.Height := GH_FFT;
|
||||
Grp := MkGrp(PnlFFT, 'FFT / Analyzer', GH_FFT);
|
||||
MakeLbl(Grp, 'FFT Size:', LX, R1+4, LW);
|
||||
FCmbFFTSize := MakeCombo(Grp, CX, R1, 130, OnFFTSizeChange);
|
||||
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);
|
||||
for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]);
|
||||
|
||||
// ── Separator ─────────────────────────────────────────────────────────────
|
||||
MkSep(FTabDispRX1, SEP);
|
||||
|
||||
// ── Middle row: Spectrum (left) | Waterfall (right) at the same level ─────
|
||||
PnlMid := TPanel.Create(Self);
|
||||
PnlMid.Parent := FTabDispRX1;
|
||||
PnlMid.BevelOuter := bvNone;
|
||||
PnlMid.Color := CLR_BG;
|
||||
PnlMid.Align := alTop;
|
||||
PnlMid.Height := GH3;
|
||||
|
||||
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);
|
||||
PnlSpec := TPanel.Create(Self);
|
||||
PnlSpec.Parent := FTabDispRX1;
|
||||
PnlSpec.BevelOuter := bvNone;
|
||||
PnlSpec.Color := CLR_BG;
|
||||
PnlSpec.Align := alTop;
|
||||
PnlSpec.Height := GH_SPEC;
|
||||
Grp := MkGrp(PnlSpec, 'Spectrum', GH_SPEC);
|
||||
MakeLbl(Grp, 'Detector:', LX, R1+4, LW);
|
||||
FCmbSpecDet := MakeCombo(Grp, CX, R1, 172, OnSpecDetChange);
|
||||
for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]);
|
||||
MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW);
|
||||
FCmbSpecAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnSpecAvgModeChange);
|
||||
for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]);
|
||||
MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW);
|
||||
FSpinSpecCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnSpecCountChange);
|
||||
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';
|
||||
MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW);
|
||||
FSpecAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);
|
||||
|
||||
// ── Waterfall (right column) ──────────────────────────────────────────────
|
||||
Grp := MkGrp(PnlR, 'Waterfall', GH3);
|
||||
MkSep(FTabDispRX1, SEP);
|
||||
|
||||
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);
|
||||
FCmbWfDet := MakeCombo(Grp, CX, R1, 172, OnWfDetChange);
|
||||
for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]);
|
||||
MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW);
|
||||
FCmbWfAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnWfAvgModeChange);
|
||||
for i := 0 to High(AVGMODE_NAMES) do FCmbWfAvg.Items.Add(AVGMODE_NAMES[i]);
|
||||
MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW);
|
||||
FSpinWfCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnWfCountChange);
|
||||
MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64);
|
||||
FEdWfBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnWfBackmultChange);
|
||||
FEdWfBackmult.Text := '0.45';
|
||||
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
|
||||
MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW);
|
||||
FWfAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange);
|
||||
MakeLbl(Grp, 'High (dBm):', LX, R1+3*STEP+4, LW);
|
||||
FWfHigh := MkFloat(Grp, CX, R1+3*STEP, 92, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange);
|
||||
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);
|
||||
|
||||
// ── Spectrum Grid (full width, alTop) ─────────────────────────────────────
|
||||
Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH2);
|
||||
Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH_GRID);
|
||||
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';
|
||||
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';
|
||||
MakeLbl(Grp, 'Grid Step:', CX+90, R1+STEP+4, 72);
|
||||
FCmbGridStep := MakeCombo(Grp, CX+90+76, R1+STEP, 80, OnGridStepChange);
|
||||
@@ -656,10 +624,10 @@ end;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TSettingsForm.LoadValues(
|
||||
FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer;
|
||||
SpecBackmult: Double;
|
||||
WfDet, WfAvgMode, WfAvgCount: Integer;
|
||||
WfBackmult: Double;
|
||||
FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
|
||||
SpecAvgTimeMS: Double;
|
||||
WfDet, WfAvgMode: Integer;
|
||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
|
||||
RefLevel, Range, GridStep: Double;
|
||||
const AudioOutDevName: string;
|
||||
const AudioInDevName: string);
|
||||
@@ -669,7 +637,7 @@ begin
|
||||
FLoading := True;
|
||||
try
|
||||
// FFT Size
|
||||
Idx := 2; // default 4096
|
||||
Idx := 0; // default 4096
|
||||
for i := 0 to High(FFT_SIZES) do
|
||||
if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end;
|
||||
FCmbFFTSize.ItemIndex := Idx;
|
||||
@@ -678,7 +646,7 @@ begin
|
||||
if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then
|
||||
FCmbWindow.ItemIndex := WinType
|
||||
else
|
||||
FCmbWindow.ItemIndex := 5; // Kaiser
|
||||
FCmbWindow.ItemIndex := 2; // Hann
|
||||
|
||||
// Spectrum Detector
|
||||
if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then
|
||||
@@ -687,13 +655,11 @@ begin
|
||||
FCmbSpecDet.ItemIndex := 0;
|
||||
|
||||
// Spectrum Avg Mode (WDSP value → combo index)
|
||||
Idx := 2; // default Recursive
|
||||
Idx := 0; // default None
|
||||
for i := 0 to High(AVGMODE_VALUES) do
|
||||
if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end;
|
||||
FCmbSpecAvg.ItemIndex := Idx;
|
||||
|
||||
FSpinSpecCount.Value := EnsureRange(SpecAvgCount, 1, 256);
|
||||
FEdSpecBackmult.Text := FormatFloat('0.##', SpecBackmult);
|
||||
FSpecAvgTime.Value := EnsureRange(SpecAvgTimeMS, 0.0, 5000.0);
|
||||
|
||||
// Waterfall Detector
|
||||
if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then
|
||||
@@ -702,13 +668,14 @@ begin
|
||||
FCmbWfDet.ItemIndex := 2; // Average
|
||||
|
||||
// Waterfall Avg Mode
|
||||
Idx := 4; // default Log Recursive
|
||||
for i := 0 to High(AVGMODE_VALUES) do
|
||||
if AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end;
|
||||
Idx := 1; // default Recursive
|
||||
for i := 0 to High(WF_AVGMODE_VALUES) do
|
||||
if WF_AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end;
|
||||
FCmbWfAvg.ItemIndex := Idx;
|
||||
|
||||
FSpinWfCount.Value := EnsureRange(WfAvgCount, 1, 256);
|
||||
FEdWfBackmult.Text := FormatFloat('0.##', WfBackmult);
|
||||
FWfAvgTime.Value := EnsureRange(WfAvgTimeMS, 0.0, 5000.0);
|
||||
FWfHigh.Value := EnsureRange(WfHigh, -160.0, 20.0);
|
||||
FWfLow.Value := EnsureRange(WfLow, -180.0, 0.0);
|
||||
FWfAGCOffset.Value := EnsureRange(WfAGCOffset, -40.0, 40.0);
|
||||
|
||||
// Grid
|
||||
FEdRefLevel.Text := FormatFloat('0.#', RefLevel);
|
||||
@@ -817,20 +784,29 @@ begin
|
||||
Result := 4096;
|
||||
end;
|
||||
|
||||
function TSettingsForm.GetSpecBackmult: Double;
|
||||
var V: Double; Err: Integer;
|
||||
function TSettingsForm.GetSpecAvgTimeMS: Double;
|
||||
begin
|
||||
Val(FEdSpecBackmult.Text, V, Err);
|
||||
if Err <> 0 then V := 0.45;
|
||||
Result := EnsureRange(V, 0.0, 1.0);
|
||||
Result := EnsureRange(FSpecAvgTime.Value, 0.0, 5000.0);
|
||||
end;
|
||||
|
||||
function TSettingsForm.GetWfBackmult: Double;
|
||||
var V: Double; Err: Integer;
|
||||
function TSettingsForm.GetWfAvgTimeMS: Double;
|
||||
begin
|
||||
Val(FEdWfBackmult.Text, V, Err);
|
||||
if Err <> 0 then V := 0.45;
|
||||
Result := EnsureRange(V, 0.0, 1.0);
|
||||
Result := EnsureRange(FWfAvgTime.Value, 0.0, 5000.0);
|
||||
end;
|
||||
|
||||
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;
|
||||
|
||||
function TSettingsForm.GetRefLevel: Double;
|
||||
@@ -880,8 +856,7 @@ begin
|
||||
FCmbWindow.ItemIndex,
|
||||
FCmbSpecDet.ItemIndex,
|
||||
AvgVal,
|
||||
FSpinSpecCount.Value,
|
||||
GetSpecBackmult);
|
||||
GetSpecAvgTimeMS);
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.FireWaterfallChange;
|
||||
@@ -892,15 +867,17 @@ begin
|
||||
if FLoading then Exit;
|
||||
if not Assigned(FOnWaterfallChange) then Exit;
|
||||
AvgIdx := FCmbWfAvg.ItemIndex;
|
||||
if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then
|
||||
AvgVal := AVGMODE_VALUES[AvgIdx]
|
||||
if (AvgIdx >= 0) and (AvgIdx <= High(WF_AVGMODE_VALUES)) then
|
||||
AvgVal := WF_AVGMODE_VALUES[AvgIdx]
|
||||
else
|
||||
AvgVal := 3;
|
||||
AvgVal := 1;
|
||||
FOnWaterfallChange(
|
||||
FCmbWfDet.ItemIndex,
|
||||
AvgVal,
|
||||
FSpinWfCount.Value,
|
||||
GetWfBackmult);
|
||||
GetWfAvgTimeMS,
|
||||
GetWfHigh,
|
||||
GetWfLow,
|
||||
GetWfAGCOffset);
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.FireGridChange;
|
||||
@@ -926,10 +903,7 @@ begin FireDisplayChange; end;
|
||||
procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject);
|
||||
begin FireDisplayChange; end;
|
||||
|
||||
procedure TSettingsForm.OnSpecCountChange(Sender: TObject);
|
||||
begin FireDisplayChange; end;
|
||||
|
||||
procedure TSettingsForm.OnSpecBackmultChange(Sender: TObject);
|
||||
procedure TSettingsForm.OnSpecAvgTimeChange(Sender: TObject);
|
||||
begin FireDisplayChange; end;
|
||||
|
||||
procedure TSettingsForm.OnWfDetChange(Sender: TObject);
|
||||
@@ -938,10 +912,10 @@ begin FireWaterfallChange; end;
|
||||
procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject);
|
||||
begin FireWaterfallChange; end;
|
||||
|
||||
procedure TSettingsForm.OnWfCountChange(Sender: TObject);
|
||||
procedure TSettingsForm.OnWfAvgTimeChange(Sender: TObject);
|
||||
begin FireWaterfallChange; end;
|
||||
|
||||
procedure TSettingsForm.OnWfBackmultChange(Sender: TObject);
|
||||
procedure TSettingsForm.OnWfThresholdChange(Sender: TObject);
|
||||
begin FireWaterfallChange; end;
|
||||
|
||||
procedure TSettingsForm.OnRefLevelChange(Sender: TObject);
|
||||
|
||||
+332
-85
@@ -98,6 +98,7 @@ const
|
||||
DISP_ID = 0; // ID для XCreateAnalyzer
|
||||
DSP_BUFSIZE = 1024;
|
||||
SPECTRUM_PIXELS = 1024;
|
||||
DISPLAY_BLOCK_SIZE = 64;
|
||||
|
||||
// Очередь IQ пакетов между сетевым и DSP потоком
|
||||
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
|
||||
@@ -150,11 +151,15 @@ type
|
||||
|
||||
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
|
||||
FSpecBuf: array of Double;
|
||||
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
|
||||
FDispBuf: array of Double;
|
||||
FDispPos: Integer;
|
||||
// Аудио выходные буферы — аллоцируем один раз
|
||||
FOutL: array of Single;
|
||||
FOutR: array of Single;
|
||||
// DSP поток + очередь пакетов
|
||||
FDSPThread: TThread;
|
||||
FDisplayThread:TThread;
|
||||
FQueue: array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem;
|
||||
FQueueHead: Integer; // пишет сетевой поток
|
||||
FQueueTail: Integer; // читает DSP поток
|
||||
@@ -168,14 +173,15 @@ type
|
||||
// Настройки анализатора
|
||||
FFFTSize: Integer;
|
||||
FWindowType: Integer;
|
||||
FDisplayFPS: Integer;
|
||||
FEffectiveDisplayFPS: Double;
|
||||
FDisplayPixelCount: Integer;
|
||||
FSpecDetector: Integer;
|
||||
FSpecAvgMode: Integer;
|
||||
FSpecAvgCount: Integer;
|
||||
FSpecBackmult: Double;
|
||||
FSpecAvgTimeMS: Double;
|
||||
FWfDetector: Integer;
|
||||
FWfAvgMode: Integer;
|
||||
FWfAvgCount: Integer;
|
||||
FWfBackmult: Double;
|
||||
FWfAvgTimeMS: Double;
|
||||
|
||||
FOnAudio: TOnAudioReady;
|
||||
FOnSpectrum: TOnSpectrumReady;
|
||||
@@ -213,11 +219,20 @@ type
|
||||
|
||||
function ModeToWDSP(Mode: Integer): Integer;
|
||||
procedure ApplyDefaultFilter;
|
||||
procedure FeedDisplaySample(const I, Q: Double);
|
||||
procedure ProcessRXBlock;
|
||||
procedure PushIQItemToDSP(const Item: TIQQueueItem);
|
||||
procedure OpenAnalyzer;
|
||||
procedure CloseAnalyzer;
|
||||
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 ApplyNBState;
|
||||
procedure ApplySNBState;
|
||||
@@ -274,8 +289,9 @@ type
|
||||
|
||||
// Настройки анализатора (применяются немедленно если открыт)
|
||||
procedure SetFFTParams(FFTSize, WinType: Integer);
|
||||
procedure SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
|
||||
procedure SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
|
||||
procedure SetDisplayFPS(FPS: Integer);
|
||||
procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||
procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||
|
||||
// Spectrum — вызывать из таймера (~20 fps)
|
||||
procedure UpdateSpectrum;
|
||||
@@ -304,12 +320,10 @@ type
|
||||
property WindowType: Integer read FWindowType;
|
||||
property SpecDetector: Integer read FSpecDetector;
|
||||
property SpecAvgMode: Integer read FSpecAvgMode;
|
||||
property SpecAvgCount: Integer read FSpecAvgCount;
|
||||
property SpecBackmult: Double read FSpecBackmult;
|
||||
property SpecAvgTimeMS: Double read FSpecAvgTimeMS;
|
||||
property WfDetector: Integer read FWfDetector;
|
||||
property WfAvgMode: Integer read FWfAvgMode;
|
||||
property WfAvgCount: Integer read FWfAvgCount;
|
||||
property WfBackmult: Double read FWfBackmult;
|
||||
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
||||
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||
@@ -325,6 +339,15 @@ implementation
|
||||
// Аналог tx_thread в piHPSDR/transmitter.c
|
||||
// ===========================================================================
|
||||
type
|
||||
TDisplayThread = class(TThread)
|
||||
private
|
||||
FEngine: TWDSPEngine;
|
||||
protected
|
||||
procedure Execute; override;
|
||||
public
|
||||
constructor Create(AEngine: TWDSPEngine);
|
||||
end;
|
||||
|
||||
TTXDSPThread = class(TThread)
|
||||
private
|
||||
FEngine: TWDSPEngine;
|
||||
@@ -334,6 +357,48 @@ type
|
||||
constructor Create(AEngine: TWDSPEngine);
|
||||
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);
|
||||
begin
|
||||
FEngine := AEngine;
|
||||
@@ -342,24 +407,44 @@ begin
|
||||
end;
|
||||
|
||||
procedure TTXDSPThread.Execute;
|
||||
// Аналог tx_thread в piHPSDR/transmitter.c:
|
||||
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
|
||||
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
|
||||
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
|
||||
var
|
||||
Avail: Integer;
|
||||
Avail: Integer;
|
||||
PeriodMs: Integer;
|
||||
begin
|
||||
// 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс
|
||||
PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate);
|
||||
|
||||
while not Terminated do
|
||||
begin
|
||||
RTLEventWaitFor(FEngine.FTXMicSem, 50);
|
||||
// Ждём сигнала от mic-данных ИЛИ истечения периода —
|
||||
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
|
||||
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
|
||||
if Terminated then Break;
|
||||
if not FEngine.FTXActive then Continue;
|
||||
|
||||
// Обрабатываем все накопившиеся блоки
|
||||
// Обрабатываем накопившиеся блоки с реальными mic-данными
|
||||
repeat
|
||||
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
||||
and (TX_MIC_RING - 1);
|
||||
if Avail >= FEngine.FAudioBufSize then
|
||||
if (Avail >= FEngine.FAudioBufSize) and not Terminated then
|
||||
FEngine.ProcessTXBlock
|
||||
else
|
||||
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;
|
||||
|
||||
@@ -467,15 +552,16 @@ begin
|
||||
FRXAccPos := 0;
|
||||
// Analyzer defaults
|
||||
FFFTSize := 4096;
|
||||
FWindowType := 5; // Kaiser
|
||||
FWindowType := 2; // Hann
|
||||
FDisplayFPS := 20;
|
||||
FEffectiveDisplayFPS := 20.0;
|
||||
FDisplayPixelCount := SPECTRUM_PIXELS;
|
||||
FSpecDetector := 0; // Peak
|
||||
FSpecAvgMode := 3; // Log Recursive
|
||||
FSpecAvgCount := 2;
|
||||
FSpecBackmult := 0.45;
|
||||
FWfDetector := 2; // Average
|
||||
FSpecAvgTimeMS := 30.0;
|
||||
FWfDetector := 0; // Peak
|
||||
FWfAvgMode := 3; // Log Recursive
|
||||
FWfAvgCount := 2;
|
||||
FWfBackmult := 0.45;
|
||||
FWfAvgTimeMS := 120.0;
|
||||
|
||||
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
||||
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
||||
@@ -484,6 +570,8 @@ begin
|
||||
SetLength(FRXAccI, FBufSize);
|
||||
SetLength(FRXAccQ, FBufSize);
|
||||
SetLength(FSpecBuf, FBufSize * 2);
|
||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
FDispPos := 0;
|
||||
SetLength(FOutL, FAudioBufSize);
|
||||
SetLength(FOutR, FAudioBufSize);
|
||||
|
||||
@@ -493,6 +581,7 @@ begin
|
||||
FDSPRunning := False;
|
||||
FQueueSem := RTLEventCreate;
|
||||
FDSPThread := nil;
|
||||
FDisplayThread := nil;
|
||||
|
||||
// TX инициализация
|
||||
FTXMicHead := 0;
|
||||
@@ -523,47 +612,75 @@ begin
|
||||
if FAnalyzerOpen then Exit;
|
||||
|
||||
Success := -1;
|
||||
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
|
||||
XCreateAnalyzer(DISP_ID, @Success, 262144, 1, 1, nil);
|
||||
if Success <> 0 then Exit;
|
||||
|
||||
FFlp[0] := 0;
|
||||
ApplyAnalyzerSettings;
|
||||
|
||||
FAnalyzerOpen := True;
|
||||
if FDisplayThread = nil then
|
||||
FDisplayThread := TDisplayThread.Create(Self);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||
// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
|
||||
var
|
||||
AnalyzerFFT: Integer;
|
||||
AvgCount: Integer;
|
||||
Backmult: Double;
|
||||
WaterfallAvgCount: Integer;
|
||||
WaterfallBackmult: Double;
|
||||
MaxW: Integer;
|
||||
Overlap: Integer;
|
||||
const
|
||||
KEEP_TIME_SEC = 0.10;
|
||||
var
|
||||
DisplaySampleRate: Integer;
|
||||
DisplayPixels: Integer;
|
||||
begin
|
||||
// MaxW = FFTSize — минимально допустимо (piHPSDR approach)
|
||||
MaxW := FFFTSize;
|
||||
// FFT Size пользователя управляет именно display-analyzer, как в Thetis,
|
||||
// но визуальная нормализация должна идти к ширине видимого окна, как в 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 — водопад
|
||||
SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], FFFTSize, FAudioBufSize, FWindowType, 14.0,
|
||||
0, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW);
|
||||
// Раздельные выходы display: pixout=0 панорама, pixout=1 водопад.
|
||||
SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE, FWindowType, 14.0,
|
||||
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);
|
||||
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
|
||||
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 0, 0);
|
||||
SetDisplayNumAverage(DISP_ID, 0, AvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 0, Backmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||
|
||||
// Водопад (pixout=1)
|
||||
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
|
||||
SetDisplayNumAverage(DISP_ID, 1, WaterfallAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, WaterfallBackmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
||||
|
||||
SetDisplaySampleRate(DISP_ID, FSampleRate);
|
||||
// В Thetis сюда идёт реальный sample rate приёмника.
|
||||
SetDisplaySampleRate(DISP_ID, DisplaySampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.CloseAnalyzer;
|
||||
begin
|
||||
if not FAnalyzerOpen then Exit;
|
||||
if Assigned(FDisplayThread) then
|
||||
begin
|
||||
FDisplayThread.Terminate;
|
||||
FDisplayThread.WaitFor;
|
||||
FreeAndNil(FDisplayThread);
|
||||
end;
|
||||
DestroyAnalyzer(DISP_ID);
|
||||
FAnalyzerOpen := False;
|
||||
end;
|
||||
@@ -719,6 +836,8 @@ begin
|
||||
SetLength(FSpecBuf, FBufSize * 2);
|
||||
SetLength(FRXAccI, FBufSize);
|
||||
SetLength(FRXAccQ, FBufSize);
|
||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
FDispPos := 0;
|
||||
|
||||
// Переинициализируем WDSP с новым rate
|
||||
Open;
|
||||
@@ -771,6 +890,8 @@ begin
|
||||
if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
|
||||
if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);
|
||||
|
||||
FeedDisplaySample(IR * SCALE, QR * SCALE);
|
||||
|
||||
FRXAccI[FRXAccPos] := IR * SCALE;
|
||||
FRXAccQ[FRXAccPos] := QR * SCALE;
|
||||
Inc(FRXAccPos);
|
||||
@@ -780,6 +901,22 @@ begin
|
||||
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;
|
||||
var
|
||||
i: Integer;
|
||||
@@ -797,21 +934,10 @@ begin
|
||||
FRXIn[i * 2 + 1] := FRXAccQ[i];
|
||||
end;
|
||||
|
||||
// Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn
|
||||
if FAnalyzerOpen then
|
||||
begin
|
||||
Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double));
|
||||
end;
|
||||
|
||||
// DSP обработка
|
||||
Err := 0;
|
||||
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.
|
||||
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
|
||||
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
|
||||
@@ -933,7 +1059,11 @@ end;
|
||||
|
||||
procedure TWDSPEngine.SetSpectrumWidth(W: Integer);
|
||||
begin
|
||||
if W > 0 then FLastSpectrumW := W;
|
||||
if W <= 0 then Exit;
|
||||
if FLastSpectrumW = W then Exit;
|
||||
FLastSpectrumW := W;
|
||||
if FAnalyzerOpen then
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer);
|
||||
@@ -1104,6 +1234,7 @@ end;
|
||||
procedure TWDSPEngine.SetTXRun(Run: Boolean);
|
||||
begin
|
||||
FTXActive := Run;
|
||||
FDispPos := 0;
|
||||
|
||||
if Run then
|
||||
begin
|
||||
@@ -1135,22 +1266,35 @@ end;
|
||||
|
||||
procedure TWDSPEngine.ProcessTXBlock;
|
||||
// Вызывается из 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
|
||||
i, tail: Integer;
|
||||
Err: Integer;
|
||||
Avail: Integer;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
|
||||
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
|
||||
tail := FTXMicTail;
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||||
|
||||
if Avail >= FAudioBufSize then
|
||||
begin
|
||||
FTXIn[i * 2] := FTXMicRing[tail];
|
||||
FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал
|
||||
tail := (tail + 1) and (TX_MIC_RING - 1);
|
||||
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
|
||||
tail := FTXMicTail;
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
FTXIn[i * 2] := FTXMicRing[tail];
|
||||
FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал
|
||||
tail := (tail + 1) and (TX_MIC_RING - 1);
|
||||
end;
|
||||
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
|
||||
end else
|
||||
begin
|
||||
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
|
||||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||||
end;
|
||||
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
|
||||
|
||||
Err := 0;
|
||||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||||
@@ -1205,30 +1349,29 @@ procedure TWDSPEngine.UpdateSpectrum;
|
||||
var
|
||||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
Flag: Integer;
|
||||
i: Integer;
|
||||
i, N: Integer;
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||||
|
||||
// Спектр (pixout=0)
|
||||
Flag := 0;
|
||||
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to SPECTRUM_PIXELS - 1 do
|
||||
for i := 0 to N - 1 do
|
||||
FSpectrumPixels[i] := PixBuf[i];
|
||||
if Assigned(FOnSpectrum) then
|
||||
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
|
||||
FOnSpectrum(FSpectrumPixels, N);
|
||||
end;
|
||||
|
||||
// Водопад (pixout=1)
|
||||
Flag := 0;
|
||||
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to SPECTRUM_PIXELS - 1 do
|
||||
for i := 0 to N - 1 do
|
||||
FWaterfallPixels[i] := PixBuf[i];
|
||||
if Assigned(FOnWaterfall) then
|
||||
FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS);
|
||||
FOnWaterfall(FWaterfallPixels, N);
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -1254,46 +1397,150 @@ end;
|
||||
// Настройки анализатора (применяются немедленно)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
|
||||
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
|
||||
begin
|
||||
// Ограничения: FFT size должен быть >= bf_sz (FAudioBufSize = 1024)
|
||||
// и степенью двойки, не превышать m_size=16384 (из XCreateAnalyzer)
|
||||
if FFTSize < FAudioBufSize then FFTSize := FAudioBufSize;
|
||||
if FFTSize > 16384 then FFTSize := 16384;
|
||||
// Thetis-style display FFT range.
|
||||
if FFTSize < 4096 then FFTSize := 4096;
|
||||
Result := 4096;
|
||||
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 > 6 then WinType := 6;
|
||||
FFFTSize := FFTSize;
|
||||
|
||||
if (FFFTSize = NewFFT) and (FWindowType = WinType) then Exit;
|
||||
|
||||
FFFTSize := NewFFT;
|
||||
FWindowType := WinType;
|
||||
if FAnalyzerOpen then
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer;
|
||||
Backmult: Double);
|
||||
procedure TWDSPEngine.SetDisplayFPS(FPS: Integer);
|
||||
begin
|
||||
FSpecDetector := Detector;
|
||||
FSpecAvgMode := AvgMode;
|
||||
FSpecAvgCount := AvgCount;
|
||||
FSpecBackmult := Backmult;
|
||||
if not FAnalyzerOpen then Exit;
|
||||
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
|
||||
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
|
||||
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult);
|
||||
FPS := EnsureRange(FPS, 1, 100);
|
||||
if FDisplayFPS = FPS then Exit;
|
||||
FDisplayFPS := FPS;
|
||||
if FAnalyzerOpen then
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer;
|
||||
Backmult: Double);
|
||||
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode: Integer;
|
||||
AvgTimeMS: Double);
|
||||
var
|
||||
AvgCount: Integer;
|
||||
Backmult: Double;
|
||||
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;
|
||||
FWfAvgMode := AvgMode;
|
||||
FWfAvgCount := AvgCount;
|
||||
FWfBackmult := Backmult;
|
||||
FWfAvgTimeMS := AvgTimeMS;
|
||||
if not FAnalyzerOpen then Exit;
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
|
||||
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
|
||||
SetDisplayNumAverage(DISP_ID, 1, AvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, Backmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
+9
-3
@@ -56,6 +56,7 @@ begin
|
||||
'button:hover{background:linear-gradient(180deg,#4c4c4c,#383838)}' +
|
||||
'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.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}' +
|
||||
'.sl-g{display:flex;align-items:center;gap:3px}' +
|
||||
'.sl-lb{font-size:10px;color:#777;white-space:nowrap}' +
|
||||
@@ -110,6 +111,7 @@ begin
|
||||
'<button id="toneBtn">TONE</button>' +
|
||||
'<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>' +
|
||||
'<button id="moxBtn">MOX</button>' +
|
||||
'<span class="sl-sep">|</span>' +
|
||||
'<div class="tsep"></div>' +
|
||||
'<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("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 обновляется локально сразу, не ждём ответа сервера
|
||||
'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());}' +
|
||||
|
||||
// Маппинг: 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){' +
|
||||
'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("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' +
|
||||
'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);}' +
|
||||
'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' +
|
||||
'paintSM(Number(cur.smeter_dbm||-130));' +
|
||||
'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 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];}' +
|
||||
|
||||
+37
-8
@@ -124,6 +124,8 @@ type
|
||||
TWebCmdFreqB = procedure(Hz: Double) of object;
|
||||
TWebCmdActiveVfo = procedure(Idx: Integer) of object;
|
||||
TWebCmdCenter = procedure(Hz: Double) of object;
|
||||
TWebCmdMOX = procedure(On_: Boolean) of object;
|
||||
TWebCmdDrive = procedure(V: Integer) of object;
|
||||
|
||||
// ── Главный класс сервера ─────────────────────────────────────────────────
|
||||
TWebServer = class
|
||||
@@ -204,6 +206,10 @@ type
|
||||
FOnFreqB: TWebCmdFreqB;
|
||||
FOnActiveVfo: TWebCmdActiveVfo;
|
||||
FOnCenter: TWebCmdCenter;
|
||||
FOnMOX: TWebCmdMOX;
|
||||
FOnDrive: TWebCmdDrive;
|
||||
FTransmitting: Boolean;
|
||||
FDriveLevel: Integer;
|
||||
|
||||
FWebClientActive: Boolean;
|
||||
|
||||
@@ -246,7 +252,8 @@ type
|
||||
TrxRunning, Muted, Ctun: Boolean;
|
||||
NRMode, NBMode: Integer; SNB, ANF: Boolean;
|
||||
CenterHz: Double; FilterIdx: Integer;
|
||||
VfoB: Double; ActiveVfo: Integer);
|
||||
VfoB: Double; ActiveVfo: Integer;
|
||||
Transmitting: Boolean; DriveLevel: Integer);
|
||||
|
||||
property WebClientActive: Boolean read FWebClientActive;
|
||||
|
||||
@@ -270,6 +277,8 @@ type
|
||||
property OnFreqB: TWebCmdFreqB read FOnFreqB write FOnFreqB;
|
||||
property OnActiveVfo: TWebCmdActiveVfo read FOnActiveVfo write FOnActiveVfo;
|
||||
property OnCenter: TWebCmdCenter read FOnCenter write FOnCenter;
|
||||
property OnMOX: TWebCmdMOX read FOnMOX write FOnMOX;
|
||||
property OnDrive: TWebCmdDrive read FOnDrive write FOnDrive;
|
||||
end;
|
||||
|
||||
implementation
|
||||
@@ -981,6 +990,20 @@ begin
|
||||
HzF := JsonGetFloat(Json, 'hz', FCenterHz);
|
||||
FStateLock.Enter; FCenterHz := HzF; FStateLock.Leave;
|
||||
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;
|
||||
|
||||
@@ -1077,7 +1100,8 @@ begin
|
||||
'"span_hz":%.0f,"center_hz":%.0f,"volume":%d,' +
|
||||
'"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' +
|
||||
'"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,
|
||||
FMode, MODE_N[FMode mod 8],
|
||||
FFilterIdx, FFilterBW,
|
||||
@@ -1095,7 +1119,9 @@ begin
|
||||
BoolToStr(FNBMode <> 0, 'true', 'false'),
|
||||
BoolToStr(FSNB, 'true', 'false'),
|
||||
BoolToStr(FANF, 'true', 'false'),
|
||||
BoolToStr(FConnected, 'true', 'false')
|
||||
BoolToStr(FConnected, 'true', 'false'),
|
||||
BoolToStr(FTransmitting, 'true', 'false'),
|
||||
FDriveLevel
|
||||
]);
|
||||
finally
|
||||
FStateLock.Leave;
|
||||
@@ -1160,7 +1186,8 @@ procedure TWebServer.PushSpectrum(
|
||||
TrxRunning, Muted, Ctun: Boolean;
|
||||
NRMode, NBMode: Integer; SNB, ANF: Boolean;
|
||||
CenterHz: Double; FilterIdx: Integer;
|
||||
VfoB: Double; ActiveVfo: Integer);
|
||||
VfoB: Double; ActiveVfo: Integer;
|
||||
Transmitting: Boolean; DriveLevel: Integer);
|
||||
var N, i: Integer;
|
||||
begin
|
||||
if not FRunning then Exit;
|
||||
@@ -1188,10 +1215,12 @@ begin
|
||||
FNBMode := NBMode;
|
||||
FSNB := SNB;
|
||||
FANF := ANF;
|
||||
FCenterHz := CenterHz;
|
||||
FFilterIdx := FilterIdx;
|
||||
FVfoB := VfoB;
|
||||
FActiveVfo := ActiveVfo;
|
||||
FCenterHz := CenterHz;
|
||||
FFilterIdx := FilterIdx;
|
||||
FVfoB := VfoB;
|
||||
FActiveVfo := ActiveVfo;
|
||||
FTransmitting := Transmitting;
|
||||
FDriveLevel := DriveLevel;
|
||||
finally
|
||||
FStateLock.Leave;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user