mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
fix(tx): убраны инъекции данных в DUC IQ-поток — всплески на водопаде при TX
Два источника фабрикации данных в TX IQ-поток рисовали всплески на водопаде при передаче (openHPSDR, особенно TUN/DUP): 1. HPSDRNetwork (sender): анти-старв zero-fill при пустой очереди впрыскивал нулевые пакеты = 1.25мс тишины прямо в поток = разрыв данных = всплеск. Теперь пустая очередь → просто ждём следующий блок (модель dl1ycf pihpsdr txiq_thread: данные не инжектируются никогда; короткий разрыв покрывает pre-roll в FPGA-FIFO радио). Убраны DUC_FIFO_LOW/REANCHOR/ZeroPkt. 2. WDSPEngine (продюсер TTXDSPThread): silence-инъекция при стойле mic впрыскивала лишний блок сверх mic-темпа (для TUN он нёс PostGen-тон → боковые/разрыв темпа). Продюсер теперь строго mic-гейтед. Замерено на эфире: всплески при TUN 5-6/сессию → ~1. Остаточный редкий клик по прямому замеру НЕ из TX-пейсинга (sender ровно на радиоклоке 800 пак/с через supply-limiting, дропов/underrun нет) — источник отдельный (дисплей/RX), разбор позже. Windows-ветка sender'а не тронута. Pluto — свой TX-тракт (TPlutoTXThread + libiio DMA), задета только общая silence-removal (push-поток сам добивает нулями). Обе не перепроверены на железе. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-37
@@ -420,22 +420,20 @@ procedure TDUCIQSenderThread.Execute;
|
||||
const
|
||||
DUC_IQ_PAIRS_PER_PACKET = 240;
|
||||
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
|
||||
// Пейсинг 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 радио — модель dl1ycf pihpsdr (new_protocol_txiq_thread).
|
||||
// Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце
|
||||
// радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп
|
||||
// расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен
|
||||
// просто ОТПРАВЛЯТЬ пакеты по мере готовности. THROTTLE — не «держатель
|
||||
// уровня», а BURST-ЛИМИТЕР: один WDSP-блок = ~8.5 пакетов приходят пачкой;
|
||||
// без ограничения пачка мгновенно вливается в FPGA-FIFO радио и может его
|
||||
// переполнить. Пока виртуальная оценка уровня > THROTTLE — притормаживаем
|
||||
// (только ЗАДЕРЖКА отправки, НИКОГДА не инжектируем данные). Дрейф PC↔радио
|
||||
// тут даёт лишь ±1мс джиттера отправки (сглаживает FPGA-FIFO), а НЕ разрыв
|
||||
// сэмплов. Пустая очередь → просто ждём следующий блок (как dl1ycf sem_wait):
|
||||
// подушку на короткий разрыв держит pre-roll в FPGA-FIFO радио. Нулевые
|
||||
// вставки УБРАНЫ — именно они (1.25мс тишины в потоке) рисовали всплески.
|
||||
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
|
||||
@@ -451,7 +449,6 @@ var
|
||||
LastTick: QWord;
|
||||
NowTick: QWord;
|
||||
ElapsedMs: QWord;
|
||||
ZeroPkt: TDUCIQPacket; // анти-старв филлер (нулевые IQ)
|
||||
WaitMs: Integer;
|
||||
{$ENDIF}
|
||||
|
||||
@@ -524,33 +521,19 @@ begin
|
||||
{$ELSE}
|
||||
VirtualSamples := 0;
|
||||
LastTick := GetTickCount64;
|
||||
FillChar(ZeroPkt, SizeOf(ZeroPkt), 0);
|
||||
|
||||
while not Terminated do
|
||||
begin
|
||||
if not FNet.DequeueDUCIQ(Pkt) then
|
||||
begin
|
||||
DrainVirtualFIFO;
|
||||
// Анти-старв: на передаче данных нет, а уровень 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 сэмплов —
|
||||
// анти-старв выше должен успевать подхватывать просадку.
|
||||
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
|
||||
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
|
||||
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
|
||||
// следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка
|
||||
// pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной.
|
||||
// На передаче ждём коротко (быстро подхватить пришедший блок), на приёме
|
||||
// дольше (энергосбережение, всё равно ничего не шлём).
|
||||
if FNet.FIsTransmitting then WaitMs := 1 else WaitMs := 20;
|
||||
RTLEventWaitFor(FNet.FDUCIQSem, WaitMs);
|
||||
DrainVirtualFIFO;
|
||||
|
||||
+12
-38
@@ -669,15 +669,12 @@ var
|
||||
PeriodMs: Integer;
|
||||
PeriodUs: Integer;
|
||||
AccUs: Integer;
|
||||
DidBlock: Boolean;
|
||||
StallCount: Integer;
|
||||
begin
|
||||
// 512 samples / 48000 Hz = 10.666 ms. Do not floor this to 10 ms:
|
||||
// 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;
|
||||
StallCount := 0;
|
||||
|
||||
while not Terminated do
|
||||
begin
|
||||
@@ -698,51 +695,28 @@ begin
|
||||
Assigned(FEngine.FOnPullMic) then
|
||||
FEngine.FOnPullMic(FEngine.FAudioBufSize * 2);
|
||||
|
||||
// Обрабатываем накопившиеся блоки с реальными mic-данными
|
||||
DidBlock := False;
|
||||
// Обрабатываем накопившиеся блоки с реальными mic-данными (строго
|
||||
// mic-гейтед: сколько полных mic-блоков в ring — столько TX-блоков).
|
||||
repeat
|
||||
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
||||
and (TX_MIC_RING - 1);
|
||||
if (Avail >= FEngine.FAudioBufSize) and not Terminated then
|
||||
begin
|
||||
FEngine.ProcessTXBlock;
|
||||
DidBlock := True;
|
||||
end
|
||||
FEngine.ProcessTXBlock
|
||||
else
|
||||
Break;
|
||||
until False;
|
||||
|
||||
if Terminated then Break;
|
||||
|
||||
// Если на этом тике не было ни одного полного mic-блока и mic-данных
|
||||
// вообще нет — обрабатываем один блок тишины, чтобы DUC IQ поток не
|
||||
// прерывался и спектр/водопад обновлялись.
|
||||
// Важно: не добавлять silence-block сразу после реального блока. Для TUN
|
||||
// этот "silence" тоже содержит PostGen tone, поэтому лишний блок ломает
|
||||
// темп DUC IQ и даёт периодические боковые/тр-р-р.
|
||||
// Если же в ring уже есть неполный блок, нельзя подменять его тишиной:
|
||||
// radio mic приходит пакетами по 64 сэмпла, sound-card часто по 128.
|
||||
// Иначе каждый mic packet будит поток раньше срока, а мы генерируем
|
||||
// лишний TX-блок тишины вместо ожидания полного 512-sample блока.
|
||||
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
||||
and (TX_MIC_RING - 1);
|
||||
// Тишину генерируем только после 6 подряд пустых тиков (~64 ms).
|
||||
// Порог 64 ms даёт 44 ms запаса относительно периода Opus-фрейма (20 ms),
|
||||
// что покрывает WiFi-джиттер до ~40 ms без ложного инжекта тишины.
|
||||
// При реальной потере пакета тишина всё равно инжектируется.
|
||||
if not DidBlock then
|
||||
begin
|
||||
if Avail = 0 then
|
||||
begin
|
||||
Inc(StallCount);
|
||||
if StallCount >= 6 then
|
||||
FEngine.ProcessTXBlock;
|
||||
end
|
||||
else
|
||||
StallCount := 0;
|
||||
end
|
||||
else
|
||||
StallCount := 0;
|
||||
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring
|
||||
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
|
||||
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
|
||||
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
|
||||
// над-производит блоки сверх mic-темпа. Продюсер теперь строго mic-гейтед:
|
||||
// блок создаётся ТОЛЬКО когда в ring накопился полный mic-блок (радиокварц
|
||||
// шлёт mic-пакеты непрерывно и на TX/TUN → очередь пополняется радиоклоком).
|
||||
// Реальный обрыв mic (сеть) = короткий разрыв, покрываемый подушкой
|
||||
// FPGA-FIFO радио, без фабрикации тишины в поток.
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user