diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index 23ae7f4..583c7a2 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -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; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 0d6af72..f9c0158 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -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;