fix TX треск: SoundCard mic не подменять тишиной + DUC packetizer с переносом хвоста

Два источника искажений при передаче с микрофона компьютера:

1. SoundCard mic путь в TX-потоке (WDSPEngine.pas) при неполном блоке
   подменял голосовой хвост в кольце тишиной. PortAudio отдаёт по 128
   сэмплов, в кольце часто < FAudioBufSize → ProcessTXBlock уходил в
   else-ветку и слал блок нулей, оставляя голос на следующий тик.
   Результат: чередование «голос/тишина» = треск ≈50–100 Гц.
   Теперь подмена тишиной только при Avail=0; иначе ждём следующий тик.
   Добавлен PushTXMicSamplesD — батч-пуш с одной будилкой семафора.

2. DUC IQ packetizer (MainForm.pas/OnTXIQReady) добивал последний
   неполный пакет нулями. FTXOutBufSize не кратен 240 (4096 mod 240=16),
   из-за чего в каждом TX-блоке возникал провал ≈1.16 мс каждые ≈21 мс
   ≈ 47 Гц гул. Теперь хвост переносится в следующий вызов через
   FDUCPendingI/Q/Count; шлются только полные пакеты по 240 пар.

Дополнительно: ScaleIQ24 клампит результат в [-2^23, 2^23-1] чтобы
пики от EQ/компрессора (V чуть >1.0) не оборачивались через 24-bit
wrap → щелчки на громких сэмплах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 10:06:59 +03:00
co-authored by Claude Opus 4.7
parent 9bf21f6971
commit 617b1b840b
2 changed files with 106 additions and 37 deletions
+36 -6
View File
@@ -383,8 +383,10 @@ type
// Если MicSource=SoundCard, эти сэмплы игнорируются.
procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
// Прямой push double-сэмпла (-1..+1), для sound-card пути.
// Должен вызываться из ОДНОГО потока (PortAudio callback).
// Должен вызываться из ОДНОГО потока.
procedure PushTXMicSampleD(const S: Double);
// Batch-вариант: пушит N сэмплов одним вызовом, будит TX-поток один раз.
procedure PushTXMicSamplesD(const Src: array of Double; N: Integer);
// Настройки анализатора (применяются немедленно если открыт)
procedure SetFFTParams(FFTSize, WinType: Integer);
@@ -563,13 +565,18 @@ begin
if Terminated then Break;
// Если mic-данных не было — всё равно обрабатываем один блок тишины.
// Аналог piHPSDR: если mic_ring пуст, tx_add_mic_sample получает 0.0.
// Это держит DUC IQ поток и обновляет спектр/водопад TX-сигналом.
// Если 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 < FEngine.FAudioBufSize then
FEngine.ProcessTXBlock;
if (Avail = 0) or (FEngine.FTXMicSource <> txmsSoundCard) then
if Avail < FEngine.FAudioBufSize then
FEngine.ProcessTXBlock;
end;
end;
@@ -1624,6 +1631,29 @@ begin
RTLEventSetEvent(FTXMicSem);
end;
procedure TWDSPEngine.PushTXMicSamplesD(const Src: array of Double; N: Integer);
// Batch-вариант: одна будилка семафора на N сэмплов.
var
i, cnt, newHead: Integer;
begin
cnt := N;
if cnt > Length(Src) then cnt := Length(Src);
for i := 0 to cnt - 1 do
begin
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
if newHead <> FTXMicTail then
begin
FTXMicRing[FTXMicHead] := Src[i];
FTXMicHead := newHead;
end;
// overrun → дроп
end;
if cnt > 0 then
RTLEventSetEvent(FTXMicSem);
end;
// ---------------------------------------------------------------------------
// TX chain: применение всех TX-параметров к WDSP-каналу
// ---------------------------------------------------------------------------