Files
ewsdr/WDSPEngine.pas
T
ew8bakandClaude Opus 4.8 69717c2eda fix(tx): убраны инъекции данных в DUC IQ-поток — всплески на водопаде при TX
Два источника фабрикации данных в TX IQ-поток рисовали всплески на водопаде
при передаче (openHPSDR, особенно TUN/DUP):

1. HPSDRNetwork (sender): анти-старв zero-fill при пустой очереди впрыскивал
   нулевые пакеты = 1.25мс тишины прямо в поток = разрыв данных = всплеск.
   Теперь пустая очередь → просто ждём следующий блок (модель dl1ycf pihpsdr
   txiq_thread: данные не инжектируются никогда; короткий разрыв покрывает
   pre-roll в FPGA-FIFO радио). Убраны DUC_FIFO_LOW/REANCHOR/ZeroPkt.

2. WDSPEngine (продюсер TTXDSPThread): silence-инъекция при стойле mic
   впрыскивала лишний блок сверх mic-темпа (для TUN он нёс PostGen-тон →
   боковые/разрыв темпа). Продюсер теперь строго mic-гейтед.

Замерено на эфире: всплески при TUN 5-6/сессию → ~1. Остаточный редкий клик
по прямому замеру НЕ из TX-пейсинга (sender ровно на радиоклоке 800 пак/с
через supply-limiting, дропов/underrun нет) — источник отдельный (дисплей/RX),
разбор позже.

Windows-ветка sender'а не тронута. Pluto — свой TX-тракт (TPlutoTXThread +
libiio DMA), задета только общая silence-removal (push-поток сам добивает
нулями). Обе не перепроверены на железе.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:43:17 +03:00

2774 lines
104 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit WDSPEngine;
{
WDSP DSP Engine for OpenHPSDR Transceiver
Correct WDSP API usage based on actual WDSP.pas bindings:
Spectrum pipeline:
XCreateAnalyzer(disp, ...) — создать analyzer display
SetDisplay*(disp, ...) — настроить detector/average/rate
SetRXASpectrum(ch, 1, disp, 0, 0) — подключить RXA к display
fexchange0() в цикле → WDSP внутри пишет данные в display буфер
Spectrum0(1, disp, 0, 0, nil) — тригер snapshot
GetPixels(disp, 0, pix, flag) — забрать пиксели
OpenChannel сигнатура (13 параметров):
channel, in_size, dsp_size, in_rate, dsp_rate, out_rate,
atype, state, tdelayup, tslewup, tdelaydown, tslewdown, bfo
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math, SyncObjs,
BeaconDecoder,
WDSP;
const
// WDSP mode integers (нет именованных констант в WDSP.pas)
WDSP_LSB = 0;
WDSP_USB = 1;
WDSP_DSB = 2;
WDSP_CWL = 3;
WDSP_CWU = 4;
WDSP_FM = 5;
WDSP_AM = 6;
WDSP_SAM = 12;
// Наши индексы режимов (совпадают с кнопками MainForm)
MODE_LSB = 0;
MODE_USB = 1;
MODE_DSB = 2;
MODE_CWL = 3;
MODE_CWU = 4;
MODE_FM = 5;
MODE_AM = 6;
MODE_SAM = 7;
// AGC modes
AGC_OFF = 0;
AGC_LONG = 1;
AGC_SLOW = 2;
AGC_MEDIUM = 3;
AGC_FAST = 4;
// RX filter modes
NR_MODE_OFF = 0;
NR_MODE_1 = 1;
NR_MODE_2 = 2;
NR_MODE_3 = 3;
NR_MODE_4 = 4;
NB_MODE_OFF = 0;
NB_MODE_1 = 1;
NB_MODE_2 = 2;
FILTER_NR_AGC_POSITION = 1;
FILTER_NB_TAU = 0.00001;
FILTER_NB_ADVTIME = 0.00001;
FILTER_NB_HANGTIME = 0.00001;
FILTER_NB_THRESHOLD = 4.95;
FILTER_NB2_MODE = 0;
FILTER_NR2_GAIN_METHOD = 2;
FILTER_NR2_NPE_METHOD = 0;
FILTER_NR2_POST_RUN = 0;
FILTER_NR2_POST_TAPER = 12;
FILTER_NR2_POST_NLEVEL = 15.0;
FILTER_NR2_POST_FACTOR = 15.0;
FILTER_NR2_POST_RATE = 5.0;
FILTER_NR2_TRAINED_THRESHOLD = -0.5;
FILTER_NR2_TRAINED_T2 = 0.2;
FILTER_NR4_REDUCTION_AMOUNT = 10.0;
FILTER_NR4_SMOOTHING_FACTOR = 20.0;
FILTER_NR4_WHITENING_FACTOR = 0.0;
FILTER_NR4_NOISE_RESCALE = 2.0;
FILTER_NR4_POST_THRESHOLD = -3.0;
FILTER_NR4_NOISE_SCALING_TYPE = 0;
// S-meter types для GetRXAMeter
RXA_S_PK = 0; // peak, dBm
RXA_S_AV = 1; // average, dBm
RXA_CHAN = 0;
TXA_CHAN = 1; // используем разные каналы для RX и TX
// --- Мультислайсы (софт-fan-out) ---
MAX_SLICES = 6; // потолок одновременных софтовых слайсов
SLICE_CHAN_BASE = 2; // WDSP-каналы слайсов: 0=RXA, 1=TXA, слайсы с 2+
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
DSP_BUFSIZE = 1024;
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
DISPLAY_BLOCK_SIZE = 64;
// Очередь IQ пакетов между сетевым и DSP потоком
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
// TX mic ring buffer — должен быть степенью двойки
TX_MIC_RING = 8192;
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0),
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
TX_SAMPLE_RATE = 192000;
type
// Пакет в очереди между сетевым и DSP потоком
TIQQueueItem = record
Data: array[0..IQ_PKT_MAXBYTES - 1] of Byte;
DataLen: Integer; // реальная длина данных
IQPairs: Integer; // количество IQ пар
end;
TWDSPAGCMode = (agcOff = 0, agcLong = 1, agcSlow = 2,
agcMedium = 3, agcFast = 4);
TOnAudioReady = procedure(const Left, Right: array of Single;
Count: Integer) of object;
TOnSpectrumReady = procedure(const Pixels: array of Single;
Count: Integer) of object;
// TX IQ callback — вызывается из TTXDSPThread при готовности блока
// Buf: interleaved [I0,Q0,I1,Q1,...] doubles, Count — кол-во IQ пар
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
TOnWaterfallReady = procedure(const Pixels: array of Single;
Count: Integer) of object;
// Аудио готового слайса — вызывается из DSP-потока по каждому активному слайсу.
// SliceId — логический id (из RadioController), не WDSP-канал.
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
Count: Integer) of object;
// Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком,
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
TPullMicSamplesFunc = function(Max: Integer): Integer of object;
// TX mic source: откуда получаем сэмплы для модулятора.
TTXMicSource = (txmsRadio, txmsSoundCard, txmsWeb);
// Софтовый слайс: отдельный WDSP-канал, кормится тем же IQ-блоком, что и
// RXA_CHAN, но со своим NCO-сдвигом/режимом/фильтром/AGC/громкостью и своим
// аудио-callback'ом. Геометрия канала = как у RXA_CHAN.
TSliceChan = record
Active: Boolean; // слот занят
Id: Integer; // логический id слайса
Chan: Integer; // WDSP-канал (SLICE_CHAN_BASE + слот)
ShiftHz: Double; // NCO-сдвиг = TargetHz - CapturedCenterHz
Mode: Integer; // индекс режима (MODE_*)
FilterLo: Integer;
FilterHi: Integer;
AGCMode: TWDSPAGCMode;
Volume: Double; // 0..1
Muted: Boolean;
NRMode: Integer; // 0=off,1..4
NBMode: Integer; // 0=off,1=NB,2=NB2
SNBOn: Boolean;
ANFOn: Boolean;
SMeter: Double; // последнее показание S-метра канала (dBm), пишет DSP-поток
Opened: Boolean; // WDSP-канал реально открыт (после Open движка)
end;
{ TWDSPEngine }
TWDSPEngine = class
private
FInitialized: Boolean;
FAnalyzerOpen: Boolean;
FSampleRate: Integer;
FAudioRate: Integer;
FBufSize: Integer; // входной буфер @ FSampleRate
FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate)
FTXSampleRate: Integer; // фиксированный rate TX/DUC (TX_SAMPLE_RATE), не меняется при ChangeSampleRate
FTXOutBufSize: Integer; // выходной буфер TXA в IQ парах @ FTXSampleRate (= FAudioBufSize * FTXSampleRate / FAudioRate)
// Interleaved double I/Q буферы для fexchange0
FRXIn: array of Double;
FRXOut: array of Double;
FTXIn: array of Double;
FTXOut: array of Double;
// Накопитель входного буфера (DDC пакеты могут быть мельче FBufSize)
FRXAccI: array of Double;
FRXAccQ: array of Double;
FRXAccPos: Integer;
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSpecBuf: array of Double;
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
FDispBuf: array of Double;
FDispPos: Integer;
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
// по DISPLAY_BLOCK_SIZE из FTXOut в ProcessTXBlock.
FTXDispBuf: array of Double;
FTXDispPos: Integer;
// Аудио выходные буферы — аллоцируем один раз
FOutL: array of Single;
FOutR: array of Single;
// DSP поток + очередь пакетов
FDSPThread: TThread;
FDisplayThread:TThread;
FQueue: array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem;
FQueueHead: Integer; // пишет сетевой поток
FQueueTail: Integer; // читает DSP поток
FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent)
FDSPRunning: Boolean;
FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
// Настройки анализатора
FFFTSize: Integer;
FWindowType: Integer;
FDisplayFPS: Integer;
FEffectiveDisplayFPS: Double;
FDisplayPixelCount: Integer;
FSpecDetector: Integer;
FSpecAvgMode: Integer;
FSpecAvgTimeMS: Double;
FWfDetector: Integer;
FWfAvgMode: Integer;
FWfAvgTimeMS: Double;
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
// пересчитываются в ApplyRXAnalyzerSettings.
FZoomFactor: Double;
FPanSlider: Double;
FViewLowHz: Double;
FViewHighHz: Double;
FOnAudio: TOnAudioReady;
FOnSpectrum: TOnSpectrumReady;
FOnWaterfall: TOnWaterfallReady;
FOnTXIQ: TOnTXIQReady;
// --- Мультислайсы ---
FSlices: array[0..MAX_SLICES-1] of TSliceChan;
FSliceLock: TCriticalSection; // сериализует add/remove/set vs DSP-обработку
FSliceIn: array of Double; // interleaved вход fexchange0 (переиспользуем)
FSliceOut: array of Double; // interleaved выход fexchange0
FSliceOutL: array of Single; // деинтерливленный L для callback
FSliceOutR: array of Single; // R
FOnSliceAudio: TOnSliceAudio;
// TX mic ring buffer (lock-free: один writer — receive thread,
// один reader — TX DSP thread)
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
FTXMicHead: Integer; // пишет receive thread
FTXMicTail: Integer; // читает TX DSP thread
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
FTXThread: TThread;
FTXActive: Boolean;
// DUP-флаг от UI. True — RX-анализатор и RXA-канал продолжают работать
// во время TX (full-duplex). False — RX-IQ пакеты дропаются на входе DSP
// потока и не кормят RXA/анализатор. Это не трогает SetChannelState
// (мы выяснили на практике что стоп/старт RXA добавляет задержки на
// Windows без видимого выигрыша).
FKeepRXDuringTX: Boolean;
// Полный дуплекс QO-100: пропускать RX-аудио на колонки и во время TX (слышим
// свой downlink через транспондер). Имеет смысл только при FKeepRXDuringTX.
// По умолчанию False — обычное поведение (RX-аудио глушится на TX).
FMonitorRXDuringTX: Boolean;
// Дедлайн (GetTickCount64), до которого RX-аудио не пишется в FOnAudio.
// Используется на Windows для маскировки «хвоста» TX leakage (radio
// FPGA TX-buffer + PA slew-down → RX1 IQ → audio) после MOX-off.
// На Linux выставляется в 0 (UI не зовёт BeginPostTXMute) — поведение
// не меняется.
FPostTXMuteUntil: QWord;
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.
FTXFilterLow: Integer;
FTXFilterHigh: Integer;
FTXFilterNC: Integer;
FTXFilterMP: Integer;
FTXFilterWindow: Integer;
FTXMicGainDB: Double;
FTXCompressorOn: Boolean;
FTXCompressorGain:Double;
FTXLevelerOn: Boolean;
FTXLevelerTop: Double;
FTXLevelerDecay: Integer;
FTXALCOn: Boolean;
FTXALCMaxGain: Double;
FTXALCDecay: Integer;
FTXPhaseRotOn: Boolean;
FTXPhaseRotStages:Integer;
FTXPhaseRotFreq: Double;
FTXEQOn: Boolean;
FTXEQNumBands: Integer;
FTXEQGains: array[0..10] of Double;
FTXEQFreqs: array[0..10] of Double;
FTXAMCarrier: Double;
FTXFMDeviation: Double;
FTXFMLowCut: Integer;
FTXFMHighCut: Integer;
FTXFMEmphPos: Integer;
FTXCTCSSOn: Boolean;
FTXCTCSSFreq: Double;
// TX-display параметры (отдельные от RX) — применяются к TX_DISP_ID.
FTXFFTSize: Integer;
FTXWindowType: Integer;
FTXSpecDetector: Integer;
FTXSpecAvgMode: Integer;
FTXSpecAvgTimeMS: Double;
FTXWfDetector: Integer;
FTXWfAvgMode: Integer;
FTXWfAvgTimeMS: Double;
FMode: Integer;
FSMeter: Double;
FFilterLow: Integer;
FFilterHigh: Integer;
FAGCMode: TWDSPAGCMode;
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
FAGCHangThreshold: Integer; // порог hang 0..100
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
FAGCThresh: Double; // читается из WDSP (GetRXAAGCThresh)
FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс)
FDisplayResLimit: Integer; // потолок точек анализатора (настройка display_pixels)
FShiftHz: Double; // NCO shift для CTUN
FNRMode: Integer;
FNBMode: Integer;
FSNBEnabled: Boolean;
FANFEnabled: Boolean;
FMuted: Boolean;
FVolume: Double;
FLastError: string;
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
function ModeToWDSP(Mode: Integer): Integer;
procedure ApplyDefaultFilter;
// --- Мультислайсы ---
function FindSliceIdx(Id: Integer): Integer; // -1 если нет
procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки
procedure CloseSliceChannel(Idx: Integer); // CloseChannel (запись хранит)
procedure ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; FixedGain: Double);
procedure ProcessSlices; // fan-out в ProcessRXBlock
procedure AllocSliceBuffers; // (пере)аллокация буферов
procedure FeedDisplaySample(const I, Q: Double);
procedure ProcessRXBlock;
procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer;
procedure CloseAnalyzer;
procedure ApplyAnalyzerSettings;
function CalcDisplayFFTSize: Integer;
procedure CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
out EffectiveFPS: Double);
function NormalizeFFTSize(FFTSize: Integer): Integer;
procedure NormalizeDisplayParams(var Detector, AvgMode: Integer;
var AvgTimeMS: Double; ForWaterfall: Boolean);
procedure AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
out AvgCount: Integer; out Backmult: Double);
procedure ApplyNRState;
procedure ApplyNBState;
procedure ApplySNBState;
procedure ApplyANFState;
procedure ApplyNRToChan(Chan, NRMode: Integer);
procedure ApplyNBToChan(Chan, NBMode: Integer);
procedure ApplySNBToChan(Chan: Integer; On_: Boolean);
procedure ApplyANFToChan(Chan: Integer; On_: Boolean);
procedure ApplyNoiseFilterState;
procedure ProcessTXBlock; // вызывается из TTXDSPThread
procedure FeedTXDisplaySample(const I, Q: Double);
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
public
constructor Create(SampleRate: Integer = 192000;
AudioRate: Integer = 48000;
BufSize: Integer = DSP_BUFSIZE);
destructor Destroy; override;
// Открыть DSP каналы
function Open: Boolean;
procedure Close;
procedure ChangeSampleRate(NewRate: Integer);
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
procedure SetBeaconDecoder(D: TBeaconDecoder);
// --- Мультислайсы (софт-fan-out) ---
// Добавляет слайс с логическим Id (уникальным). ShiftHz = TargetHz - центр
// захвата. Возвращает False если нет свободных слотов или Id уже есть.
function AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean;
procedure RemoveSlice(Id: Integer);
procedure SetSliceShift(Id: Integer; ShiftHz: Double);
procedure SetSliceMode(Id, Mode: Integer);
procedure SetSliceFilter(Id, Low, High: Integer);
procedure SetSliceAGC(Id: Integer; Mode: TWDSPAGCMode);
procedure SetSliceVolume(Id: Integer; Vol: Double);
procedure SetSliceMute(Id: Integer; Mute: Boolean);
procedure SetSliceNRMode(Id, Mode: Integer);
procedure SetSliceNBMode(Id, Mode: Integer);
procedure SetSliceSNB(Id: Integer; On_: Boolean);
procedure SetSliceANF(Id: Integer; On_: Boolean);
function GetSliceSMeter(Id: Integer): Double;
function SliceCount: Integer;
function HasSlice(Id: Integer): Boolean;
// Подача 24-bit big-endian IQ из DDC пакета
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
procedure PushDDCPacket(const Buf: array of Byte;
DataOffset: Integer;
IQPairs: Integer);
procedure FlushRX;
// Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это
// на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio
// буфер удерживает «хвост» с TX leakage из радио, который AudioOut.Clear
// не достаёт. На Linux буфер маленький и проблемы нет, поэтому метод
// не вызывается.
procedure BeginPostTXMute(DurationMs: Integer);
// RX управление
procedure SetMode(Mode: Integer);
procedure SetFilter(Low, High: Integer);
procedure SetAGC(Mode: TWDSPAGCMode; FixedGain: Double = 0.0);
procedure SetAGCTop(TopDBm: Double);
procedure SetAGCSlope(Slope: Integer);
procedure SetAGCHangThreshold(Threshold: Integer);
procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
procedure SetDisplayResLimit(V: Integer); // потолок точек анализатора (1024/2048/4096)
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг
procedure SetNRMode(Mode: Integer);
procedure SetNR(Enable: Boolean);
procedure SetNBMode(Mode: Integer);
procedure SetNB(Enable: Boolean);
procedure SetSNB(Enable: Boolean);
procedure SetANF(Enable: Boolean);
procedure SetRXFMDeviation(Deviation: Double);
procedure SetFMSquelch(Enable: Boolean; Level: Integer);
procedure SetVolume(Vol: Double);
procedure SetMute(Mute: Boolean);
// TX управление
procedure SetTXMode(Mode: Integer);
procedure SetTXFilter(Low, High: Integer);
procedure SetDriveLevel(Level: Double);
procedure SetMicGain(GainDB: Double);
procedure SetTXRun(Run: Boolean);
// DUP: переключить ИСТОЧНИК пикселей display'а независимо от TX-аудио
// (SetTXRun меняет и аудио, и источник; этот метод позволяет вернуть
// RX-источник пока TX-аудио продолжает работать).
procedure SetDisplaySourceTX(UseTX: Boolean);
// TUN: WDSP TXA PostGen — гонит чистый тон в DSP-цепь после всей
// обработки. Mag — амплитуда в [0..1], Freq — Hz (audio).
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
// Расширенное TX управление (применяются мгновенно если канал открыт)
procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer);
procedure SetTXCompressor(Enable: Boolean; GainDB: Double);
procedure SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
procedure SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
procedure SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
procedure SetTXEQ(Enable: Boolean; NumBands: Integer;
const Gains, Freqs: array of Double);
procedure SetTXAMCarrierLevel(Level: Double);
procedure SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
procedure SetTXCTCSS(Enable: Boolean; FreqHz: Double);
// Источник микрофона: Radio (HW mic-stream) или SoundCard (PortAudio).
procedure SetTXMicSource(Source: TTXMicSource);
// TX display: отдельные FFT/window/detector/avg для TX-analyzer (TX_DISP_ID).
// Аналог RX-сеттеров, но применяются только к TX-anaylzer.
procedure SetTXFFTParams(FFTSize, WinType: Integer);
procedure SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
procedure SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
// TX mic вход — вызывается из receive thread (thread-safe lock-free).
// Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов.
// Если MicSource=SoundCard, эти сэмплы игнорируются.
procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
// Прямой push double-сэмпла (-1..+1), для sound-card пути.
// Должен вызываться из ОДНОГО потока.
procedure PushTXMicSampleD(const S: Double);
// Batch-вариант: пушит N сэмплов одним вызовом, будит TX-поток один раз.
procedure PushTXMicSamplesD(const Src: array of Double; N: Integer);
// Настройки анализатора (применяются немедленно если открыт)
procedure SetFFTParams(FFTSize, WinType: Integer);
procedure SetDisplayFPS(FPS: Integer);
procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
// Spectrum — вызывать из таймера (~20 fps)
procedure UpdateSpectrum;
procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);
// S-meter
function GetSMeterDBm: Double;
// Zoom/pan: задать коэффициент зума и положение пана и переармировать
// RX-анализатор. View* — границы видимого окна (Гц отн. DDC-центра).
procedure SetZoomPan(AZoom, APan: Double);
property ZoomFactor: Double read FZoomFactor;
property PanSlider: Double read FPanSlider;
property ViewLowHz: Double read FViewLowHz;
property ViewHighHz: Double read FViewHighHz;
property Initialized: Boolean read FInitialized;
property SampleRate: Integer read FSampleRate;
property TXSampleRate: Integer read FTXSampleRate;
property AudioBufSize: Integer read FAudioBufSize;
property AGCTop: Double read FAGCTop;
property AGCSlope: Integer read FAGCSlope;
property AGCHangThreshold: Integer read FAGCHangThreshold;
property AGCHangLevel: Double read FAGCHangLevel;
property AGCThresh: Double read FAGCThresh;
property Mode: Integer read FMode;
property FilterLow: Integer read FFilterLow;
property FilterHigh: Integer read FFilterHigh;
property NRMode: Integer read FNRMode;
property NBMode: Integer read FNBMode;
property SNBEnabled: Boolean read FSNBEnabled;
property LastError: string read FLastError;
property SMeter: Double read FSMeter;
property FFTSize: Integer read FFFTSize;
property WindowType: Integer read FWindowType;
property SpecDetector: Integer read FSpecDetector;
property SpecAvgMode: Integer read FSpecAvgMode;
property SpecAvgTimeMS: Double read FSpecAvgTimeMS;
property WfDetector: Integer read FWfDetector;
property WfAvgMode: Integer read FWfAvgMode;
property WfAvgTimeMS: Double read FWfAvgTimeMS;
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
property TXActive: Boolean read FTXActive;
// Включает full-duplex поведение RX-тракта: RXA не останавливается на TX,
// RX-анализатор продолжает получать IQ. Должен ставиться UI ДО SetTXRun.
property KeepRXDuringTX: Boolean read FKeepRXDuringTX
write FKeepRXDuringTX;
// Full-duplex self-monitor: при TX пропускать RX-аудио на колонки (QO-100 —
// слышим себя со спутника). Действует только вместе с KeepRXDuringTX.
property MonitorRXDuringTX: Boolean read FMonitorRXDuringTX
write FMonitorRXDuringTX;
// Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive).
// SpectrumView/Waterfall должны читать пиксели именно из этого ID.
property ActiveDisplayID: Integer read FActiveDisplayID;
property TXMicSource: TTXMicSource read FTXMicSource;
property OnPullMicSamples: TPullMicSamplesFunc
read FOnPullMic write FOnPullMic;
// Диагностика mic ring buffer: сколько сэмплов накоплено TX-цепью.
function TXMicRingAvail: Integer;
property TXFFTSize: Integer read FTXFFTSize;
property TXWindowType: Integer read FTXWindowType;
property TXSpecDetector: Integer read FTXSpecDetector;
property TXSpecAvgMode: Integer read FTXSpecAvgMode;
property TXSpecAvgTimeMS: Double read FTXSpecAvgTimeMS;
property TXWfDetector: Integer read FTXWfDetector;
property TXWfAvgMode: Integer read FTXWfAvgMode;
property TXWfAvgTimeMS: Double read FTXWfAvgTimeMS;
end;
implementation
// ===========================================================================
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
// Аналог tx_thread в piHPSDR/transmitter.c
// ===========================================================================
type
TDisplayThread = class(TThread)
private
FEngine: TWDSPEngine;
protected
procedure Execute; override;
public
constructor Create(AEngine: TWDSPEngine);
end;
TTXDSPThread = class(TThread)
private
FEngine: TWDSPEngine;
protected
procedure Execute; override;
public
constructor Create(AEngine: TWDSPEngine);
end;
function UsePanNormOneHz(Detector: Integer): Integer;
begin
// В Thetis нормализация к 1 Гц включается только для panadapter
// и только для detector modes 2/3/4.
if Detector in [2, 3, 4] then
Result := 1
else
Result := 0;
end;
constructor TDisplayThread.Create(AEngine: TWDSPEngine);
begin
FEngine := AEngine;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TDisplayThread.Execute;
var
NextTick: QWord;
NowTick: QWord;
DelayMS: Integer;
begin
NextTick := GetTickCount64;
while not Terminated do
begin
if FEngine.FInitialized and FEngine.FAnalyzerOpen then
FEngine.UpdateSpectrum;
Inc(NextTick, QWord(Max(1, Round(1000.0 / Max(1, FEngine.FDisplayFPS)))));
NowTick := GetTickCount64;
if NextTick > NowTick then
begin
DelayMS := Integer(NextTick - NowTick);
if DelayMS > 0 then
Sleep(DelayMS);
end
else
NextTick := NowTick;
end;
end;
constructor TTXDSPThread.Create(AEngine: TWDSPEngine);
begin
FEngine := AEngine;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TTXDSPThread.Execute;
// Аналог tx_thread в piHPSDR/transmitter.c:
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
var
Avail: Integer;
PeriodMs: Integer;
PeriodUs: Integer;
AccUs: Integer;
begin
// 512 samples / 48000 Hz = 10.666 ms. Do not floor this to 10 ms:
// that over-produces TX IQ by ~6.7%, eventually forcing DUC queue
// corrections/drops and producing periodic sidebands on a pure TUN tone.
PeriodUs := Max(1000, FEngine.FAudioBufSize * 1000000 div FEngine.FAudioRate);
AccUs := 0;
while not Terminated do
begin
Inc(AccUs, PeriodUs);
PeriodMs := Max(1, AccUs div 1000);
Dec(AccUs, PeriodMs * 1000);
// Ждём сигнала от mic-данных ИЛИ истечения периода —
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
if Terminated then Break;
if not FEngine.FTXActive then Continue;
// Если выбран sound-card mic — тянем сэмплы из внешнего источника
// (PortAudio через FOnPullMic в MainForm) ПЕРЕД проверкой ring buffer.
// Callback сам пушит сэмплы в FTXMicRing через PushTXMicSampleD.
if (FEngine.FTXMicSource = txmsSoundCard) and
Assigned(FEngine.FOnPullMic) then
FEngine.FOnPullMic(FEngine.FAudioBufSize * 2);
// Обрабатываем накопившиеся блоки с реальными mic-данными (строго
// mic-гейтед: сколько полных mic-блоков в ring — столько TX-блоков).
repeat
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1);
if (Avail >= FEngine.FAudioBufSize) and not Terminated then
FEngine.ProcessTXBlock
else
Break;
until False;
if Terminated then Break;
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
// над-производит блоки сверх mic-темпа. Продюсер теперь строго mic-гейтед:
// блок создаётся ТОЛЬКО когда в ring накопился полный mic-блок (радиокварц
// шлёт mic-пакеты непрерывно и на TX/TUN → очередь пополняется радиоклоком).
// Реальный обрыв mic (сеть) = короткий разрыв, покрываемый подушкой
// FPGA-FIFO радио, без фабрикации тишины в поток.
end;
end;
// ===========================================================================
// DSP поток — обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
// Аналог iq_thread в piHPSDR/new_protocol.c
// ===========================================================================
type
TDSPThread = class(TThread)
private
FEngine: TWDSPEngine;
protected
procedure Execute; override;
public
constructor Create(AEngine: TWDSPEngine);
end;
constructor TDSPThread.Create(AEngine: TWDSPEngine);
begin
FEngine := AEngine;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TDSPThread.Execute;
var
Item: ^TIQQueueItem;
begin
while not Terminated do
begin
// Ждём сигнала от сетевого потока (как sem_wait в piHPSDR)
RTLEventWaitFor(FEngine.FQueueSem, 100);
if Terminated then Break;
// Разбираем все накопившиеся пакеты
while FEngine.FQueueTail <> FEngine.FQueueHead do
begin
Item := @FEngine.FQueue[FEngine.FQueueTail];
FEngine.PushIQItemToDSP(Item^);
FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1);
end;
end;
end;
// ---------------------------------------------------------------------------
function TWDSPEngine.ModeToWDSP(Mode: Integer): Integer;
begin
case Mode of
MODE_LSB: Result := WDSP_LSB;
MODE_USB: Result := WDSP_USB;
MODE_DSB: Result := WDSP_DSB;
MODE_CWL: Result := WDSP_CWL;
MODE_CWU: Result := WDSP_CWU;
MODE_FM: Result := WDSP_FM;
MODE_AM: Result := WDSP_AM;
MODE_SAM: Result := WDSP_SAM;
else Result := WDSP_USB;
end;
end;
procedure TWDSPEngine.ApplyDefaultFilter;
begin
case FMode of
MODE_LSB: SetFilter(-2400, -100);
MODE_USB: SetFilter( 100, 2400);
MODE_DSB: SetFilter(-2400, 2400);
MODE_CWL: SetFilter( -800, -200);
MODE_CWU: SetFilter( 200, 800);
MODE_FM: SetFilter(-5000, 5000);
MODE_AM: SetFilter(-4000, 4000);
MODE_SAM: SetFilter(-4000, 4000);
end;
end;
// ---------------------------------------------------------------------------
// Constructor / Destructor
// ---------------------------------------------------------------------------
constructor TWDSPEngine.Create(SampleRate, AudioRate, BufSize: Integer);
begin
inherited Create;
FSampleRate := SampleRate;
FAudioRate := AudioRate;
// BufSize = dsp_size @ AudioRate (внутренний DSP буфер, piHPSDR buffer_size = 1024)
// FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
FTXSampleRate := TX_SAMPLE_RATE;
FTXOutBufSize := BufSize * FTXSampleRate div AudioRate; // = 4096 @ 48k audio, 192k tx
FInitialized := False;
FAnalyzerOpen := False;
FMode := MODE_USB;
FFilterLow := 100;
FFilterHigh := 2400;
FAGCMode := agcMedium;
FShiftHz := 0.0;
FAGCTop := -90.0;
FVolume := 0.7;
FSMeter := -130.0;
FNRMode := NR_MODE_OFF;
FNBMode := NB_MODE_OFF;
FSNBEnabled := False;
FANFEnabled := False;
FRXAccPos := 0;
// Analyzer defaults
FFFTSize := 131072;
FWindowType := 2; // Hann
FDisplayFPS := 60;
FEffectiveDisplayFPS := 60.0;
FDisplayPixelCount := SPECTRUM_PIXELS;
FDisplayResLimit := SPECTRUM_PIXELS;
FSpecDetector := 0; // Peak
FSpecAvgMode := 3; // Log Recursive
FSpecAvgTimeMS := 30.0;
FWfDetector := 0; // Peak
FWfAvgMode := 3; // Log Recursive
FWfAvgTimeMS := 120.0;
FZoomFactor := 0.0; // 0.0 = весь span, без зума
FPanSlider := 0.5; // окно по центру
FViewLowHz := -FSampleRate / 2.0;
FViewHighHz := FSampleRate / 2.0;
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
SetLength(FTXIn, FAudioBufSize * 2); // in_size пар @ FAudioRate
SetLength(FTXOut, FTXOutBufSize * 2); // out_size пар @ FTXSampleRate (фикс)
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
SetLength(FSpecBuf, FBufSize * 2);
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
SetLength(FTXDispBuf, DISPLAY_BLOCK_SIZE * 2);
FDispPos := 0;
FTXDispPos := 0;
SetLength(FOutL, FAudioBufSize);
SetLength(FOutR, FAudioBufSize);
// Очередь и DSP поток
FQueueHead := 0;
FQueueTail := 0;
FDSPRunning := False;
FQueueSem := RTLEventCreate;
FDSPThread := nil;
FDisplayThread := nil;
// Мультислайсы
FSliceLock := TCriticalSection.Create;
FillChar(FSlices, SizeOf(FSlices), 0);
FOnSliceAudio := nil;
AllocSliceBuffers;
// TX инициализация
FTXMicHead := 0;
FTXMicTail := 0;
FTXActive := False;
FTXToneOn := False;
FKeepRXDuringTX := False;
FMonitorRXDuringTX := False;
FPostTXMuteUntil := 0;
FActiveDisplayID := RX_DISP_ID;
FTXMicSource := txmsRadio;
FOnPullMic := nil;
FTXThread := nil;
FTXMicSem := RTLEventCreate;
FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
// TX-цепь — дефолты, согласуются с Settings.DefaultTX
FTXFilterLow := 100;
FTXFilterHigh := 2800;
FTXFilterNC := 1024;
FTXFilterMP := 1;
FTXFilterWindow := 1;
FTXMicGainDB := 10.0;
FTXCompressorOn := False;
FTXCompressorGain:= 5.0;
FTXLevelerOn := True;
FTXLevelerTop := 5.0;
FTXLevelerDecay := 500;
FTXALCOn := True;
FTXALCMaxGain := 0.0;
FTXALCDecay := 10;
FTXPhaseRotOn := False;
FTXPhaseRotStages:= 8;
FTXPhaseRotFreq := 338.0;
FTXEQOn := False;
FTXEQNumBands := 10;
FillChar(FTXEQGains, SizeOf(FTXEQGains), 0);
// Дефолтные центры полос EQ (Hz). Должны быть валидными ДАЖЕ при EQOn=False —
// SetTXAEQProfile вызывается в ApplyTXChainSettings всегда, и нулевые freqs
// могут сломать TX-фильтры внутри WDSP.
FTXEQFreqs[0] := 0;
FTXEQFreqs[1] := 100;
FTXEQFreqs[2] := 200;
FTXEQFreqs[3] := 400;
FTXEQFreqs[4] := 700;
FTXEQFreqs[5] := 1100;
FTXEQFreqs[6] := 1500;
FTXEQFreqs[7] := 2000;
FTXEQFreqs[8] := 2500;
FTXEQFreqs[9] := 3000;
FTXEQFreqs[10] := 3500;
FTXAMCarrier := 0.5;
FTXFMDeviation := 5000.0;
FTXFMLowCut := 300;
FTXFMHighCut := 3000;
FTXFMEmphPos := 1;
FTXCTCSSOn := False;
FTXCTCSSFreq := 100.0;
// TX-display defaults
FTXFFTSize := 16384;
FTXWindowType := 2;
FTXSpecDetector := 0;
FTXSpecAvgMode := 3;
FTXSpecAvgTimeMS := 30.0;
FTXWfDetector := 0;
FTXWfAvgMode := 3;
FTXWfAvgTimeMS := 120.0;
end;
destructor TWDSPEngine.Destroy;
begin
SetTXRun(False); // останавливаем TX поток если запущен
Close;
RTLEventDestroy(FQueueSem);
RTLEventDestroy(FTXMicSem);
FSliceLock.Free;
inherited;
end;
// ---------------------------------------------------------------------------
// Analyzer display
// ---------------------------------------------------------------------------
procedure TWDSPEngine.OpenAnalyzer;
var
Success: Integer;
begin
if FAnalyzerOpen then Exit;
Success := -1;
XCreateAnalyzer(RX_DISP_ID, @Success, 262144, 1, 1, nil);
if Success <> 0 then
begin
FLastError := Format('XCreateAnalyzer(RX) failed: %d', [Success]);
Exit;
end;
// Отдельный analyzer для TX-спектра/водопада. Ошибку логируем в LastError,
// но не считаем фатальной — RX без TX-display продолжает работать.
Success := -1;
XCreateAnalyzer(TX_DISP_ID, @Success, 262144, 1, 1, nil);
if Success <> 0 then
FLastError := Format('XCreateAnalyzer(TX) failed: %d', [Success]);
FFlp[0] := 0;
ApplyAnalyzerSettings;
FAnalyzerOpen := True;
if FDisplayThread = nil then
FDisplayThread := TDisplayThread.Create(Self);
end;
procedure TWDSPEngine.ApplyRXAnalyzerSettings;
// Применяет FFFTSize / FWindowType / FSpec* / FWf* к RX_DISP_ID.
var
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
Backmult, WfBackmult: Double;
Bins, SpanClipL, SpanClipH, ZWidth: Integer;
BinWidth, BW, ZoomSlider: Double;
const
KEEP_TIME_SEC = 0.10;
ZOOM_LIMIT = 100.0; // максимальная кратность зума
begin
AnalyzerFFT := CalcDisplayFFTSize;
FDispPos := 0;
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
DisplayPixels := EnsureRange(FLastSpectrumW, 64,
EnsureRange(FDisplayResLimit, 64, SPECTRUM_PIXELS));
FDisplayPixelCount := DisplayPixels;
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
// Zoom/pan: считаем span-clip для SetAnalyzer (fscLin/fscHin).
// clp=0 — оставляем полный span FFT, чтобы при зуме 0.0 видимая полоса была
// ровно == FSampleRate (остальной код это предполагает). Зум делает только
// span-clip: FZoomFactor=0.0 → ZWidth=Bins → SpanClipL=SpanClipH=0.
BinWidth := FSampleRate / AnalyzerFFT;
Bins := AnalyzerFFT;
BW := Bins * BinWidth;
ZoomSlider := Log10(9.0 * EnsureRange(FZoomFactor, 0.0, 1.0) + 1.0);
ZWidth := Round(Bins * (1.0 - (1.0 - 1.0 / ZOOM_LIMIT) * ZoomSlider));
ZWidth := EnsureRange(ZWidth, 1, Bins);
SpanClipL := Floor(EnsureRange(FPanSlider, 0.0, 1.0) * (Bins - ZWidth));
SpanClipH := Bins - ZWidth - SpanClipL;
// Видимое окно в Гц относительно DDC-центра (low отрицательный). Поправка
// BinWidth/2 — у комплексного FFT на одну отрицательную ячейку больше.
FViewLowHz := -(0.5 * BW - SpanClipL * BinWidth + BinWidth / 2.0);
FViewHighHz := (0.5 * BW - SpanClipH * BinWidth - BinWidth / 2.0);
SetAnalyzer(RX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
FWindowType, 14.0, Overlap, 0, SpanClipL, SpanClipH, DisplayPixels,
1, 0, 0.0, 0.0, MaxW);
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode (RX_DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage (RX_DISP_ID, 0, AvgCount);
SetDisplayAvBackmult (RX_DISP_ID, 0, Backmult);
SetDisplayNormOneHz (RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
SetDisplayDetectorMode(RX_DISP_ID, 1, FWfDetector);
SetDisplayAverageMode (RX_DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage (RX_DISP_ID, 1, WfAvgCount);
SetDisplayAvBackmult (RX_DISP_ID, 1, WfBackmult);
SetDisplayNormOneHz (RX_DISP_ID, 1, 0);
SetDisplaySampleRate (RX_DISP_ID, FSampleRate);
end;
procedure TWDSPEngine.ApplyTXAnalyzerSettings;
// Применяет FTXFFTSize / FTXWindowType / FTXSpec* / FTXWf* к TX_DISP_ID.
// FFT/Window/Det/Avg отдельные от RX — TX-спектр обычно нужен мельче по FFT
// (TX-полоса узкая) и с другим усреднением (быстрее реагировать на голос).
var
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
Backmult, WfBackmult: Double;
EffectiveFPS: Double;
const KEEP_TIME_SEC = 0.10;
begin
AnalyzerFFT := NormalizeFFTSize(FTXFFTSize);
FTXDispPos := 0;
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FTXSampleRate,
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffectiveFPS);
DisplayPixels := EnsureRange(FLastSpectrumW, 64,
EnsureRange(FDisplayResLimit, 64, SPECTRUM_PIXELS));
AverageTimeToParams(FTXSpecAvgMode, FTXSpecAvgTimeMS, AvgCount, Backmult);
AverageTimeToParams(FTXWfAvgMode, FTXWfAvgTimeMS, WfAvgCount, WfBackmult);
SetAnalyzer(TX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
FTXWindowType, 14.0, Overlap, 0, 0.0, 0.0, DisplayPixels,
1, 0, 0.0, 0.0, MaxW);
SetDisplayDetectorMode(TX_DISP_ID, 0, FTXSpecDetector);
SetDisplayAverageMode (TX_DISP_ID, 0, FTXSpecAvgMode);
SetDisplayNumAverage (TX_DISP_ID, 0, AvgCount);
SetDisplayAvBackmult (TX_DISP_ID, 0, Backmult);
SetDisplayNormOneHz (TX_DISP_ID, 0, UsePanNormOneHz(FTXSpecDetector));
SetDisplayDetectorMode(TX_DISP_ID, 1, FTXWfDetector);
SetDisplayAverageMode (TX_DISP_ID, 1, FTXWfAvgMode);
SetDisplayNumAverage (TX_DISP_ID, 1, WfAvgCount);
SetDisplayAvBackmult (TX_DISP_ID, 1, WfBackmult);
SetDisplayNormOneHz (TX_DISP_ID, 1, 0);
SetDisplaySampleRate (TX_DISP_ID, FTXSampleRate);
end;
procedure TWDSPEngine.ApplyAnalyzerSettings;
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у.
begin
ApplyRXAnalyzerSettings;
ApplyTXAnalyzerSettings;
end;
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
// Задаёт зум/пан и переармирует RX-анализатор (SetAnalyzer внутри WDSP
// защищён собственной критической секцией, дисплей-поток не мешает).
begin
FZoomFactor := EnsureRange(AZoom, 0.0, 1.0);
FPanSlider := EnsureRange(APan, 0.0, 1.0);
if FAnalyzerOpen then
ApplyRXAnalyzerSettings;
end;
procedure TWDSPEngine.CloseAnalyzer;
begin
if not FAnalyzerOpen then Exit;
if Assigned(FDisplayThread) then
begin
FDisplayThread.Terminate;
FDisplayThread.WaitFor;
FreeAndNil(FDisplayThread);
end;
DestroyAnalyzer(RX_DISP_ID);
DestroyAnalyzer(TX_DISP_ID);
FAnalyzerOpen := False;
end;
// ---------------------------------------------------------------------------
// Open / Close
// ---------------------------------------------------------------------------
function TWDSPEngine.Open: Boolean;
var
DllPath: string;
i: Integer;
begin
Result := False;
FLastError := '';
if FInitialized then Exit;
if not Assigned(@OpenChannel) or not Assigned(@XCreateAnalyzer) or
not Assigned(@SetAnalyzer) then
begin
FInitialized := False;
DllPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + WDSP_LIB;
if FileExists(DllPath) then
FLastError := Format('Symbols from %s are not resolved. Check architecture and library version (need WDSP with analyzer API).', [DllPath])
else
FLastError := Format('WDSP library not found: %s', [DllPath]);
Exit;
end;
try
// Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
OpenAnalyzer;
if not FAnalyzerOpen then
begin
FLastError := 'XCreateAnalyzer failed (WDSP init failed).';
Exit;
end;
OpenChannel(
RXA_CHAN,
FBufSize,
FAudioBufSize,
FSampleRate,
FAudioRate,
FAudioRate,
0,
0, // state=0: создать но не запускать
0.010, 0.010, 0.010, 0.010,
0
);
create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
SetRXABandpassWindow(RXA_CHAN, 1);
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
SetRXAShiftRun(RXA_CHAN, 1);
SetRXAShiftFreq(RXA_CHAN, 0.0);
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
SetAGC(FAGCMode, 50.0);
SetRXAPanelGain1(RXA_CHAN, FVolume);
SetRXAPanelSelect(RXA_CHAN, 3);
SetRXAPanelRun(RXA_CHAN, 1);
SetChannelState(RXA_CHAN, 1, 0);
// TXA: mic @ FAudioRate → IQ @ FTXSampleRate (фиксированный DUC rate, не зависит от RX).
// out_size = FAudioBufSize * FTXSampleRate/FAudioRate = FTXOutBufSize (4096 @ 192k tx).
// FTXOut уже имеет размер FTXOutBufSize*2.
OpenChannel(
TXA_CHAN,
FAudioBufSize,
FAudioBufSize,
FAudioRate,
FAudioRate,
FTXSampleRate,
1,
0,
0.010, 0.010, 0.010, 0.010,
0
);
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
SetTXAPanelRun(TXA_CHAN, 1);
// PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I.
SetTXAPanelSelect(TXA_CHAN, 2);
FInitialized := True;
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
ApplyTXChainSettings;
ApplyNoiseFilterState;
// Переоткрываем WDSP-каналы активных слайсов (например после ChangeSampleRate,
// где Close закрыл их, но записи FSlices сохранились).
FSliceLock.Enter;
try
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and not FSlices[i].Opened then
OpenSliceChannel(i);
finally
FSliceLock.Leave;
end;
Result := True;
// Запускаем DSP поток после успешной инициализации WDSP
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
FDSPThread := TDSPThread.Create(Self);
FDSPThread.Priority := tpHighest; // DSP критический поток
except
on E: Exception do
begin
FLastError := E.ClassName + ': ' + E.Message;
FInitialized := False;
Result := False;
end;
end;
end;
procedure TWDSPEngine.Close;
var
i: Integer;
begin
if not FInitialized then Exit;
// Останавливаем DSP поток перед закрытием WDSP каналов
if Assigned(FDSPThread) then
begin
FDSPThread.Terminate;
RTLEventSetEvent(FQueueSem); // разбудить чтобы увидел Terminated
FDSPThread.WaitFor;
FreeAndNil(FDSPThread);
end;
// TX-DSP поток живёт весь сеанс (не пересоздаётся на T/R) — гасим его здесь,
// до закрытия WDSP-каналов (он зовёт fexchange0 на TXA_CHAN).
FTXActive := False;
if Assigned(FTXThread) then
begin
FTXThread.Terminate;
RTLEventSetEvent(FTXMicSem);
FTXThread.WaitFor;
FreeAndNil(FTXThread);
end;
// Закрываем WDSP-каналы слайсов (записи FSlices сохраняем — переоткроются в Open).
FSliceLock.Enter;
try
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Opened then CloseSliceChannel(i);
finally
FSliceLock.Leave;
end;
CloseAnalyzer;
destroy_anbEXT(RXA_CHAN);
destroy_nobEXT(RXA_CHAN);
CloseChannel(RXA_CHAN);
CloseChannel(TXA_CHAN);
FInitialized := False;
end;
procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open.
begin
if NewRate = FSampleRate then Exit;
// Останавливаем текущий канал
Close;
// Сбрасываем очередь IQ пакетов — старые пакеты не совместимы с новым rate.
// Без этого DSP поток тратит секунды на разбор накопившихся пакетов.
FQueueHead := 0;
FQueueTail := 0;
FRXAccPos := 0;
// Обновляем параметры
FSampleRate := NewRate;
FBufSize := FAudioBufSize * NewRate div FAudioRate;
// Перераспределяем буферы под новый размер
SetLength(FRXIn, FBufSize * 2);
SetLength(FRXOut, FBufSize * 2);
SetLength(FSpecBuf, FBufSize * 2);
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
FDispPos := 0;
AllocSliceBuffers;
// Переинициализируем WDSP с новым rate
Open;
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
end;
procedure TWDSPEngine.SetBeaconDecoder(D: TBeaconDecoder);
begin
FBeaconDec := D;
if (D <> nil) and (FSampleRate > 0) then D.Configure(FSampleRate);
end;
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
DataOffset: Integer; IQPairs: Integer);
// Вызывается из СЕТЕВОГО потока — только кладём в очередь и возвращаемся немедленно
// DSP поток разбудится семафором и займётся обработкой
var
NextHead: Integer;
Item: ^TIQQueueItem;
DataBytes: Integer;
begin
NextHead := (FQueueHead + 1) and (IQ_QUEUE_SIZE - 1);
if NextHead = FQueueTail then Exit; // очередь полна — пропускаем пакет
Item := @FQueue[FQueueHead];
DataBytes := IQPairs * 6;
if DataBytes > IQ_PKT_MAXBYTES then DataBytes := IQ_PKT_MAXBYTES;
Move(Buf[DataOffset], Item^.Data[0], DataBytes);
Item^.DataLen := DataBytes;
Item^.IQPairs := IQPairs;
FQueueHead := NextHead;
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
end;
procedure TWDSPEngine.FlushRX;
begin
FQueueHead := FQueueTail;
FRXAccPos := 0;
FDispPos := 0;
if Length(FRXAccI) > 0 then FillChar(FRXAccI[0], Length(FRXAccI) * SizeOf(Double), 0);
if Length(FRXAccQ) > 0 then FillChar(FRXAccQ[0], Length(FRXAccQ) * SizeOf(Double), 0);
if Length(FRXIn) > 0 then FillChar(FRXIn[0], Length(FRXIn) * SizeOf(Double), 0);
if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0);
end;
procedure TWDSPEngine.BeginPostTXMute(DurationMs: Integer);
begin
if DurationMs <= 0 then
FPostTXMuteUntil := 0
else
FPostTXMuteUntil := GetTickCount64 + QWord(DurationMs);
end;
procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem);
// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize.
// В non-DUP TX (FTXActive and not FKeepRXDuringTX) полностью пропускаем
// IQ-пакет: не кормим RX-анализатор и не накапливаем для fexchange0.
// Иначе TX leakage из DDC оседает в RXA pipeline / RX_DISP_ID FFT-истории
// и звучит/виден на водопаде первые секунды после возврата на RX.
var
i, Pos: Integer;
IR, QR: LongInt;
const
SCALE = 1.0 / 8388608.0;
begin
if FTXActive and not FKeepRXDuringTX then Exit;
// Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта.
// Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не
// кормится), и аудио (RXA не запускается, FOnAudio не вызывается).
// Радио продолжает дотравливать FPGA TX-buffer + PA slew-down первые
// ~50200мс после MOX-off; этот хвост попадает в RX1 как leak. Дропаем
// его на входе — single-source-of-truth.
if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit;
for i := 0 to Item.IQPairs - 1 do
begin
Pos := i * 6;
if Pos + 5 >= Item.DataLen then Break;
IR := (LongInt(Item.Data[Pos]) shl 16) or
(LongInt(Item.Data[Pos+1]) shl 8) or
LongInt(Item.Data[Pos+2]);
QR := (LongInt(Item.Data[Pos+3]) shl 16) or
(LongInt(Item.Data[Pos+4]) shl 8) or
LongInt(Item.Data[Pos+5]);
if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);
FeedDisplaySample(IR * SCALE, QR * SCALE);
if FBeaconDec <> nil then FBeaconDec.Feed(IR * SCALE, QR * SCALE);
FRXAccI[FRXAccPos] := IR * SCALE;
FRXAccQ[FRXAccPos] := QR * SCALE;
Inc(FRXAccPos);
if FRXAccPos >= FBufSize then
ProcessRXBlock;
end;
end;
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
begin
if not FAnalyzerOpen then Exit;
FDispBuf[FDispPos * 2] := I;
FDispBuf[FDispPos * 2 + 1] := Q;
Inc(FDispPos);
if FDispPos >= DISPLAY_BLOCK_SIZE then
begin
Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]);
FDispPos := 0;
end;
end;
procedure TWDSPEngine.FeedTXDisplaySample(const I, Q: Double);
begin
if not FAnalyzerOpen then Exit;
FTXDispBuf[FTXDispPos * 2] := I;
FTXDispBuf[FTXDispPos * 2 + 1] := Q;
Inc(FTXDispPos);
if FTXDispPos >= DISPLAY_BLOCK_SIZE then
begin
Spectrum0(1, TX_DISP_ID, 0, 0, @FTXDispBuf[0]);
FTXDispPos := 0;
end;
end;
procedure TWDSPEngine.ProcessRXBlock;
var
i: Integer;
Err: Integer;
// OutL/OutR теперь поля класса — не аллоцируем каждый раз
begin
FRXAccPos := 0;
if not FInitialized then Exit;
try
// Упаковываем в interleaved буфер для fexchange0
for i := 0 to FBufSize - 1 do
begin
FRXIn[i * 2] := FRXAccI[i];
FRXIn[i * 2 + 1] := FRXAccQ[i];
end;
// DSP обработка
Err := 0;
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
// через unsigned-сравнение, GetTickCount64 не зовётся когда дедлайн = 0.
if (FPostTXMuteUntil <> 0) and (GetTickCount64 >= FPostTXMuteUntil) then
FPostTXMuteUntil := 0;
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
if Assigned(FOnAudio) and not FMuted
and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
and (FPostTXMuteUntil = 0) then
begin
for i := 0 to FAudioBufSize - 1 do
begin
FOutL[i] := FRXOut[i * 2] * FVolume;
FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
end;
FOnAudio(FOutL, FOutR, FAudioBufSize);
end;
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
ProcessSlices;
except
// Защита от падения libwdsp в потоке — просто пропускаем блок
end;
end;
// ---------------------------------------------------------------------------
// Мультислайсы (софт-fan-out)
// ---------------------------------------------------------------------------
procedure TWDSPEngine.AllocSliceBuffers;
begin
SetLength(FSliceIn, FBufSize * 2);
SetLength(FSliceOut, FBufSize * 2); // с запасом (>= FAudioBufSize*2)
SetLength(FSliceOutL, FAudioBufSize);
SetLength(FSliceOutR, FAudioBufSize);
end;
function TWDSPEngine.FindSliceIdx(Id: Integer): Integer;
var i: Integer;
begin
Result := -1;
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
end;
// Применяет AGC к произвольному каналу — вынесено из SetAGC чтобы делить с слайсами.
procedure TWDSPEngine.ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode;
FixedGain: Double);
begin
SetRXAAGCMode(Chan, Ord(Mode));
SetRXAAGCSlope(Chan, FAGCSlope);
SetRXAAGCTop(Chan, FAGCTop);
case Mode of
agcOff:
SetRXAAGCFixed(Chan, FixedGain);
agcLong: begin
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 2000);
SetRXAAGCDecay(Chan, 2000); SetRXAAGCHangThreshold(Chan, FAGCHangThreshold);
end;
agcSlow: begin
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 1000);
SetRXAAGCDecay(Chan, 500); SetRXAAGCHangThreshold(Chan, FAGCHangThreshold);
end;
agcMedium: begin
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 0);
SetRXAAGCDecay(Chan, 250); SetRXAAGCHangThreshold(Chan, 100);
end;
agcFast: begin
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 0);
SetRXAAGCDecay(Chan, 50); SetRXAAGCHangThreshold(Chan, 100);
end;
end;
end;
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
// под FSliceLock и только при FInitialized.
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
var Chan: Integer;
begin
if not FInitialized then Exit;
if FSlices[Idx].Opened then Exit;
Chan := FSlices[Idx].Chan;
OpenChannel(Chan, FBufSize, FAudioBufSize, FSampleRate, FAudioRate, FAudioRate,
0, 0, 0.010, 0.010, 0.010, 0.010, 0);
// NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open.
create_anbEXT(Chan, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
create_nobEXT(Chan, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
SetRXABandpassWindow(Chan, 1);
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
SetRXAShiftRun(Chan, 1);
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz);
RXASetPassband(Chan, FSlices[Idx].FilterLo, FSlices[Idx].FilterHi);
ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0);
// NR/NB/SNB/ANF из сохранённого состояния слайса.
ApplyNRToChan(Chan, FSlices[Idx].NRMode);
ApplyNBToChan(Chan, FSlices[Idx].NBMode);
ApplySNBToChan(Chan, FSlices[Idx].SNBOn);
ApplyANFToChan(Chan, FSlices[Idx].ANFOn);
SetRXAPanelGain1(Chan, FSlices[Idx].Volume);
SetRXAPanelSelect(Chan, 3);
SetRXAPanelRun(Chan, 1);
SetChannelState(Chan, 1, 0);
FSlices[Idx].Opened := True;
end;
procedure TWDSPEngine.CloseSliceChannel(Idx: Integer);
begin
if not FSlices[Idx].Opened then Exit;
SetChannelState(FSlices[Idx].Chan, 0, 1);
destroy_anbEXT(FSlices[Idx].Chan);
destroy_nobEXT(FSlices[Idx].Chan);
CloseChannel(FSlices[Idx].Chan);
FSlices[Idx].Opened := False;
end;
// Fan-out: прогоняет тот же входной IQ-блок через каждый активный слайс-канал.
// Вызывается из ProcessRXBlock (DSP-поток) после основного канала.
procedure TWDSPEngine.ProcessSlices;
var
s, i, k, Err: Integer;
V: Double;
begin
if not Assigned(FOnSliceAudio) then Exit;
FSliceLock.Enter;
try
for s := 0 to MAX_SLICES - 1 do
begin
if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
if FSlices[s].Muted then Continue;
// Свежая копия входного IQ — fexchange0 портит вход in-place.
for i := 0 to FBufSize - 1 do
begin
FSliceIn[i * 2] := FRXAccI[i];
FSliceIn[i * 2 + 1] := FRXAccQ[i];
end;
Err := 0;
fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err);
// S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор.
FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV);
V := FSlices[s].Volume;
for k := 0 to FAudioBufSize - 1 do
begin
FSliceOutL[k] := FSliceOut[k * 2] * V;
FSliceOutR[k] := FSliceOut[k * 2 + 1] * V;
end;
FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize);
end;
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean;
var i, slot: Integer;
begin
Result := False;
FSliceLock.Enter;
try
if FindSliceIdx(Id) >= 0 then Exit; // уже существует
slot := -1;
for i := 0 to MAX_SLICES - 1 do
if not FSlices[i].Active then begin slot := i; Break; end;
if slot < 0 then Exit; // нет свободных слотов
FSlices[slot].Active := True;
FSlices[slot].Id := Id;
FSlices[slot].Chan := SLICE_CHAN_BASE + slot;
FSlices[slot].Mode := Mode;
FSlices[slot].FilterLo := FilterLo;
FSlices[slot].FilterHi := FilterHi;
FSlices[slot].ShiftHz := ShiftHz;
FSlices[slot].Volume := Volume;
FSlices[slot].AGCMode := AGC;
FSlices[slot].Muted := False;
FSlices[slot].NRMode := NR_MODE_OFF;
FSlices[slot].NBMode := NB_MODE_OFF;
FSlices[slot].SNBOn := False;
FSlices[slot].ANFOn := False;
FSlices[slot].SMeter := -130.0;
FSlices[slot].Opened := False;
if FInitialized then OpenSliceChannel(slot);
Result := True;
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.RemoveSlice(Id: Integer);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id);
if idx < 0 then Exit;
CloseSliceChannel(idx);
FSlices[idx].Active := False;
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceShift(Id: Integer; ShiftHz: Double);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].ShiftHz := ShiftHz;
if FSlices[idx].Opened then SetRXAShiftFreq(FSlices[idx].Chan, ShiftHz);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceMode(Id, Mode: Integer);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].Mode := Mode;
if FSlices[idx].Opened then SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode));
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceFilter(Id, Low, High: Integer);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].FilterLo := Low;
FSlices[idx].FilterHi := High;
if FSlices[idx].Opened then RXASetPassband(FSlices[idx].Chan, Low, High);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceAGC(Id: Integer; Mode: TWDSPAGCMode);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].AGCMode := Mode;
if FSlices[idx].Opened then ApplyAGCToChan(FSlices[idx].Chan, Mode, 50.0);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceVolume(Id: Integer; Vol: Double);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].Volume := Vol;
if FSlices[idx].Opened then SetRXAPanelGain1(FSlices[idx].Chan, Vol);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceMute(Id: Integer; Mute: Boolean);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].Muted := Mute;
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceNRMode(Id, Mode: Integer);
var idx: Integer;
begin
if Mode < NR_MODE_OFF then Mode := NR_MODE_OFF;
if Mode > NR_MODE_4 then Mode := NR_MODE_4;
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].NRMode := Mode;
if FSlices[idx].Opened then ApplyNRToChan(FSlices[idx].Chan, Mode);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceNBMode(Id, Mode: Integer);
var idx: Integer;
begin
if Mode < NB_MODE_OFF then Mode := NB_MODE_OFF;
if Mode > NB_MODE_2 then Mode := NB_MODE_2;
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].NBMode := Mode;
if FSlices[idx].Opened then ApplyNBToChan(FSlices[idx].Chan, Mode);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceSNB(Id: Integer; On_: Boolean);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].SNBOn := On_;
if FSlices[idx].Opened then ApplySNBToChan(FSlices[idx].Chan, On_);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceANF(Id: Integer; On_: Boolean);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].ANFOn := On_;
if FSlices[idx].Opened then ApplyANFToChan(FSlices[idx].Chan, On_);
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.GetSliceSMeter(Id: Integer): Double;
var idx: Integer;
begin
Result := -130.0;
FSliceLock.Enter;
try
idx := FindSliceIdx(Id);
if idx >= 0 then Result := FSlices[idx].SMeter;
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.SliceCount: Integer;
var i: Integer;
begin
Result := 0;
FSliceLock.Enter;
try
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active then Inc(Result);
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.HasSlice(Id: Integer): Boolean;
begin
FSliceLock.Enter;
try
Result := FindSliceIdx(Id) >= 0;
finally
FSliceLock.Leave;
end;
end;
// ---------------------------------------------------------------------------
// RX управление
// ---------------------------------------------------------------------------
procedure TWDSPEngine.SetMode(Mode: Integer);
begin
FMode := Mode;
if not FInitialized then Exit;
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
end;
procedure TWDSPEngine.SetFilter(Low, High: Integer);
begin
FFilterLow := Low;
FFilterHigh := High;
if not FInitialized then Exit;
// RXASetPassband — правильный unified API, пересчитывает фильтр целиком
RXASetPassband(RXA_CHAN, Low, High);
end;
procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double);
begin
FAGCMode := Mode;
if not FInitialized then Exit;
// Точно по piHPSDR receiver.c: set_agc()
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
// Читаем обратно hang level и thresh для линии на спектре
UpdateAGCLines(FLastSpectrumW);
end;
procedure TWDSPEngine.SetShift(ShiftHz: Double);
begin
FShiftHz := ShiftHz;
if not FInitialized then Exit;
SetRXAShiftFreq(RXA_CHAN, ShiftHz);
end;
procedure TWDSPEngine.SetAGCTop(TopDBm: Double);
begin
FAGCTop := TopDBm;
if not FInitialized then Exit;
SetRXAAGCTop(RXA_CHAN, TopDBm);
UpdateAGCLines(FLastSpectrumW);
end;
procedure TWDSPEngine.SetAGCSlope(Slope: Integer);
begin
FAGCSlope := Slope;
if not FInitialized then Exit;
SetRXAAGCSlope(RXA_CHAN, Slope);
end;
procedure TWDSPEngine.SetAGCHangThreshold(Threshold: Integer);
begin
FAGCHangThreshold := Threshold;
if not FInitialized then Exit;
SetRXAAGCHangThreshold(RXA_CHAN, Threshold);
UpdateAGCLines(FLastSpectrumW);
end;
procedure TWDSPEngine.SetSpectrumWidth(W: Integer);
begin
if W <= 0 then Exit;
if FLastSpectrumW = W then Exit;
FLastSpectrumW := W;
if FAnalyzerOpen then
ApplyAnalyzerSettings;
end;
procedure TWDSPEngine.SetDisplayResLimit(V: Integer);
begin
V := EnsureRange(V, 64, SPECTRUM_PIXELS);
if FDisplayResLimit = V then Exit;
FDisplayResLimit := V;
if FAnalyzerOpen then
ApplyAnalyzerSettings;
end;
procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer);
begin
if not FInitialized then Exit;
if SpectrumW <= 0 then Exit; // окно ещё не показано — не читаем
GetRXAAGCHangLevel(RXA_CHAN, @FAGCHangLevel);
GetRXAAGCThresh(RXA_CHAN, @FAGCThresh,
SpectrumW, // реальная ширина дисплея в пикселях
FSampleRate);
end;
// Параметризованные по каналу версии — делятся между главным каналом и слайсами.
procedure TWDSPEngine.ApplyNRToChan(Chan, NRMode: Integer);
begin
SetRXAANRVals(Chan, 64, 16, 16e-4, 10e-7);
SetRXAANRPosition(Chan, FILTER_NR_AGC_POSITION);
SetRXAANRRun(Chan, Ord(NRMode = NR_MODE_1));
SetRXAEMNRPosition(Chan, FILTER_NR_AGC_POSITION);
SetRXAEMNRgainMethod(Chan, FILTER_NR2_GAIN_METHOD);
SetRXAEMNRnpeMethod(Chan, FILTER_NR2_NPE_METHOD);
SetRXAEMNRtrainZetaThresh(Chan, FILTER_NR2_TRAINED_THRESHOLD);
SetRXAEMNRtrainT2(Chan, FILTER_NR2_TRAINED_T2);
SetRXAEMNRpost2Taper(Chan, FILTER_NR2_POST_TAPER);
SetRXAEMNRpost2Nlevel(Chan, FILTER_NR2_POST_NLEVEL);
SetRXAEMNRpost2Factor(Chan, FILTER_NR2_POST_FACTOR);
SetRXAEMNRpost2Rate(Chan, FILTER_NR2_POST_RATE);
SetRXAEMNRaeRun(Chan, 1);
SetRXAEMNRpost2Run(Chan, FILTER_NR2_POST_RUN);
SetRXAEMNRRun(Chan, Ord(NRMode = NR_MODE_2));
SetRXARNNRPosition(Chan, FILTER_NR_AGC_POSITION);
SetRXARNNRRun(Chan, Ord(NRMode = NR_MODE_3));
SetRXASBNRreductionAmount(Chan, FILTER_NR4_REDUCTION_AMOUNT);
SetRXASBNRsmoothingFactor(Chan, FILTER_NR4_SMOOTHING_FACTOR);
SetRXASBNRwhiteningFactor(Chan, FILTER_NR4_WHITENING_FACTOR);
SetRXASBNRnoiseRescale(Chan, FILTER_NR4_NOISE_RESCALE);
SetRXASBNRpostFilterThreshold(Chan, FILTER_NR4_POST_THRESHOLD);
SetRXASBNRnoiseScalingType(Chan, FILTER_NR4_NOISE_SCALING_TYPE);
SetRXASBNRPosition(Chan, FILTER_NR_AGC_POSITION);
SetRXASBNRRun(Chan, Ord(NRMode = NR_MODE_4));
end;
procedure TWDSPEngine.ApplyNBToChan(Chan, NBMode: Integer);
begin
SetEXTANBTau(Chan, FILTER_NB_TAU);
SetEXTANBHangtime(Chan, FILTER_NB_HANGTIME);
SetEXTANBAdvtime(Chan, FILTER_NB_ADVTIME);
SetEXTANBThreshold(Chan, FILTER_NB_THRESHOLD);
SetEXTANBRun(Chan, Ord(NBMode = NB_MODE_1));
SetEXTNOBMode(Chan, FILTER_NB2_MODE);
SetEXTNOBTau(Chan, FILTER_NB_TAU);
SetEXTNOBHangtime(Chan, FILTER_NB_HANGTIME);
SetEXTNOBAdvtime(Chan, FILTER_NB_ADVTIME);
SetEXTNOBThreshold(Chan, FILTER_NB_THRESHOLD);
SetEXTNOBRun(Chan, Ord(NBMode = NB_MODE_2));
end;
procedure TWDSPEngine.ApplySNBToChan(Chan: Integer; On_: Boolean);
begin
SetRXASNBARun(Chan, Ord(On_));
end;
procedure TWDSPEngine.ApplyANFToChan(Chan: Integer; On_: Boolean);
begin
SetRXAANFPosition(Chan, FILTER_NR_AGC_POSITION);
SetRXAANFRun(Chan, Ord(On_));
end;
procedure TWDSPEngine.ApplyNRState;
begin
if not FInitialized then Exit;
ApplyNRToChan(RXA_CHAN, FNRMode);
end;
procedure TWDSPEngine.ApplyNBState;
begin
if not FInitialized then Exit;
ApplyNBToChan(RXA_CHAN, FNBMode);
end;
procedure TWDSPEngine.ApplySNBState;
begin
if not FInitialized then Exit;
ApplySNBToChan(RXA_CHAN, FSNBEnabled);
end;
procedure TWDSPEngine.ApplyANFState;
begin
if not FInitialized then Exit;
ApplyANFToChan(RXA_CHAN, FANFEnabled);
end;
procedure TWDSPEngine.ApplyNoiseFilterState;
begin
ApplyNRState;
ApplyNBState;
ApplySNBState;
ApplyANFState;
end;
procedure TWDSPEngine.SetNRMode(Mode: Integer);
begin
if Mode < NR_MODE_OFF then Mode := NR_MODE_OFF;
if Mode > NR_MODE_4 then Mode := NR_MODE_4;
FNRMode := Mode;
ApplyNRState;
end;
procedure TWDSPEngine.SetNR(Enable: Boolean);
begin
if Enable then SetNRMode(NR_MODE_1)
else SetNRMode(NR_MODE_OFF);
end;
procedure TWDSPEngine.SetNBMode(Mode: Integer);
begin
if Mode < NB_MODE_OFF then Mode := NB_MODE_OFF;
if Mode > NB_MODE_2 then Mode := NB_MODE_2;
FNBMode := Mode;
ApplyNBState;
end;
procedure TWDSPEngine.SetNB(Enable: Boolean);
begin
if Enable then SetNBMode(NB_MODE_1)
else SetNBMode(NB_MODE_OFF);
end;
procedure TWDSPEngine.SetSNB(Enable: Boolean);
begin
FSNBEnabled := Enable;
ApplySNBState;
end;
procedure TWDSPEngine.SetANF(Enable: Boolean);
begin
FANFEnabled := Enable;
ApplyANFState;
end;
procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double);
begin
if not FInitialized then Exit;
SetRXAFMDeviation(RXA_CHAN, Deviation);
end;
procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer);
var Threshold: Double;
begin
if not FInitialized then Exit;
if Enable then
begin
Threshold := Level / 200.0; // 0..100 → 0.0..0.5
SetRXAFMSQThreshold(RXA_CHAN, Threshold);
SetRXAFMSQRun(RXA_CHAN, 1);
end
else
SetRXAFMSQRun(RXA_CHAN, 0);
end;
procedure TWDSPEngine.SetVolume(Vol: Double);
begin
FVolume := Max(0.0, Min(1.0, Vol));
if not FInitialized then Exit;
SetRXAPanelGain1(RXA_CHAN, FVolume);
end;
procedure TWDSPEngine.SetMute(Mute: Boolean);
begin
FMuted := Mute;
if not FInitialized then Exit;
SetRXAPanelRun(RXA_CHAN, Ord(not Mute));
end;
// ---------------------------------------------------------------------------
// TX управление
// ---------------------------------------------------------------------------
procedure TWDSPEngine.SetTXMode(Mode: Integer);
begin
if not FInitialized then Exit;
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
end;
procedure TWDSPEngine.SetTXFilter(Low, High: Integer);
begin
if not FInitialized then Exit;
SetTXABandpassFreqs(TXA_CHAN, Low, High);
end;
procedure TWDSPEngine.SetDriveLevel(Level: Double);
begin
if not FInitialized then Exit;
SetTXAALCMaxGain(TXA_CHAN, Max(0.0, Min(1.0, Level)));
end;
procedure TWDSPEngine.SetMicGain(GainDB: Double);
begin
FTXMicGainDB := GainDB;
if not FInitialized then Exit;
SetTXAPanelGain1(TXA_CHAN, Power(10.0, GainDB / 20.0));
end;
procedure TWDSPEngine.SetTXRun(Run: Boolean);
var
k, Err: Integer;
begin
FDispPos := 0;
FTXDispPos := 0;
if Run then
begin
FActiveDisplayID := TX_DISP_ID;
// Сброс mic ring ДО активации, чтобы простаивавший поток не прочитал старое.
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).
if not Assigned(FTXThread) then
begin
FTXThread := TTXDSPThread.Create(Self);
FTXThread.Priority := tpHighest;
end;
FTXActive := True; // активируем последним
end else begin
FTXActive := False; // поток уходит в простой (не трогает канал/ring)
FActiveDisplayID := RX_DISP_ID;
if FInitialized then
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;
end;
procedure TWDSPEngine.SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
var
SignedFreq: Double;
begin
if not FInitialized then Exit;
FTXToneOn := Enabled; // мьют mic-входа на TUN (см. ProcessTXBlock)
if Enabled then
begin
// gen1 (PostGen) стоит после всех модуляторов и бандпасса.
// Положительная частота даёт тон ВЫШЕ несущей (USB-сторона).
// Для LSB и CWL нужен тон НИЖЕ несущей — инвертируем знак.
if (FMode = MODE_LSB) or (FMode = MODE_CWL) then
SignedFreq := -Abs(FreqHz)
else
SignedFreq := Abs(FreqHz);
SetTXAPostGenMode(TXA_CHAN, 0); // 0 = tone
SetTXAPostGenToneFreq(TXA_CHAN, SignedFreq);
SetTXAPostGenToneMag(TXA_CHAN, Max(0.0, Min(1.0, Mag)));
SetTXAPostGenRun(TXA_CHAN, 1);
end else
SetTXAPostGenRun(TXA_CHAN, 0);
end;
procedure TWDSPEngine.SetDisplaySourceTX(UseTX: Boolean);
begin
// Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID
// независимы, но мы переключаем источник, и старые байты в накапливающем
// буфере на новой стороне могут повести себя как «всплеск», если их не
// обнулить.
FDispPos := 0;
FTXDispPos := 0;
if UseTX then
FActiveDisplayID := TX_DISP_ID
else
FActiveDisplayID := RX_DISP_ID;
end;
procedure TWDSPEngine.ProcessTXBlock;
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
// если mic ring buffer содержит достаточно данных — берём их,
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
var
i, tail: Integer;
Err: Integer;
Avail: Integer;
begin
if not FInitialized then Exit;
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
if Avail >= FAudioBufSize then
begin
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
tail := FTXMicTail;
for i := 0 to FAudioBufSize - 1 do
begin
FTXIn[i * 2] := FTXMicRing[tail];
FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал
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 заполняется нулями)
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end;
Err := 0;
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
// TX сигнал → отдельный TX-analyzer (TX_DISP_ID).
// Подаём чанками по DISPLAY_BLOCK_SIZE — analyzer ожидает именно такой
// размер блока (см. SetAnalyzer). Если подавать FTXOut целиком
// (FTXOutBufSize пар), analyzer прочитает только первые DISPLAY_BLOCK_SIZE
// и проигнорирует остальное → спектр будет пустой.
if FAnalyzerOpen then
for i := 0 to FTXOutBufSize - 1 do
FeedTXDisplaySample(FTXOut[i * 2], FTXOut[i * 2 + 1]);
// FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double)
if Assigned(FOnTXIQ) then
FOnTXIQ(FTXOut, FTXOutBufSize);
end;
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
// Вызывается из receive thread — lock-free, кладёт сэмплы в ring buffer
// 16-bit big-endian samples из аппаратного микрофона
var
i, cnt, newHead: Integer;
S: SmallInt;
begin
cnt := N;
if cnt > Length(Src) then cnt := Length(Src);
for i := 0 to cnt - 1 do
begin
// Аппаратный mic приходит в big-endian, нужно swap байт
S := SmallInt(((Src[i] and $FF) shl 8) or ((Src[i] shr 8) and $FF));
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
if newHead <> FTXMicTail then // буфер не полон
begin
FTXMicRing[FTXMicHead] := S / 32768.0;
FTXMicHead := newHead;
end;
// если буфер полон — сэмпл отбрасывается (overrun)
end;
RTLEventSetEvent(FTXMicSem); // будим TX поток
end;
procedure TWDSPEngine.PushTXMicSampleD(const S: Double);
var newHead: Integer;
begin
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
if newHead <> FTXMicTail then
begin
FTXMicRing[FTXMicHead] := S;
FTXMicHead := newHead;
end;
// overrun → дроп, как в HW path
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-каналу
// ---------------------------------------------------------------------------
procedure TWDSPEngine.ApplyTXChainSettings;
// Вызывается после OpenChannel(TXA) и при старте TX. Прокидывает все
// FTX*-поля в WDSP. Идемпотентна, ничего не меняет если канал не открыт.
var
i: Integer;
Gains, Freqs, Qs: array[0..10] of Double;
begin
if not FInitialized then Exit;
// Bandpass
if (FTXFilterLow > 0) and (FTXFilterHigh > FTXFilterLow) then
SetTXABandpassFreqs(TXA_CHAN, FTXFilterLow, FTXFilterHigh);
if FTXFilterWindow >= 0 then
SetTXABandpassWindow(TXA_CHAN, FTXFilterWindow);
if FTXFilterNC > 0 then
TXASetNC(TXA_CHAN, FTXFilterNC);
if FTXFilterMP >= 0 then
TXASetMP(TXA_CHAN, FTXFilterMP);
// Mic gain (panel gain1)
SetTXAPanelGain1(TXA_CHAN, Power(10.0, FTXMicGainDB / 20.0));
// Compressor
SetTXACompressorRun(TXA_CHAN, Ord(FTXCompressorOn));
SetTXACompressorGain(TXA_CHAN, FTXCompressorGain);
// Leveler
SetTXALevelerSt(TXA_CHAN, Ord(FTXLevelerOn));
SetTXALevelerTop(TXA_CHAN, FTXLevelerTop);
SetTXALevelerDecay(TXA_CHAN, FTXLevelerDecay);
// ALC
SetTXAALCSt(TXA_CHAN, Ord(FTXALCOn));
SetTXAALCMaxGain(TXA_CHAN, FTXALCMaxGain);
SetTXAALCDecay(TXA_CHAN, FTXALCDecay);
// Phase Rotator
SetTXAPHROTRun(TXA_CHAN, Ord(FTXPhaseRotOn));
SetTXAPHROTNstages(TXA_CHAN, FTXPhaseRotStages);
SetTXAPHROTCorner(TXA_CHAN, FTXPhaseRotFreq);
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
for i := 0 to 10 do
begin
Gains[i] := FTXEQGains[i];
Freqs[i] := FTXEQFreqs[i];
Qs[i] := 0.707; // умолчание Q
end;
SetTXAEQProfile(TXA_CHAN, FTXEQNumBands, @Freqs[0], @Gains[0]);
SetTXAEQRun(TXA_CHAN, Ord(FTXEQOn));
// AM
SetTXAAMCarrierLevel(TXA_CHAN, FTXAMCarrier);
// FM
SetTXAFMDeviation(TXA_CHAN, FTXFMDeviation);
if (FTXFMLowCut < FTXFMHighCut) then
SetTXAFMAFFilter(TXA_CHAN, FTXFMLowCut, FTXFMHighCut);
SetTXAFMEmphPosition(TXA_CHAN, FTXFMEmphPos);
// CTCSS
SetTXACTCSSFreq(TXA_CHAN, FTXCTCSSFreq);
SetTXACTCSSRun(TXA_CHAN, Ord(FTXCTCSSOn));
end;
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer);
begin
FTXFilterLow := Low;
FTXFilterHigh := High;
FTXFilterNC := NC;
FTXFilterMP := MP;
FTXFilterWindow := Window;
FFilterLow := Low;
FFilterHigh := High;
if not FInitialized then Exit;
SetTXABandpassFreqs(TXA_CHAN, Low, High);
SetTXABandpassWindow(TXA_CHAN, Window);
if NC > 0 then TXASetNC(TXA_CHAN, NC);
if MP >= 0 then TXASetMP(TXA_CHAN, MP);
end;
procedure TWDSPEngine.SetTXCompressor(Enable: Boolean; GainDB: Double);
begin
FTXCompressorOn := Enable;
FTXCompressorGain := GainDB;
if not FInitialized then Exit;
SetTXACompressorGain(TXA_CHAN, GainDB);
SetTXACompressorRun(TXA_CHAN, Ord(Enable));
end;
procedure TWDSPEngine.SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
begin
FTXLevelerOn := Enable;
FTXLevelerTop := TopDB;
FTXLevelerDecay := DecayMS;
if not FInitialized then Exit;
SetTXALevelerTop(TXA_CHAN, TopDB);
SetTXALevelerDecay(TXA_CHAN, DecayMS);
SetTXALevelerSt(TXA_CHAN, Ord(Enable));
end;
procedure TWDSPEngine.SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
begin
FTXALCOn := Enable;
FTXALCMaxGain := MaxGainDB;
FTXALCDecay := DecayMS;
if not FInitialized then Exit;
SetTXAALCMaxGain(TXA_CHAN, MaxGainDB);
SetTXAALCDecay(TXA_CHAN, DecayMS);
SetTXAALCSt(TXA_CHAN, Ord(Enable));
end;
procedure TWDSPEngine.SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
begin
FTXPhaseRotOn := Enable;
FTXPhaseRotStages := Stages;
FTXPhaseRotFreq := FreqHz;
if not FInitialized then Exit;
SetTXAPHROTNstages(TXA_CHAN, Stages);
SetTXAPHROTCorner(TXA_CHAN, FreqHz);
SetTXAPHROTRun(TXA_CHAN, Ord(Enable));
end;
procedure TWDSPEngine.SetTXEQ(Enable: Boolean; NumBands: Integer;
const Gains, Freqs: array of Double);
var
i, n: Integer;
G, F: array[0..10] of Double;
begin
FTXEQOn := Enable;
FTXEQNumBands := NumBands;
n := Min(11, Min(Length(Gains), Length(Freqs)));
for i := 0 to n - 1 do
begin
FTXEQGains[i] := Gains[i];
FTXEQFreqs[i] := Freqs[i];
G[i] := Gains[i];
F[i] := Freqs[i];
end;
if not FInitialized then Exit;
SetTXAEQProfile(TXA_CHAN, NumBands, @F[0], @G[0]);
SetTXAEQRun(TXA_CHAN, Ord(Enable));
end;
procedure TWDSPEngine.SetTXAMCarrierLevel(Level: Double);
begin
FTXAMCarrier := Level;
if not FInitialized then Exit;
WDSP.SetTXAAMCarrierLevel(TXA_CHAN, Level);
end;
procedure TWDSPEngine.SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
begin
FTXFMDeviation := Deviation;
FTXFMLowCut := LowCut;
FTXFMHighCut := HighCut;
FTXFMEmphPos := EmphPos;
if not FInitialized then Exit;
SetTXAFMDeviation(TXA_CHAN, Deviation);
if LowCut < HighCut then
SetTXAFMAFFilter(TXA_CHAN, LowCut, HighCut);
SetTXAFMEmphPosition(TXA_CHAN, EmphPos);
end;
procedure TWDSPEngine.SetTXCTCSS(Enable: Boolean; FreqHz: Double);
begin
FTXCTCSSOn := Enable;
FTXCTCSSFreq := FreqHz;
if not FInitialized then Exit;
SetTXACTCSSFreq(TXA_CHAN, FreqHz);
SetTXACTCSSRun(TXA_CHAN, Ord(Enable));
end;
function TWDSPEngine.TXMicRingAvail: Integer;
begin
Result := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
end;
procedure TWDSPEngine.SetTXMicSource(Source: TTXMicSource);
begin
if FTXMicSource = Source then Exit;
FTXMicSource := Source;
// При смене источника сбрасываем ring, чтобы не подмешивать остатки
// от предыдущего producer'а (race window игнорируется — TX обычно неактивен).
FTXMicHead := 0;
FTXMicTail := 0;
end;
procedure TWDSPEngine.SetTXFFTParams(FFTSize, WinType: Integer);
begin
FTXFFTSize := NormalizeFFTSize(FFTSize);
FTXWindowType := EnsureRange(WinType, 0, 6);
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
end;
procedure TWDSPEngine.SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
begin
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);
FTXSpecDetector := Detector;
FTXSpecAvgMode := AvgMode;
FTXSpecAvgTimeMS := AvgTimeMS;
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
end;
procedure TWDSPEngine.SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
begin
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);
FTXWfDetector := Detector;
FTXWfAvgMode := AvgMode;
FTXWfAvgTimeMS := AvgTimeMS;
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
end;
// ---------------------------------------------------------------------------
// Spectrum
// ---------------------------------------------------------------------------
procedure TWDSPEngine.UpdateSpectrum;
// Вызывается из таймера главного потока (настраиваемый FPS).
// Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
// буфера (который fexchange0 непрерывно заполняет из DSP-потока).
// Это обеспечивает равномерный FPS независимо от размера FFT:
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
// буфера, а не ждёт следующего полного блока.
var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer;
i, N: Integer;
DispID: Integer;
begin
if not FInitialized or not FAnalyzerOpen then Exit;
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
Flag := 0;
GetPixels(DispID, 0, @PixBuf[0], @Flag);
if Flag <> 0 then
begin
for i := 0 to N - 1 do
FSpectrumPixels[i] := PixBuf[i];
if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, N);
end;
Flag := 0;
GetPixels(DispID, 1, @PixBuf[0], @Flag);
if Flag <> 0 then
begin
for i := 0 to N - 1 do
FWaterfallPixels[i] := PixBuf[i];
if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, N);
end;
end;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
var Count: Integer);
var
N: Integer;
begin
N := Min(SPECTRUM_PIXELS, Length(Pixels));
if N > 0 then
Move(FSpectrumPixels[0], Pixels[0], N * SizeOf(Single));
Count := N;
end;
function TWDSPEngine.GetSMeterDBm: Double;
begin
// НЕ вызываем GetRXAMeter здесь повторно — значение уже обновлено
// в DSP-колбэке. Повторный вызов сбрасывает аккумулятор WDSP → 0.
Result := FSMeter;
end;
// ---------------------------------------------------------------------------
// Настройки анализатора (применяются немедленно)
// ---------------------------------------------------------------------------
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
begin
// Thetis-style display FFT range.
if FFTSize < 4096 then FFTSize := 4096;
Result := 4096;
while Result < FFTSize do
Result := Result shl 1;
if Result > 262144 then
Result := 262144;
end;
function TWDSPEngine.CalcDisplayFFTSize: Integer;
begin
// FFT Size теперь снова управляет display-analyzer как в Thetis,
// но не влияет на аудио DSP цепочку.
Result := FFFTSize;
end;
procedure TWDSPEngine.CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
out EffectiveFPS: Double);
var
TargetFPS: Double;
begin
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
// Thetis/piHPSDR-style timing:
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
EffectiveFPS := TargetFPS;
end;
procedure TWDSPEngine.NormalizeDisplayParams(var Detector, AvgMode: Integer;
var AvgTimeMS: Double; ForWaterfall: Boolean);
begin
Detector := EnsureRange(Detector, 0, 4);
if ForWaterfall then
begin
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
if AvgMode < 0 then AvgMode := 1;
end
else
if AvgMode < -1 then AvgMode := -1;
if AvgMode > 3 then AvgMode := 3;
if AvgMode <= 0 then
AvgTimeMS := 0.0
else
AvgTimeMS := EnsureRange(AvgTimeMS, 10.0, 5000.0);
end;
procedure TWDSPEngine.AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
out AvgCount: Integer; out Backmult: Double);
var
TimeSec: Double;
Frames: Double;
FPSv: Double;
begin
if AvgMode <= 0 then
begin
AvgCount := 1;
Backmult := 0.0;
Exit;
end;
TimeSec := EnsureRange(AvgTimeMS, 10.0, 5000.0) * 0.001;
FPSv := Max(1.0, FEffectiveDisplayFPS);
Frames := FPSv * TimeSec;
AvgCount := EnsureRange(Round(Frames), 2, 60);
Backmult := Exp(-1.0 / Max(1E-6, FPSv * TimeSec));
Backmult := EnsureRange(Backmult, 0.0, 0.9999);
end;
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
var
NewFFT: Integer;
begin
NewFFT := NormalizeFFTSize(FFTSize);
if WinType < 0 then WinType := 0;
if WinType > 6 then WinType := 6;
if (FFFTSize = NewFFT) and (FWindowType = WinType) then Exit;
FFFTSize := NewFFT;
FWindowType := WinType;
if FAnalyzerOpen then
ApplyAnalyzerSettings;
end;
procedure TWDSPEngine.SetDisplayFPS(FPS: Integer);
begin
FPS := EnsureRange(FPS, 1, 100);
if FDisplayFPS = FPS then Exit;
FDisplayFPS := FPS;
if FAnalyzerOpen then
ApplyAnalyzerSettings;
end;
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode: Integer;
AvgTimeMS: Double);
var
AvgCount: Integer;
Backmult: Double;
begin
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);
if (FSpecDetector = Detector) and
(FSpecAvgMode = AvgMode) and
(Abs(FSpecAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;
FSpecDetector := Detector;
FSpecAvgMode := AvgMode;
FSpecAvgTimeMS := AvgTimeMS;
if not FAnalyzerOpen then Exit;
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(RX_DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(RX_DISP_ID, 0, AvgCount);
SetDisplayAvBackmult(RX_DISP_ID, 0, Backmult);
SetDisplayNormOneHz(RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
end;
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
AvgTimeMS: Double);
var
AvgCount: Integer;
Backmult: Double;
begin
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);
if (FWfDetector = Detector) and
(FWfAvgMode = AvgMode) and
(Abs(FWfAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;
FWfDetector := Detector;
FWfAvgMode := AvgMode;
FWfAvgTimeMS := AvgTimeMS;
if not FAnalyzerOpen then Exit;
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
SetDisplayDetectorMode(RX_DISP_ID, 1, FWfDetector);
SetDisplayAverageMode(RX_DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount);
SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult);
SetDisplayNormOneHz(RX_DISP_ID, 1, 0);
end;
end.