mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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
|
const
|
||||||
DUC_IQ_PAIRS_PER_PACKET = 240;
|
DUC_IQ_PAIRS_PER_PACKET = 240;
|
||||||
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
|
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
|
||||||
// Пейсинг DUC-FIFO радио (виртуальная оценка уровня, Linux-ветка).
|
// Пейсинг DUC-FIFO радио — модель dl1ycf pihpsdr (new_protocol_txiq_thread).
|
||||||
// Режим «подушка в очереди»: pre-roll на старте TX (~THROTTLE+блоки нулей)
|
// Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце
|
||||||
// наполняет FIFO радио до THROTTLE, а остаток (~3 WDSP-блока) остаётся лежать
|
// радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп
|
||||||
// в FDUCIQQueue. Дальше очередь на каждом приходе блока непуста → sender
|
// расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен
|
||||||
// непрерывно метрит пакеты у порога THROTTLE, уровень FIFO радио прижат к
|
// просто ОТПРАВЛЯТЬ пакеты по мере готовности. THROTTLE — не «держатель
|
||||||
// THROTTLE..THROTTLE+240 (а не ходит пилой до дна). Опоздание WDSP-блока
|
// уровня», а BURST-ЛИМИТЕР: один WDSP-блок = ~8.5 пакетов приходят пачкой;
|
||||||
// (замерено до 25мс на старте под нагрузкой DUP) съедает подушку очереди,
|
// без ограничения пачка мгновенно вливается в FPGA-FIFO радио и может его
|
||||||
// но FIFO радио не проседает — нулевые вставки (LOW) не нужны.
|
// переполнить. Пока виртуальная оценка уровня > THROTTLE — притормаживаем
|
||||||
// THROTTLE=2000: FIFO радио ~2000..2400 — запас и от пустоты (2000), и от
|
// (только ЗАДЕРЖКА отправки, НИКОГДА не инжектируем данные). Дрейф PC↔радио
|
||||||
// переполнения (4096−2400) под дрейф клоков радио/ПК.
|
// тут даёт лишь ±1мс джиттера отправки (сглаживает FPGA-FIFO), а НЕ разрыв
|
||||||
|
// сэмплов. Пустая очередь → просто ждём следующий блок (как dl1ycf sem_wait):
|
||||||
|
// подушку на короткий разрыв держит pre-roll в FPGA-FIFO радио. Нулевые
|
||||||
|
// вставки УБРАНЫ — именно они (1.25мс тишины в потоке) рисовали всплески.
|
||||||
DUC_FIFO_THROTTLE = 2000;
|
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_PACKET_INTERVAL_US = 1250; // 240 / 192000 seconds
|
||||||
DUC_WIN_SPIN_US = 350; // final part of 1.25 ms interval: avoid Sleep jitter
|
DUC_WIN_SPIN_US = 350; // final part of 1.25 ms interval: avoid Sleep jitter
|
||||||
var
|
var
|
||||||
@@ -451,7 +449,6 @@ var
|
|||||||
LastTick: QWord;
|
LastTick: QWord;
|
||||||
NowTick: QWord;
|
NowTick: QWord;
|
||||||
ElapsedMs: QWord;
|
ElapsedMs: QWord;
|
||||||
ZeroPkt: TDUCIQPacket; // анти-старв филлер (нулевые IQ)
|
|
||||||
WaitMs: Integer;
|
WaitMs: Integer;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
@@ -524,33 +521,19 @@ begin
|
|||||||
{$ELSE}
|
{$ELSE}
|
||||||
VirtualSamples := 0;
|
VirtualSamples := 0;
|
||||||
LastTick := GetTickCount64;
|
LastTick := GetTickCount64;
|
||||||
FillChar(ZeroPkt, SizeOf(ZeroPkt), 0);
|
|
||||||
|
|
||||||
while not Terminated do
|
while not Terminated do
|
||||||
begin
|
begin
|
||||||
if not FNet.DequeueDUCIQ(Pkt) then
|
if not FNet.DequeueDUCIQ(Pkt) then
|
||||||
begin
|
begin
|
||||||
DrainVirtualFIFO;
|
DrainVirtualFIFO;
|
||||||
// Анти-старв: на передаче данных нет, а уровень FIFO радио проседает —
|
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
|
||||||
// шлём нулевые пакеты (тишина вместо underrun-мусора DAC). В устоявшемся
|
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
|
||||||
// режиме сюда не заходим (подушка очереди держит уровень у THROTTLE);
|
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
|
||||||
// срабатывает на старте TX (первый WDSP-блок через 10-25мс), в окне
|
// следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка
|
||||||
// WDSP-фейда после PTT-off (продюсер уже молчит, PTT ещё поднят) и
|
// pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной.
|
||||||
// после стайлов планировщика. Доливаем сразу до 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;
|
if FNet.FIsTransmitting then WaitMs := 1 else WaitMs := 20;
|
||||||
RTLEventWaitFor(FNet.FDUCIQSem, WaitMs);
|
RTLEventWaitFor(FNet.FDUCIQSem, WaitMs);
|
||||||
DrainVirtualFIFO;
|
DrainVirtualFIFO;
|
||||||
|
|||||||
+12
-38
@@ -669,15 +669,12 @@ var
|
|||||||
PeriodMs: Integer;
|
PeriodMs: Integer;
|
||||||
PeriodUs: Integer;
|
PeriodUs: Integer;
|
||||||
AccUs: Integer;
|
AccUs: Integer;
|
||||||
DidBlock: Boolean;
|
|
||||||
StallCount: Integer;
|
|
||||||
begin
|
begin
|
||||||
// 512 samples / 48000 Hz = 10.666 ms. Do not floor this to 10 ms:
|
// 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
|
// that over-produces TX IQ by ~6.7%, eventually forcing DUC queue
|
||||||
// corrections/drops and producing periodic sidebands on a pure TUN tone.
|
// corrections/drops and producing periodic sidebands on a pure TUN tone.
|
||||||
PeriodUs := Max(1000, FEngine.FAudioBufSize * 1000000 div FEngine.FAudioRate);
|
PeriodUs := Max(1000, FEngine.FAudioBufSize * 1000000 div FEngine.FAudioRate);
|
||||||
AccUs := 0;
|
AccUs := 0;
|
||||||
StallCount := 0;
|
|
||||||
|
|
||||||
while not Terminated do
|
while not Terminated do
|
||||||
begin
|
begin
|
||||||
@@ -698,51 +695,28 @@ begin
|
|||||||
Assigned(FEngine.FOnPullMic) then
|
Assigned(FEngine.FOnPullMic) then
|
||||||
FEngine.FOnPullMic(FEngine.FAudioBufSize * 2);
|
FEngine.FOnPullMic(FEngine.FAudioBufSize * 2);
|
||||||
|
|
||||||
// Обрабатываем накопившиеся блоки с реальными mic-данными
|
// Обрабатываем накопившиеся блоки с реальными mic-данными (строго
|
||||||
DidBlock := False;
|
// mic-гейтед: сколько полных mic-блоков в ring — столько TX-блоков).
|
||||||
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
|
||||||
begin
|
FEngine.ProcessTXBlock
|
||||||
FEngine.ProcessTXBlock;
|
|
||||||
DidBlock := True;
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
Break;
|
Break;
|
||||||
until False;
|
until False;
|
||||||
|
|
||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
|
|
||||||
// Если на этом тике не было ни одного полного mic-блока и mic-данных
|
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring
|
||||||
// вообще нет — обрабатываем один блок тишины, чтобы DUC IQ поток не
|
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
|
||||||
// прерывался и спектр/водопад обновлялись.
|
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
|
||||||
// Важно: не добавлять silence-block сразу после реального блока. Для TUN
|
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
|
||||||
// этот "silence" тоже содержит PostGen tone, поэтому лишний блок ломает
|
// над-производит блоки сверх mic-темпа. Продюсер теперь строго mic-гейтед:
|
||||||
// темп DUC IQ и даёт периодические боковые/тр-р-р.
|
// блок создаётся ТОЛЬКО когда в ring накопился полный mic-блок (радиокварц
|
||||||
// Если же в ring уже есть неполный блок, нельзя подменять его тишиной:
|
// шлёт mic-пакеты непрерывно и на TX/TUN → очередь пополняется радиоклоком).
|
||||||
// radio mic приходит пакетами по 64 сэмпла, sound-card часто по 128.
|
// Реальный обрыв mic (сеть) = короткий разрыв, покрываемый подушкой
|
||||||
// Иначе каждый mic packet будит поток раньше срока, а мы генерируем
|
// FPGA-FIFO радио, без фабрикации тишины в поток.
|
||||||
// лишний 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;
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user