fix RX leak after TX→RX: drop IQ during TX in non-DUP, Windows post-TX hold

В non-DUP при MOX/TUN на возврате слышен и виден на водопаде хвост
своего сигнала. Во время TX RX-IQ оседал в RXA pipeline / FFT-истории
RX-анализатора и «выстреливал» при открытии гейта.

  - WDSPEngine: KeepRXDuringTX (DUP-флаг). В non-DUP TX PushIQItemToDSP
    дропает пакеты на входе DSP-потока — RXA и анализатор не кормятся.
  - Windows-only post-TX hold (250 мс): дропаем И входящие RX-IQ, И аудио
    в окне после MOX-off, чтобы скрыть «хвост» от FPGA TX-buffer/PA
    slew-down радио. На Linux код-путь не активируется (буфер OS мелкий).
  - Network: ClearDUCIQQueue + IsTransmitting guard в DUC sender thread,
    чтобы недопереданные TX-IQ пакеты не уходили после MOX-off.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 13:39:45 +03:00
co-authored by Claude Opus 4.7
parent 28859d3e16
commit 9f208006aa
3 changed files with 84 additions and 3 deletions
+10
View File
@@ -193,6 +193,7 @@ type
procedure SendFullHP; procedure SendFullHP;
procedure SendDDCAudio(const LeftRight: array of SmallInt); procedure SendDDCAudio(const LeftRight: array of SmallInt);
procedure SendDUCIQ(const IData, QData: array of Integer); procedure SendDUCIQ(const IData, QData: array of Integer);
procedure ClearDUCIQQueue;
property Connected: Boolean read FConnected; property Connected: Boolean read FConnected;
property Running: Boolean read FRunning; property Running: Boolean read FRunning;
@@ -493,6 +494,8 @@ begin
WaitUntil(NextDueUs); WaitUntil(NextDueUs);
if Terminated then Break; if Terminated then Break;
if not FNet.FIsTransmitting then
Continue;
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ)); FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress, FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
@@ -519,6 +522,8 @@ begin
if Terminated then Break; if Terminated then Break;
DrainVirtualFIFO; DrainVirtualFIFO;
if not FNet.FIsTransmitting then
Continue;
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ)); FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress, FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
@@ -646,6 +651,11 @@ begin
end; end;
end; end;
procedure THPSDRNetwork.ClearDUCIQQueue;
begin
ResetDUCIQQueue;
end;
procedure THPSDRNetwork.EnqueueDUCIQ(const Pkt: TDUCIQPacket); procedure THPSDRNetwork.EnqueueDUCIQ(const Pkt: TDUCIQPacket);
begin begin
FDUCIQLock.Enter; FDUCIQLock.Enter;
+22 -1
View File
@@ -4468,24 +4468,45 @@ begin
FSpecView.TXOverlay := FTransmitting; FSpecView.TXOverlay := FTransmitting;
// Перерисовываем сетку спектра под активный тракт (Thetis-style). // Перерисовываем сетку спектра под активный тракт (Thetis-style).
ApplySpecViewGridFromState; ApplySpecViewGridFromState;
if FTransmitting and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.ClearDUCIQQueue;
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel,
True, True, True);
FNetwork.SendFullHP;
end;
if FWDSPReady then if FWDSPReady then
begin begin
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX,
// чтобы TX leakage не накапливался в RXA pipeline и FFT-истории
// RX-анализатора. В DUP RX-тракт работает как обычно.
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
FDSPEngine.SetTXRun(FTransmitting); FDSPEngine.SetTXRun(FTransmitting);
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
begin begin
FDSPEngine.FlushRX; FDSPEngine.FlushRX;
if Assigned(FAudioOut) then if Assigned(FAudioOut) then
FAudioOut.Clear; FAudioOut.Clear;
{$IFDEF WINDOWS}
// Windows: на TX→RX дропаем И RX-IQ пакеты, И аудио-выход на это окно.
// Источник «хвоста» — radio FPGA TX-buffer + PA slew-down: после MOX-off
// радио дотравливает буфер передатчика, RX1 ловит это как leak, и WDSP
// отдаёт это и в водопад, и в звук. На Linux наблюдается окно <30мс,
// на Windows может тянуться 200–500мс. Подобрать значение можно здесь
// без перекомпиляции engine. На Linux код-путь не активируется.
FDSPEngine.BeginPostTXMute(250);
{$ENDIF}
end; end;
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем // SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
// источник на RX, чтобы видеть приём во время передачи. // источник на RX, чтобы видеть приём во время передачи.
if FTransmitting and FDisplayDuplex then if FTransmitting and FDisplayDuplex then
FDSPEngine.SetDisplaySourceTX(False); FDSPEngine.SetDisplaySourceTX(False);
end; end;
if FNetwork.Connected and FNetwork.Running then if (not FTransmitting) and FNetwork.Connected and FNetwork.Running then
begin begin
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel,
FTransmitting, True, True); FTransmitting, True, True);
FNetwork.ClearDUCIQQueue;
FNetwork.SendFullHP; FNetwork.SendFullHP;
end; end;
end; end;
+52 -2
View File
@@ -216,6 +216,18 @@ type
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
FTXThread: TThread; FTXThread: TThread;
FTXActive: Boolean; FTXActive: Boolean;
// DUP-флаг от UI. True — RX-анализатор и RXA-канал продолжают работать
// во время TX (full-duplex). False — RX-IQ пакеты дропаются на входе DSP
// потока и не кормят RXA/анализатор. Это не трогает SetChannelState
// (мы выяснили на практике что стоп/старт RXA добавляет задержки на
// Windows без видимого выигрыша).
FKeepRXDuringTX: Boolean;
// Дедлайн (GetTickCount64), до которого RX-аудио не пишется в FOnAudio.
// Используется на Windows для маскировки «хвоста» TX leakage (radio
// FPGA TX-buffer + PA slew-down → RX1 IQ → audio) после MOX-off.
// На Linux выставляется в 0 (UI не зовёт BeginPostTXMute) — поведение
// не меняется.
FPostTXMuteUntil: QWord;
FActiveDisplayID: Integer; // RX_DISP_ID или TX_DISP_ID FActiveDisplayID: Integer; // RX_DISP_ID или TX_DISP_ID
FTXMicSource: TTXMicSource; FTXMicSource: TTXMicSource;
FOnPullMic: TPullMicSamplesFunc; FOnPullMic: TPullMicSamplesFunc;
@@ -325,6 +337,12 @@ type
DataOffset: Integer; DataOffset: Integer;
IQPairs: Integer); IQPairs: Integer);
procedure FlushRX; procedure FlushRX;
// Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это
// на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio
// буфер удерживает «хвост» с TX leakage из радио, который AudioOut.Clear
// не достаёт. На Linux буфер маленький и проблемы нет, поэтому метод
// не вызывается.
procedure BeginPostTXMute(DurationMs: Integer);
// RX управление // RX управление
procedure SetMode(Mode: Integer); procedure SetMode(Mode: Integer);
@@ -432,6 +450,10 @@ type
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall; property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ; property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
property TXActive: Boolean read FTXActive; property TXActive: Boolean read FTXActive;
// Включает full-duplex поведение RX-тракта: RXA не останавливается на TX,
// RX-анализатор продолжает получать IQ. Должен ставиться UI ДО SetTXRun.
property KeepRXDuringTX: Boolean read FKeepRXDuringTX
write FKeepRXDuringTX;
// Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive). // Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive).
// SpectrumView/Waterfall должны читать пиксели именно из этого ID. // SpectrumView/Waterfall должны читать пиксели именно из этого ID.
property ActiveDisplayID: Integer read FActiveDisplayID; property ActiveDisplayID: Integer read FActiveDisplayID;
@@ -741,6 +763,8 @@ begin
FTXMicHead := 0; FTXMicHead := 0;
FTXMicTail := 0; FTXMicTail := 0;
FTXActive := False; FTXActive := False;
FKeepRXDuringTX := False;
FPostTXMuteUntil := 0;
FActiveDisplayID := RX_DISP_ID; FActiveDisplayID := RX_DISP_ID;
FTXMicSource := txmsRadio; FTXMicSource := txmsRadio;
FOnPullMic := nil; FOnPullMic := nil;
@@ -1124,14 +1148,34 @@ begin
if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0); if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0);
end; end;
procedure TWDSPEngine.BeginPostTXMute(DurationMs: Integer);
begin
if DurationMs <= 0 then
FPostTXMuteUntil := 0
else
FPostTXMuteUntil := GetTickCount64 + QWord(DurationMs);
end;
procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem); procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem);
// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize // Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize.
// В non-DUP TX (FTXActive and not FKeepRXDuringTX) полностью пропускаем
// IQ-пакет: не кормим RX-анализатор и не накапливаем для fexchange0.
// Иначе TX leakage из DDC оседает в RXA pipeline / RX_DISP_ID FFT-истории
// и звучит/виден на водопаде первые секунды после возврата на RX.
var var
i, Pos: Integer; i, Pos: Integer;
IR, QR: LongInt; IR, QR: LongInt;
const const
SCALE = 1.0 / 8388608.0; SCALE = 1.0 / 8388608.0;
begin begin
if FTXActive and not FKeepRXDuringTX then Exit;
// Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта.
// Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не
// кормится), и аудио (RXA не запускается, FOnAudio не вызывается).
// Радио продолжает дотравливать FPGA TX-buffer + PA slew-down первые
// ~50200мс после MOX-off; этот хвост попадает в RX1 как leak. Дропаем
// его на входе — single-source-of-truth.
if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit;
for i := 0 to Item.IQPairs - 1 do for i := 0 to Item.IQPairs - 1 do
begin begin
Pos := i * 6; Pos := i * 6;
@@ -1217,7 +1261,13 @@ begin
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1). // Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX), // При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
// иначе оператор слышит свой эфир через RX-цепь и получает feedback. // иначе оператор слышит свой эфир через RX-цепь и получает feedback.
if Assigned(FOnAudio) and not FMuted and not FTXActive then // Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
// через unsigned-сравнение, GetTickCount64 не зовётся когда дедлайн = 0.
if (FPostTXMuteUntil <> 0) and (GetTickCount64 >= FPostTXMuteUntil) then
FPostTXMuteUntil := 0;
if Assigned(FOnAudio) and not FMuted and not FTXActive
and (FPostTXMuteUntil = 0) then
begin begin
for i := 0 to FAudioBufSize - 1 do for i := 0 to FAudioBufSize - 1 do
begin begin