mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
start work tx
This commit is contained in:
+148
-26
@@ -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;
|
||||
|
||||
|
||||
+33
-21
@@ -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;
|
||||
|
||||
+569
-46
@@ -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
|
||||
|
||||
+115
-1
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+327
-55
@@ -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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -99,10 +99,18 @@
|
||||
<Filename Value="AudioOutput.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="AudioInput.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="DeviceForm.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="SettingsForm.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="FlatButton.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
|
||||
Reference in New Issue
Block a user