From e813ada6b258ba3ff9337b1aee83b2d6e9e9ba95 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Thu, 7 May 2026 10:25:16 +0300 Subject: [PATCH] =?UTF-8?q?fix=20TX=20clicks=20=D0=B2=20TUN:=201=20Process?= =?UTF-8?q?TXBlock=20=D0=BD=D0=B0=20=D1=82=D0=B8=D0=BA=20=D0=B2=D0=BC?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=202-3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Причина щелчков (особенно слышных в TUN с непрерывной несущей): TX-поток ждал FTXMicSem с таймаутом PeriodMs (auto-reset event), а PushTXMicSamples* сигналили семафор каждый push. PullSoundCardMic зовётся из самого TX-потока → сем сигналится → следующий WaitFor возвращается немедленно. Плюс bottom-check всегда фигачил silence-блок, если Avail --- WDSPEngine.pas | 63 +++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 9edfe60..ac6d7b2 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -527,56 +527,52 @@ begin end; procedure TTXDSPThread.Execute; -// Аналог tx_thread в piHPSDR/transmitter.c: -// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate). -// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину. -// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде. +// Тикает по таймеру с периодом одного mic-буфера (FAudioBufSize/FAudioRate). +// Семафор НЕ используется как сигнал «есть mic-данные» — он бы давал +// немедленные возвраты из WaitFor (auto-reset event) и приводил к +// over-production IQ-потока: за один реальный период радио мы шлём +// 2–3 блока вместо одного. Это и было слышно в TUN как щелчки. +// Семафор оставлен только для будилки на shutdown (Terminate → Set). +// +// За один тик: +// - Pull mic из PortAudio (sound-card path). +// - Inner loop: процессим все накопившиеся полные блоки (обычно 1). +// - Если ничего реального не процессили — гоним один блок тишины, +// чтобы DUC IQ поток не прерывался и FIFO радио не голодал. var - Avail: Integer; - PeriodMs: Integer; + Avail: Integer; + PeriodMs: Integer; + ProcessedAny: Boolean; begin - // 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate); while not Terminated do begin - // Ждём сигнала от mic-данных ИЛИ истечения периода — - // в piHPSDR timing идёт от receive thread; здесь — от таймаута + // Ждём ровно PeriodMs. Сем сигналится только при Terminate → быстрый shutdown. RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs); if Terminated then Break; if not FEngine.FTXActive then Continue; - // Если выбран sound-card mic — тянем сэмплы из внешнего источника - // (PortAudio через FOnPullMic в MainForm) ПЕРЕД проверкой ring buffer. - // Callback сам пушит сэмплы в FTXMicRing через PushTXMicSampleD. if (FEngine.FTXMicSource = txmsSoundCard) and Assigned(FEngine.FOnPullMic) then FEngine.FOnPullMic(FEngine.FAudioBufSize * 2); - // Обрабатываем накопившиеся блоки с реальными mic-данными + ProcessedAny := False; repeat Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); if (Avail >= FEngine.FAudioBufSize) and not Terminated then - FEngine.ProcessTXBlock - else + begin + FEngine.ProcessTXBlock; + ProcessedAny := True; + end else Break; until False; if Terminated then Break; - // Если mic-данных не было — обрабатываем один блок тишины, чтобы - // DUC IQ поток не прерывался и спектр/водопад обновлялись. - // НО для sound-card mic неполный блок (0 < Avail < AudioBufSize) нельзя - // подменять тишиной: PortAudio часто отдаёт 128-сэмпловые callback'и, - // и если в кольце уже есть голосовой хвост — он застрянет, а в эфир - // уйдёт тишина. Слышно как чередование «голос/тишина» = треск ≈50–100 Гц. - // В этом случае ждём следующий тик, хвост накопится до полного блока. - Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) - and (TX_MIC_RING - 1); - if (Avail = 0) or (FEngine.FTXMicSource <> txmsSoundCard) then - if Avail < FEngine.FAudioBufSize then - FEngine.ProcessTXBlock; + if not ProcessedAny then + FEngine.ProcessTXBlock; end; end; @@ -1614,8 +1610,10 @@ begin end; // если буфер полон — сэмпл отбрасывается (overrun) end; - - RTLEventSetEvent(FTXMicSem); // будим TX поток + // Семафор намеренно не сигналим — TX-поток тикает по таймеру + // (PeriodMs = AudioBufSize/AudioRate). Сигнал давал бы немедленный + // возврат из WaitFor → лишний ProcessTXBlock → over-production + // IQ-потока относительно темпа радио → щелчки/FIFO-overrun. end; procedure TWDSPEngine.PushTXMicSampleD(const S: Double); @@ -1627,8 +1625,7 @@ begin FTXMicRing[FTXMicHead] := S; FTXMicHead := newHead; end; - // overrun → дроп, как в HW path - RTLEventSetEvent(FTXMicSem); + // overrun → дроп, как в HW path. Сем не сигналим — см. PushTXMicSamples16. end; procedure TWDSPEngine.PushTXMicSamplesD(const Src: array of Double; N: Integer); @@ -1649,9 +1646,7 @@ begin end; // overrun → дроп end; - - if cnt > 0 then - RTLEventSetEvent(FTXMicSem); + // Сем не сигналим — см. PushTXMicSamples16. end; // ---------------------------------------------------------------------------