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 RX_DISP_ID = 0; // analyzer для RX-спектра/водопада TX_DISP_ID = 1; // analyzer для TX-спектра/водопада DSP_BUFSIZE = 1024; SPECTRUM_PIXELS = 1024; 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; // Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком, // чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio). // Реализация должна вернуть кол-во отправленных в WDSP сэмплов. TPullMicSamplesFunc = function(Max: Integer): Integer of object; // TX mic source: откуда получаем сэмплы для модулятора. TTXMicSource = (txmsRadio, txmsSoundCard, txmsWeb); { 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; // 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; // Кэшированные 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; // последняя известная ширина спектра (пикс) 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; 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 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); // Подача 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 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 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; DidBlock: Boolean; StallCount: 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; StallCount := 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-данными DidBlock := False; repeat Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); if (Avail >= FEngine.FAudioBufSize) and not Terminated then begin FEngine.ProcessTXBlock; DidBlock := True; end else Break; until False; if Terminated then Break; // Если на этом тике не было ни одного полного mic-блока и mic-данных // вообще нет — обрабатываем один блок тишины, чтобы DUC IQ поток не // прерывался и спектр/водопад обновлялись. // Важно: не добавлять silence-block сразу после реального блока. Для TUN // этот "silence" тоже содержит PostGen tone, поэтому лишний блок ломает // темп DUC IQ и даёт периодические боковые/тр-р-р. // Если же в ring уже есть неполный блок, нельзя подменять его тишиной: // radio mic приходит пакетами по 64 сэмпла, sound-card часто по 128. // Иначе каждый mic packet будит поток раньше срока, а мы генерируем // лишний TX-блок тишины вместо ожидания полного 512-sample блока. Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); // Тишину генерируем только после 6 подряд пустых тиков (~64 ms). // Порог 64 ms даёт 44 ms запаса относительно периода Opus-фрейма (20 ms), // что покрывает WiFi-джиттер до ~40 ms без ложного инжекта тишины. // При реальной потере пакета тишина всё равно инжектируется. if not DidBlock then begin if Avail = 0 then begin Inc(StallCount); if StallCount >= 6 then FEngine.ProcessTXBlock; end else StallCount := 0; end else StallCount := 0; 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; 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; // TX инициализация FTXMicHead := 0; FTXMicTail := 0; FTXActive := 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); 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, 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, 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; 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; 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; begin if not FInitialized then Exit; // Останавливаем DSP поток перед закрытием WDSP каналов if Assigned(FDSPThread) then begin FDSPThread.Terminate; RTLEventSetEvent(FQueueSem); // разбудить чтобы увидел Terminated FDSPThread.WaitFor; FreeAndNil(FDSPThread); 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; // Переинициализируем 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 первые // ~50–200мс после 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; except // Защита от падения libwdsp в потоке — просто пропускаем блок 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() SetRXAAGCMode(RXA_CHAN, Ord(Mode)); SetRXAAGCSlope(RXA_CHAN, FAGCSlope); SetRXAAGCTop(RXA_CHAN, FAGCTop); case Mode of agcOff: SetRXAAGCFixed(RXA_CHAN, FixedGain); agcLong: begin SetRXAAGCAttack(RXA_CHAN, 2); SetRXAAGCHang(RXA_CHAN, 2000); SetRXAAGCDecay(RXA_CHAN, 2000); SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold); end; agcSlow: begin SetRXAAGCAttack(RXA_CHAN, 2); SetRXAAGCHang(RXA_CHAN, 1000); SetRXAAGCDecay(RXA_CHAN, 500); SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold); end; agcMedium: begin SetRXAAGCAttack(RXA_CHAN, 2); SetRXAAGCHang(RXA_CHAN, 0); SetRXAAGCDecay(RXA_CHAN, 250); SetRXAAGCHangThreshold(RXA_CHAN, 100); end; agcFast: begin SetRXAAGCAttack(RXA_CHAN, 2); SetRXAAGCHang(RXA_CHAN, 0); SetRXAAGCDecay(RXA_CHAN, 50); SetRXAAGCHangThreshold(RXA_CHAN, 100); end; end; // Читаем обратно 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.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.ApplyNRState; begin if not FInitialized then Exit; SetRXAANRVals(RXA_CHAN, 64, 16, 16e-4, 10e-7); SetRXAANRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); SetRXAANRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_1)); SetRXAEMNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); SetRXAEMNRgainMethod(RXA_CHAN, FILTER_NR2_GAIN_METHOD); SetRXAEMNRnpeMethod(RXA_CHAN, FILTER_NR2_NPE_METHOD); SetRXAEMNRtrainZetaThresh(RXA_CHAN, FILTER_NR2_TRAINED_THRESHOLD); SetRXAEMNRtrainT2(RXA_CHAN, FILTER_NR2_TRAINED_T2); SetRXAEMNRpost2Taper(RXA_CHAN, FILTER_NR2_POST_TAPER); SetRXAEMNRpost2Nlevel(RXA_CHAN, FILTER_NR2_POST_NLEVEL); SetRXAEMNRpost2Factor(RXA_CHAN, FILTER_NR2_POST_FACTOR); SetRXAEMNRpost2Rate(RXA_CHAN, FILTER_NR2_POST_RATE); SetRXAEMNRaeRun(RXA_CHAN, 1); SetRXAEMNRpost2Run(RXA_CHAN, FILTER_NR2_POST_RUN); SetRXAEMNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_2)); SetRXARNNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); SetRXARNNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_3)); SetRXASBNRreductionAmount(RXA_CHAN, FILTER_NR4_REDUCTION_AMOUNT); SetRXASBNRsmoothingFactor(RXA_CHAN, FILTER_NR4_SMOOTHING_FACTOR); SetRXASBNRwhiteningFactor(RXA_CHAN, FILTER_NR4_WHITENING_FACTOR); SetRXASBNRnoiseRescale(RXA_CHAN, FILTER_NR4_NOISE_RESCALE); SetRXASBNRpostFilterThreshold(RXA_CHAN, FILTER_NR4_POST_THRESHOLD); SetRXASBNRnoiseScalingType(RXA_CHAN, FILTER_NR4_NOISE_SCALING_TYPE); SetRXASBNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); SetRXASBNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_4)); end; procedure TWDSPEngine.ApplyNBState; begin if not FInitialized then Exit; SetEXTANBTau(RXA_CHAN, FILTER_NB_TAU); SetEXTANBHangtime(RXA_CHAN, FILTER_NB_HANGTIME); SetEXTANBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME); SetEXTANBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD); SetEXTANBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_1)); SetEXTNOBMode(RXA_CHAN, FILTER_NB2_MODE); SetEXTNOBTau(RXA_CHAN, FILTER_NB_TAU); SetEXTNOBHangtime(RXA_CHAN, FILTER_NB_HANGTIME); SetEXTNOBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME); SetEXTNOBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD); SetEXTNOBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_2)); end; procedure TWDSPEngine.ApplySNBState; begin if not FInitialized then Exit; SetRXASNBARun(RXA_CHAN, Ord(FSNBEnabled)); end; procedure TWDSPEngine.ApplyANFState; begin if not FInitialized then Exit; SetRXAANFPosition(RXA_CHAN, FILTER_NR_AGC_POSITION); SetRXAANFRun(RXA_CHAN, Ord(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); begin FTXActive := Run; FDispPos := 0; FTXDispPos := 0; if Run then FActiveDisplayID := TX_DISP_ID else FActiveDisplayID := RX_DISP_ID; if Run then begin if FInitialized then SetChannelState(TXA_CHAN, 1, 0); // run, no delay // Сброс mic ring buffer и запуск TX потока FTXMicHead := 0; FTXMicTail := 0; if not Assigned(FTXThread) then begin FTXThread := TTXDSPThread.Create(Self); FTXThread.Priority := tpHighest; end; end else begin if FInitialized then SetChannelState(TXA_CHAN, 0, 1); // stop, with slew // Останавливаем TX поток if Assigned(FTXThread) then begin FTXThread.Terminate; RTLEventSetEvent(FTXMicSem); FTXThread.WaitFor; FreeAndNil(FTXThread); end; FTXMicHead := 0; FTXMicTail := 0; end; end; procedure TWDSPEngine.SetTXTone(Enabled: Boolean; FreqHz, Mag: Double); var SignedFreq: Double; begin if not FInitialized then Exit; 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) 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.