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
+52 -2
View File
@@ -216,6 +216,18 @@ type
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
FTXThread: TThread;
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
FTXMicSource: TTXMicSource;
FOnPullMic: TPullMicSamplesFunc;
@@ -325,6 +337,12 @@ type
DataOffset: Integer;
IQPairs: Integer);
procedure FlushRX;
// Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это
// на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio
// буфер удерживает «хвост» с TX leakage из радио, который AudioOut.Clear
// не достаёт. На Linux буфер маленький и проблемы нет, поэтому метод
// не вызывается.
procedure BeginPostTXMute(DurationMs: Integer);
// RX управление
procedure SetMode(Mode: Integer);
@@ -432,6 +450,10 @@ type
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
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).
// SpectrumView/Waterfall должны читать пиксели именно из этого ID.
property ActiveDisplayID: Integer read FActiveDisplayID;
@@ -741,6 +763,8 @@ begin
FTXMicHead := 0;
FTXMicTail := 0;
FTXActive := False;
FKeepRXDuringTX := False;
FPostTXMuteUntil := 0;
FActiveDisplayID := RX_DISP_ID;
FTXMicSource := txmsRadio;
FOnPullMic := nil;
@@ -1124,14 +1148,34 @@ begin
if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0);
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);
// Вызывается из 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
i, Pos: Integer;
IR, QR: LongInt;
const
SCALE = 1.0 / 8388608.0;
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
begin
Pos := i * 6;
@@ -1217,7 +1261,13 @@ begin
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
// иначе оператор слышит свой эфир через 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
for i := 0 to FAudioBufSize - 1 do
begin