fix fft, waterfall

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