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 @@
+
+
+
+
+
+
+
+
+
+