start work tx

This commit is contained in:
2026-03-26 22:50:55 +03:00
parent 7664907b3e
commit ae532a33cd
9 changed files with 1373 additions and 149 deletions
+133 -11
View File
@@ -56,10 +56,25 @@ type
statusFlags: LongWord; statusFlags: LongWord;
userData: Pointer): LongInt; cdecl; 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_Initialize = function: TPaError; cdecl;
TPa_Terminate = function: TPaError; cdecl; TPa_Terminate = function: TPaError; cdecl;
TPa_GetDefaultOutputDevice = function: TPaDeviceIndex; 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; TPa_OpenStream = function(stream: PPaStream;
inputParam: PPaStreamParameters; inputParam: PPaStreamParameters;
outputParam: PPaStreamParameters; outputParam: PPaStreamParameters;
@@ -89,10 +104,13 @@ type
FOutPt: Integer; // audio_buffer_outpt (read) FOutPt: Integer; // audio_buffer_outpt (read)
FMutex: TCriticalSection; FMutex: TCriticalSection;
FDeviceIndex: Integer; // -1 = default output device
// PortAudio functions // PortAudio functions
FPa_Initialize: TPa_Initialize; FPa_Initialize: TPa_Initialize;
FPa_Terminate: TPa_Terminate; FPa_Terminate: TPa_Terminate;
FPa_GetDefaultOutputDevice: TPa_GetDefaultOutputDevice; FPa_GetDefaultOutputDevice: TPa_GetDefaultOutputDevice;
FPa_GetDeviceCount: TPa_GetDeviceCount;
FPa_GetDeviceInfo: TPa_GetDeviceInfo;
FPa_OpenStream: TPa_OpenStream; FPa_OpenStream: TPa_OpenStream;
FPa_StartStream: TPa_StartStream; FPa_StartStream: TPa_StartStream;
FPa_StopStream: TPa_StopStream; FPa_StopStream: TPa_StopStream;
@@ -106,9 +124,19 @@ type
constructor Create(SampleRate: Integer = 48000); constructor Create(SampleRate: Integer = 48000);
destructor Destroy; override; destructor Destroy; override;
function Initialize: Boolean; // загружает PA, вызывает Pa_Initialize (без открытия стрима)
function Open: Boolean; function Open: Boolean;
procedure Close; 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() в оригинале // Пишем стерео double сэмплы — как audio_write() в оригинале
procedure WriteDouble(Left, Right: Double); procedure WriteDouble(Left, Right: Double);
// Convenience: массив Single // Convenience: массив Single
@@ -116,6 +144,8 @@ type
property IsOpen: Boolean read FOpen; property IsOpen: Boolean read FOpen;
property LastError: string read GetLastError; property LastError: string read GetLastError;
property DeviceIndex: Integer read FDeviceIndex write FDeviceIndex;
property SampleRate: Integer read FSampleRate write FSampleRate;
end; end;
// Глобальный callback (cdecl, не метод) // Глобальный callback (cdecl, не метод)
@@ -203,6 +233,7 @@ constructor TAudioOutput.Create(SampleRate: Integer);
begin begin
inherited Create; inherited Create;
FSampleRate := SampleRate; FSampleRate := SampleRate;
FDeviceIndex := -1; // -1 = default output device
FOpen := False; FOpen := False;
FPAInited := False; FPAInited := False;
FStream := nil; FStream := nil;
@@ -258,6 +289,8 @@ begin
FPa_Initialize := TPa_Initialize(GetProcAddress(FLibHandle, 'Pa_Initialize')); FPa_Initialize := TPa_Initialize(GetProcAddress(FLibHandle, 'Pa_Initialize'));
FPa_Terminate := TPa_Terminate(GetProcAddress(FLibHandle, 'Pa_Terminate')); FPa_Terminate := TPa_Terminate(GetProcAddress(FLibHandle, 'Pa_Terminate'));
FPa_GetDefaultOutputDevice := TPa_GetDefaultOutputDevice(GetProcAddress(FLibHandle, 'Pa_GetDefaultOutputDevice')); 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_OpenStream := TPa_OpenStream(GetProcAddress(FLibHandle, 'Pa_OpenStream'));
FPa_StartStream := TPa_StartStream(GetProcAddress(FLibHandle, 'Pa_StartStream')); FPa_StartStream := TPa_StartStream(GetProcAddress(FLibHandle, 'Pa_StartStream'));
FPa_StopStream := TPa_StopStream(GetProcAddress(FLibHandle, 'Pa_StopStream')); FPa_StopStream := TPa_StopStream(GetProcAddress(FLibHandle, 'Pa_StopStream'));
@@ -276,22 +309,17 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Open — точная последовательность как в audio_open_output() // Initialize — загружает PA библиотеку и вызывает Pa_Initialize без открытия стрима.
// Нужно для перечисления устройств в настройках до старта аудио.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function TAudioOutput.Open: Boolean; function TAudioOutput.Initialize: Boolean;
var var
OutParam: TPaStreamParameters;
Err: TPaError; Err: TPaError;
Dev: TPaDeviceIndex;
begin begin
Result := False; Result := False;
if FOpen then begin Result := True; Exit; end;
if not LoadLib then Exit; if not LoadLib then Exit;
if FPAInited then begin Result := True; Exit; end;
// Pa_Initialize — один раз (как в audio_get_cards)
if not FPAInited then
begin
Err := FPa_Initialize(); Err := FPa_Initialize();
if Err <> PA_NO_ERROR then if Err <> PA_NO_ERROR then
begin begin
@@ -303,12 +331,106 @@ begin
Exit; Exit;
end; end;
FPAInited := True; FPAInited := True;
Result := True;
end; 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()
// ---------------------------------------------------------------------------
function TAudioOutput.Open: Boolean;
var
OutParam: TPaStreamParameters;
Err: TPaError;
Dev: TPaDeviceIndex;
begin
Result := False;
if FOpen then begin Result := True; Exit; end;
if not Initialize then Exit; // загрузка PA + Pa_Initialize
// Выбор устройства: FDeviceIndex или default
if FDeviceIndex >= 0 then
Dev := FDeviceIndex
else
begin
Dev := FPa_GetDefaultOutputDevice(); Dev := FPa_GetDefaultOutputDevice();
end;
if Dev = PA_NO_DEV then if Dev = PA_NO_DEV then
begin begin
FLastError := 'No default output device'; FLastError := 'No output device available';
Exit; Exit;
end; end;
+31 -19
View File
@@ -19,7 +19,7 @@ uses
{$ELSE} {$ELSE}
Sockets, BaseUnix, Sockets, BaseUnix,
{$ENDIF} {$ENDIF}
HPSDRProtocol; HPSDRProtocol, SyncObjs;
{$IFDEF WINDOWS} {$IFDEF WINDOWS}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -129,6 +129,8 @@ type
FPAEnabled: Boolean; FPAEnabled: Boolean;
FAlexEnabled: Boolean; FAlexEnabled: Boolean;
FSendLock: TCriticalSection; // защита concurrent UDP sends
function DoCreateSocket: TSocket; function DoCreateSocket: TSocket;
procedure DoCloseSocket(var S: TSocket); procedure DoCloseSocket(var S: TSocket);
function DoSendTo(S: TSocket; const Buf; BufLen: Integer; function DoSendTo(S: TSocket; const Buf; BufLen: Integer;
@@ -423,11 +425,13 @@ begin
FIsTransmitting := False; FIsTransmitting := False;
FPAEnabled := True; FPAEnabled := True;
FAlexEnabled := True; FAlexEnabled := True;
FSendLock := TCriticalSection.Create;
end; end;
destructor THPSDRNetwork.Destroy; destructor THPSDRNetwork.Destroy;
begin begin
Disconnect; Disconnect;
FSendLock.Free;
inherited; inherited;
end; end;
@@ -529,7 +533,12 @@ begin
Addr.sin_family := AF_INET; Addr.sin_family := AF_INET;
Addr.sin_port := htons(DestPort); Addr.sin_port := htons(DestPort);
Addr.sin_addr := StrToNetAddr(DestIP); Addr.sin_addr := StrToNetAddr(DestIP);
FSendLock.Enter;
try
Result := fpSendTo(S, @Buf, BufLen, 0, @Addr, SizeOf(Addr)) = BufLen; Result := fpSendTo(S, @Buf, BufLen, 0, @Addr, SizeOf(Addr)) = BufLen;
finally
FSendLock.Leave;
end;
end; end;
function THPSDRNetwork.DoRecvFrom(S: TSocket; var Buf; BufLen: Integer; function THPSDRNetwork.DoRecvFrom(S: TSocket; var Buf; BufLen: Integer;
@@ -768,18 +777,10 @@ end;
procedure THPSDRNetwork.HandleMicData(const Buf: array of Byte; Len: Integer); procedure THPSDRNetwork.HandleMicData(const Buf: array of Byte; Len: Integer);
var var
Pkt: TMicDataPacket; Pkt: TMicDataPacket;
Sync: TMicSync;
M: TThreadMethod;
begin begin
if not Assigned(FOnMic) then Exit; if not Assigned(FOnMic) then Exit;
Move(Buf[0], Pkt, SizeOf(Pkt)); Move(Buf[0], Pkt, SizeOf(Pkt));
Sync := TMicSync.Create(FOnMic, Pkt); FOnMic(Pkt); // прямой вызов из receive thread — TX обработка не касается UI
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -820,8 +821,14 @@ begin
Result := 0; Result := 0;
// TX relay при передаче // TX relay при передаче
// Bit 27 ($08000000) = T/R relay для всех плат
// Bit 18 ($00040000) = TxRx Status для Orion MkII / Saturn / G2
if Transmitting then 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) // RX HPF (для ANAN-100/200) или BPF (для ANAN-7000)
if IsOrion2 then if IsOrion2 then
@@ -876,15 +883,15 @@ var
begin begin
FillChar(Pkt, SizeOf(Pkt), 0); FillChar(Pkt, SizeOf(Pkt), 0);
// ANAN-7000/8000 имеют 2 ADC // ANAN-7000/8000/Saturn имеют 2 ADC
if FDevice.BoardType in [4, 5] then // ORION=4, ORION2=5 if FDevice.BoardType in [4, 5, 10] then // ORION=4, ORION2=5, SATURN=10
Pkt.NumADCs := 2 Pkt.NumADCs := 2
else else
Pkt.NumADCs := 1; Pkt.NumADCs := 1;
// Для ANGELIA/ORION/ORION2 DDC начинается с индекса 2 (DDC0/1 — PureSignal) // Для ANGELIA/ORION/ORION2/SATURN DDC начинается с индекса 2 (DDC0/1 — PureSignal)
// Для HERMES/HL2 — с 0 // Для 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 DDCBase := 2
else else
DDCBase := 0; DDCBase := 0;
@@ -960,11 +967,16 @@ begin
Inc(FSeqHP); Inc(FSeqHP);
// Byte 4: Run | PTT // 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 // DDC base: ORION/ORION2/ANGELIA/SATURN начинают с DDC2
IsOrion2 := (FDevice.BoardType = 5); // ORION-II = 5 // IsOrion2: платы с Alex BPF и TxRx Status bit18 (ORION_MK2=5, SATURN=10)
if FDevice.BoardType in [3, 4, 5] then // ANGELIA=3, ORION=4, ORION2=5 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 DDCBase := 2
else else
DDCBase := 0; DDCBase := 0;
+550 -27
View File
@@ -26,7 +26,7 @@ uses
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types, StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType, LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork, HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput, WDSPEngine, AudioOutput, AudioInput,
IntfGraphics, FPImage, IntfGraphics, FPImage,
Settings, Settings,
WebServer; WebServer;
@@ -150,8 +150,9 @@ type
FSWRV: Double; FSWRV: Double;
FSupplyV: Double; FSupplyV: Double;
FPLLLock: Boolean; FPLLLock: Boolean;
FHWPTT: Boolean;
public public
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL: Boolean); constructor Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean);
procedure Execute; procedure Execute;
end; end;
@@ -182,6 +183,9 @@ type
// ---- DSP + Audio ---- // ---- DSP + Audio ----
FDSPEngine: TWDSPEngine; FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput; FAudioOut: TAudioOutput;
FAudioIn: TAudioInput;
FAudioOutDevName: string; // текущее имя выходного устройства
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
FWDSPReady: Boolean; // True когда WDSP открыт и работает FWDSPReady: Boolean; // True когда WDSP открыт и работает
// ---- State ---- // ---- State ----
@@ -203,8 +207,11 @@ type
FMarkerActive: Boolean; // True = линия видима FMarkerActive: Boolean; // True = линия видима
FMarkerX: Integer; // X в пикселях (относительно ширины панели) FMarkerX: Integer; // X в пикселях (относительно ширины панели)
FDriveLevel: Byte; FDriveLevel: Byte;
FPAMaxPower: Double;
FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон
FRunning: Boolean; FRunning: Boolean;
FTransmitting: Boolean; FTransmitting: Boolean;
FHWPTTActive: Boolean; // True = hardware PTT нажата
FMuted: Boolean; FMuted: Boolean;
FVolume: Integer; FVolume: Integer;
FLastSMeter: Double; FLastSMeter: Double;
@@ -223,16 +230,25 @@ type
// ---- Spectrum ---- // ---- Spectrum ----
FSpectrumBuf: array[0..1023] of Single; FSpectrumBuf: array[0..1023] of Single;
FWfAvgBuf: array[0..1023] of Single; FWfAvgBuf: array[0..1023] of Single;
FWaterfallBuf: array[0..1023] of Single; // WDSP waterfall pixout=1 data
FSpectrumBitmap: TBitmap; FSpectrumBitmap: TBitmap;
FWfAGCEnabled: Boolean; FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean; FWfNFEnabled: Boolean;
FWfHigh: Double; FWfHigh: Double;
FWfLow: 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; FWaterfallBitmap:TBitmap;
FWaterfallTemp: TBitmap; FWaterfallTemp: TBitmap;
FSpectrumWidth: Integer; FSpectrumWidth: Integer;
FSpectrumHeight: Integer; FSpectrumHeight: Integer;
FWaterfallHeight:Integer; FWaterfallHeight:Integer;
// ---- Visibility settings ----
FShowSpectrum: Boolean;
FShowWaterfall: Boolean;
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30)
// ---- Splitter между спектром и водопадом ---- // ---- Splitter между спектром и водопадом ----
FSplitter: TPanel; // визуальная полоса-разделитель FSplitter: TPanel; // визуальная полоса-разделитель
FSplitterDrag: Boolean; // True = идёт перетаскивание FSplitterDrag: Boolean; // True = идёт перетаскивание
@@ -268,6 +284,7 @@ type
FWebSyncBool: Boolean; FWebSyncBool: Boolean;
FWebSyncM: TThreadMethod; FWebSyncM: TThreadMethod;
FCurrentBand: Integer; // текущий активный диапазон 0..10 FCurrentBand: Integer; // текущий активный диапазон 0..10
FSettingsForm: TObject; // TSettingsForm (cast при использовании)
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
@@ -285,6 +302,7 @@ type
PanelToolbar: TPanel; PanelToolbar: TPanel;
BtnDiscover: TFlatButton; BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном) BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
BtnSettings: TFlatButton; // открыть диалог настроек
// ---- Left panel ---- // ---- Left panel ----
PanelLeft: TPanel; PanelLeft: TPanel;
@@ -397,10 +415,11 @@ type
// DSP/Audio callbacks (вызываются из рабочих потоков) // DSP/Audio callbacks (вызываются из рабочих потоков)
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer); procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: 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) // Public UI update (called from sync objects)
procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string); 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 DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord); procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
@@ -503,11 +522,18 @@ type
procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfAGCClick(Sender: TObject);
procedure BtnWfNFClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject);
procedure BtnHidePanelClick(Sender: TObject); procedure BtnHidePanelClick(Sender: TObject);
procedure BtnSettingsClick(Sender: TObject);
procedure InvalidateGridCache;
procedure PositionVfoOverlay; procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer); procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
procedure PbSpectrumDblClick(Sender: TObject); procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject); procedure TrkDriveChange(Sender: TObject);
procedure TrkVolumeChange(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 BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject); procedure TrkAGCChange(Sender: TObject);
procedure PbFwdPowerPaint(Sender: TObject); procedure PbFwdPowerPaint(Sender: TObject);
@@ -527,6 +553,18 @@ type
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton; procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer); 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
// Обработчики событий формы — должны быть в published для RTTI/LFM // Обработчики событий формы — должны быть в published для RTTI/LFM
procedure FormCreate(Sender: TObject); procedure FormCreate(Sender: TObject);
@@ -542,6 +580,8 @@ var
implementation implementation
uses SettingsForm;
{$R *.lfm} {$R *.lfm}
// =========================================================================== // ===========================================================================
@@ -581,7 +621,7 @@ begin
end; end;
constructor TStatusUISync.Create(AForm: TObject; constructor TStatusUISync.Create(AForm: TObject;
FW, SW, SV: Double; PLL: Boolean); FW, SW, SV: Double; PLL, HWPTT: Boolean);
begin begin
inherited Create; inherited Create;
FForm := AForm; FForm := AForm;
@@ -589,11 +629,12 @@ begin
FSWRV := SW; FSWRV := SW;
FSupplyV := SV; FSupplyV := SV;
FPLLLock := PLL; FPLLLock := PLL;
FHWPTT := HWPTT;
end; end;
procedure TStatusUISync.Execute; procedure TStatusUISync.Execute;
begin begin
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock); TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock, FHWPTT);
end; end;
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord); constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
@@ -647,9 +688,13 @@ begin
end; end;
function TMainForm.MakeGlobalSettings: TGlobalSettings; function TMainForm.MakeGlobalSettings: TGlobalSettings;
var i: Integer;
begin begin
Result.Volume := FVolume; 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.ActiveVfo := FActiveVfo;
Result.NRMode := BtnNR.Tag; Result.NRMode := BtnNR.Tag;
Result.NBMode := BtnNB.Tag; Result.NBMode := BtnNB.Tag;
@@ -661,6 +706,43 @@ begin
Result.WfNFEnabled := FWfNFEnabled; Result.WfNFEnabled := FWfNFEnabled;
Result.LastBand := FCurrentBand; Result.LastBand := FCurrentBand;
Result.SampleRate := FSampleRate; 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; end;
procedure TMainForm.SaveCurrentBand; procedure TMainForm.SaveCurrentBand;
@@ -751,7 +833,7 @@ end;
procedure TMainForm.FormCreate(Sender: TObject); procedure TMainForm.FormCreate(Sender: TObject);
var var
bi_, StartupRate: Integer; bi_, StartupRate, i: Integer;
StartupVfoA, StartupVfoB: Double; StartupVfoA, StartupVfoB: Double;
begin begin
FVfoA := 14200000; FVfoA := 14200000;
@@ -765,7 +847,9 @@ begin
FCTun := False; FCTun := False;
FFilterBW := 2700; FFilterBW := 2700;
FSpecDrag := False; FSpecDrag := False;
FDriveLevel := 100; FDriveLevel := 0;
FPAMaxPower := 100.0;
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
FRunning := False; FRunning := False;
FTransmitting := False; FTransmitting := False;
FMuted := False; FMuted := False;
@@ -782,6 +866,11 @@ begin
FWfAGCEnabled := False; FWfAGCEnabled := False;
FWfNFEnabled := False; FWfNFEnabled := False;
FDevConnected := False; FDevConnected := False;
// Spectrum grid settings defaults
FSpecRefLevel := -20.0;
FSpecRange := 110.0;
FSpecGridStep := 10.0;
FSettingsForm := nil;
FillChar(FDevMAC, SizeOf(FDevMAC), 0); FillChar(FDevMAC, SizeOf(FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями // Инициализируем кэш диапазонов умолчаниями
@@ -848,6 +937,9 @@ begin
FVfoOverlay.OnSelect := OnModeFilterSelect; FVfoOverlay.OnSelect := OnModeFilterSelect;
FVfoOverlay.Visible := False; FVfoOverlay.Visible := False;
FPanelHidden := False; FPanelHidden := False;
FShowSpectrum := True;
FShowWaterfall := True;
FDisplayFPS := 20;
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
FSplitterDrag := False; FSplitterDrag := False;
@@ -896,10 +988,13 @@ begin
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512); FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady; FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady; FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
// Audio output — создаём объект сейчас, открываем после показа формы // Audio output/input — создаём объекты сейчас, открываем после показа формы
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC) // (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
FAudioOut := TAudioOutput.Create(48000); FAudioOut := TAudioOutput.Create(48000);
FAudioIn := TAudioInput.Create(48000);
FMeterTimer := TTimer.Create(Self); FMeterTimer := TTimer.Create(Self);
FMeterTimer.Interval := 100; FMeterTimer.Interval := 100;
@@ -937,6 +1032,8 @@ begin
FNetwork.Free; FNetwork.Free;
FAudioOut.Close; FAudioOut.Close;
FAudioOut.Free; FAudioOut.Free;
FAudioIn.Close;
FAudioIn.Free;
FDSPEngine.Close; FDSPEngine.Close;
FDSPEngine.Free; FDSPEngine.Free;
FSpectrumBitmap.Free; FSpectrumBitmap.Free;
@@ -1034,6 +1131,7 @@ begin
X := 4; X := 4;
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick); BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick); BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick);
// ---- Status bar ---- // ---- Status bar ----
StatusBar1 := TStatusBar.Create(Self); StatusBar1 := TStatusBar.Create(Self);
@@ -1546,6 +1644,125 @@ begin
TopOff := PanelSpanButtons.Height; TopOff := PanelSpanButtons.Height;
// S-метр позиционируется отдельно в ResizeSMeter // S-метр позиционируется отдельно в ResizeSMeter
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; AvailH := RH - TopOff - RULER_H - SPLITTER_H;
if AvailH < (MIN_SH + MIN_WH) then Exit; if AvailH < (MIN_SH + MIN_WH) then Exit;
@@ -1564,7 +1781,6 @@ begin
// Линейка частот — всегда прижата к низу спектра // Линейка частот — всегда прижата к низу спектра
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H); 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); PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
@@ -1816,10 +2032,19 @@ var i: Integer;
begin begin
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0; for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0; for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
for i := 0 to 1023 do FWaterfallBuf[i] := -120.0;
FWfHigh := -50.0; FWfHigh := -50.0;
FWfLow := -120.0; FWfLow := -120.0;
end; end;
procedure TMainForm.InvalidateGridCache;
// Сбрасываем кэш фона/сетки — перерисуется на следующем тике DrawSpectrum
begin
FGridBitmapW := 0;
FGridBitmapH := 0;
FSpectrumDirty := True;
end;
procedure TMainForm.FillDemoSpectrum; procedure TMainForm.FillDemoSpectrum;
var var
i: Integer; i: Integer;
@@ -2035,7 +2260,8 @@ begin
if (W <= 0) or (H <= 0) then Exit; if (W <= 0) or (H <= 0) then Exit;
C := FSpectrumBitmap.Canvas; C := FSpectrumBitmap.Canvas;
DBmin := -130.0; DBmax := -20.0; DBmax := FSpecRefLevel;
DBmin := FSpecRefLevel - FSpecRange;
InvRange := 1.0 / (DBmax - DBmin); InvRange := 1.0 / (DBmax - DBmin);
// ── 1. Фон+сетка из кэша ─────────────────────────────────────────────── // ── 1. Фон+сетка из кэша ───────────────────────────────────────────────
@@ -2056,7 +2282,7 @@ begin
GC.Font.Name := 'Courier New'; GC.Font.Name := 'Courier New';
GC.Font.Color := TColor($00A8C9D9); GC.Font.Color := TColor($00A8C9D9);
GC.Brush.Style := bsClear; GC.Brush.Style := bsClear;
dB := -40.0; dB := DBmax - FSpecGridStep;
while dB >= DBmin do while dB >= DBmin do
begin begin
Yp := Round((DBmax - dB) * InvRange * H); Yp := Round((DBmax - dB) * InvRange * H);
@@ -2064,7 +2290,7 @@ begin
GC.Pen.Width := 1; GC.Pen.Width := 1;
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp); GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB])); GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
dB := dB - 10; dB := dB - FSpecGridStep;
end; end;
for i := 0 to 8 do for i := 0 to 8 do
begin begin
@@ -2206,8 +2432,24 @@ begin
H := FWaterfallBitmap.Height; H := FWaterfallBitmap.Height;
if (W <= 0) or (H <= 0) then Exit; 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; PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
if FWDSPReady then
begin
// Используем 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 for X := 0 to 1023 do
begin begin
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC; AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
@@ -2215,6 +2457,7 @@ begin
if AvgDB > PeakDB then PeakDB := AvgDB; if AvgDB > PeakDB then PeakDB := AvgDB;
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end; if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
end; end;
end;
// ── AGC High/Low ──────────────────────────────────────────────────────── // ── AGC High/Low ────────────────────────────────────────────────────────
if FWfAGCEnabled then if FWfAGCEnabled then
@@ -2789,10 +3032,16 @@ var
NowTick: QWord; NowTick: QWord;
ElapsedTick: QWord; ElapsedTick: QWord;
begin begin
// Синхронизируем размеры если PaintBox изменился (после ресайза) // Синхронизируем размеры если размер панели изменился (после ресайза).
if (PbSpectrum.Width <> FSpectrumWidth) or // Важно: проверяем только видимые панели — когда спектр скрыт,
(PbSpectrum.Height <> FSpectrumHeight) or // PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
(FSpectrumWidth = 0) then // что вызвало бы сброс буфера водопада на каждом тике.
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; ResizeSpectrumPanels;
ResizeSMeter; ResizeSMeter;
if FSpectrumWidth <= 0 then Exit; if FSpectrumWidth <= 0 then Exit;
@@ -2977,7 +3226,8 @@ begin
else else
SWRV := 1.0; SWRV := 1.0;
Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV, 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 try
M := Sync.Execute; M := Sync.Execute;
TThread.Synchronize(nil, M); TThread.Synchronize(nil, M);
@@ -2986,7 +3236,7 @@ begin
end; end;
end; end;
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean); procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
var var
PLLStr: string; PLLStr: string;
begin begin
@@ -2998,7 +3248,15 @@ begin
StatusBar1.Panels[2].Text := StatusBar1.Panels[2].Text :=
Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s', Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s',
[SupplyV, FwdW, SWRV, PLLStr]); [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; end;
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket); procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
@@ -3034,7 +3292,8 @@ end;
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket); procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
begin begin
// TODO: WDSP TXA if FTransmitting and FWDSPReady then
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
end; end;
// =========================================================================== // ===========================================================================
@@ -3149,6 +3408,8 @@ begin
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady; FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady; FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
end; end;
procedure TMainForm.BtnDiscoverClick(Sender: TObject); procedure TMainForm.BtnDiscoverClick(Sender: TObject);
@@ -3297,6 +3558,7 @@ var
GenPkt: TGeneralPacket; GenPkt: TGeneralPacket;
DUCPkt: TDUCSpecificPacket; DUCPkt: TDUCSpecificPacket;
G_Settings: TGlobalSettings; G_Settings: TGlobalSettings;
i: Integer;
begin begin
BtnStartStop.Enabled := True; BtnStartStop.Enabled := True;
@@ -3312,8 +3574,14 @@ begin
FDevConnected := True; FDevConnected := True;
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache); FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
FVolume := G_Settings.Volume; FVolume := G_Settings.Volume;
FDriveLevel := G_Settings.DriveLevel;
FActiveVfo := G_Settings.ActiveVfo; 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; FCurrentBand := G_Settings.LastBand;
// SampleRate — глобальный, загружаем до RestoreBand // SampleRate — глобальный, загружаем до RestoreBand
if G_Settings.SampleRate > 0 then if G_Settings.SampleRate > 0 then
@@ -3327,6 +3595,53 @@ begin
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton; BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled); FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled); FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
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); RestoreBand(FCurrentBand);
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress; StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
@@ -4117,9 +4432,14 @@ begin
end; end;
end; end;
procedure TMainForm.BtnMOXClick(Sender: TObject); function TMainForm.ActiveTXFreqHz: Double;
begin 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 if FTransmitting then
begin begin
BtnMOX.ClrNorm := TColor($00000044); BtnMOX.ClrNorm := TColor($00000044);
@@ -4128,7 +4448,19 @@ begin
BtnMOX.Active := True; BtnMOX.Active := True;
end else end else
StyleButton(BtnMOX, False); 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; end;
procedure TMainForm.BtnMuteClick(Sender: TObject); procedure TMainForm.BtnMuteClick(Sender: TObject);
@@ -4784,11 +5116,62 @@ end;
procedure TMainForm.TrkDriveChange(Sender: TObject); procedure TMainForm.TrkDriveChange(Sender: TObject);
begin begin
FDriveLevel := Round(TrkDrive.Position / 100.0 * 255); FDriveLevel := CalcDriveByte;
if FWDSPReady then if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0); FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
end; 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); procedure TMainForm.TrkVolumeChange(Sender: TObject);
begin begin
FVolume := TrkVolume.Position; FVolume := TrkVolume.Position;
@@ -4903,6 +5286,146 @@ begin
FSpectrumDirty := True; FSpectrumDirty := True;
end; 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); procedure TMainForm.AfterShowTick(Sender: TObject);
begin begin
+115 -1
View File
@@ -48,6 +48,32 @@ type
WindowTop: Integer; WindowTop: Integer;
WindowWidth: Integer; WindowWidth: Integer;
WindowHeight: 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; end;
TSettingsManager = class TSettingsManager = class
@@ -61,9 +87,11 @@ type
function JI(O: TJSONObject; const K: string; Def: Integer): Integer; function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
function JD(O: TJSONObject; const K: string; Def: Double): Double; function JD(O: TJSONObject; const K: string; Def: Double): Double;
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean; 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: Integer); overload;
procedure JW(O: TJSONObject; const K: string; V: Double); overload; procedure JW(O: TJSONObject; const K: string; V: Double); overload;
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload; procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
procedure JWS(O: TJSONObject; const K: string; const V: string);
public public
constructor Create(const FilePath: string = SETTINGS_FILE); constructor Create(const FilePath: string = SETTINGS_FILE);
destructor Destroy; override; destructor Destroy; override;
@@ -110,12 +138,35 @@ begin
end; end;
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings); class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
var i: Integer;
begin begin
FillChar(G, SizeOf(G), 0); FillChar(G, SizeOf(G), 0);
G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0; G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0;
G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False; G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False;
G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5; G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5;
G.SampleRate := 192000; 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; end;
constructor TSettingsManager.Create(const FilePath: string); constructor TSettingsManager.Create(const FilePath: string);
@@ -226,6 +277,17 @@ begin
O.Add(K, V); O.Add(K, V);
end; 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; function TSettingsManager.LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings; out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean; var Bands: array of TBandSettings): Boolean;
@@ -249,6 +311,32 @@ begin
G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False); G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False);
G.LastBand := JI(GObj,'last_band',5); G.LastBand := JI(GObj,'last_band',5);
G.SampleRate := JI(GObj,'sample_rate',192000); G.SampleRate := JI(GObj,'sample_rate',192000);
// 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 for i := 0 to CFG_BAND_COUNT-1 do
begin begin
@@ -268,7 +356,7 @@ end;
procedure TSettingsManager.SaveGlobal(const MAC: array of Byte; procedure TSettingsManager.SaveGlobal(const MAC: array of Byte;
const G: TGlobalSettings); const G: TGlobalSettings);
var O: TJSONObject; var O: TJSONObject; i: Integer;
begin begin
O := GetGlobalObj(GetDevObj(MacToStr(MAC))); O := GetGlobalObj(GetDevObj(MacToStr(MAC)));
JW(O,'volume',G.Volume); JW(O,'drive_level',G.DriveLevel); 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,'wf_nf_enabled',G.WfNFEnabled);
JW(O,'last_band',G.LastBand); JW(O,'last_band',G.LastBand);
JW(O,'sample_rate',G.SampleRate); 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; end;
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer; procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
+173
View File
@@ -30,6 +30,10 @@ uses
FlatButton, AudioOutput, AudioInput; FlatButton, AudioOutput, AudioInput;
type 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, TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
SpecAvgMode, SpecAvgCount: Integer; SpecBackmult: Double) of object; SpecAvgMode, SpecAvgCount: Integer; SpecBackmult: Double) of object;
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer; TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer;
@@ -92,10 +96,15 @@ type
FChkShowWaterfall: TCheckBox; FChkShowWaterfall: TCheckBox;
FCmbFPS: TComboBox; FCmbFPS: TComboBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TSpinEdit;
FEdBandCal: array[0..10] of TFloatSpinEdit;
// ---- Close button ---- // ---- Close button ----
FBtnClose: TFlatButton; FBtnClose: TFlatButton;
// ---- Callbacks ---- // ---- Callbacks ----
FOnPAChange: TOnPASettingsChange;
FOnDisplayChange: TOnDisplayParamChange; FOnDisplayChange: TOnDisplayParamChange;
FOnWaterfallChange: TOnWaterfallParamChange; FOnWaterfallChange: TOnWaterfallParamChange;
FOnGridChange: TOnGridParamChange; FOnGridChange: TOnGridParamChange;
@@ -111,6 +120,7 @@ type
procedure BuildAudioTab; procedure BuildAudioTab;
procedure BuildDisplayTab; procedure BuildDisplayTab;
procedure BuildRX1Tab; procedure BuildRX1Tab;
procedure BuildPATab;
procedure ApplyTheme; procedure ApplyTheme;
// Helpers // Helpers
@@ -139,6 +149,8 @@ type
procedure OnTXDevChange(Sender: TObject); procedure OnTXDevChange(Sender: TObject);
procedure OnVisibilityChkChange(Sender: TObject); procedure OnVisibilityChkChange(Sender: TObject);
procedure OnFPSCmbChange(Sender: TObject); procedure OnFPSCmbChange(Sender: TObject);
procedure OnMaxPowerChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
procedure BtnCloseClick(Sender: TObject); procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange; procedure FireDisplayChange;
@@ -171,7 +183,10 @@ type
procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
// Загрузить текущий FPS (без fire события) // Загрузить текущий FPS (без fire события)
procedure LoadFPS(FPS: Integer); 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 OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange; 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_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_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 } { TSettingsForm }
constructor TSettingsForm.Create(AOwner: TComponent); constructor TSettingsForm.Create(AOwner: TComponent);
@@ -312,6 +330,7 @@ begin
BuildAudioTab; BuildAudioTab;
BuildDisplayTab; BuildDisplayTab;
BuildPATab;
// Close button // Close button
FBtnClose := TFlatButton.Create(Self); FBtnClose := TFlatButton.Create(Self);
@@ -1030,6 +1049,160 @@ begin
end; end;
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); procedure TSettingsForm.BtnCloseClick(Sender: TObject);
begin begin
Close; Close;
+316 -44
View File
@@ -103,6 +103,9 @@ const
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски) IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6) IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
// TX mic ring buffer — должен быть степенью двойки
TX_MIC_RING = 8192;
type type
// Пакет в очереди между сетевым и DSP потоком // Пакет в очереди между сетевым и DSP потоком
TIQQueueItem = record TIQQueueItem = record
@@ -118,6 +121,11 @@ type
Count: Integer) of object; Count: Integer) of object;
TOnSpectrumReady = procedure(const Pixels: array of Single; TOnSpectrumReady = procedure(const Pixels: array of Single;
Count: Integer) of object; 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 }
TWDSPEngine = class TWDSPEngine = class
@@ -140,8 +148,8 @@ type
FRXAccQ: array of Double; FRXAccQ: array of Double;
FRXAccPos: Integer; FRXAccPos: Integer;
// Snapshot буфер для Spectrum0 // Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSnapBuf: array of Double; FSpecBuf: array of Double;
// Аудио выходные буферы — аллоцируем один раз // Аудио выходные буферы — аллоцируем один раз
FOutL: array of Single; FOutL: array of Single;
FOutR: array of Single; FOutR: array of Single;
@@ -152,14 +160,36 @@ type
FQueueTail: Integer; // читает DSP поток FQueueTail: Integer; // читает DSP поток
FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent) FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent)
FDSPRunning: Boolean; FDSPRunning: Boolean;
// Double буфер для Spectrum0 (принимает PDouble, не PdINREAL)
FSpecBuf: array of Double; // Double буфер для Spectrum0 (PDouble)
FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single; 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 FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
// Настройки анализатора
FFFTSize: Integer;
FWindowType: Integer;
FSpecDetector: Integer;
FSpecAvgMode: Integer;
FSpecAvgCount: Integer;
FSpecBackmult: Double;
FWfDetector: Integer;
FWfAvgMode: Integer;
FWfAvgCount: Integer;
FWfBackmult: Double;
FOnAudio: TOnAudioReady; FOnAudio: TOnAudioReady;
FOnSpectrum: TOnSpectrumReady; 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; FMode: Integer;
FSMeter: Double; FSMeter: Double;
@@ -187,11 +217,13 @@ type
procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer; procedure OpenAnalyzer;
procedure CloseAnalyzer; procedure CloseAnalyzer;
procedure ApplyAnalyzerSettings;
procedure ApplyNRState; procedure ApplyNRState;
procedure ApplyNBState; procedure ApplyNBState;
procedure ApplySNBState; procedure ApplySNBState;
procedure ApplyANFState; procedure ApplyANFState;
procedure ApplyNoiseFilterState; procedure ApplyNoiseFilterState;
procedure ProcessTXBlock; // вызывается из TTXDSPThread
public public
constructor Create(SampleRate: Integer = 192000; constructor Create(SampleRate: Integer = 192000;
@@ -236,6 +268,15 @@ type
procedure SetMicGain(GainDB: Double); procedure SetMicGain(GainDB: Double);
procedure SetTXRun(Run: Boolean); 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) // Spectrum — вызывать из таймера (~20 fps)
procedure UpdateSpectrum; procedure UpdateSpectrum;
procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer); procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);
@@ -259,12 +300,69 @@ type
property SNBEnabled: Boolean read FSNBEnabled; property SNBEnabled: Boolean read FSNBEnabled;
property LastError: string read FLastError; property LastError: string read FLastError;
property SMeter: Double read FSMeter; property SMeter: Double read FSMeter;
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 OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
property TXActive: Boolean read FTXActive;
end; end;
implementation 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 поток — обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP // Сетевой поток только кладёт пакеты, этот поток занимается DSP
@@ -367,6 +465,17 @@ begin
FSNBEnabled := False; FSNBEnabled := False;
FANFEnabled := False; FANFEnabled := False;
FRXAccPos := 0; 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(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2) SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
@@ -374,10 +483,9 @@ begin
SetLength(FTXOut, FBufSize * 2); SetLength(FTXOut, FBufSize * 2);
SetLength(FRXAccI, FBufSize); SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize); SetLength(FRXAccQ, FBufSize);
SetLength(FSnapBuf, FBufSize * 2); SetLength(FSpecBuf, FBufSize * 2);
SetLength(FOutL, FAudioBufSize); SetLength(FOutL, FAudioBufSize);
SetLength(FOutR, FAudioBufSize); SetLength(FOutR, FAudioBufSize);
SetLength(FSpecBuf, FBufSize * 2);
// Очередь и DSP поток // Очередь и DSP поток
FQueueHead := 0; FQueueHead := 0;
@@ -385,12 +493,22 @@ begin
FDSPRunning := False; FDSPRunning := False;
FQueueSem := RTLEventCreate; FQueueSem := RTLEventCreate;
FDSPThread := nil; FDSPThread := nil;
// TX инициализация
FTXMicHead := 0;
FTXMicTail := 0;
FTXActive := False;
FTXThread := nil;
FTXMicSem := RTLEventCreate;
FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
end; end;
destructor TWDSPEngine.Destroy; destructor TWDSPEngine.Destroy;
begin begin
SetTXRun(False); // останавливаем TX поток если запущен
Close; Close;
RTLEventDestroy(FQueueSem); RTLEventDestroy(FQueueSem);
RTLEventDestroy(FTXMicSem);
inherited; inherited;
end; end;
@@ -401,9 +519,6 @@ end;
procedure TWDSPEngine.OpenAnalyzer; procedure TWDSPEngine.OpenAnalyzer;
var var
Success: Integer; Success: Integer;
MaxW: Integer;
Ovrlp: Integer;
FRAME_RATE: Integer;
begin begin
if FAnalyzerOpen then Exit; if FAnalyzerOpen then Exit;
@@ -411,30 +526,41 @@ begin
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil); XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
if Success <> 0 then Exit; 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; FFlp[0] := 0;
SetAnalyzer(DISP_ID, 1, 1, 1, @FFlp[0], 4096, FAudioBufSize, 1, 14.0, ApplyAnalyzerSettings;
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);
FAnalyzerOpen := True; FAnalyzerOpen := True;
end; 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; procedure TWDSPEngine.CloseAnalyzer;
begin begin
if not FAnalyzerOpen then Exit; if not FAnalyzerOpen then Exit;
@@ -501,13 +627,16 @@ begin
SetRXAPanelRun(RXA_CHAN, 1); SetRXAPanelRun(RXA_CHAN, 1);
SetChannelState(RXA_CHAN, 1, 0); SetChannelState(RXA_CHAN, 1, 0);
// TXA: mic @ FAudioRate → IQ @ FSampleRate (DUC rate)
// out_size = FAudioBufSize * FSampleRate/FAudioRate = FBufSize (4096 @ 192kHz)
// FTXOut уже имеет размер FBufSize*2 — подходит идеально
OpenChannel( OpenChannel(
TXA_CHAN, TXA_CHAN,
FAudioBufSize, FAudioBufSize,
FAudioBufSize, FAudioBufSize,
FAudioRate, FAudioRate,
FAudioRate, FAudioRate,
FAudioRate, FSampleRate,
1, 1,
0, 0,
0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010,
@@ -668,18 +797,19 @@ begin
FRXIn[i * 2 + 1] := FRXAccQ[i]; FRXIn[i * 2 + 1] := FRXAccQ[i];
end; end;
// Копируем входные IQ ДО fexchange0 для Spectrum0 // Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn
// fexchange0 in-place перезапишет FRXIn выходными данными @ 48kHz
if FAnalyzerOpen then if FAnalyzerOpen then
for i := 0 to FBufSize * 2 - 1 do begin
FSpecBuf[i] := FRXIn[i]; // Single→Double, входные IQ @ 192kHz Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double));
end;
// DSP обработка // DSP обработка
Err := 0; Err := 0;
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
// Spectrum0 с входными данными (скопированными до fexchange0) // Подаём RX IQ в анализатор только когда не передаём.
if FAnalyzerOpen then // Во время TX анализатор кормит ProcessTXBlock (TX сигнал на той же частоте).
if FAnalyzerOpen and not FTXActive then
Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]); Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]);
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter. // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
@@ -973,11 +1103,92 @@ end;
procedure TWDSPEngine.SetTXRun(Run: Boolean); procedure TWDSPEngine.SetTXRun(Run: Boolean);
begin begin
if not FInitialized then Exit; FTXActive := Run;
if Run then if Run then
SetChannelState(TXA_CHAN, 1, 0) // run, no delay begin
else 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 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; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -985,9 +1196,12 @@ end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TWDSPEngine.UpdateSpectrum; procedure TWDSPEngine.UpdateSpectrum;
// Вызывается из таймера главного потока (~20fps) // Вызывается из таймера главного потока (настраиваемый FPS).
// Spectrum0 уже вызван в сетевом потоке после каждого fexchange0 // Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
// Здесь только читаем готовые пиксели // буфера (который fexchange0 непрерывно заполняет из DSP-потока).
// Это обеспечивает равномерный FPS независимо от размера FFT:
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
// буфера, а не ждёт следующего полного блока.
var var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single; PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer; Flag: Integer;
@@ -995,17 +1209,29 @@ var
begin begin
if not FInitialized or not FAnalyzerOpen then Exit; if not FInitialized or not FAnalyzerOpen then Exit;
// Спектр (pixout=0)
Flag := 0; Flag := 0;
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag); 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 for i := 0 to SPECTRUM_PIXELS - 1 do
FSpectrumPixels[i] := PixBuf[i]; FSpectrumPixels[i] := PixBuf[i];
if Assigned(FOnSpectrum) then if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS); FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
end; 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
FWaterfallPixels[i] := PixBuf[i];
if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS);
end;
end;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single; procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
var Count: Integer); var Count: Integer);
var var
@@ -1024,4 +1250,50 @@ begin
Result := FSMeter; Result := FSMeter;
end; 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. end.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -99,10 +99,18 @@
<Filename Value="AudioOutput.pas"/> <Filename Value="AudioOutput.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>
</Unit> </Unit>
<Unit>
<Filename Value="AudioInput.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit> <Unit>
<Filename Value="DeviceForm.pas"/> <Filename Value="DeviceForm.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>
</Unit> </Unit>
<Unit>
<Filename Value="SettingsForm.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit> <Unit>
<Filename Value="FlatButton.pas"/> <Filename Value="FlatButton.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>