diff --git a/MainForm.pas b/MainForm.pas index 6d675dc..f7e183c 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 1e4c4af..9edfe60 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -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-каналу // ---------------------------------------------------------------------------