Fix DUC IQ pacing for TX tune

This commit is contained in:
2026-05-07 22:42:13 +03:00
parent c59c6c8bcc
commit 6067aae3dc
2 changed files with 256 additions and 18 deletions
+227 -5
View File
@@ -21,6 +21,9 @@ uses
{$ENDIF} {$ENDIF}
HPSDRProtocol, SyncObjs; HPSDRProtocol, SyncObjs;
const
DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun
{$IFDEF WINDOWS} {$IFDEF WINDOWS}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// WinSock2 — type aliases и forward declarations // WinSock2 — type aliases и forward declarations
@@ -108,6 +111,7 @@ type
FReceiveThread: TThread; FReceiveThread: TThread;
FKeepaliveThread: TThread; FKeepaliveThread: TThread;
FDUCIQThread: TThread;
FOnDeviceFound: TOnDeviceFound; FOnDeviceFound: TOnDeviceFound;
FOnDDCIQ: TOnDDCIQPacket; FOnDDCIQ: TOnDDCIQPacket;
@@ -140,6 +144,16 @@ type
FSendLock: TCriticalSection; // защита concurrent UDP sends FSendLock: TCriticalSection; // защита concurrent UDP sends
// DUC IQ sender: WDSP отдаёт IQ блоками, а железу нужен равномерный UDP
// поток 240 IQ-пар/пакет @ 192 kS/s, то есть пакет каждые 1.25 ms.
FDUCIQLock: TCriticalSection;
FDUCIQSem: PRTLEvent;
FDUCIQQueue: array[0..DUC_TX_QUEUE_SIZE - 1] of TDUCIQPacket;
FDUCIQHead: Integer;
FDUCIQTail: Integer;
FDUCIQCount: Integer;
FDUCIQDrops: LongWord;
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;
@@ -153,6 +167,9 @@ type
procedure HandleMicData(const Buf: array of Byte; Len: Integer); procedure HandleMicData(const Buf: array of Byte; Len: Integer);
procedure StartThreads; procedure StartThreads;
procedure StopThreads; procedure StopThreads;
procedure ResetDUCIQQueue;
procedure EnqueueDUCIQ(const Pkt: TDUCIQPacket);
function DequeueDUCIQ(var Pkt: TDUCIQPacket): Boolean;
function NextSeq(var S: LongWord): LongWord; function NextSeq(var S: LongWord): LongWord;
procedure PackSeqBytes(var B: array of Byte; Seq: LongWord); procedure PackSeqBytes(var B: array of Byte; Seq: LongWord);
@@ -194,6 +211,9 @@ implementation
{$IFDEF WINDOWS} {$IFDEF WINDOWS}
var var
WSAData_: WinSock2.TWSAData; WSAData_: WinSock2.TWSAData;
function QueryPerformanceCounter(var lpPerformanceCount: Int64): LongBool; stdcall; external 'kernel32.dll';
function QueryPerformanceFrequency(var lpFrequency: Int64): LongBool; stdcall; external 'kernel32.dll';
{$ENDIF} {$ENDIF}
{$IFDEF WINDOWS} {$IFDEF WINDOWS}
@@ -378,6 +398,142 @@ begin
end; end;
end; end;
// ===========================================================================
// DUC IQ paced sender thread
// ===========================================================================
type
TDUCIQSenderThread = class(TThread)
private
FNet: THPSDRNetwork;
protected
procedure Execute; override;
public
constructor Create(ANet: THPSDRNetwork);
end;
constructor TDUCIQSenderThread.Create(ANet: THPSDRNetwork);
begin
FNet := ANet;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TDUCIQSenderThread.Execute;
const
DUC_IQ_PAIRS_PER_PACKET = 240;
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
DUC_FIFO_THROTTLE = 1250; // piHPSDR-style virtual radio FIFO limit
DUC_PACKET_INTERVAL_US = 1250; // 240 / 192000 seconds
DUC_WIN_SPIN_US = 350; // final part of 1.25 ms interval: avoid Sleep jitter
var
Pkt: TDUCIQPacket;
{$IFDEF WINDOWS}
QPCFreq: Int64;
QPCValue: Int64;
NextDueUs: Int64;
NowUs: Int64;
WaitUs: Int64;
{$ELSE}
VirtualSamples: Integer;
LastTick: QWord;
NowTick: QWord;
ElapsedMs: QWord;
{$ENDIF}
{$IFDEF WINDOWS}
function ClockUs: Int64;
begin
QueryPerformanceCounter(QPCValue);
Result := (QPCValue * 1000000) div QPCFreq;
end;
procedure WaitUntil(DueUs: Int64);
begin
while not Terminated do
begin
NowUs := ClockUs;
WaitUs := DueUs - NowUs;
if WaitUs <= 0 then Break;
if WaitUs > 2500 then
Sleep(1)
else if WaitUs > DUC_WIN_SPIN_US then
Sleep(0);
end;
end;
{$ELSE}
procedure DrainVirtualFIFO;
begin
NowTick := GetTickCount64;
if NowTick > LastTick then
begin
ElapsedMs := NowTick - LastTick;
Dec(VirtualSamples, Integer(ElapsedMs) * (DUC_SAMPLE_RATE div 1000));
if VirtualSamples < 0 then VirtualSamples := 0;
LastTick := NowTick;
end;
end;
{$ENDIF}
begin
{$IFDEF WINDOWS}
if not QueryPerformanceFrequency(QPCFreq) or (QPCFreq <= 0) then
QPCFreq := 1000;
NextDueUs := ClockUs;
while not Terminated do
begin
if not FNet.DequeueDUCIQ(Pkt) then
begin
RTLEventWaitFor(FNet.FDUCIQSem, 20);
NowUs := ClockUs;
if NextDueUs < NowUs - DUC_PACKET_INTERVAL_US then
NextDueUs := NowUs;
Continue;
end;
WaitUntil(NextDueUs);
if Terminated then Break;
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
FNet.FPortDUCIQ);
Inc(NextDueUs, DUC_PACKET_INTERVAL_US);
NowUs := ClockUs;
if NextDueUs < NowUs - DUC_PACKET_INTERVAL_US then
NextDueUs := NowUs;
end;
{$ELSE}
VirtualSamples := 0;
LastTick := GetTickCount64;
while not Terminated do
begin
if not FNet.DequeueDUCIQ(Pkt) then
begin
DrainVirtualFIFO;
RTLEventWaitFor(FNet.FDUCIQSem, 20);
DrainVirtualFIFO;
Continue;
end;
if Terminated then Break;
DrainVirtualFIFO;
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
FNet.FPortDUCIQ);
Inc(VirtualSamples, DUC_IQ_PAIRS_PER_PACKET);
while (not Terminated) and (VirtualSamples > DUC_FIFO_THROTTLE) do
begin
Sleep(1);
DrainVirtualFIFO;
end;
end;
{$ENDIF}
end;
// =========================================================================== // ===========================================================================
// Keepalive thread // Keepalive thread
// =========================================================================== // ===========================================================================
@@ -436,6 +592,7 @@ begin
FLocalPort := 0; FLocalPort := 0;
FReceiveThread := nil; FReceiveThread := nil;
FKeepaliveThread := nil; FKeepaliveThread := nil;
FDUCIQThread := nil;
FPortDDCSpec := PORT_DDC_SPECIFIC; FPortDDCSpec := PORT_DDC_SPECIFIC;
FPortDUCSpec := PORT_DUC_SPECIFIC; FPortDUCSpec := PORT_DUC_SPECIFIC;
FPortHPFromPC := PORT_HP_FROM_PC; FPortHPFromPC := PORT_HP_FROM_PC;
@@ -448,11 +605,16 @@ begin
FPAEnabled := True; FPAEnabled := True;
FAlexEnabled := True; FAlexEnabled := True;
FSendLock := TCriticalSection.Create; FSendLock := TCriticalSection.Create;
FDUCIQLock := TCriticalSection.Create;
FDUCIQSem := RTLEventCreate;
ResetDUCIQQueue;
end; end;
destructor THPSDRNetwork.Destroy; destructor THPSDRNetwork.Destroy;
begin begin
Disconnect; Disconnect;
RTLEventDestroy(FDUCIQSem);
FDUCIQLock.Free;
FSendLock.Free; FSendLock.Free;
inherited; inherited;
end; end;
@@ -471,6 +633,54 @@ begin
B[3] := Seq and $FF; B[3] := Seq and $FF;
end; end;
procedure THPSDRNetwork.ResetDUCIQQueue;
begin
FDUCIQLock.Enter;
try
FDUCIQHead := 0;
FDUCIQTail := 0;
FDUCIQCount := 0;
FDUCIQDrops := 0;
finally
FDUCIQLock.Leave;
end;
end;
procedure THPSDRNetwork.EnqueueDUCIQ(const Pkt: TDUCIQPacket);
begin
FDUCIQLock.Enter;
try
if FDUCIQCount >= DUC_TX_QUEUE_SIZE then
begin
FDUCIQTail := (FDUCIQTail + 1) and (DUC_TX_QUEUE_SIZE - 1);
Dec(FDUCIQCount);
Inc(FDUCIQDrops);
end;
FDUCIQQueue[FDUCIQHead] := Pkt;
FDUCIQHead := (FDUCIQHead + 1) and (DUC_TX_QUEUE_SIZE - 1);
Inc(FDUCIQCount);
RTLEventSetEvent(FDUCIQSem);
finally
FDUCIQLock.Leave;
end;
end;
function THPSDRNetwork.DequeueDUCIQ(var Pkt: TDUCIQPacket): Boolean;
begin
Result := False;
FDUCIQLock.Enter;
try
if FDUCIQCount <= 0 then Exit;
Pkt := FDUCIQQueue[FDUCIQTail];
FDUCIQTail := (FDUCIQTail + 1) and (DUC_TX_QUEUE_SIZE - 1);
Dec(FDUCIQCount);
Result := True;
finally
FDUCIQLock.Leave;
end;
end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Сокеты (только FPC RTL Sockets unit) // Сокеты (только FPC RTL Sockets unit)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -708,6 +918,7 @@ begin
FSeqHP := 0; FSeqHP := 0;
FSeqAudio := 0; FSeqAudio := 0;
FSeqDUCIQ := 0; FSeqDUCIQ := 0;
ResetDUCIQQueue;
StartThreads; StartThreads;
Result := True; Result := True;
@@ -738,15 +949,22 @@ begin
{$ENDIF} {$ENDIF}
FReceiveThread := TReceiveThread.Create(Self); FReceiveThread := TReceiveThread.Create(Self);
FReceiveThread.Priority := tpHighest; // Сетевой поток — критический FReceiveThread.Priority := tpHighest; // Сетевой поток — критический
FDUCIQThread := TDUCIQSenderThread.Create(Self);
FDUCIQThread.Priority := tpHighest; // TX packet pacing is timing-sensitive
FKeepaliveThread := TKeepaliveThread.Create(Self); FKeepaliveThread := TKeepaliveThread.Create(Self);
FKeepaliveThread.Priority := tpNormal; FKeepaliveThread.Priority := tpNormal;
end; end;
procedure THPSDRNetwork.StopThreads; procedure THPSDRNetwork.StopThreads;
begin begin
{$IFDEF WINDOWS} if Assigned(FDUCIQThread) then
timeEndPeriod(1); begin
{$ENDIF} FDUCIQThread.Terminate;
RTLEventSetEvent(FDUCIQSem);
FDUCIQThread.WaitFor;
FreeAndNil(FDUCIQThread);
end;
if Assigned(FReceiveThread) then if Assigned(FReceiveThread) then
begin begin
FReceiveThread.Terminate; FReceiveThread.Terminate;
@@ -764,6 +982,10 @@ begin
FKeepaliveThread.WaitFor; FKeepaliveThread.WaitFor;
FreeAndNil(FKeepaliveThread); FreeAndNil(FKeepaliveThread);
end; end;
ResetDUCIQQueue;
{$IFDEF WINDOWS}
timeEndPeriod(1);
{$ENDIF}
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1081,8 +1303,8 @@ var
i, Off: Integer; i, Off: Integer;
IV, QV: Integer; IV, QV: Integer;
begin begin
if not FConnected then Exit;
FillChar(Pkt, SizeOf(Pkt), 0); FillChar(Pkt, SizeOf(Pkt), 0);
PackSeqBytes(Pkt.Seq, NextSeq(FSeqDUCIQ));
for i := 0 to Min(239, Min(High(IData), High(QData))) do for i := 0 to Min(239, Min(High(IData), High(QData))) do
begin begin
Off := i * 6; Off := i * 6;
@@ -1095,7 +1317,7 @@ begin
Pkt.IQData[Off + 4] := Byte((QV shr 8) and $FF); Pkt.IQData[Off + 4] := Byte((QV shr 8) and $FF);
Pkt.IQData[Off + 5] := Byte( QV and $FF); Pkt.IQData[Off + 5] := Byte( QV and $FF);
end; end;
DoSendTo(FSocket, Pkt, SizeOf(Pkt), FDevice.IPAddress, FPortDUCIQ); EnqueueDUCIQ(Pkt);
end; end;
+29 -13
View File
@@ -534,12 +534,22 @@ procedure TTXDSPThread.Execute;
var var
Avail: Integer; Avail: Integer;
PeriodMs: Integer; PeriodMs: Integer;
PeriodUs: Integer;
AccUs: Integer;
DidBlock: Boolean;
begin begin
// 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс // 512 samples / 48000 Hz = 10.666 ms. Do not floor this to 10 ms:
PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate); // that over-produces TX IQ by ~6.7%, eventually forcing DUC queue
// corrections/drops and producing periodic sidebands on a pure TUN tone.
PeriodUs := Max(1000, FEngine.FAudioBufSize * 1000000 div FEngine.FAudioRate);
AccUs := 0;
while not Terminated do while not Terminated do
begin begin
Inc(AccUs, PeriodUs);
PeriodMs := Max(1, AccUs div 1000);
Dec(AccUs, PeriodMs * 1000);
// Ждём сигнала от mic-данных ИЛИ истечения периода — // Ждём сигнала от mic-данных ИЛИ истечения периода —
// в piHPSDR timing идёт от receive thread; здесь — от таймаута // в piHPSDR timing идёт от receive thread; здесь — от таймаута
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs); RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
@@ -554,29 +564,35 @@ begin
FEngine.FOnPullMic(FEngine.FAudioBufSize * 2); FEngine.FOnPullMic(FEngine.FAudioBufSize * 2);
// Обрабатываем накопившиеся блоки с реальными mic-данными // Обрабатываем накопившиеся блоки с реальными mic-данными
DidBlock := False;
repeat repeat
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1); and (TX_MIC_RING - 1);
if (Avail >= FEngine.FAudioBufSize) and not Terminated then if (Avail >= FEngine.FAudioBufSize) and not Terminated then
FEngine.ProcessTXBlock begin
FEngine.ProcessTXBlock;
DidBlock := True;
end
else else
Break; Break;
until False; until False;
if Terminated then Break; if Terminated then Break;
// Если mic-данных не было — обрабатываем один блок тишины, чтобы // Если на этом тике не было ни одного полного mic-блока и mic-данных
// DUC IQ поток не прерывался и спектр/водопад обновлялись. // вообще нет — обрабатываем один блок тишины, чтобы DUC IQ поток не
// НО для sound-card mic неполный блок (0 < Avail < AudioBufSize) нельзя // прерывался и спектр/водопад обновлялись.
// подменять тишиной: PortAudio часто отдаёт 128-сэмпловые callback'и, // Важно: не добавлять silence-block сразу после реального блока. Для TUN
// и если в кольце уже есть голосовой хвост — он застрянет, а в эфир // этот "silence" тоже содержит PostGen tone, поэтому лишний блок ломает
// уйдёт тишина. Слышно как чередование «голос/тишина» = треск ≈50–100 Гц. // темп DUC IQ и даёт периодические боковые/тр-р-р.
// В этом случае ждём следующий тик, хвост накопится до полного блока. // Если же в ring уже есть неполный блок, нельзя подменять его тишиной:
// radio mic приходит пакетами по 64 сэмпла, sound-card часто по 128.
// Иначе каждый mic packet будит поток раньше срока, а мы генерируем
// лишний TX-блок тишины вместо ожидания полного 512-sample блока.
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1); and (TX_MIC_RING - 1);
if (Avail = 0) or (FEngine.FTXMicSource <> txmsSoundCard) then if (not DidBlock) and (Avail = 0) then
if Avail < FEngine.FAudioBufSize then FEngine.ProcessTXBlock;
FEngine.ProcessTXBlock;
end; end;
end; end;