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;
|
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,16 +124,28 @@ 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
|
||||||
procedure Write(const Left, Right: array of Single; Count: Integer);
|
procedure Write(const Left, Right: array of Single; Count: Integer);
|
||||||
|
|
||||||
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, не метод)
|
||||||
@@ -202,12 +232,13 @@ end;
|
|||||||
constructor TAudioOutput.Create(SampleRate: Integer);
|
constructor TAudioOutput.Create(SampleRate: Integer);
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
FSampleRate := SampleRate;
|
FSampleRate := SampleRate;
|
||||||
FOpen := False;
|
FDeviceIndex := -1; // -1 = default output device
|
||||||
FPAInited := False;
|
FOpen := False;
|
||||||
FStream := nil;
|
FPAInited := False;
|
||||||
FLibHandle := NilHandle;
|
FStream := nil;
|
||||||
FLastError := '';
|
FLibHandle := NilHandle;
|
||||||
|
FLastError := '';
|
||||||
FInPt := 0;
|
FInPt := 0;
|
||||||
FOutPt := 0;
|
FOutPt := 0;
|
||||||
FillChar(FBuf, SizeOf(FBuf), 0);
|
FillChar(FBuf, SizeOf(FBuf), 0);
|
||||||
@@ -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'));
|
||||||
@@ -275,6 +308,105 @@ begin
|
|||||||
Result := True;
|
Result := True;
|
||||||
end;
|
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()
|
// Open — точная последовательность как в audio_open_output()
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -287,28 +419,18 @@ var
|
|||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
if FOpen then begin Result := True; Exit; end;
|
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)
|
// Выбор устройства: FDeviceIndex или default
|
||||||
if not FPAInited then
|
if FDeviceIndex >= 0 then
|
||||||
|
Dev := FDeviceIndex
|
||||||
|
else
|
||||||
begin
|
begin
|
||||||
Err := FPa_Initialize();
|
Dev := FPa_GetDefaultOutputDevice();
|
||||||
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;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
Dev := FPa_GetDefaultOutputDevice();
|
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
+33
-21
@@ -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);
|
||||||
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;
|
end;
|
||||||
|
|
||||||
function THPSDRNetwork.DoRecvFrom(S: TSocket; var Buf; BufLen: Integer;
|
function THPSDRNetwork.DoRecvFrom(S: TSocket; var Buf; BufLen: Integer;
|
||||||
@@ -767,19 +776,11 @@ 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;
|
||||||
|
|||||||
+569
-46
@@ -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;
|
||||||
|
|
||||||
@@ -180,9 +181,12 @@ type
|
|||||||
FPendingBoardType: Integer; // BoardType устройства для подключения
|
FPendingBoardType: Integer; // BoardType устройства для подключения
|
||||||
|
|
||||||
// ---- DSP + Audio ----
|
// ---- DSP + Audio ----
|
||||||
FDSPEngine: TWDSPEngine;
|
FDSPEngine: TWDSPEngine;
|
||||||
FAudioOut: TAudioOutput;
|
FAudioOut: TAudioOutput;
|
||||||
FWDSPReady: Boolean; // True когда WDSP открыт и работает
|
FAudioIn: TAudioInput;
|
||||||
|
FAudioOutDevName: string; // текущее имя выходного устройства
|
||||||
|
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
|
||||||
|
FWDSPReady: Boolean; // True когда WDSP открыт и работает
|
||||||
|
|
||||||
// ---- State ----
|
// ---- State ----
|
||||||
FVfoA: Double;
|
FVfoA: Double;
|
||||||
@@ -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; // кэш диапазонов
|
||||||
@@ -283,8 +300,9 @@ type
|
|||||||
|
|
||||||
// ---- Toolbar ----
|
// ---- Toolbar ----
|
||||||
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,9 +847,11 @@ begin
|
|||||||
FCTun := False;
|
FCTun := False;
|
||||||
FFilterBW := 2700;
|
FFilterBW := 2700;
|
||||||
FSpecDrag := False;
|
FSpecDrag := False;
|
||||||
FDriveLevel := 100;
|
FDriveLevel := 0;
|
||||||
FRunning := False;
|
FPAMaxPower := 100.0;
|
||||||
FTransmitting := False;
|
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
|
||||||
|
FRunning := False;
|
||||||
|
FTransmitting := False;
|
||||||
FMuted := False;
|
FMuted := False;
|
||||||
FVolume := 70;
|
FVolume := 70;
|
||||||
FCenterFreq := FVfoA;
|
FCenterFreq := FVfoA;
|
||||||
@@ -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);
|
||||||
|
|
||||||
// Инициализируем кэш диапазонов умолчаниями
|
// Инициализируем кэш диапазонов умолчаниями
|
||||||
@@ -847,7 +936,10 @@ begin
|
|||||||
FVfoOverlay := TVfoOverlay.Create(Self);
|
FVfoOverlay := TVfoOverlay.Create(Self);
|
||||||
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;
|
||||||
|
|
||||||
@@ -894,12 +986,15 @@ begin
|
|||||||
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
|
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
|
||||||
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
|
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
|
||||||
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,8 +1781,7 @@ 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);
|
||||||
|
|
||||||
@@ -1814,12 +2030,21 @@ end;
|
|||||||
procedure TMainForm.ResetSpectrumBuf;
|
procedure TMainForm.ResetSpectrumBuf;
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
|
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
|
||||||
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
|
for i := 0 to 1023 do 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,14 +2432,31 @@ 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;
|
||||||
for X := 0 to 1023 do
|
if FWDSPReady then
|
||||||
begin
|
begin
|
||||||
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
|
// Используем WDSP waterfall данные напрямую
|
||||||
FWfAvgBuf[X] := AvgDB;
|
for X := 0 to 1023 do
|
||||||
if AvgDB > PeakDB then PeakDB := AvgDB;
|
begin
|
||||||
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
|
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;
|
end;
|
||||||
|
|
||||||
// ── AGC High/Low ────────────────────────────────────────────────────────
|
// ── AGC High/Low ────────────────────────────────────────────────────────
|
||||||
@@ -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;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -3147,8 +3406,10 @@ begin
|
|||||||
end;
|
end;
|
||||||
FWDSPReady := False;
|
FWDSPReady := False;
|
||||||
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
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+327
-55
@@ -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;
|
||||||
FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
|
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;
|
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 OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
property FFTSize: Integer read FFFTSize;
|
||||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
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;
|
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;
|
||||||
|
|
||||||
@@ -400,10 +518,7 @@ 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, 0, 1); // stop, with slew
|
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;
|
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,15 +1209,27 @@ 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
|
||||||
|
FSpectrumPixels[i] := PixBuf[i];
|
||||||
|
if Assigned(FOnSpectrum) then
|
||||||
|
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
|
||||||
|
end;
|
||||||
|
|
||||||
for i := 0 to SPECTRUM_PIXELS - 1 do
|
// Водопад (pixout=1)
|
||||||
FSpectrumPixels[i] := PixBuf[i];
|
Flag := 0;
|
||||||
|
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
|
||||||
if Assigned(FOnSpectrum) then
|
if Flag <> 0 then
|
||||||
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
|
begin
|
||||||
|
for i := 0 to SPECTRUM_PIXELS - 1 do
|
||||||
|
FWaterfallPixels[i] := PixBuf[i];
|
||||||
|
if Assigned(FOnWaterfall) then
|
||||||
|
FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
||||||
@@ -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.
@@ -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"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user