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