diff --git a/AudioOutput.pas b/AudioOutput.pas
index 93d1db0..c17c705 100644
--- a/AudioOutput.pas
+++ b/AudioOutput.pas
@@ -56,10 +56,25 @@ type
statusFlags: LongWord;
userData: Pointer): LongInt; cdecl;
+ TPaDeviceInfo = record
+ structVersion: Integer;
+ name: PAnsiChar;
+ hostApi: Integer;
+ maxInputChannels: Integer;
+ maxOutputChannels: Integer;
+ defaultLowInputLatency: TPaTime;
+ defaultLowOutputLatency: TPaTime;
+ defaultHighInputLatency: TPaTime;
+ defaultHighOutputLatency:TPaTime;
+ defaultSampleRate: Double;
+ end;
+ PPaDeviceInfo = ^TPaDeviceInfo;
+
TPa_Initialize = function: TPaError; cdecl;
TPa_Terminate = function: TPaError; cdecl;
TPa_GetDefaultOutputDevice = function: TPaDeviceIndex; cdecl;
- TPa_GetDeviceInfo = function(device: TPaDeviceIndex): Pointer; cdecl;
+ TPa_GetDeviceCount = function: TPaDeviceIndex; cdecl;
+ TPa_GetDeviceInfo = function(device: TPaDeviceIndex): PPaDeviceInfo; cdecl;
TPa_OpenStream = function(stream: PPaStream;
inputParam: PPaStreamParameters;
outputParam: PPaStreamParameters;
@@ -89,10 +104,13 @@ type
FOutPt: Integer; // audio_buffer_outpt (read)
FMutex: TCriticalSection;
+ FDeviceIndex: Integer; // -1 = default output device
// PortAudio functions
FPa_Initialize: TPa_Initialize;
FPa_Terminate: TPa_Terminate;
FPa_GetDefaultOutputDevice: TPa_GetDefaultOutputDevice;
+ FPa_GetDeviceCount: TPa_GetDeviceCount;
+ FPa_GetDeviceInfo: TPa_GetDeviceInfo;
FPa_OpenStream: TPa_OpenStream;
FPa_StartStream: TPa_StartStream;
FPa_StopStream: TPa_StopStream;
@@ -106,16 +124,28 @@ type
constructor Create(SampleRate: Integer = 48000);
destructor Destroy; override;
+ function Initialize: Boolean; // загружает PA, вызывает Pa_Initialize (без открытия стрима)
function Open: Boolean;
procedure Close;
+ // Перечисление устройств вывода: заполняет Names списком имён.
+ // Возвращает True если PA инициализирована и устройства доступны.
+ // Indices[i] — индекс PA устройства для Names[i].
+ function EnumOutputDevices(Names: TStrings; out Indices: array of Integer;
+ out Count: Integer): Boolean;
+
+ // Найти индекс устройства по имени (-1 если не найдено)
+ function FindDeviceByName(const AName: string): Integer;
+
// Пишем стерео double сэмплы — как audio_write() в оригинале
procedure WriteDouble(Left, Right: Double);
// Convenience: массив Single
procedure Write(const Left, Right: array of Single; Count: Integer);
- property IsOpen: Boolean read FOpen;
- property LastError: string read GetLastError;
+ property IsOpen: Boolean read FOpen;
+ property LastError: string read GetLastError;
+ property DeviceIndex: Integer read FDeviceIndex write FDeviceIndex;
+ property SampleRate: Integer read FSampleRate write FSampleRate;
end;
// Глобальный callback (cdecl, не метод)
@@ -202,12 +232,13 @@ end;
constructor TAudioOutput.Create(SampleRate: Integer);
begin
inherited Create;
- FSampleRate := SampleRate;
- FOpen := False;
- FPAInited := False;
- FStream := nil;
- FLibHandle := NilHandle;
- FLastError := '';
+ FSampleRate := SampleRate;
+ FDeviceIndex := -1; // -1 = default output device
+ FOpen := False;
+ FPAInited := False;
+ FStream := nil;
+ FLibHandle := NilHandle;
+ FLastError := '';
FInPt := 0;
FOutPt := 0;
FillChar(FBuf, SizeOf(FBuf), 0);
@@ -258,6 +289,8 @@ begin
FPa_Initialize := TPa_Initialize(GetProcAddress(FLibHandle, 'Pa_Initialize'));
FPa_Terminate := TPa_Terminate(GetProcAddress(FLibHandle, 'Pa_Terminate'));
FPa_GetDefaultOutputDevice := TPa_GetDefaultOutputDevice(GetProcAddress(FLibHandle, 'Pa_GetDefaultOutputDevice'));
+ FPa_GetDeviceCount := TPa_GetDeviceCount(GetProcAddress(FLibHandle, 'Pa_GetDeviceCount'));
+ FPa_GetDeviceInfo := TPa_GetDeviceInfo(GetProcAddress(FLibHandle, 'Pa_GetDeviceInfo'));
FPa_OpenStream := TPa_OpenStream(GetProcAddress(FLibHandle, 'Pa_OpenStream'));
FPa_StartStream := TPa_StartStream(GetProcAddress(FLibHandle, 'Pa_StartStream'));
FPa_StopStream := TPa_StopStream(GetProcAddress(FLibHandle, 'Pa_StopStream'));
@@ -275,6 +308,105 @@ begin
Result := True;
end;
+// ---------------------------------------------------------------------------
+// Initialize — загружает PA библиотеку и вызывает Pa_Initialize без открытия стрима.
+// Нужно для перечисления устройств в настройках до старта аудио.
+// ---------------------------------------------------------------------------
+
+function TAudioOutput.Initialize: Boolean;
+var
+ Err: TPaError;
+begin
+ Result := False;
+ if not LoadLib then Exit;
+ if FPAInited then begin Result := True; Exit; end;
+ Err := FPa_Initialize();
+ if Err <> PA_NO_ERROR then
+ begin
+ FLastError := 'Pa_Initialize: ';
+ if Assigned(FPa_GetErrorText) then
+ FLastError := FLastError + string(FPa_GetErrorText(Err))
+ else
+ FLastError := FLastError + IntToStr(Err);
+ Exit;
+ end;
+ FPAInited := True;
+ Result := True;
+end;
+
+// ---------------------------------------------------------------------------
+// EnumOutputDevices — перечисляет устройства вывода.
+// Names получает имена устройств; Indices — соответствующие PA-индексы;
+// Count — реальное количество (может быть меньше Length(Indices)).
+// ---------------------------------------------------------------------------
+
+function TAudioOutput.EnumOutputDevices(Names: TStrings;
+ out Indices: array of Integer; out Count: Integer): Boolean;
+var
+ I, N: Integer;
+ Info: PPaDeviceInfo;
+ DevName: string;
+begin
+ Count := 0;
+ Result := False;
+ if not Initialize then Exit;
+ if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit;
+
+ N := FPa_GetDeviceCount();
+ if N <= 0 then Exit;
+
+ for I := 0 to N - 1 do
+ begin
+ Info := FPa_GetDeviceInfo(I);
+ if Info = nil then Continue;
+ if Info^.maxOutputChannels <= 0 then Continue;
+ if Count >= Length(Indices) then Break;
+
+ if Info^.name <> nil then
+ DevName := string(AnsiString(Info^.name))
+ else
+ DevName := Format('Device %d', [I]);
+
+ Names.Add(DevName);
+ Indices[Count] := I;
+ Inc(Count);
+ end;
+ Result := Count > 0;
+end;
+
+// ---------------------------------------------------------------------------
+// FindDeviceByName — возвращает PA device index по имени (-1 если не найден).
+// ---------------------------------------------------------------------------
+
+function TAudioOutput.FindDeviceByName(const AName: string): Integer;
+var
+ I, N: Integer;
+ Info: PPaDeviceInfo;
+ DevName: string;
+begin
+ Result := -1;
+ if AName = '' then Exit;
+ if not Initialize then Exit;
+ if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit;
+
+ N := FPa_GetDeviceCount();
+ for I := 0 to N - 1 do
+ begin
+ Info := FPa_GetDeviceInfo(I);
+ if Info = nil then Continue;
+ if Info^.maxOutputChannels <= 0 then Continue;
+ if Info^.name <> nil then
+ DevName := string(AnsiString(Info^.name))
+ else
+ DevName := Format('Device %d', [I]);
+ if SameText(DevName, AName) then
+ begin
+ Result := I;
+ Exit;
+ end;
+ end;
+end;
+
// ---------------------------------------------------------------------------
// Open — точная последовательность как в audio_open_output()
// ---------------------------------------------------------------------------
@@ -287,28 +419,18 @@ var
begin
Result := False;
if FOpen then begin Result := True; Exit; end;
- if not LoadLib then Exit;
+ if not Initialize then Exit; // загрузка PA + Pa_Initialize
- // Pa_Initialize — один раз (как в audio_get_cards)
- if not FPAInited then
+ // Выбор устройства: FDeviceIndex или default
+ if FDeviceIndex >= 0 then
+ Dev := FDeviceIndex
+ else
begin
- Err := FPa_Initialize();
- if Err <> PA_NO_ERROR then
- begin
- FLastError := 'Pa_Initialize: ';
- if Assigned(FPa_GetErrorText) then
- FLastError := FLastError + string(FPa_GetErrorText(Err))
- else
- FLastError := FLastError + IntToStr(Err);
- Exit;
- end;
- FPAInited := True;
+ Dev := FPa_GetDefaultOutputDevice();
end;
-
- Dev := FPa_GetDefaultOutputDevice();
if Dev = PA_NO_DEV then
begin
- FLastError := 'No default output device';
+ FLastError := 'No output device available';
Exit;
end;
diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas
index 5ee7ff0..7b5b4de 100644
--- a/HPSDRNetwork.pas
+++ b/HPSDRNetwork.pas
@@ -19,7 +19,7 @@ uses
{$ELSE}
Sockets, BaseUnix,
{$ENDIF}
- HPSDRProtocol;
+ HPSDRProtocol, SyncObjs;
{$IFDEF WINDOWS}
// ---------------------------------------------------------------------------
@@ -129,6 +129,8 @@ type
FPAEnabled: Boolean;
FAlexEnabled: Boolean;
+ FSendLock: TCriticalSection; // защита concurrent UDP sends
+
function DoCreateSocket: TSocket;
procedure DoCloseSocket(var S: TSocket);
function DoSendTo(S: TSocket; const Buf; BufLen: Integer;
@@ -423,11 +425,13 @@ begin
FIsTransmitting := False;
FPAEnabled := True;
FAlexEnabled := True;
+ FSendLock := TCriticalSection.Create;
end;
destructor THPSDRNetwork.Destroy;
begin
Disconnect;
+ FSendLock.Free;
inherited;
end;
@@ -529,7 +533,12 @@ begin
Addr.sin_family := AF_INET;
Addr.sin_port := htons(DestPort);
Addr.sin_addr := StrToNetAddr(DestIP);
- Result := fpSendTo(S, @Buf, BufLen, 0, @Addr, SizeOf(Addr)) = BufLen;
+ FSendLock.Enter;
+ try
+ Result := fpSendTo(S, @Buf, BufLen, 0, @Addr, SizeOf(Addr)) = BufLen;
+ finally
+ FSendLock.Leave;
+ end;
end;
function THPSDRNetwork.DoRecvFrom(S: TSocket; var Buf; BufLen: Integer;
@@ -767,19 +776,11 @@ end;
procedure THPSDRNetwork.HandleMicData(const Buf: array of Byte; Len: Integer);
var
- Pkt: TMicDataPacket;
- Sync: TMicSync;
- M: TThreadMethod;
+ Pkt: TMicDataPacket;
begin
if not Assigned(FOnMic) then Exit;
Move(Buf[0], Pkt, SizeOf(Pkt));
- Sync := TMicSync.Create(FOnMic, Pkt);
- try
- M := Sync.Execute;
- TThread.Synchronize(nil, M);
- finally
- Sync.Free;
- end;
+ FOnMic(Pkt); // прямой вызов из receive thread — TX обработка не касается UI
end;
// ---------------------------------------------------------------------------
@@ -820,8 +821,14 @@ begin
Result := 0;
// TX relay при передаче
+ // Bit 27 ($08000000) = T/R relay для всех плат
+ // Bit 18 ($00040000) = TxRx Status для Orion MkII / Saturn / G2
if Transmitting then
- Result := Result or $08000000; // ALEX_TX_RELAY
+ begin
+ Result := Result or $08000000; // ALEX_TX_RELAY bit27
+ if IsOrion2 then
+ Result := Result or $00040000; // TxRx Status bit18 (ANAN-7/8000, Saturn)
+ end;
// RX HPF (для ANAN-100/200) или BPF (для ANAN-7000)
if IsOrion2 then
@@ -876,15 +883,15 @@ var
begin
FillChar(Pkt, SizeOf(Pkt), 0);
- // ANAN-7000/8000 имеют 2 ADC
- if FDevice.BoardType in [4, 5] then // ORION=4, ORION2=5
+ // ANAN-7000/8000/Saturn имеют 2 ADC
+ if FDevice.BoardType in [4, 5, 10] then // ORION=4, ORION2=5, SATURN=10
Pkt.NumADCs := 2
else
Pkt.NumADCs := 1;
- // Для ANGELIA/ORION/ORION2 DDC начинается с индекса 2 (DDC0/1 — PureSignal)
+ // Для ANGELIA/ORION/ORION2/SATURN DDC начинается с индекса 2 (DDC0/1 — PureSignal)
// Для HERMES/HL2 — с 0
- if FDevice.BoardType in [3, 4, 5] then // ANGELIA=3, ORION=4, ORION2=5
+ if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION2=5, SATURN=10
DDCBase := 2
else
DDCBase := 0;
@@ -960,11 +967,16 @@ begin
Inc(FSeqHP);
// Byte 4: Run | PTT
- if FRunning then Buf[4] := HP_RUN;
+ if FRunning then
+ begin
+ Buf[4] := HP_RUN;
+ if FIsTransmitting then Buf[4] := Buf[4] or HP_PTT0;
+ end;
- // DDC base: ORION/ORION2/ANGELIA начинают с DDC2
- IsOrion2 := (FDevice.BoardType = 5); // ORION-II = 5
- if FDevice.BoardType in [3, 4, 5] then // ANGELIA=3, ORION=4, ORION2=5
+ // DDC base: ORION/ORION2/ANGELIA/SATURN начинают с DDC2
+ // IsOrion2: платы с Alex BPF и TxRx Status bit18 (ORION_MK2=5, SATURN=10)
+ IsOrion2 := FDevice.BoardType in [5, 10]; // ORION_MK2=5, SATURN=10
+ if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION_MK2=5, SATURN=10
DDCBase := 2
else
DDCBase := 0;
diff --git a/MainForm.pas b/MainForm.pas
index 6ac9192..f80a302 100644
--- a/MainForm.pas
+++ b/MainForm.pas
@@ -26,7 +26,7 @@ uses
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
- WDSPEngine, AudioOutput,
+ WDSPEngine, AudioOutput, AudioInput,
IntfGraphics, FPImage,
Settings,
WebServer;
@@ -150,8 +150,9 @@ type
FSWRV: Double;
FSupplyV: Double;
FPLLLock: Boolean;
+ FHWPTT: Boolean;
public
- constructor Create(AForm: TObject; FW, SW, SV: Double; PLL: Boolean);
+ constructor Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean);
procedure Execute;
end;
@@ -180,9 +181,12 @@ type
FPendingBoardType: Integer; // BoardType устройства для подключения
// ---- DSP + Audio ----
- FDSPEngine: TWDSPEngine;
- FAudioOut: TAudioOutput;
- FWDSPReady: Boolean; // True когда WDSP открыт и работает
+ FDSPEngine: TWDSPEngine;
+ FAudioOut: TAudioOutput;
+ FAudioIn: TAudioInput;
+ FAudioOutDevName: string; // текущее имя выходного устройства
+ FAudioInDevName: string; // текущее имя входного устройства (TX mic)
+ FWDSPReady: Boolean; // True когда WDSP открыт и работает
// ---- State ----
FVfoA: Double;
@@ -203,8 +207,11 @@ type
FMarkerActive: Boolean; // True = линия видима
FMarkerX: Integer; // X в пикселях (относительно ширины панели)
FDriveLevel: Byte;
+ FPAMaxPower: Double;
+ FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон
FRunning: Boolean;
FTransmitting: Boolean;
+ FHWPTTActive: Boolean; // True = hardware PTT нажата
FMuted: Boolean;
FVolume: Integer;
FLastSMeter: Double;
@@ -223,16 +230,25 @@ 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
FSpectrumBitmap: TBitmap;
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FWfHigh: Double;
FWfLow: Double;
+ // Spectrum grid settings
+ FSpecRefLevel: Double; // dBm top of scale (e.g. -20)
+ FSpecRange: Double; // dB range (e.g. 110 → bottom = -130)
+ FSpecGridStep: Double; // grid step in dB (e.g. 10)
FWaterfallBitmap:TBitmap;
FWaterfallTemp: TBitmap;
FSpectrumWidth: Integer;
FSpectrumHeight: Integer;
FWaterfallHeight:Integer;
+ // ---- Visibility settings ----
+ FShowSpectrum: Boolean;
+ FShowWaterfall: Boolean;
+ FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30)
// ---- Splitter между спектром и водопадом ----
FSplitter: TPanel; // визуальная полоса-разделитель
FSplitterDrag: Boolean; // True = идёт перетаскивание
@@ -268,6 +284,7 @@ type
FWebSyncBool: Boolean;
FWebSyncM: TThreadMethod;
FCurrentBand: Integer; // текущий активный диапазон 0..10
+ FSettingsForm: TObject; // TSettingsForm (cast при использовании)
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
@@ -283,8 +300,9 @@ type
// ---- Toolbar ----
PanelToolbar: TPanel;
- BtnDiscover: TFlatButton;
+ BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
+ BtnSettings: TFlatButton; // открыть диалог настроек
// ---- Left panel ----
PanelLeft: TPanel;
@@ -397,10 +415,11 @@ type
// DSP/Audio callbacks (вызываются из рабочих потоков)
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
+ procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
// Public UI update (called from sync objects)
procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
- procedure DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
+ procedure DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
@@ -503,11 +522,18 @@ type
procedure BtnWfAGCClick(Sender: TObject);
procedure BtnWfNFClick(Sender: TObject);
procedure BtnHidePanelClick(Sender: TObject);
+ procedure BtnSettingsClick(Sender: TObject);
+ procedure InvalidateGridCache;
procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject);
procedure TrkVolumeChange(Sender: TObject);
+ function CalcDriveByte: Byte;
+ function ActiveTXFreqHz: Double;
+ procedure ApplyMOX(Active: Boolean);
+ procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
+ procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject);
procedure PbFwdPowerPaint(Sender: TObject);
@@ -527,6 +553,18 @@ type
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
+ public
+ // Методы для SettingsForm — немедленное применение настроек
+ procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
+ SpecAvgCount: Integer; SpecBackmult: Double);
+ procedure ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
+ WfBackmult: Double);
+ procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
+ procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
+ procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
+ procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
+ procedure ApplyFPS(FPS: Integer);
+
published
// Обработчики событий формы — должны быть в published для RTTI/LFM
procedure FormCreate(Sender: TObject);
@@ -542,6 +580,8 @@ var
implementation
+uses SettingsForm;
+
{$R *.lfm}
// ===========================================================================
@@ -581,7 +621,7 @@ begin
end;
constructor TStatusUISync.Create(AForm: TObject;
- FW, SW, SV: Double; PLL: Boolean);
+ FW, SW, SV: Double; PLL, HWPTT: Boolean);
begin
inherited Create;
FForm := AForm;
@@ -589,11 +629,12 @@ begin
FSWRV := SW;
FSupplyV := SV;
FPLLLock := PLL;
+ FHWPTT := HWPTT;
end;
procedure TStatusUISync.Execute;
begin
- TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock);
+ TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock, FHWPTT);
end;
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
@@ -647,9 +688,13 @@ begin
end;
function TMainForm.MakeGlobalSettings: TGlobalSettings;
+var i: Integer;
begin
Result.Volume := FVolume;
- Result.DriveLevel := FDriveLevel;
+ Result.DriveLevel := TrkDrive.Position;
+ Result.PAMaxPower := FPAMaxPower;
+ for i := 0 to BAND_COUNT - 1 do
+ Result.PABandCal[i] := FPABandCal[i];
Result.ActiveVfo := FActiveVfo;
Result.NRMode := BtnNR.Tag;
Result.NBMode := BtnNB.Tag;
@@ -661,6 +706,43 @@ begin
Result.WfNFEnabled := FWfNFEnabled;
Result.LastBand := FCurrentBand;
Result.SampleRate := FSampleRate;
+ // Display settings
+ if FWDSPReady then
+ begin
+ Result.FFTSize := FDSPEngine.FFTSize;
+ Result.WindowType := FDSPEngine.WindowType;
+ Result.SpecDetector := FDSPEngine.SpecDetector;
+ Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
+ Result.SpecAvgCount := FDSPEngine.SpecAvgCount;
+ Result.SpecBackmult := FDSPEngine.SpecBackmult;
+ Result.WfDetector := FDSPEngine.WfDetector;
+ Result.WfAvgMode := FDSPEngine.WfAvgMode;
+ Result.WfAvgCount := FDSPEngine.WfAvgCount;
+ Result.WfBackmult := FDSPEngine.WfBackmult;
+ end
+ else
+ begin
+ Result.FFTSize := 4096;
+ Result.WindowType := 5;
+ Result.SpecDetector := 0;
+ Result.SpecAvgMode := 3;
+ Result.SpecAvgCount := 2;
+ Result.SpecBackmult := 0.45;
+ Result.WfDetector := 2;
+ Result.WfAvgMode := 3;
+ Result.WfAvgCount := 2;
+ Result.WfBackmult := 0.45;
+ end;
+ Result.SpecRefLevel := FSpecRefLevel;
+ Result.SpecRange := FSpecRange;
+ Result.SpecGridStep := FSpecGridStep;
+ Result.AudioSampleRate := FAudioOut.SampleRate;
+ Result.ShowSpectrum := FShowSpectrum;
+ Result.ShowWaterfall := FShowWaterfall;
+ Result.DisplayFPS := FDisplayFPS;
+ // Audio device names
+ Result.AudioOutDevice := FAudioOutDevName;
+ Result.AudioInDevice := FAudioInDevName;
end;
procedure TMainForm.SaveCurrentBand;
@@ -751,7 +833,7 @@ end;
procedure TMainForm.FormCreate(Sender: TObject);
var
- bi_, StartupRate: Integer;
+ bi_, StartupRate, i: Integer;
StartupVfoA, StartupVfoB: Double;
begin
FVfoA := 14200000;
@@ -765,9 +847,11 @@ begin
FCTun := False;
FFilterBW := 2700;
FSpecDrag := False;
- FDriveLevel := 100;
- FRunning := False;
- FTransmitting := False;
+ FDriveLevel := 0;
+ FPAMaxPower := 100.0;
+ for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
+ FRunning := False;
+ FTransmitting := False;
FMuted := False;
FVolume := 70;
FCenterFreq := FVfoA;
@@ -782,6 +866,11 @@ begin
FWfAGCEnabled := False;
FWfNFEnabled := False;
FDevConnected := False;
+ // Spectrum grid settings defaults
+ FSpecRefLevel := -20.0;
+ FSpecRange := 110.0;
+ FSpecGridStep := 10.0;
+ FSettingsForm := nil;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями
@@ -847,7 +936,10 @@ begin
FVfoOverlay := TVfoOverlay.Create(Self);
FVfoOverlay.OnSelect := OnModeFilterSelect;
FVfoOverlay.Visible := False;
- FPanelHidden := False;
+ FPanelHidden := False;
+ FShowSpectrum := True;
+ FShowWaterfall := True;
+ FDisplayFPS := 20;
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
FSplitterDrag := False;
@@ -894,12 +986,15 @@ begin
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
- FDSPEngine.OnAudio := OnAudioReady;
- FDSPEngine.OnSpectrum := OnSpectrumReady;
+ FDSPEngine.OnAudio := OnAudioReady;
+ FDSPEngine.OnSpectrum := OnSpectrumReady;
+ FDSPEngine.OnWaterfall := OnWaterfallReady;
+ FDSPEngine.OnTXIQ := OnTXIQReady;
- // Audio output — создаём объект сейчас, открываем после показа формы
+ // Audio output/input — создаём объекты сейчас, открываем после показа формы
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
FAudioOut := TAudioOutput.Create(48000);
+ FAudioIn := TAudioInput.Create(48000);
FMeterTimer := TTimer.Create(Self);
FMeterTimer.Interval := 100;
@@ -937,6 +1032,8 @@ begin
FNetwork.Free;
FAudioOut.Close;
FAudioOut.Free;
+ FAudioIn.Close;
+ FAudioIn.Free;
FDSPEngine.Close;
FDSPEngine.Free;
FSpectrumBitmap.Free;
@@ -1034,6 +1131,7 @@ begin
X := 4;
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
+ BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick);
// ---- Status bar ----
StatusBar1 := TStatusBar.Create(Self);
@@ -1546,6 +1644,125 @@ begin
TopOff := PanelSpanButtons.Height;
// S-метр позиционируется отдельно в ResizeSMeter
ResizeSMeter;
+
+ // ---- Оба скрыты ----
+ if (not FShowSpectrum) and (not FShowWaterfall) then
+ begin
+ PbSpectrum.Visible := False;
+ PbRuler.Visible := False;
+ PanelSplitter.Visible := False;
+ PbWaterfall.Visible := False;
+ FSpectrumWidth := RW;
+ FSpectrumHeight := 0;
+ FWaterfallHeight := 0;
+ PositionSpanOverlayButtons;
+ Exit;
+ end;
+
+ // ---- Только спектр (водопад скрыт) ----
+ if FShowSpectrum and (not FShowWaterfall) then
+ begin
+ AvailH := RH - TopOff - RULER_H;
+ if AvailH < MIN_SH then AvailH := MIN_SH;
+ SH := AvailH;
+ WH := 0;
+ PbSpectrum.SetBounds(0, TopOff, RW, SH);
+ PbSpectrum.Visible := True;
+ PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
+ PbRuler.Visible := True;
+ PanelSplitter.Visible := False;
+ PbWaterfall.Visible := False;
+ PositionSpanOverlayButtons;
+ FSpectrumWidth := RW;
+ FSpectrumHeight := SH;
+ FWaterfallHeight := 0;
+ if (FSpectrumBitmap <> nil) and (RW > 0) and (SH > 0) then
+ begin
+ FSpectrumBitmap.SetSize(RW, SH);
+ FSpectrumBitmap.Canvas.Brush.Color := CLR_BG;
+ FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, RW, SH));
+ FreeAndNil(FSpecGradImg);
+ FGridBitmapW := 0;
+ FGridBitmapH := 0;
+ FSpPtsLen := 0;
+ end;
+ if (FWaterfallBitmap <> nil) then
+ begin
+ FWaterfallBitmap.SetSize(1, 1);
+ FWfBitmapW := 0;
+ FWfBitmapH := 0;
+ SetLength(FWfPixels, 0);
+ end;
+ if (FRulerBitmap <> nil) and (RW > 0) then
+ begin
+ FRulerBitmap.SetSize(RW, RULER_H);
+ FRulerLastFreq := -1.0;
+ FRulerLastVfo := -1.0;
+ end;
+ if RW > 0 then
+ begin
+ DrawSpectrum;
+ PbSpectrum.Invalidate;
+ PbRuler.Invalidate;
+ end;
+ Exit;
+ end;
+
+ // ---- Только водопад (спектр скрыт): линейка под водопадом ----
+ if (not FShowSpectrum) and FShowWaterfall then
+ begin
+ AvailH := RH - TopOff - RULER_H;
+ if AvailH < MIN_WH then AvailH := MIN_WH;
+ SH := 0;
+ WH := AvailH;
+ PbSpectrum.Visible := False;
+ PanelSplitter.Visible := False;
+ PbWaterfall.SetBounds(0, TopOff, RW, WH);
+ PbWaterfall.Visible := True;
+ PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
+ PbRuler.Visible := True;
+ PositionSpanOverlayButtons;
+ FSpectrumWidth := RW;
+ FSpectrumHeight := 0;
+ FWaterfallHeight := WH;
+ if (FSpectrumBitmap <> nil) then
+ begin
+ FSpectrumBitmap.SetSize(1, 1);
+ FreeAndNil(FSpecGradImg);
+ FGridBitmapW := 0;
+ FGridBitmapH := 0;
+ FSpPtsLen := 0;
+ end;
+ if (FWaterfallBitmap <> nil) and (RW > 0) and (WH > 0) then
+ begin
+ FWaterfallBitmap.SetSize(RW, WH);
+ FWaterfallBitmap.Canvas.Brush.Color := clBlack;
+ FWaterfallBitmap.Canvas.FillRect(Rect(0, 0, RW, WH));
+ FWfBitmapW := 0;
+ FWfBitmapH := 0;
+ SetLength(FWfPixels, 0);
+ end;
+ if (FRulerBitmap <> nil) and (RW > 0) then
+ begin
+ FRulerBitmap.SetSize(RW, RULER_H);
+ FRulerLastFreq := -1.0;
+ FRulerLastVfo := -1.0;
+ end;
+ if RW > 0 then
+ begin
+ DrawWaterfall;
+ PbWaterfall.Invalidate;
+ PbRuler.Invalidate;
+ end;
+ Exit;
+ end;
+
+ // ---- Оба видимы: стандартный режим со сплиттером ----
+ PbSpectrum.Visible := True;
+ PbRuler.Visible := True;
+ PanelSplitter.Visible := True;
+ PbWaterfall.Visible := True;
+
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
if AvailH < (MIN_SH + MIN_WH) then Exit;
@@ -1564,8 +1781,7 @@ begin
// Линейка частот — всегда прижата к низу спектра
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
// Сплиттер — между линейкой и водопадом
- if PanelSplitter <> nil then
- PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
+ PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
// Водопад — под сплиттером
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
@@ -1814,12 +2030,21 @@ 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 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;
end;
+procedure TMainForm.InvalidateGridCache;
+// Сбрасываем кэш фона/сетки — перерисуется на следующем тике DrawSpectrum
+begin
+ FGridBitmapW := 0;
+ FGridBitmapH := 0;
+ FSpectrumDirty := True;
+end;
+
procedure TMainForm.FillDemoSpectrum;
var
i: Integer;
@@ -2035,7 +2260,8 @@ begin
if (W <= 0) or (H <= 0) then Exit;
C := FSpectrumBitmap.Canvas;
- DBmin := -130.0; DBmax := -20.0;
+ DBmax := FSpecRefLevel;
+ DBmin := FSpecRefLevel - FSpecRange;
InvRange := 1.0 / (DBmax - DBmin);
// ── 1. Фон+сетка из кэша ───────────────────────────────────────────────
@@ -2056,7 +2282,7 @@ begin
GC.Font.Name := 'Courier New';
GC.Font.Color := TColor($00A8C9D9);
GC.Brush.Style := bsClear;
- dB := -40.0;
+ dB := DBmax - FSpecGridStep;
while dB >= DBmin do
begin
Yp := Round((DBmax - dB) * InvRange * H);
@@ -2064,7 +2290,7 @@ begin
GC.Pen.Width := 1;
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
- dB := dB - 10;
+ dB := dB - FSpecGridStep;
end;
for i := 0 to 8 do
begin
@@ -2206,14 +2432,31 @@ begin
H := FWaterfallBitmap.Height;
if (W <= 0) or (H <= 0) then Exit;
- // ── EMA + Peak + NF — один цикл по 1024 бинам ─────────────────────────
+ // ── Источник данных водопада ─────────────────────────────────────────────
+ // При работе WDSP: FWaterfallBuf заполнен из pixout=1 (уже усреднённые WDSP данные)
+ // Без WDSP (demo): делаем EMA FSpectrumBuf → FWfAvgBuf
PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
- for X := 0 to 1023 do
+ if FWDSPReady 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;
+ // Используем WDSP waterfall данные напрямую
+ for X := 0 to 1023 do
+ 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
+ else
+ begin
+ // Fallback: Pascal EMA от демо-спектра
+ for X := 0 to 1023 do
+ 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;
+ end;
end;
// ── AGC High/Low ────────────────────────────────────────────────────────
@@ -2789,10 +3032,16 @@ var
NowTick: QWord;
ElapsedTick: QWord;
begin
- // Синхронизируем размеры если PaintBox изменился (после ресайза)
- if (PbSpectrum.Width <> FSpectrumWidth) or
- (PbSpectrum.Height <> FSpectrumHeight) or
- (FSpectrumWidth = 0) then
+ // Синхронизируем размеры если размер панели изменился (после ресайза).
+ // Важно: проверяем только видимые панели — когда спектр скрыт,
+ // PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
+ // что вызвало бы сброс буфера водопада на каждом тике.
+ if (FSpectrumWidth = 0) or
+ (FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or
+ (PbSpectrum.Height <> FSpectrumHeight))) or
+ (FShowWaterfall and not FShowSpectrum and
+ ((PbWaterfall.Width <> FSpectrumWidth) or
+ (PbWaterfall.Height <> FWaterfallHeight))) then
ResizeSpectrumPanels;
ResizeSMeter;
if FSpectrumWidth <= 0 then Exit;
@@ -2977,7 +3226,8 @@ begin
else
SWRV := 1.0;
Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV,
- (Status.StatusBits and HPS_PLL_LOCKED) <> 0);
+ (Status.StatusBits and HPS_PLL_LOCKED) <> 0,
+ (Status.StatusBits and HPS_PTT) <> 0);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
@@ -2986,7 +3236,7 @@ begin
end;
end;
-procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
+procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
var
PLLStr: string;
begin
@@ -2998,7 +3248,15 @@ begin
StatusBar1.Panels[2].Text :=
Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s',
[SupplyV, FwdW, SWRV, PLLStr]);
- // Panels[3] зарезервирована для RX stats (DDC seq/pkt count)
+
+ // Hardware PTT (foot switch / mic PTT) — обнаружение фронта
+ if HWPTT <> FHWPTTActive then
+ begin
+ FHWPTTActive := HWPTT;
+ // Hardware PTT включает/выключает передачу только если BtnMOX не нажата
+ if not BtnMOX.Active then
+ ApplyMOX(HWPTT);
+ end;
end;
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
@@ -3034,7 +3292,8 @@ end;
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
begin
- // TODO: WDSP TXA
+ if FTransmitting and FWDSPReady then
+ FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
end;
// ===========================================================================
@@ -3147,8 +3406,10 @@ begin
end;
FWDSPReady := False;
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
- FDSPEngine.OnAudio := OnAudioReady;
- FDSPEngine.OnSpectrum := OnSpectrumReady;
+ FDSPEngine.OnAudio := OnAudioReady;
+ FDSPEngine.OnSpectrum := OnSpectrumReady;
+ FDSPEngine.OnWaterfall := OnWaterfallReady;
+ FDSPEngine.OnTXIQ := OnTXIQReady;
end;
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
@@ -3297,6 +3558,7 @@ var
GenPkt: TGeneralPacket;
DUCPkt: TDUCSpecificPacket;
G_Settings: TGlobalSettings;
+ i: Integer;
begin
BtnStartStop.Enabled := True;
@@ -3312,8 +3574,14 @@ begin
FDevConnected := True;
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
FVolume := G_Settings.Volume;
- FDriveLevel := G_Settings.DriveLevel;
FActiveVfo := G_Settings.ActiveVfo;
+ // PA settings
+ FPAMaxPower := G_Settings.PAMaxPower;
+ for i := 0 to BAND_COUNT - 1 do
+ FPABandCal[i] := G_Settings.PABandCal[i];
+ // Slider position (0..100) and calibrated drive byte
+ TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100);
+ FDriveLevel := CalcDriveByte;
FCurrentBand := G_Settings.LastBand;
// SampleRate — глобальный, загружаем до RestoreBand
if G_Settings.SampleRate > 0 then
@@ -3327,6 +3595,53 @@ begin
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
+ FShowSpectrum := G_Settings.ShowSpectrum;
+ FShowWaterfall := G_Settings.ShowWaterfall;
+ if G_Settings.DisplayFPS > 0 then
+ ApplyFPS(G_Settings.DisplayFPS);
+ // Настройки дисплея и сетки
+ if G_Settings.FFTSize > 0 then
+ begin
+ FSpecRefLevel := G_Settings.SpecRefLevel;
+ FSpecRange := G_Settings.SpecRange;
+ if G_Settings.SpecGridStep > 0 then FSpecGridStep := G_Settings.SpecGridStep;
+ InvalidateGridCache;
+ if FWDSPReady then
+ begin
+ FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
+ FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode,
+ G_Settings.SpecAvgCount, G_Settings.SpecBackmult);
+ FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
+ G_Settings.WfAvgCount, G_Settings.WfBackmult);
+ end;
+ // Аудио устройство вывода
+ FAudioOut.Close;
+ if G_Settings.AudioSampleRate > 0 then
+ FAudioOut.SampleRate := G_Settings.AudioSampleRate;
+ if G_Settings.AudioOutDevice <> '' then
+ begin
+ FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(G_Settings.AudioOutDevice);
+ FAudioOutDevName := G_Settings.AudioOutDevice;
+ end
+ else
+ begin
+ FAudioOut.DeviceIndex := -1;
+ FAudioOutDevName := '';
+ end;
+ FAudioOut.Open;
+ // Аудио устройство ввода (TX mic)
+ if G_Settings.AudioInDevice <> '' then
+ begin
+ FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
+ FAudioInDevName := G_Settings.AudioInDevice;
+ FAudioIn.Open;
+ end
+ else
+ begin
+ FAudioIn.DeviceIndex := -1;
+ FAudioInDevName := '';
+ end;
+ end;
RestoreBand(FCurrentBand);
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
@@ -4117,9 +4432,14 @@ begin
end;
end;
-procedure TMainForm.BtnMOXClick(Sender: TObject);
+function TMainForm.ActiveTXFreqHz: Double;
begin
- FTransmitting := not FTransmitting;
+ if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
+end;
+
+procedure TMainForm.ApplyMOX(Active: Boolean);
+begin
+ FTransmitting := Active;
if FTransmitting then
begin
BtnMOX.ClrNorm := TColor($00000044);
@@ -4128,7 +4448,19 @@ begin
BtnMOX.Active := True;
end else
StyleButton(BtnMOX, False);
- if FWDSPReady then FDSPEngine.SetTXRun(FTransmitting);
+ if FWDSPReady then
+ FDSPEngine.SetTXRun(FTransmitting);
+ if FNetwork.Connected and FNetwork.Running then
+ begin
+ FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel,
+ FTransmitting, True, True);
+ FNetwork.SendFullHP;
+ end;
+end;
+
+procedure TMainForm.BtnMOXClick(Sender: TObject);
+begin
+ ApplyMOX(not FTransmitting);
end;
procedure TMainForm.BtnMuteClick(Sender: TObject);
@@ -4784,11 +5116,62 @@ end;
procedure TMainForm.TrkDriveChange(Sender: TObject);
begin
- FDriveLevel := Round(TrkDrive.Position / 100.0 * 255);
+ FDriveLevel := CalcDriveByte;
if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
end;
+function TMainForm.CalcDriveByte: Byte;
+var Cal: Double;
+begin
+ if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then
+ Cal := FPABandCal[FCurrentBand]
+ else
+ Cal := 100.0;
+ Result := Round(TrkDrive.Position / 100.0 * Cal / 100.0 * 255.0);
+end;
+
+procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer);
+var
+ IData: array[0..239] of Integer;
+ QData: array[0..239] of Integer;
+ Offset, n, i: Integer;
+begin
+ if not FNetwork.Connected then Exit;
+ Offset := 0;
+ while Offset < Count do
+ begin
+ FillChar(IData, SizeOf(IData), 0);
+ FillChar(QData, SizeOf(QData), 0);
+ n := Count - Offset;
+ if n > 240 then n := 240;
+ for i := 0 to n - 1 do
+ begin
+ IData[i] := Round(Buf[(Offset + i) * 2] * 8388607.0);
+ QData[i] := Round(Buf[(Offset + i) * 2 + 1] * 8388607.0);
+ end;
+ FNetwork.SendDUCIQ(IData, QData);
+ Inc(Offset, 240);
+ end;
+end;
+
+procedure TMainForm.OnPASettingsChange(MaxPower: Double;
+ const BandCal: array of Double);
+var i: Integer;
+begin
+ FPAMaxPower := MaxPower;
+ for i := 0 to BAND_COUNT - 1 do
+ begin
+ if i <= High(BandCal) then
+ FPABandCal[i] := BandCal[i]
+ else
+ FPABandCal[i] := 100.0;
+ end;
+ FDriveLevel := CalcDriveByte;
+ if FNetwork.Connected and FNetwork.Running then
+ FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
+end;
+
procedure TMainForm.TrkVolumeChange(Sender: TObject);
begin
FVolume := TrkVolume.Position;
@@ -4903,6 +5286,146 @@ begin
FSpectrumDirty := True;
end;
+procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
+ Count: Integer);
+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];
+end;
+
+// ===========================================================================
+// Settings apply methods (вызываются из TSettingsForm)
+// ===========================================================================
+
+procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
+ SpecAvgCount: Integer; SpecBackmult: Double);
+begin
+ if FWDSPReady then
+ FDSPEngine.SetFFTParams(FFTSize, WinType);
+ if FWDSPReady then
+ FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgCount, SpecBackmult);
+end;
+
+procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
+ WfBackmult: Double);
+begin
+ if FWDSPReady then
+ FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgCount, WfBackmult);
+end;
+
+procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
+begin
+ FSpecRefLevel := RefLevel;
+ FSpecRange := Range;
+ if GridStep > 0 then FSpecGridStep := GridStep;
+ InvalidateGridCache;
+end;
+
+procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string);
+begin
+ if FAudioOut.IsOpen then
+ FAudioOut.Close;
+ FAudioOut.DeviceIndex := DevIndex;
+ FAudioOutDevName := DevName;
+ try
+ if not FAudioOut.Open then
+ StatusBar1.Panels[3].Text := 'Audio out: ' + FAudioOut.LastError
+ else
+ StatusBar1.Panels[3].Text := 'Audio out: ' + IfThen(DevName = '', 'Default', DevName);
+ except
+ on E: Exception do
+ StatusBar1.Panels[3].Text := 'Audio out err: ' + E.Message;
+ end;
+end;
+
+procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
+begin
+ if FAudioIn.IsOpen then
+ FAudioIn.Close;
+ FAudioIn.DeviceIndex := DevIndex;
+ FAudioInDevName := DevName;
+ if DevIndex >= 0 then
+ begin
+ try
+ if not FAudioIn.Open then
+ StatusBar1.Panels[3].Text := 'Mic: ' + FAudioIn.LastError
+ else
+ StatusBar1.Panels[3].Text := 'Mic: ' + DevName;
+ except
+ on E: Exception do
+ StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
+ end;
+ end
+ else
+ StatusBar1.Panels[3].Text := 'Mic: none';
+end;
+
+procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
+begin
+ FShowSpectrum := ShowSpectrum;
+ FShowWaterfall := ShowWaterfall;
+ ResizeSpectrumPanels;
+end;
+
+procedure TMainForm.ApplyFPS(FPS: Integer);
+begin
+ if FPS < 1 then FPS := 1;
+ if FPS > 60 then FPS := 60;
+ FDisplayFPS := FPS;
+ FSpectrumTimer.Interval := 1000 div FPS;
+end;
+
+procedure TMainForm.BtnSettingsClick(Sender: TObject);
+var
+ SF: TSettingsForm;
+begin
+ if not Assigned(FSettingsForm) then
+ begin
+ SF := TSettingsForm.Create(Self);
+ FSettingsForm := SF;
+ SF.OnDisplayChange := ApplyDisplayParams;
+ SF.OnWaterfallChange := ApplyWaterfallParams;
+ SF.OnGridChange := ApplyGridParams;
+ SF.OnAudioDevChange := ApplyAudioDevice;
+ SF.OnAudioInDevChange := ApplyAudioInputDevice;
+ SF.OnVisibilityChange := ApplyVisibility;
+ SF.OnFPSChange := ApplyFPS;
+ SF.OnPAChange := OnPASettingsChange;
+ end;
+ SF := TSettingsForm(FSettingsForm);
+ SF.LoadPASettings(FPAMaxPower, FPABandCal);
+ // Перечисляем PA устройства
+ SF.RefreshAudioDevices(FAudioOut, FAudioIn);
+ SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
+ SF.LoadFPS(FDisplayFPS);
+ // Загружаем текущие значения
+ if FWDSPReady then
+ SF.LoadValues(
+ FDSPEngine.FFTSize,
+ FDSPEngine.WindowType,
+ FDSPEngine.SpecDetector,
+ FDSPEngine.SpecAvgMode,
+ FDSPEngine.SpecAvgCount,
+ FDSPEngine.SpecBackmult,
+ FDSPEngine.WfDetector,
+ FDSPEngine.WfAvgMode,
+ FDSPEngine.WfAvgCount,
+ FDSPEngine.WfBackmult,
+ FSpecRefLevel, FSpecRange, FSpecGridStep,
+ FAudioOutDevName, FAudioInDevName)
+ else
+ SF.LoadValues(
+ 4096, 5, 0, 3, 2, 0.45,
+ 2, 3, 2, 0.45,
+ FSpecRefLevel, FSpecRange, FSpecGridStep,
+ FAudioOutDevName, FAudioInDevName);
+ SF.Show;
+end;
procedure TMainForm.AfterShowTick(Sender: TObject);
begin
diff --git a/Settings.pas b/Settings.pas
index 917f71e..5fa8166 100644
--- a/Settings.pas
+++ b/Settings.pas
@@ -48,6 +48,32 @@ type
WindowTop: Integer;
WindowWidth: Integer;
WindowHeight: Integer;
+ // --- Display settings ---
+ FFTSize: Integer; // 1024,2048,4096,8192,16384
+ 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
+ WfDetector: Integer; // waterfall detector
+ WfAvgMode: Integer; // waterfall average mode
+ WfAvgCount: Integer; // waterfall average count
+ WfBackmult: Double; // waterfall backmult
+ 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
+ // --- Audio settings ---
+ AudioOutDevice: string; // PortAudio output device name ('' = default)
+ AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
+ AudioInDevice: string; // PortAudio input device name for TX mic ('' = none)
+ // --- Visibility settings ---
+ 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)
+ // --- PA (Power Amplifier) settings ---
+ PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
+ PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
end;
TSettingsManager = class
@@ -61,9 +87,11 @@ type
function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
function JD(O: TJSONObject; const K: string; Def: Double): Double;
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
+ function JS(O: TJSONObject; const K: string; const Def: string): string;
procedure JW(O: TJSONObject; const K: string; V: Integer); overload;
procedure JW(O: TJSONObject; const K: string; V: Double); overload;
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
+ procedure JWS(O: TJSONObject; const K: string; const V: string);
public
constructor Create(const FilePath: string = SETTINGS_FILE);
destructor Destroy; override;
@@ -110,12 +138,35 @@ begin
end;
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
+var i: Integer;
begin
FillChar(G, SizeOf(G), 0);
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.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.SpecDetector := 0; // Peak
+ G.SpecAvgMode := 3; // Log Recursive
+ G.SpecAvgCount := 2;
+ G.SpecBackmult := 0.45;
+ G.WfDetector := 2; // Average
+ G.WfAvgMode := 3; // Log Recursive
+ G.WfAvgCount := 2;
+ G.WfBackmult := 0.45;
+ G.SpecRefLevel := -20.0;
+ G.SpecRange := 110.0;
+ G.SpecGridStep := 10.0;
+ G.AudioOutDevice := '';
+ G.AudioSampleRate := 48000;
+ G.AudioInDevice := '';
+ G.ShowSpectrum := True;
+ G.ShowWaterfall := True;
+ G.DisplayFPS := 20;
end;
constructor TSettingsManager.Create(const FilePath: string);
@@ -226,6 +277,17 @@ begin
O.Add(K, V);
end;
+function TSettingsManager.JS(O: TJSONObject; const K: string; const Def: string): string;
+var D: TJSONData;
+begin D := O.Find(K); if D<>nil then try Result:=D.AsString; except Result:=Def; end else Result:=Def; end;
+
+procedure TSettingsManager.JWS(O: TJSONObject; const K: string; const V: string);
+var Idx: Integer;
+begin
+ Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
+ O.Add(K, V);
+end;
+
function TSettingsManager.LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean;
@@ -249,6 +311,32 @@ begin
G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False);
G.LastBand := JI(GObj,'last_band',5);
G.SampleRate := JI(GObj,'sample_rate',192000);
+ // Display settings
+ 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.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.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);
+ // Audio settings
+ G.AudioOutDevice := JS(GObj,'audio_out_device','');
+ G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
+ G.AudioInDevice := JS(GObj,'audio_in_device','');
+ // Visibility settings
+ G.ShowSpectrum := JB(GObj,'show_spectrum',True);
+ G.ShowWaterfall := JB(GObj,'show_waterfall',True);
+ G.DisplayFPS := JI(GObj,'display_fps',20);
+ // PA settings
+ G.PAMaxPower := JD(GObj,'pa_max_power',100.0);
+ for i := 0 to CFG_BAND_COUNT-1 do
+ G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i),100.0);
for i := 0 to CFG_BAND_COUNT-1 do
begin
@@ -268,7 +356,7 @@ end;
procedure TSettingsManager.SaveGlobal(const MAC: array of Byte;
const G: TGlobalSettings);
-var O: TJSONObject;
+var O: TJSONObject; i: Integer;
begin
O := GetGlobalObj(GetDevObj(MacToStr(MAC)));
JW(O,'volume',G.Volume); JW(O,'drive_level',G.DriveLevel);
@@ -283,6 +371,32 @@ begin
JW(O,'wf_nf_enabled',G.WfNFEnabled);
JW(O,'last_band',G.LastBand);
JW(O,'sample_rate',G.SampleRate);
+ // Display settings
+ JW(O,'fft_size',G.FFTSize);
+ 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,'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,'spec_ref_level',G.SpecRefLevel);
+ JW(O,'spec_range',G.SpecRange);
+ JW(O,'spec_grid_step',G.SpecGridStep);
+ // Audio settings
+ JWS(O,'audio_out_device',G.AudioOutDevice);
+ JW(O,'audio_sample_rate',G.AudioSampleRate);
+ JWS(O,'audio_in_device',G.AudioInDevice);
+ // Visibility settings
+ JW(O,'show_spectrum',G.ShowSpectrum);
+ JW(O,'show_waterfall',G.ShowWaterfall);
+ JW(O,'display_fps',G.DisplayFPS);
+ // PA settings
+ JW(O,'pa_max_power',G.PAMaxPower);
+ for i := 0 to CFG_BAND_COUNT-1 do
+ JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]);
end;
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
diff --git a/SettingsForm.pas b/SettingsForm.pas
index 3564001..9546596 100644
--- a/SettingsForm.pas
+++ b/SettingsForm.pas
@@ -30,6 +30,10 @@ uses
FlatButton, AudioOutput, AudioInput;
type
+ // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
+ TOnPASettingsChange = procedure(MaxPower: Double;
+ 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;
@@ -92,10 +96,15 @@ type
FChkShowWaterfall: TCheckBox;
FCmbFPS: TComboBox;
+ // ---- PA Settings tab controls ----
+ FEdMaxPower: TSpinEdit;
+ FEdBandCal: array[0..10] of TFloatSpinEdit;
+
// ---- Close button ----
FBtnClose: TFlatButton;
// ---- Callbacks ----
+ FOnPAChange: TOnPASettingsChange;
FOnDisplayChange: TOnDisplayParamChange;
FOnWaterfallChange: TOnWaterfallParamChange;
FOnGridChange: TOnGridParamChange;
@@ -111,6 +120,7 @@ type
procedure BuildAudioTab;
procedure BuildDisplayTab;
procedure BuildRX1Tab;
+ procedure BuildPATab;
procedure ApplyTheme;
// Helpers
@@ -139,6 +149,8 @@ type
procedure OnTXDevChange(Sender: TObject);
procedure OnVisibilityChkChange(Sender: TObject);
procedure OnFPSCmbChange(Sender: TObject);
+ procedure OnMaxPowerChange(Sender: TObject);
+ procedure OnBandCalChange(Sender: TObject);
procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange;
@@ -171,7 +183,10 @@ type
procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
// Загрузить текущий FPS (без fire события)
procedure LoadFPS(FPS: Integer);
+ // Загрузить PA настройки (без fire событий)
+ procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
+ property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange;
@@ -216,6 +231,9 @@ const
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');
+ PA_BAND_NAMES: array[0..10] of string =
+ ('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
+
{ TSettingsForm }
constructor TSettingsForm.Create(AOwner: TComponent);
@@ -312,6 +330,7 @@ begin
BuildAudioTab;
BuildDisplayTab;
+ BuildPATab;
// Close button
FBtnClose := TFlatButton.Create(Self);
@@ -1030,6 +1049,160 @@ begin
end;
end;
+// ---------------------------------------------------------------------------
+// BuildPATab — вкладка PA Settings: MaxPower + per-band calibration
+// ---------------------------------------------------------------------------
+
+procedure TSettingsForm.BuildPATab;
+const
+ LBL_W = 46; // ширина метки диапазона
+ ED_W = 72; // ширина SpinEdit (с кнопками)
+ COL_W = LBL_W + ED_W + 8; // ширина одной пары
+ COLS = 4; // колонок на строку
+ ROW_H = 32; // высота строки (spin edits чуть выше plain edit)
+ GRP_PAD = 12; // отступ внутри группы
+ GRP_TOP = 24; // высота caption группы
+ ROWS = 3; // строк для 11 диапазонов (ceil(11/4))
+
+ function MakeFloatSpin(AParent: TWinControl; ALeft, ATop: Integer;
+ AChange: TNotifyEvent): TFloatSpinEdit;
+ var E: TFloatSpinEdit;
+ begin
+ E := TFloatSpinEdit.Create(Self);
+ E.Parent := AParent;
+ E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2);
+ E.Color := CLR_PANEL;
+ E.Font.Color := CLR_TEXT;
+ E.Font.Name := 'Courier New';
+ E.Font.Size := 8;
+ E.MinValue := 38.8;
+ E.MaxValue := 100.0;
+ E.Increment := 0.1;
+ E.DecimalPlaces := 1;
+ E.Value := 100.0;
+ E.OnChange := AChange;
+ Result := E;
+ end;
+
+var
+ Grp: TGroupBox;
+ Sep: TPanel;
+ Lbl: TLabel;
+ Spin: TSpinEdit;
+ i, r, c, X, Y: Integer;
+begin
+ // ── Top padding ───────────────────────────────────────────────────────────
+ Sep := TPanel.Create(Self);
+ Sep.Parent := FTabPA;
+ Sep.BevelOuter := bvNone;
+ Sep.Color := CLR_BG;
+ Sep.Align := alTop;
+ Sep.Height := 10;
+
+ // ── Group Power & Calibration ─────────────────────────────────────────────
+ Grp := TGroupBox.Create(Self);
+ Grp.Parent := FTabPA;
+ Grp.Caption := 'Power & Calibration';
+ Grp.Color := CLR_GROUP;
+ Grp.Font.Color := CLR_ACCENT;
+ Grp.Font.Name := 'Courier New';
+ Grp.Font.Size := 8;
+ Grp.Align := alTop;
+ Grp.Height := GRP_TOP + ROW_H + 20 + ROWS * ROW_H + GRP_PAD + 16;
+ Grp.AutoSize := False;
+
+ // Max Power строка
+ Lbl := TLabel.Create(Self);
+ Lbl.Parent := Grp;
+ Lbl.Caption := 'Max Power (W):';
+ Lbl.SetBounds(GRP_PAD, GRP_TOP + 6, 120, 18);
+ Lbl.Font.Color := CLR_TEXTDIM;
+ Lbl.Font.Name := 'Courier New';
+ Lbl.Font.Size := 8;
+
+ Spin := TSpinEdit.Create(Self);
+ Spin.Parent := Grp;
+ Spin.SetBounds(GRP_PAD + 124, GRP_TOP, ED_W, BTN_H + 2);
+ Spin.Color := CLR_PANEL;
+ Spin.Font.Color := CLR_TEXT;
+ Spin.Font.Name := 'Courier New';
+ Spin.Font.Size := 8;
+ Spin.MinValue := 5;
+ Spin.MaxValue := 200;
+ Spin.Increment := 1;
+ Spin.Value := 100;
+ Spin.OnChange := OnMaxPowerChange;
+ FEdMaxPower := Spin;
+
+ // Метка для калибровки
+ Lbl := TLabel.Create(Self);
+ Lbl.Parent := Grp;
+ Lbl.Caption := 'Calibration per band (38.8 ... 100.0):';
+ Lbl.SetBounds(GRP_PAD, GRP_TOP + ROW_H + 8, 280, 18);
+ Lbl.Font.Color := CLR_TEXTDIM;
+ Lbl.Font.Name := 'Courier New';
+ Lbl.Font.Size := 8;
+
+ // Grid 4 columns × 3 rows for 11 bands
+ for i := 0 to 10 do
+ begin
+ r := i div COLS;
+ c := i mod COLS;
+ X := GRP_PAD + c * (COL_W + 4);
+ Y := GRP_TOP + ROW_H + 26 + r * ROW_H;
+
+ Lbl := TLabel.Create(Self);
+ Lbl.Parent := Grp;
+ Lbl.Caption := PA_BAND_NAMES[i] + ':';
+ Lbl.SetBounds(X, Y + 5, LBL_W, 18);
+ Lbl.Font.Color := CLR_TEXTDIM;
+ Lbl.Font.Name := 'Courier New';
+ Lbl.Font.Size := 8;
+
+ FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange);
+ FEdBandCal[i].Tag := i;
+ end;
+end;
+
+procedure TSettingsForm.LoadPASettings(MaxPower: Double;
+ const BandCal: array of Double);
+var i, n: Integer;
+begin
+ FLoading := True;
+ try
+ FEdMaxPower.Value := Round(EnsureRange(MaxPower, 5.0, 200.0));
+ n := Min(10, High(BandCal));
+ for i := 0 to n do
+ FEdBandCal[i].Value := EnsureRange(BandCal[i], 38.8, 100.0);
+ finally
+ FLoading := False;
+ end;
+end;
+
+procedure TSettingsForm.OnMaxPowerChange(Sender: TObject);
+var
+ BandCal: array[0..10] of Double;
+ i: Integer;
+begin
+ if FLoading then Exit;
+ if not Assigned(FOnPAChange) then Exit;
+ for i := 0 to 10 do
+ BandCal[i] := FEdBandCal[i].Value;
+ FOnPAChange(FEdMaxPower.Value, BandCal);
+end;
+
+procedure TSettingsForm.OnBandCalChange(Sender: TObject);
+var
+ BandCal: array[0..10] of Double;
+ i: Integer;
+begin
+ if FLoading then Exit;
+ if not Assigned(FOnPAChange) then Exit;
+ for i := 0 to 10 do
+ BandCal[i] := FEdBandCal[i].Value;
+ FOnPAChange(FEdMaxPower.Value, BandCal);
+end;
+
procedure TSettingsForm.BtnCloseClick(Sender: TObject);
begin
Close;
diff --git a/WDSPEngine.pas b/WDSPEngine.pas
index 466da27..6e7226a 100644
--- a/WDSPEngine.pas
+++ b/WDSPEngine.pas
@@ -103,6 +103,9 @@ const
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
+ // TX mic ring buffer — должен быть степенью двойки
+ TX_MIC_RING = 8192;
+
type
// Пакет в очереди между сетевым и DSP потоком
TIQQueueItem = record
@@ -118,6 +121,11 @@ type
Count: Integer) of object;
TOnSpectrumReady = procedure(const Pixels: array of Single;
Count: Integer) of object;
+ // TX IQ callback — вызывается из TTXDSPThread при готовности блока
+ // Buf: interleaved [I0,Q0,I1,Q1,...] doubles, Count — кол-во IQ пар
+ TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
+ TOnWaterfallReady = procedure(const Pixels: array of Single;
+ Count: Integer) of object;
{ TWDSPEngine }
TWDSPEngine = class
@@ -140,8 +148,8 @@ type
FRXAccQ: array of Double;
FRXAccPos: Integer;
- // Snapshot буфер для Spectrum0
- FSnapBuf: array of Double;
+ // Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
+ FSpecBuf: array of Double;
// Аудио выходные буферы — аллоцируем один раз
FOutL: array of Single;
FOutR: array of Single;
@@ -152,14 +160,36 @@ type
FQueueTail: Integer; // читает DSP поток
FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent)
FDSPRunning: Boolean;
- // Double буфер для Spectrum0 (принимает PDouble, не PdINREAL)
- FSpecBuf: array of Double; // Double буфер для Spectrum0 (PDouble)
- FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
- FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
+ FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
+ FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
+ FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
- FOnAudio: TOnAudioReady;
- FOnSpectrum: TOnSpectrumReady;
+ // Настройки анализатора
+ FFFTSize: Integer;
+ FWindowType: Integer;
+ FSpecDetector: Integer;
+ FSpecAvgMode: Integer;
+ FSpecAvgCount: Integer;
+ FSpecBackmult: Double;
+ FWfDetector: Integer;
+ FWfAvgMode: Integer;
+ FWfAvgCount: Integer;
+ FWfBackmult: Double;
+
+ FOnAudio: TOnAudioReady;
+ FOnSpectrum: TOnSpectrumReady;
+ FOnWaterfall: TOnWaterfallReady;
+ FOnTXIQ: TOnTXIQReady;
+
+ // TX mic ring buffer (lock-free: один writer — receive thread,
+ // один reader — TX DSP thread)
+ FTXMicRing: array[0..TX_MIC_RING-1] of Double;
+ FTXMicHead: Integer; // пишет receive thread
+ FTXMicTail: Integer; // читает TX DSP thread
+ FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
+ FTXThread: TThread;
+ FTXActive: Boolean;
FMode: Integer;
FSMeter: Double;
@@ -187,11 +217,13 @@ type
procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer;
procedure CloseAnalyzer;
+ procedure ApplyAnalyzerSettings;
procedure ApplyNRState;
procedure ApplyNBState;
procedure ApplySNBState;
procedure ApplyANFState;
procedure ApplyNoiseFilterState;
+ procedure ProcessTXBlock; // вызывается из TTXDSPThread
public
constructor Create(SampleRate: Integer = 192000;
@@ -236,6 +268,15 @@ type
procedure SetMicGain(GainDB: Double);
procedure SetTXRun(Run: Boolean);
+ // TX mic вход — вызывается из receive thread (thread-safe lock-free)
+ // Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов
+ procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
+
+ // Настройки анализатора (применяются немедленно если открыт)
+ procedure SetFFTParams(FFTSize, WinType: Integer);
+ procedure SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
+ procedure SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
+
// Spectrum — вызывать из таймера (~20 fps)
procedure UpdateSpectrum;
procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);
@@ -259,12 +300,69 @@ type
property SNBEnabled: Boolean read FSNBEnabled;
property LastError: string read FLastError;
property SMeter: Double read FSMeter;
- property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
- property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
+ property FFTSize: Integer read FFFTSize;
+ 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 WfDetector: Integer read FWfDetector;
+ property WfAvgMode: Integer read FWfAvgMode;
+ property WfAvgCount: Integer read FWfAvgCount;
+ property WfBackmult: Double read FWfBackmult;
+ property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
+ property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
+ property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
+ property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
+ property TXActive: Boolean read FTXActive;
end;
implementation
+// ===========================================================================
+// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
+// сигнализирует о готовых IQ данных через FOnTXIQ callback.
+// Аналог tx_thread в piHPSDR/transmitter.c
+// ===========================================================================
+type
+ TTXDSPThread = class(TThread)
+ private
+ FEngine: TWDSPEngine;
+ protected
+ procedure Execute; override;
+ public
+ constructor Create(AEngine: TWDSPEngine);
+ end;
+
+constructor TTXDSPThread.Create(AEngine: TWDSPEngine);
+begin
+ FEngine := AEngine;
+ FreeOnTerminate := False;
+ inherited Create(False);
+end;
+
+procedure TTXDSPThread.Execute;
+var
+ Avail: Integer;
+begin
+ while not Terminated do
+ begin
+ RTLEventWaitFor(FEngine.FTXMicSem, 50);
+ if Terminated then Break;
+ if not FEngine.FTXActive then Continue;
+
+ // Обрабатываем все накопившиеся блоки
+ repeat
+ Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
+ and (TX_MIC_RING - 1);
+ if Avail >= FEngine.FAudioBufSize then
+ FEngine.ProcessTXBlock
+ else
+ Break;
+ until Terminated;
+ end;
+end;
+
// ===========================================================================
// DSP поток — обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
@@ -367,6 +465,17 @@ begin
FSNBEnabled := False;
FANFEnabled := False;
FRXAccPos := 0;
+ // Analyzer defaults
+ FFFTSize := 4096;
+ FWindowType := 5; // Kaiser
+ FSpecDetector := 0; // Peak
+ FSpecAvgMode := 3; // Log Recursive
+ FSpecAvgCount := 2;
+ FSpecBackmult := 0.45;
+ FWfDetector := 2; // Average
+ FWfAvgMode := 3; // Log Recursive
+ FWfAvgCount := 2;
+ FWfBackmult := 0.45;
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
@@ -374,10 +483,9 @@ begin
SetLength(FTXOut, FBufSize * 2);
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
- SetLength(FSnapBuf, FBufSize * 2);
+ SetLength(FSpecBuf, FBufSize * 2);
SetLength(FOutL, FAudioBufSize);
SetLength(FOutR, FAudioBufSize);
- SetLength(FSpecBuf, FBufSize * 2);
// Очередь и DSP поток
FQueueHead := 0;
@@ -385,12 +493,22 @@ begin
FDSPRunning := False;
FQueueSem := RTLEventCreate;
FDSPThread := nil;
+
+ // TX инициализация
+ FTXMicHead := 0;
+ FTXMicTail := 0;
+ FTXActive := False;
+ FTXThread := nil;
+ FTXMicSem := RTLEventCreate;
+ FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
end;
destructor TWDSPEngine.Destroy;
begin
+ SetTXRun(False); // останавливаем TX поток если запущен
Close;
RTLEventDestroy(FQueueSem);
+ RTLEventDestroy(FTXMicSem);
inherited;
end;
@@ -400,10 +518,7 @@ end;
procedure TWDSPEngine.OpenAnalyzer;
var
- Success: Integer;
- MaxW: Integer;
- Ovrlp: Integer;
- FRAME_RATE: Integer;
+ Success: Integer;
begin
if FAnalyzerOpen then Exit;
@@ -411,30 +526,41 @@ begin
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
if Success <> 0 then Exit;
- FRAME_RATE := 20;
- // max_w: WDSP аллоцирует ring-buffer и FFT планы на max_w сэмплов.
- // Формула: fft_size + min(0.1*samplerate, 0.1*fft_size*fps)
- // = 4096 + min(19200, 8192) = 12288 → 1700ms инициализации!
- // Реально нам нужен только один FFT-кадр без усреднения по времени.
- // MaxW = sz = 4096 — минимально допустимое значение (должно быть ≥ sz).
- // piHPSDR использует именно sz без добавки keep_time.
- MaxW := 4096;
- Ovrlp := 0;
-
FFlp[0] := 0;
- SetAnalyzer(DISP_ID, 1, 1, 1, @FFlp[0], 4096, FAudioBufSize, 1, 14.0,
- Ovrlp, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW);
-
- SetDisplayDetectorMode(DISP_ID, 0, DETECTOR_MODE_AVERAGE);
- SetDisplayAverageMode(DISP_ID, 0, AVERAGE_MODE_LOG_RECURSIVE);
- SetDisplayNumAverage(DISP_ID, 0, Max(2, Trunc(FRAME_RATE * 0.12)));
- SetDisplayAvBackmult(DISP_ID, 0, 0.45);
- SetDisplaySampleRate(DISP_ID, FSampleRate);
- SetDisplayNormOneHz(DISP_ID, 0, 0);
+ ApplyAnalyzerSettings;
FAnalyzerOpen := True;
end;
+procedure TWDSPEngine.ApplyAnalyzerSettings;
+// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
+var
+ MaxW: Integer;
+begin
+ // MaxW = FFTSize — минимально допустимо (piHPSDR approach)
+ MaxW := FFFTSize;
+
+ // 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);
+
+ // Спектр (pixout=0)
+ 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);
+
+ // Водопад (pixout=1)
+ SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
+ SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
+ SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
+ SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
+ SetDisplayNormOneHz(DISP_ID, 1, 0);
+
+ SetDisplaySampleRate(DISP_ID, FSampleRate);
+end;
+
procedure TWDSPEngine.CloseAnalyzer;
begin
if not FAnalyzerOpen then Exit;
@@ -501,13 +627,16 @@ begin
SetRXAPanelRun(RXA_CHAN, 1);
SetChannelState(RXA_CHAN, 1, 0);
+ // TXA: mic @ FAudioRate → IQ @ FSampleRate (DUC rate)
+ // out_size = FAudioBufSize * FSampleRate/FAudioRate = FBufSize (4096 @ 192kHz)
+ // FTXOut уже имеет размер FBufSize*2 — подходит идеально
OpenChannel(
TXA_CHAN,
FAudioBufSize,
FAudioBufSize,
FAudioRate,
FAudioRate,
- FAudioRate,
+ FSampleRate,
1,
0,
0.010, 0.010, 0.010, 0.010,
@@ -668,18 +797,19 @@ begin
FRXIn[i * 2 + 1] := FRXAccQ[i];
end;
- // Копируем входные IQ ДО fexchange0 для Spectrum0
- // fexchange0 in-place перезапишет FRXIn выходными данными @ 48kHz
+ // Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn
if FAnalyzerOpen then
- for i := 0 to FBufSize * 2 - 1 do
- FSpecBuf[i] := FRXIn[i]; // Single→Double, входные IQ @ 192kHz
+ begin
+ Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double));
+ end;
// DSP обработка
Err := 0;
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
- // Spectrum0 с входными данными (скопированными до fexchange0)
- if FAnalyzerOpen then
+ // Подаём RX IQ в анализатор только когда не передаём.
+ // Во время TX анализатор кормит ProcessTXBlock (TX сигнал на той же частоте).
+ if FAnalyzerOpen and not FTXActive then
Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]);
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
@@ -973,11 +1103,92 @@ end;
procedure TWDSPEngine.SetTXRun(Run: Boolean);
begin
- if not FInitialized then Exit;
+ FTXActive := Run;
+
if Run then
- SetChannelState(TXA_CHAN, 1, 0) // run, no delay
- else
- SetChannelState(TXA_CHAN, 0, 1); // stop, with slew
+ begin
+ if FInitialized then
+ SetChannelState(TXA_CHAN, 1, 0); // run, no delay
+ // Сброс mic ring buffer и запуск TX потока
+ FTXMicHead := 0;
+ FTXMicTail := 0;
+ if not Assigned(FTXThread) then
+ begin
+ FTXThread := TTXDSPThread.Create(Self);
+ FTXThread.Priority := tpHighest;
+ end;
+ end else begin
+ if FInitialized then
+ SetChannelState(TXA_CHAN, 0, 1); // stop, with slew
+ // Останавливаем TX поток
+ if Assigned(FTXThread) then
+ begin
+ FTXThread.Terminate;
+ RTLEventSetEvent(FTXMicSem);
+ FTXThread.WaitFor;
+ FreeAndNil(FTXThread);
+ end;
+ FTXMicHead := 0;
+ FTXMicTail := 0;
+ end;
+end;
+
+procedure TWDSPEngine.ProcessTXBlock;
+// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
+// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback
+var
+ i, tail: Integer;
+ Err: Integer;
+begin
+ if not FInitialized then Exit;
+
+ // Читаем 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)
+
+ Err := 0;
+ fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
+
+ // TX сигнал → анализатор: спектр/водопад показывают переданный сигнал
+ // FTXOut: FBufSize IQ пар @ FSampleRate — тот же формат что и FSpecBuf
+ if FAnalyzerOpen then
+ Spectrum0(1, DISP_ID, 0, 0, @FTXOut[0]);
+
+ // FTXOut содержит FBufSize IQ пар @ FSampleRate (interleaved double)
+ if Assigned(FOnTXIQ) then
+ FOnTXIQ(FTXOut, FBufSize);
+end;
+
+procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
+// Вызывается из receive thread — lock-free, кладёт сэмплы в ring buffer
+// 16-bit big-endian samples из аппаратного микрофона
+var
+ i, cnt, newHead: Integer;
+ S: SmallInt;
+begin
+ cnt := N;
+ if cnt > Length(Src) then cnt := Length(Src);
+
+ for i := 0 to cnt - 1 do
+ begin
+ // Аппаратный mic приходит в big-endian, нужно swap байт
+ S := SmallInt(((Src[i] and $FF) shl 8) or ((Src[i] shr 8) and $FF));
+ newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
+ if newHead <> FTXMicTail then // буфер не полон
+ begin
+ FTXMicRing[FTXMicHead] := S / 32768.0;
+ FTXMicHead := newHead;
+ end;
+ // если буфер полон — сэмпл отбрасывается (overrun)
+ end;
+
+ RTLEventSetEvent(FTXMicSem); // будим TX поток
end;
// ---------------------------------------------------------------------------
@@ -985,9 +1196,12 @@ end;
// ---------------------------------------------------------------------------
procedure TWDSPEngine.UpdateSpectrum;
-// Вызывается из таймера главного потока (~20fps)
-// Spectrum0 уже вызван в сетевом потоке после каждого fexchange0
-// Здесь только читаем готовые пиксели
+// Вызывается из таймера главного потока (настраиваемый FPS).
+// Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
+// буфера (который fexchange0 непрерывно заполняет из DSP-потока).
+// Это обеспечивает равномерный FPS независимо от размера FFT:
+// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
+// буфера, а не ждёт следующего полного блока.
var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer;
@@ -995,15 +1209,27 @@ var
begin
if not FInitialized or not FAnalyzerOpen then Exit;
+ // Спектр (pixout=0)
Flag := 0;
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
- if Flag = 0 then Exit; // нет нового кадра — ждём следующего тика
+ if Flag <> 0 then
+ begin
+ for i := 0 to SPECTRUM_PIXELS - 1 do
+ FSpectrumPixels[i] := PixBuf[i];
+ if Assigned(FOnSpectrum) then
+ FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
+ end;
- for i := 0 to SPECTRUM_PIXELS - 1 do
- FSpectrumPixels[i] := PixBuf[i];
-
- if Assigned(FOnSpectrum) then
- FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
+ // Водопад (pixout=1)
+ Flag := 0;
+ GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
+ if Flag <> 0 then
+ begin
+ for i := 0 to SPECTRUM_PIXELS - 1 do
+ FWaterfallPixels[i] := PixBuf[i];
+ if Assigned(FOnWaterfall) then
+ FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS);
+ end;
end;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
@@ -1024,4 +1250,50 @@ begin
Result := FSMeter;
end;
+// ---------------------------------------------------------------------------
+// Настройки анализатора (применяются немедленно)
+// ---------------------------------------------------------------------------
+
+procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
+begin
+ // Ограничения: FFT size должен быть >= bf_sz (FAudioBufSize = 1024)
+ // и степенью двойки, не превышать m_size=16384 (из XCreateAnalyzer)
+ if FFTSize < FAudioBufSize then FFTSize := FAudioBufSize;
+ if FFTSize > 16384 then FFTSize := 16384;
+ if WinType < 0 then WinType := 0;
+ if WinType > 6 then WinType := 6;
+ FFFTSize := FFTSize;
+ FWindowType := WinType;
+ if FAnalyzerOpen then
+ ApplyAnalyzerSettings;
+end;
+
+procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer;
+ Backmult: Double);
+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);
+end;
+
+procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer;
+ Backmult: Double);
+begin
+ FWfDetector := Detector;
+ FWfAvgMode := AvgMode;
+ FWfAvgCount := AvgCount;
+ FWfBackmult := Backmult;
+ if not FAnalyzerOpen then Exit;
+ SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
+ SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
+ SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
+ SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
+end;
+
end.
diff --git a/doc/WDSP Guide, Rev 1.29.pdf b/doc/WDSP Guide, Rev 1.29.pdf
new file mode 100644
index 0000000..d93d8f3
Binary files /dev/null and b/doc/WDSP Guide, Rev 1.29.pdf differ
diff --git a/doc/openHPSDR Ethernet Protocol v4.3.docx b/doc/openHPSDR Ethernet Protocol v4.3.docx
new file mode 100644
index 0000000..7dba40c
Binary files /dev/null and b/doc/openHPSDR Ethernet Protocol v4.3.docx differ
diff --git a/ewsdr.lpi b/ewsdr.lpi
index 9d69e34..b262ee0 100644
--- a/ewsdr.lpi
+++ b/ewsdr.lpi
@@ -99,10 +99,18 @@
+
+
+
+
+
+
+
+