diff --git a/AudioInput.pas b/AudioInput.pas index 367c734..82bc9bf 100644 --- a/AudioInput.pas +++ b/AudioInput.pas @@ -135,6 +135,11 @@ type // Сколько сэмплов реально лежит в ring буфере (0..MY_MIC_RING_SIZE-1). // Использовать для чтения только доступных данных без пуш-тишины. function Available: Integer; + // Сбросить накопленный бэклог (читатель догоняет писателя). Зовётся на + // старте TX: пока идёт приём, ринг наполняется до упора (~200мс), и без + // сброса весь этот «старый» звук бёрстом уходит в TX-тракт — стоячая + // задержка в DUC-очереди + передача аудио, записанного до нажатия PTT. + function Flush: Integer; property IsOpen: Boolean read FOpen; property LastError: string read GetLastError; @@ -559,4 +564,12 @@ begin Result := d; end; +function TAudioInput.Flush: Integer; +begin + // Lock-free, как ReadSample: одно присваивание Integer. Гонка с callback + // безобидна — в худшем случае останется/пропадёт несколько свежих сэмплов. + Result := Available; + FOutPt := FInPt; +end; + end. diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index d5114b8..23ae7f4 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -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; diff --git a/RadioBackend.pas b/RadioBackend.pas index f0a2101..569bdcc 100644 --- a/RadioBackend.pas +++ b/RadioBackend.pas @@ -129,6 +129,11 @@ type // ---- TX IQ ---- procedure SendDUCIQ(const IData, QData: array of Integer); virtual; procedure ClearDUCIQQueue; virtual; + // Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи — + // радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска + // первых мс, как pre-roll 1024 в piHPSDR). База/Pluto — no-op: у Pluto + // TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями. + procedure PrimeDUCIQ(Pairs: Integer); virtual; // ---- Опциональные (HPSDR-специфичные): no-op по умолчанию ---- procedure SendGeneralPacket(const Pkt: TGeneralPacket); virtual; @@ -197,6 +202,10 @@ procedure TRadioBackend.ClearDUCIQQueue; begin end; +procedure TRadioBackend.PrimeDUCIQ(Pairs: Integer); +begin +end; + procedure TRadioBackend.SendGeneralPacket(const Pkt: TGeneralPacket); begin end; diff --git a/RadioController.pas b/RadioController.pas index 55f58a0..cdb1624 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -672,6 +672,14 @@ type implementation +const + // Амплитуда TUN-тона (WDSP PostGen). НЕ 1.0: на резком старте тона фильтры + // TXA-тракта дают переходный выброс ~×1.18 (замерено: peak=1.176 при + // mag=1.0) поверх полной шкалы → жёсткий кламп 24-бит → сплэттер на старте + // TUN. 0.83×1.18≈0.98 — выброс остаётся в шкале. Мощность TUN задаёт drive + // (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel. + TUN_TONE_MAG = 0.83; + constructor TRadioController.Create; begin inherited Create; @@ -2335,7 +2343,7 @@ begin begin FDSPEngine.SetMode(FMode); // Если TUN активен — пересчитываем знак частоты под новый режим - if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0); + if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); end; ApplyModeFilter; Changed(rfMode); @@ -3071,6 +3079,14 @@ begin FDriveLevel, True, True, True); FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле + // Pre-roll: THROTTLE (2000) + ТРИ WDSP-блока (3×2048) нулевых пар. Первая + // часть улетает сразу и наполняет DUC-FIFO радио до порога THROTTLE (DAC + // не стартует с пустого буфера), остаток ~3 блока (~32мс) ложится ПОДУШКОЙ + // в очередь — дальше очередь на приходе каждого WDSP-блока непуста, sender + // метрит непрерывно, и опоздание блока не роняет FIFO радио. Замеры: под + // нагрузкой DUP и при быстром пере-PTT первый WDSP-блок опаздывает до + // 25мс — двух блоков подушки не хватало (щелчок через ~0.5с после старта). + FNetwork.PrimeDUCIQ(8144); end; if FWDSPReady and Assigned(FDSPEngine) then begin @@ -3093,6 +3109,14 @@ begin FWebMicActive := False; FDSPEngine.SetTXMicSource(DefaultMicSource); end; + // Сброс бэклога sound-card микрофона на RX→TX: пока шёл приём, ринг + // PortAudio наполнился до упора (~200мс). Без сброса весь этот «старый» + // звук бёрстом уходит в тракт: стоячая задержка ~190мс в DUC-очереди + // + в эфир передаётся аудио, записанное ДО нажатия PTT (на TUN оно + // складывается с тоном поверх полной шкалы → клип-всплеск). + if FTransmitting and (not WasTransmitting) and Assigned(FAudioIn) + and FAudioIn.IsOpen then + FAudioIn.Flush; // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы // TX leakage не накапливался в RXA pipeline / FFT-истории анализатора. FDSPEngine.KeepRXDuringTX := FDisplayDuplex; @@ -3139,7 +3163,7 @@ begin begin // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. if FWDSPReady and Assigned(FDSPEngine) then - FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0); + FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); FTuning := True; FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень) ApplyWDSPDrive(FTXSettings.TUNLevel); @@ -3370,6 +3394,16 @@ begin FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0); FRunning := False; FTransmitting := False; + // Стоп при активном TX/TUN: хвост IQ в DUC-очереди не должен пережить + // остановку (иначе при следующем старте уйдёт в эфир старый тон/голос), + // а TUN-состояние (PostGen-тон) — вернуться в выключенное. + FNetwork.ClearDUCIQQueue; + if FTuning then + begin + FTuning := False; + if FWDSPReady then FDSPEngine.SetTXTone(False, 0, 0); + Changed(rfTuning); // UI: TUN-кнопка off + end; if FWDSPReady then FDSPEngine.SetTXRun(False); Changed(rfTransmitting); // MOX-кнопка off end; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index dbaa360..0d6af72 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -285,6 +285,10 @@ type FActiveDisplayID: Integer; // RX_DISP_ID или TX_DISP_ID FTXMicSource: TTXMicSource; FOnPullMic: TPullMicSamplesFunc; + // TUN активен (PostGen-тон): mic-вход TXA зануляется в ProcessTXBlock. + // PostGen стоит ПОСЛЕ ALC — голос из микрофона складывался бы с тоном + // поверх полной шкалы (тон ~0.98) → клип 24-бит → сплэттер на спектре. + FTXToneOn: Boolean; // Кэшированные TX-параметры — применяются после OpenChannel(TXA) и при изменении. // Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS. @@ -897,6 +901,7 @@ begin FTXMicHead := 0; FTXMicTail := 0; FTXActive := False; + FTXToneOn := False; FKeepRXDuringTX := False; FMonitorRXDuringTX := False; FPostTXMuteUntil := 0; @@ -2140,6 +2145,8 @@ begin end; procedure TWDSPEngine.SetTXRun(Run: Boolean); +var + k, Err: Integer; begin FDispPos := 0; FTXDispPos := 0; @@ -2150,7 +2157,30 @@ begin FTXMicHead := 0; FTXMicTail := 0; if FInitialized then + begin SetChannelState(TXA_CHAN, 1, 0); // run, no delay + // Продувка канала: страховка от остатка прошлой TX-сессии в буферах/ + // фильтрах TXA (без неё огрызок — после TUN это полноразмахный тон — + // выплёвывался в эфир первым же блоком нового TX). Основной слив теперь + // на стопе (ниже), но нештатные пути останова канал не чистят. 4 нулевых + // блока через fexchange0, выход в мусор (не в FOnTXIQ/display); поток + // ещё idle (FTXActive=False) — гонки нет. + FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); + for k := 1 to 4 do + begin + Err := 0; + fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err); + end; + // Сброс TX-анализатора: его усредняющая история хранит спектр ПРОШЛОЙ + // сессии (после TUN — тон), и при переключении на TX-дисплей первые + // кадры показывали «всплеск» старого тона (виден в non-DUP, где на + // экране TX-спектр; эфир при этом чист — в DUP старт чистый). + // ResetPixelBuffers — лёгкое зануление pixel/average буферов (MW0LGE); + // НЕ ApplyTXAnalyzerSettings: тот зовёт SetAnalyzer (перепланирование + // FFT в UI-потоке) — давал видимую задержку отрисовки на старте TX. + if FAnalyzerOpen then + ResetPixelBuffers(TX_DISP_ID); + end; // TX-DSP поток создаётся ОДИН раз и живёт весь сеанс. НЕ пересоздаём на каждом // T/R: join (WaitFor) давал фиксированные ~100 мс фриза GUI на отпускании PTT. // При FTXActive=False поток простаивает (см. TTXDSPThread.Execute: Continue). @@ -2164,7 +2194,24 @@ begin FTXActive := False; // поток уходит в простой (не трогает канал/ring) FActiveDisplayID := RX_DISP_ID; if FInitialized then - SetChannelState(TXA_CHAN, 0, 1); // stop, with slew (поток уже idle) + begin + // Мгновенный стоп + синхронный слив. Раньше здесь был dmode=1: он + // спинует Sleep(1)×100 в ожидании фейда, который должен прокрутить + // fexchange — а продюсер уже остановлен, fexchange никто не зовёт → + // чистый таймаут ~106мс (остаточный фриз GUI на отпускании PTT) и + // канал, брошенный с недобранным сигналом в буферах. dmode=0 лишь + // взводит downflag/flushflag и возвращается сразу; down-slew (фейд + // 10мс) и штатный flush буферов исполняют наши же fexchange0 с нулевым + // входом — суммарно <1мс, выход в мусор (в эфир не идёт: DUC-очередь + // чистится контроллером на T/R). + SetChannelState(TXA_CHAN, 0, 0); // stop, без ожидания + FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); + for k := 1 to 4 do + begin + Err := 0; + fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err); + end; + end; FTXMicHead := 0; FTXMicTail := 0; end; @@ -2175,6 +2222,7 @@ var SignedFreq: Double; begin if not FInitialized then Exit; + FTXToneOn := Enabled; // мьют mic-входа на TUN (см. ProcessTXBlock) if Enabled then begin // gen1 (PostGen) стоит после всех модуляторов и бандпасса. @@ -2232,6 +2280,10 @@ begin tail := (tail + 1) and (TX_MIC_RING - 1); end; FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread) + // TUN: ринг потребляем (иначе поток зациклится на «есть полный блок»), + // но в TXA подаём тишину — тон PostGen должен идти без голоса поверх. + if FTXToneOn then + FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); end else begin // Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями) diff --git a/ewsdr.lpi b/ewsdr.lpi index 3b6971b..61aea45 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -84,6 +84,16 @@ + + + + + + + + + +