diff --git a/PlutoBackend.pas b/PlutoBackend.pas index a9b6ce3..73ff256 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -67,10 +67,13 @@ type FTxChQ: Piio_channel; FTxThread: TThread; FTxStreaming: Boolean; - FTxRatio: Integer; // FSampleRate div 192000 (целочисленный апсемпл) + FTxRatio: Integer; // FSampleRate div 192000 (целочисленный апсемпл ×L) FTxBufPairs: Integer; // размер TX-буфера в IQ-парах (по sample-rate, см. TxBufPairsForRate) - FTxPrevI: LongInt; // последний 24-бит сэмпл (линейная интерполяция между вызовами) - FTxPrevQ: LongInt; + // Полифазный FIR-интерполятор: FTxCoef[p*TAPS + k] = h[p + k*L] (фаза p, отвод k). + // Линия задержки FTxDlI/Q хранит последние TAPS входных сэмплов между вызовами. + FTxCoef: array of Double; + FTxDlI: array of Double; + FTxDlQ: array of Double; // FIFO int16-пар @ device-rate (кольцо): продюсер — SendDUCIQ, консьюмер — push-поток. FTxFifo: array of SmallInt; // интерливнутые I,Q FTxFifoCap: Integer; // ёмкость в SmallInt-элементах (пар*2) @@ -83,6 +86,7 @@ type procedure ApplyRxLO(Hz: Double); procedure ApplyTxLO(Hz: Double); procedure ApplySampleRate(Hz: Integer); + procedure BuildTxResampler; // (пере)строить полифазные коэффициенты под FTxRatio procedure ApplyGain; procedure ApplyTxAtten; procedure StartRX; @@ -146,6 +150,10 @@ const // выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN. TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k) TX_BUF_MAX = 65536; // верхняя граница (защита от гигантских буферов) + // Полифазный анти-имидж интерполятор 192k→device-rate (вместо линейной интерполяции, + // которая оставляла образы на кратных 192k с подавлением только sinc²). + TX_FIR_TAPS = 16; // отводов на фазу (групп-задержка TAPS/2 входных сэмплов) + TX_FIR_CUT = 40000; // частота среза анти-имидж ФНЧ, Гц (покрывает SSB/NFM/AM/data) PLUTO_TX_OFF = -89.75; // макс. аттенюация AD9361 — TX практически выключен type @@ -341,7 +349,7 @@ begin FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil; FStreaming := False; FRxSeq := 0; FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil; - FTxStreaming := False; FTxRatio := 8; FTxPrevI := 0; FTxPrevQ := 0; + FTxStreaming := False; FTxRatio := 8; FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация) // FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный // размер выставляется в StartTX по фактическому device-rate (TxBufPairsForRate). @@ -597,6 +605,44 @@ begin if WasTxStreaming then StartTX; // пересоздаём TX с новым FTxRatio end; +procedure TPlutoBackend.BuildTxResampler; +// Полифазный FIR-интерполятор ×L (L=FTxRatio): прототип — оконный sinc-ФНЧ длиной +// TAPS*L с частотой среза TX_FIR_CUT, нормированный так, что DC-усиление любой фазы +// ≈ 1 (полная сумма = L). Раскладка в фазы: coef[p*TAPS+k] = proto[p + k*L]. +var + L, M, n, p, k: Integer; + fc, x, s, w, sum, scale: Double; + proto: array of Double; +begin + L := FTxRatio; + if L < 1 then L := 1; + M := TX_FIR_TAPS * L; + SetLength(proto, M); + fc := TX_FIR_CUT / FSampleRate; // нормированный срез (циклов/сэмпл на выходе) + if fc > 0.49 then fc := 0.49; + sum := 0; + for n := 0 to M - 1 do + begin + x := n - (M - 1) / 2.0; + if Abs(x) < 1e-9 then s := 2.0 * fc + else s := Sin(2.0 * Pi * fc * x) / (Pi * x); + // окно Блэкмана (~ −58 дБ боковые) + w := 0.42 - 0.5 * Cos(2.0 * Pi * n / (M - 1)) + 0.08 * Cos(4.0 * Pi * n / (M - 1)); + proto[n] := s * w; + sum := sum + proto[n]; + end; + if sum = 0 then sum := 1; + scale := L / sum; // DC-усиление на фазу ≈ 1 + SetLength(FTxCoef, M); + for p := 0 to L - 1 do + for k := 0 to TX_FIR_TAPS - 1 do + FTxCoef[p * TX_FIR_TAPS + k] := proto[p + k * L] * scale; + // линия задержки на TAPS входных сэмплов (сброс) + SetLength(FTxDlI, TX_FIR_TAPS); + SetLength(FTxDlQ, TX_FIR_TAPS); + for k := 0 to TX_FIR_TAPS - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end; +end; + procedure TPlutoBackend.ApplyGain; var Ch: Piio_channel; begin @@ -666,7 +712,7 @@ begin // Апсемпл-ратио под текущий device-rate (рейты-пресеты кратны 192k → целое). FTxRatio := FSampleRate div TX_SRC_RATE; if FTxRatio < 1 then FTxRatio := 1; - FTxPrevI := 0; FTxPrevQ := 0; + BuildTxResampler; // полифазные коэффициенты + сброс линии задержки под FTxRatio // Размер push-буфера под текущий rate + FIFO на ~4 таких буфера запаса. FTxBufPairs := TxBufPairsForRate(FSampleRate); EnterCriticalSection(FTxLock); @@ -776,9 +822,10 @@ end; procedure TPlutoBackend.SendDUCIQ(const IData, QData: array of Integer); // IData/QData — 24-bit пары @ TX_SRC_RATE (192k) от WDSP через контроллер. -// Линейно апсемплим ×FTxRatio, конвертим 24→int16 (>>8, full-scale MSB-aligned), -// Q инвертируем (та же конвенция, что и на RX) и кладём в FIFO. Underrun в push- -// потоке добивается нулями, overflow (продюсер опередил) — отбрасываем старое. +// Полифазный FIR-интерполятор ×FTxRatio (анти-имидж ФНЧ, см. BuildTxResampler): +// на каждый входной сэмпл выдаём L выходных, конвертим 24→int16 (>>8, full-scale +// MSB-aligned), Q инвертируем (та же конвенция, что и на RX) и кладём в FIFO. +// Underrun в push-потоке добивается нулями, overflow — отбрасываем старое. function Clamp16(V: Int64): SmallInt; inline; begin if V > 32767 then Result := 32767 @@ -786,23 +833,32 @@ procedure TPlutoBackend.SendDUCIQ(const IData, QData: array of Integer); else Result := SmallInt(V); end; var - i, j, t: Integer; - ci, cq, oi, oq: Int64; - n: Integer; + i, p, k, t, L, taps, base, n: Integer; + ai, aq: Double; begin - if (not FTxStreaming) or (FTxRatio < 1) then Exit; + if (not FTxStreaming) or (FTxRatio < 1) or (Length(FTxCoef) = 0) then Exit; n := Length(IData); if Length(QData) < n then n := Length(QData); + L := FTxRatio; + taps := TX_FIR_TAPS; EnterCriticalSection(FTxLock); try for i := 0 to n - 1 do begin - ci := IData[i]; cq := QData[i]; - for j := 1 to FTxRatio do + // сдвигаем линию задержки (FTxDl[0] = новейший входной сэмпл) + for k := taps - 1 downto 1 do + begin FTxDlI[k] := FTxDlI[k - 1]; FTxDlQ[k] := FTxDlQ[k - 1]; end; + FTxDlI[0] := IData[i]; FTxDlQ[0] := QData[i]; + // L выходных сэмплов — фазы p = 0..L-1, каждая = свёртка с подфильтром + for p := 0 to L - 1 do begin - // линейная интерполяция prev→cur по FTxRatio точкам (последняя = cur) - oi := FTxPrevI + (ci - FTxPrevI) * j div FTxRatio; - oq := FTxPrevQ + (cq - FTxPrevQ) * j div FTxRatio; + ai := 0; aq := 0; + base := p * taps; + for k := 0 to taps - 1 do + begin + ai := ai + FTxCoef[base + k] * FTxDlI[k]; + aq := aq + FTxCoef[base + k] * FTxDlQ[k]; + end; if FTxFifoCount + 2 > FTxFifoCap then begin // overflow: сбрасываем самые старые пары, держим хвост (минимум латентности) @@ -810,12 +866,11 @@ begin Dec(FTxFifoCount, 2); end; t := FTxFifoTail; - FTxFifo[t] := Clamp16(oi div 256); // 24→16 бит - FTxFifo[t + 1] := Clamp16(-(oq div 256)); // Q инверсия (как RX) + FTxFifo[t] := Clamp16(Round(ai / 256.0)); // 24→16 бит + FTxFifo[t + 1] := Clamp16(-Round(aq / 256.0)); // Q инверсия (как RX) FTxFifoTail := (t + 2) mod FTxFifoCap; Inc(FTxFifoCount, 2); end; - FTxPrevI := ci; FTxPrevQ := cq; end; finally LeaveCriticalSection(FTxLock); @@ -823,10 +878,11 @@ begin end; procedure TPlutoBackend.ClearDUCIQQueue; +var k: Integer; begin EnterCriticalSection(FTxLock); FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0; - FTxPrevI := 0; FTxPrevQ := 0; + for k := 0 to Length(FTxDlI) - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end; LeaveCriticalSection(FTxLock); end;