mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
+70
-31
@@ -182,6 +182,12 @@ type
|
||||
FRunning: Boolean;
|
||||
FTransmitting: Boolean;
|
||||
FSplitTxB: Boolean;
|
||||
// DUC IQ packetizer: TXA отдаёт блок FTXOutBufSize пар, который
|
||||
// не обязан быть кратен 240 (DUC packet). Хвост переносим в следующий
|
||||
// вызов OnTXIQReady, иначе зануление давало 47 Гц гул.
|
||||
FDUCPendingI: array[0..239] of Integer;
|
||||
FDUCPendingQ: array[0..239] of Integer;
|
||||
FDUCPendingCount: Integer;
|
||||
// DUP: при TX продолжаем показывать RX1-водопад/панораму, а TX-фильтр
|
||||
// и TX-линии рисуем поверх. Аппаратной командой не является.
|
||||
FDisplayDuplex: Boolean;
|
||||
@@ -1005,6 +1011,7 @@ begin
|
||||
FRunning := False;
|
||||
FTransmitting := False;
|
||||
FSplitTxB := False;
|
||||
FDUCPendingCount := 0;
|
||||
FDisplayDuplex := False;
|
||||
FTuning := False;
|
||||
FMuted := False;
|
||||
@@ -2829,13 +2836,18 @@ begin
|
||||
// FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
|
||||
// ExciterPwr это мощность TX, не уровень принятого сигнала.
|
||||
FwdW := ADCToWatts100(FwdPwr);
|
||||
IsTx := (Status.StatusBits and HPS_PTT) <> 0;
|
||||
// FTransmitting — наш программный флаг (MOX/TUN/HW-PTT через ApplyMOX).
|
||||
// HPS_PTT-бит в StatusBits — это вход аппаратного PTT (footswitch/mic),
|
||||
// а не «трансивер сейчас передаёт», поэтому только на него полагаться нельзя.
|
||||
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
|
||||
// SWR: правильная формула (1+ρ)/(1-ρ), считаем только когда есть значимая
|
||||
// прямая мощность — иначе ADC-шум RevPwr даёт случайные «прыжки» SWR.
|
||||
// Не на передаче — SWR не показываем (фиксируем 1.0), Fwd принудительно 0.
|
||||
if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then
|
||||
begin
|
||||
Rho := Sqrt(RevPwr / FwdPwr);
|
||||
// ρ = sqrt(Pr/Pf). Поскольку P=V²/R и V пропорционально ADC, то
|
||||
// sqrt(Pr/Pf) = adc_r/adc_f напрямую — лишнего sqrt не нужно.
|
||||
Rho := RevPwr / FwdPwr;
|
||||
if Rho >= 0.99 then
|
||||
SWRV := SWR_CAP
|
||||
else
|
||||
@@ -2860,18 +2872,31 @@ end;
|
||||
|
||||
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
|
||||
const
|
||||
// EMA-сглаживание (Thetis/piHPSDR-style): значения с ADC шумят, особенно
|
||||
// RevPwr на низкой мощности. Эти константы подобраны на rate ~150 пакетов/с.
|
||||
ALPHA_FWD = 0.30; // быстрый отклик для текущей мощности
|
||||
ALPHA_SWR = 0.20; // SWR более инерционный
|
||||
ALPHA_SUPPLY = 0.05; // напряжение питания меняется медленно
|
||||
// Thetis/piHPSDR PEP-style баллистика для FwdW и SWR: атака мгновенная
|
||||
// (peak-hold), спад медленный — стрелка «висит» на пике и плавно опускается.
|
||||
// Это маскирует огибающую SSB-голоса, которая на ADC прыгает на 10-20 дБ
|
||||
// внутри слога. На rate ~150 пакетов/с эти α дают τ_decay ≈ 350 мс / 500 мс.
|
||||
DECAY_FWD = 0.02;
|
||||
DECAY_SWR = 0.015;
|
||||
// Напряжение питания меняется медленно — обычное симметричное EMA.
|
||||
ALPHA_SUPPLY = 0.05;
|
||||
begin
|
||||
// Запоминаем значения; вывод в UI (метки, бары, статусбар) делает
|
||||
// MeterTimerTick на 10 Гц — иначе цифры дёргаются по частоте HP Status (~150/с).
|
||||
// EMA-сглаживание здесь же (на UI-потоке через Synchronize, безопасно для
|
||||
// чтения из MeterTimerTick).
|
||||
FLastFwdW := ALPHA_FWD * FwdW + (1.0 - ALPHA_FWD) * FLastFwdW;
|
||||
FLastSWR := ALPHA_SWR * SWRV + (1.0 - ALPHA_SWR) * FLastSWR;
|
||||
// Запоминаем значения; вывод в UI делает MeterTimerTick на 10 Гц.
|
||||
// Сама обработка здесь идёт на UI-потоке через Synchronize.
|
||||
// FwdW=0 / SWRV=1 — это «не на передаче»: снапим без decay,
|
||||
// чтобы метры моментально падали при отпускании PTT.
|
||||
if FwdW <= 0 then
|
||||
FLastFwdW := 0
|
||||
else if FwdW > FLastFwdW then
|
||||
FLastFwdW := FwdW // attack: instant
|
||||
else
|
||||
FLastFwdW := DECAY_FWD * FwdW + (1.0 - DECAY_FWD) * FLastFwdW;
|
||||
if SWRV <= 1.0 then
|
||||
FLastSWR := 1.0
|
||||
else if SWRV > FLastSWR then
|
||||
FLastSWR := SWRV // attack: instant
|
||||
else
|
||||
FLastSWR := DECAY_SWR * SWRV + (1.0 - DECAY_SWR) * FLastSWR;
|
||||
if SupplyV >= 0 then
|
||||
begin
|
||||
if FLastSupplyV < 0 then
|
||||
@@ -3042,18 +3067,22 @@ function TMainForm.PullSoundCardMic(MaxN: Integer): Integer;
|
||||
// Колбэк, который TTXDSPThread зовёт при MicSource=SoundCard.
|
||||
// Тянем только реально доступные сэмплы из FAudioIn — иначе мы пушили бы
|
||||
// нули и mic-цепь в WDSP получала бы тишину вместо реального сигнала.
|
||||
// Читаем пачкой в локальный буфер и пушим одним вызовом — экономим
|
||||
// будилки семафора и предотвращаем подмену TX-блока тишиной.
|
||||
var
|
||||
i, n: Integer;
|
||||
Buf: array[0..2047] of Double;
|
||||
begin
|
||||
Result := 0;
|
||||
if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit;
|
||||
n := FAudioIn.Available;
|
||||
if n > MaxN then n := MaxN;
|
||||
if n > Length(Buf) then n := Length(Buf);
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
FDSPEngine.PushTXMicSampleD(FAudioIn.ReadSample);
|
||||
Inc(Result);
|
||||
end;
|
||||
Buf[i] := FAudioIn.ReadSample;
|
||||
if n > 0 then
|
||||
FDSPEngine.PushTXMicSamplesD(Buf, n);
|
||||
Result := n;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
@@ -4417,6 +4446,7 @@ end;
|
||||
procedure TMainForm.ApplyMOX(Active: Boolean);
|
||||
begin
|
||||
FTransmitting := Active;
|
||||
FDUCPendingCount := 0;
|
||||
if FTransmitting then
|
||||
begin
|
||||
BtnMOX.ClrNorm := TColor($00000044);
|
||||
@@ -5175,26 +5205,35 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer);
|
||||
// FTXOutBufSize не обязан быть кратен 240, поэтому копим хвост между
|
||||
// вызовами и шлём только полные DUC-пакеты. Иначе зануление хвоста
|
||||
// последнего пакета давало периодический провал на каждом TX-блоке
|
||||
// (≈47 Гц гул при 192k IQ rate). Также клампим в 24-bit чтобы пики
|
||||
// от компрессора/EQ (значение чуть >1.0) не оборачивались через wrap.
|
||||
function ScaleIQ24(V: Double): Integer;
|
||||
var S: Double;
|
||||
begin
|
||||
S := V * 8388607.0;
|
||||
if S > 8388607.0 then Result := 8388607
|
||||
else if S < -8388608.0 then Result := -8388608
|
||||
else Result := Round(S);
|
||||
end;
|
||||
var
|
||||
IData: array[0..239] of Integer;
|
||||
QData: array[0..239] of Integer;
|
||||
Offset, n, i: Integer;
|
||||
i: Integer;
|
||||
begin
|
||||
if not FNetwork.Connected then Exit;
|
||||
Offset := 0;
|
||||
while Offset < Count do
|
||||
|
||||
for i := 0 to Count - 1 do
|
||||
begin
|
||||
FillChar(IData, SizeOf(IData), 0);
|
||||
FillChar(QData, SizeOf(QData), 0);
|
||||
n := Count - Offset;
|
||||
if n > 240 then n := 240;
|
||||
for i := 0 to n - 1 do
|
||||
FDUCPendingI[FDUCPendingCount] := ScaleIQ24(Buf[i * 2]);
|
||||
FDUCPendingQ[FDUCPendingCount] := ScaleIQ24(Buf[i * 2 + 1]);
|
||||
Inc(FDUCPendingCount);
|
||||
|
||||
if FDUCPendingCount >= 240 then
|
||||
begin
|
||||
IData[i] := Round(Buf[(Offset + i) * 2] * 8388607.0);
|
||||
QData[i] := Round(Buf[(Offset + i) * 2 + 1] * 8388607.0);
|
||||
FNetwork.SendDUCIQ(FDUCPendingI, FDUCPendingQ);
|
||||
FDUCPendingCount := 0;
|
||||
end;
|
||||
FNetwork.SendDUCIQ(IData, QData);
|
||||
Inc(Offset, 240);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user