fix(tx): чистый openHPSDR TX-тракт — задержка DUP, всплески TUN, фриз PTT-off

Диагностика временным TXLog (снят) вскрыла цепочку проблем TX-тракта:

- Задержка ~200мс своего сигнала в DUP: за время приёма ринг PortAudio-
  микрофона наполнялся до упора (9600 сэмплов) и на старте TX бёрстом
  оседал стоячим запасом в DUC-очереди. Фикс: TAudioInput.Flush на RX→TX
  (заодно убрана передача в эфир аудио, записанного до нажатия PTT).

- Всплески на TUN: (1) старт — радио начинало с пустого DUC-FIFO; (2) по
  ходу — пейсинг со строгой каденцией держал запас FIFO у нуля, джиттер
  3-6мс давал underrun; (3) клип: голос из бэклога складывался с PostGen-
  тоном (стоит после ALC) поверх шкалы, а сам тон при mag=1.0 давал
  выброс Гиббса x1.18 на старте. Фиксы: pre-roll PrimeDUCIQ (THROTTLE +
  3 блока подушки в очереди — sender метрит непрерывно, FIFO радио прижат
  2000..2400), анти-старв доливка нулями до REANCHOR при просадке,
  TUN_TONE_MAG=0.83, мьют mic-входа TXA при активном тоне.

- Огрызок TUN-тона в эфир на первом блоке следующего TX: стоп канала
  dmode=1 ждал фейд, который некому прокрутить (продюсер уже остановлен) —
  канал стопился по таймауту Sleep(1)x100 с недобранным сигналом в
  буферах. Это же было и «остаточным фризом ~106мс» GUI на отпускании
  PTT: чистый таймаут, фейд никогда не исполнялся. Фикс: dmode=0 +
  синхронный слив/продувка канала 4 нулевыми fexchange0 на стопе и
  старте (<1мс, штатные down-slew и flush).

- Дисплей: ResetPixelBuffers(TX_DISP_ID) на старте TX — усредняющая
  история TX-анализатора показывала спектр тона прошлой сессии.

- StopRunning при активном TUN не чистил DUC-очередь и не сбрасывал
  FTuning/тон — закрыто.

Латентность тракта mic→радио теперь ~43мс (WDSP 11 + подушка 21 + FIFO
10.4). Проверено на эфире: q=0..30, fill/drops/underrun=0, clip=0,
клоки радио/ПК без дрейфа (micRx 48000/с).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 22:46:19 +03:00
co-authored by Claude Fable 5
parent 98493f08b3
commit 577b8add9d
6 changed files with 185 additions and 7 deletions
+62 -2
View File
@@ -245,6 +245,7 @@ type
procedure SetSpeakerAudio(AEnabled: Boolean); override;
procedure SendDUCIQ(const IData, QData: array of Integer); override;
procedure ClearDUCIQQueue; override;
procedure PrimeDUCIQ(Pairs: Integer); override;
// LocalPort — HPSDR-специфично (локальный UDP-порт), не часть базы.
property LocalPort: Word read FLocalPort;
@@ -419,7 +420,22 @@ 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-FIFO радио (виртуальная оценка уровня, Linux-ветка).
// Режим «подушка в очереди»: pre-roll на старте TX (~THROTTLE+блоки нулей)
// наполняет FIFO радио до THROTTLE, а остаток (~3 WDSP-блока) остаётся лежать
// в FDUCIQQueue. Дальше очередь на каждом приходе блока непуста → sender
// непрерывно метрит пакеты у порога THROTTLE, уровень FIFO радио прижат к
// THROTTLE..THROTTLE+240 (а не ходит пилой до дна). Опоздание WDSP-блока
// (замерено до 25мс на старте под нагрузкой DUP) съедает подушку очереди,
// но FIFO радио не проседает — нулевые вставки (LOW) не нужны.
// THROTTLE=2000: FIFO радио ~2000..2400 — запас и от пустоты (2000), и от
// переполнения (4096−2400) под дрейф клоков радио/ПК.
DUC_FIFO_THROTTLE = 2000;
DUC_FIFO_LOW = 1000; // анти-старв (аварийный): ниже — шлём нули
// Доливка при срабатывании анти-старва: не один пакет (якорь у LOW+240,
// запас 1.25мс — следующее опоздание блока снова даст вставку-щелчок),
// а до REANCHOR. Излишек осядет подушкой в очереди через троттл THROTTLE.
DUC_FIFO_REANCHOR = 1700;
DUC_PACKET_INTERVAL_US = 1250; // 240 / 192000 seconds
DUC_WIN_SPIN_US = 350; // final part of 1.25 ms interval: avoid Sleep jitter
var
@@ -435,6 +451,8 @@ var
LastTick: QWord;
NowTick: QWord;
ElapsedMs: QWord;
ZeroPkt: TDUCIQPacket; // анти-старв филлер (нулевые IQ)
WaitMs: Integer;
{$ENDIF}
{$IFDEF WINDOWS}
@@ -469,6 +487,7 @@ var
LastTick := NowTick;
end;
end;
{$ENDIF}
begin
@@ -505,13 +524,35 @@ begin
{$ELSE}
VirtualSamples := 0;
LastTick := GetTickCount64;
FillChar(ZeroPkt, SizeOf(ZeroPkt), 0);
while not Terminated do
begin
if not FNet.DequeueDUCIQ(Pkt) then
begin
DrainVirtualFIFO;
RTLEventWaitFor(FNet.FDUCIQSem, 20);
// Анти-старв: на передаче данных нет, а уровень FIFO радио проседает —
// шлём нулевые пакеты (тишина вместо underrun-мусора DAC). В устоявшемся
// режиме сюда не заходим (подушка очереди держит уровень у THROTTLE);
// срабатывает на старте TX (первый WDSP-блок через 10-25мс), в окне
// WDSP-фейда после PTT-off (продюсер уже молчит, PTT ещё поднят) и
// после стайлов планировщика. Доливаем сразу до REANCHOR: излишек
// осядет обратно подушкой в очереди, и якорь не остаётся у LOW.
if FNet.FIsTransmitting and (VirtualSamples < DUC_FIFO_LOW) then
begin
while (not Terminated) and (VirtualSamples < DUC_FIFO_REANCHOR) do
begin
FNet.PackSeqBytes(ZeroPkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
FNet.DoSendTo(FNet.FSocket, ZeroPkt, SizeOf(ZeroPkt),
FNet.FDevice.IPAddress, FNet.FPortDUCIQ);
Inc(VirtualSamples, DUC_IQ_PAIRS_PER_PACKET);
end;
Continue; // сразу пробуем данные — вдруг уже пришли
end;
// На передаче ждём коротко: за 20мс FIFO радио сливает 3840 сэмплов —
// анти-старв выше должен успевать подхватывать просадку.
if FNet.FIsTransmitting then WaitMs := 1 else WaitMs := 20;
RTLEventWaitFor(FNet.FDUCIQSem, WaitMs);
DrainVirtualFIFO;
Continue;
end;
@@ -1714,6 +1755,25 @@ begin
end;
end;
procedure THPSDRNetwork.PrimeDUCIQ(Pairs: Integer);
// Pre-roll на старте TX: Pairs нулевых IQ-пар (пакетами по 240) в очередь.
// Sender вышлет их немедленно (VirtualSamples после RX-паузы = 0, троттл
// пропустит бёрст) — DUC-FIFO радио наполняется нулями, пока реле/WDSP
// раскачиваются, и DAC не стартует с пустого буфера (underrun-всплеск).
var
Pkt: TDUCIQPacket;
n: Integer;
begin
if not FConnected then Exit;
FillChar(Pkt, SizeOf(Pkt), 0);
n := (Pairs + 239) div 240;
while n > 0 do
begin
EnqueueDUCIQ(Pkt);
Dec(n);
end;
end;
procedure THPSDRNetwork.SendDUCIQ(const IData, QData: array of Integer);
var
Pkt: TDUCIQPacket;