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, RadioModes, WDSP; const // WDSP mode integers (нет именованных констант в WDSP.pas). // Значения — enum rxaMode (RXA.h) / txaMode (TXA.h). До DRM включительно // нумерация совпадает, дальше расходится: TXA вставляет AM_LSB/AM_USB, // поэтому WFM у RXA = 12, а у TXA = 14. Отсюда две отдельные константы. WDSP_LSB = 0; WDSP_USB = 1; WDSP_DSB = 2; WDSP_CWL = 3; WDSP_CWU = 4; WDSP_FM = 5; WDSP_AM = 6; WDSP_DIGU = 7; WDSP_DIGL = 9; WDSP_SAM = 10; WDSP_RX_WFM = 12; WDSP_TX_WFM = 14; // Наши индексы режимов (совпадают с кнопками MainForm). // Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json, // band-cache, CAT (ZZMD/ZZMV) и web-API. MODE_LSB = RadioModes.MODE_LSB; MODE_USB = RadioModes.MODE_USB; MODE_DSB = RadioModes.MODE_DSB; MODE_CWL = RadioModes.MODE_CWL; MODE_CWU = RadioModes.MODE_CWU; MODE_FM = RadioModes.MODE_FM; MODE_AM = RadioModes.MODE_AM; MODE_SAM = RadioModes.MODE_SAM; MODE_DIGU = RadioModes.MODE_DIGU; MODE_DIGL = RadioModes.MODE_DIGL; MODE_WFM = RadioModes.MODE_WFM; MODE_DMR = RadioModes.MODE_DMR; MODE_FMRAW = RadioModes.MODE_FMRAW; MODE_MIN = RadioModes.MODE_MIN; MODE_MAX = RadioModes.MODE_MAX; MODE_COUNT = RadioModes.MODE_COUNT; // Минимальная длина FIR у WDSP: create_* поднимают fircore с // nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1, // так что при nc < dsp_size получается nfor = 0 — пустой массив FFT-планов и // падение в fftw_execute. Любое изменение dsp_size обязано перевыставить nc. WDSP_MIN_NC = 2048; // WFM: девиация и параметры де-эмфазиса вещательного FM. WFM_DEVIATION = 75000.0; WFM_AF_LOW = 20.0; WFM_AF_HIGH = 15000.0; WFM_TAU_DE_US = 50.0e-6; // Европа/Россия (Америка — 75 мкс) DMR_DEVIATION = 2500.0; // Потолок dsp_rate для WFM. Вещательному каналу (200 кГц) хватает 192 кГц, // а гнать цепь на полном IQ-rate нельзя: при 576 кГц dsp_size стал бы 12288, // и fircore спланировал бы FFT на 2*12288 = 24576 точек. Это не степень // двойки — такого плана нет в wdspWisdom00, и FFTW_PATIENT меряет его ~25 с // при первом входе в режим. rsmpin децимирует in_rate → dsp_rate сам. WFM_DSP_RATE = 192000; // 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-спектра/водопада // --- Панадаптеры на аппаратных DDC (этап 3) --- // Пан 0 = главный тракт (RXA/RX_DISP_ID), доп. паны 1..MAX_PANS-1 живут на // своих DDC. Единый потолок для движка/контроллера/UI. MAX_PANS = 4; // Analyzer id пана N = PAN_DISP_BASE + N (9..11). ID 4/5 заняты веткой // feature/qo100-zoom-panel — не пересекаемся при будущем merge. PAN_DISP_BASE = 8; DSP_BUFSIZE = 1024; SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS DISPLAY_BLOCK_SIZE = 64; // Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны). // 256: главный 1.536М (~6.5к пкт/с) + 3 пана по 768к (~10к пкт/с) должны // переживать scheduling-паузы ~15мс без дропов. IQ_QUEUE_SIZE = 256; // кол-во слотов (степень двойки для 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 пар PanId: Integer; // 0 = главный тракт, 1..MAX_PANS-1 = доп. пан 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; // Калибровочный офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный, // 1..MAX_PANS-1 = доп. панадаптер. Спрашивается раз в кадр из display-потока: // величина зависит от диапазона/трансвертера, о которых знает только // TRadioController (аналог SMeterCalFor для стрелки). TOnDispCalQuery = function(PanId: Integer): Double of object; // Пиксели спектра/водопада доп. пана — из display-потока (как FOnSpectrum/ // FOnWaterfall главного). Внутри колбэка НЕ звать slice/pan API движка. TOnPanPixels = procedure(PanId: Integer; 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); // Софтовый слайс: отдельный WDSP-канал, кормится тем же IQ-блоком, что и // RXA_CHAN, но со своим NCO-сдвигом/режимом/фильтром/AGC/громкостью и своим // аудио-callback'ом. Геометрия канала = как у RXA_CHAN. TSliceChan = record Active: Boolean; // слот занят Id: Integer; // логический id слайса Chan: Integer; // WDSP-канал (SLICE_CHAN_BASE + слот) PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан 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; FMSQOn: Boolean; // FM-шумоподавитель (squelch) включён FMSQLevel: Integer; // порог squelch 0..100 SMeter: Double; // последнее показание S-метра канала (dBm), пишет DSP-поток Opened: Boolean; // WDSP-канал реально открыт (после Open движка) end; // Доп. панадаптер (этап 3): свой DDC-поток (rate 192-768к), свой analyzer // (PAN_DISP_BASE+PanId), свои слайсы (TSliceChan.PanId). Доступ к записи — // только под FSliceLock (кроме read-only Active в PushPanIQ). TPanChan = record Active: Boolean; Rate: Integer; // входной rate пан-DDC, Гц (кратен FAudioRate) BufSize: Integer; // блок @ Rate = FAudioBufSize * Rate / FAudioRate AccI: array of Double; // аккумулятор входных IQ до BufSize AccQ: array of Double; AccPos: Integer; DispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар) DispPos: Integer; DispOpen: Boolean; // XCreateAnalyzer(PAN_DISP_BASE+PanId) жив ZoomFactor: Double; // зум пана 0..1 (0 = весь span), как FZoomFactor PanSlider: Double; // положение окна 0..1 end; { TWDSPEngine } TWDSPEngine = class private FInitialized: Boolean; FAnalyzerOpen: Boolean; FSampleRate: Integer; FAudioRate: Integer; // Текущий dsp_rate канала RXA. Обычно = FAudioRate; в WFM поднимается // до FSampleRate, иначе девиация 75 кГц не помещается в полосу. FRXDSPRate: Integer; FTXDSPSize: Integer; // текущий dsp_size канала TXA (нижняя граница nc) 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; // Гейт главного тракта на время хирургической смены rate (ChangeSampleRate): // True = DSP-поток дропает main-пакеты (PanId=0) не блокируясь, паны // обрабатываются как обычно. FMainLock — барьер: DSP-поток держит его на // время обработки main-пакета, ChangeSampleRate делает Enter/Leave после // установки гейта, гарантируя что fexchange0 на RXA_CHAN завершён. FMainGated: Boolean; FMainLock: TCriticalSection; 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; FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders FOnSpectrum: TOnSpectrumReady; FOnWaterfall: TOnWaterfallReady; FOnDispCal: TOnDispCalQuery; 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; FOnSliceDemodAudio: TOnSliceAudio; // pre-volume/mute tap for digital slices // --- Панадаптеры на аппаратных DDC (этап 3.2) --- FPans: array[1..MAX_PANS-1] of TPanChan; FOnPanSpectrum: TOnPanPixels; FOnPanWaterfall: TOnPanPixels; // 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; // 2TON активен (PostGen two-tone 700/1900 Гц для IMD/PureSignal-теста): // mic-вход глушится так же, как при TUN. FTXTTOn: Boolean; // ---- PureSignal ---- // FPSArmed: PS включён контроллером — PushPSFeedback подаёт feedback в // pscc (WDSP calcc). Пишется из UI-потока, читается в сетевом. FPSArmed: Boolean; // Деинтерливленные буферы feedback-пакета: interleaved I/Q пары каждого // потока (≤119 пар на поток при 238 парах в пакете; 256 пар с запасом). // Используются только сетевым потоком. FPSTxBuf: array[0..511] of Double; // DDC1 = TX DAC (чистый эталон) FPSRxBuf: array[0..511] of Double; // DDC0 = RX feedback (после PA) // Кэш PS-параметров: применяются в ApplyTXChainSettings (т.е. и после // каждого пересоздания TXA-канала в Open/ChangeSampleRate). FPSMoxDelay: Double; FPSLoopDelay: Double; FPSTXDelayNs: Integer; FPSRelaxPtol: Boolean; FPSPin: Boolean; FPSMap: Boolean; FPSStabilize: Boolean; FPSInts: Integer; FPSSpi: Integer; FPSHWPeak: Double; FPSOutlierSigma: Double; // eu2av MAD-фильтр перед фитом; 0 = выкл. // Кэшированные 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; FTXMode: Integer; // may differ from RX when a slice is TX source FRawFMDeviation: Double; 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; function ModeToWDSPTX(Mode: Integer): Integer; procedure ApplyDefaultFilter; // WFM требует dsp_rate >= 192k (девиация 75 кГц не влезает в 48 кГц). function WantWideDSP(Mode: Integer): Boolean; function WFMDSPRate: Integer; function WFMDSPRateFor(InRate: Integer): Integer; // WFMDSPRate для произвольного входа function SliceDSPRate(Mode: Integer): Integer; // dsp_rate слайса от режима и ЕГО входного rate (у пан-слайсов вход = rate пана) function SliceDSPRateFor(Mode, InRate: Integer): Integer; function ClampToNyquist(Hz, Rate: Integer): Integer; function EffectiveTXNC: Integer; procedure ApplyDSPRate(Mode: Integer); procedure ApplyRXWFMToChan(Chan: Integer); procedure ApplyRXDMRToChan(Chan: Integer); procedure ApplyRXRawToChan(Chan: Integer); procedure ApplyWFMSettings; // Mode-dependent TX overlay. FM RAW uses the flat WFM modulator and // bypasses every speech processor without overwriting the saved TX setup. procedure ApplyTXModeSettings(Mode: Integer); // --- Мультислайсы --- function FindSliceIdx(Id: Integer): Integer; // -1 если нет function SliceInRate(Idx: Integer): Integer; // входной rate слайса (его пан) function SliceInSize(Idx: Integer): Integer; // входной блок слайса (его пан) procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки procedure CloseSliceChannel(Idx: Integer); // CloseChannel (запись хранит) procedure ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; FixedGain: Double); procedure ProcessSlices; // fan-out в ProcessRXBlock // Прогон блока IQ пана через его слайсы. Вызывать ПОД FSliceLock. procedure ProcessSlicesFor(PanId: Integer; const AccI, AccQ: array of Double; N: Integer); procedure AllocSliceBuffers; // (пере)аллокация буферов // --- Панадаптеры (этап 3.2) --- procedure CreatePanAnalyzer(PanId: Integer); // XCreateAnalyzer + настройки procedure DestroyPanAnalyzer(PanId: Integer); procedure ApplyPanAnalyzerSettings(PanId: Integer); procedure ReapplyPanDisplaySettings; // все активные паны, под локом procedure PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem); procedure ProcessRXBlock; procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure OpenMainRXChannel; // RXA_CHAN под текущие FBufSize/FSampleRate procedure CloseMainRXChannel; // state=0 + EXT-блоки + CloseChannel(RXA_CHAN) 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 ApplyFMSQToChan(Chan: Integer; Enable: Boolean; Level: Integer); procedure ApplyNoiseFilterState; procedure ProcessTXBlock; // вызывается из TTXDSPThread procedure FeedTXDisplaySample(const I, Q: Double); procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc 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); // --- Панадаптеры на аппаратных DDC (этап 3.2) --- // Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer // PAN_DISP_BASE+PanId. RateHz — rate его DDC, кратен FAudioRate. // Слайсы пана создаются отдельно (AddSlice с APanId). function AddPan(PanId, RateHz: Integer): Boolean; // Убирает пан вместе с его слайсами (аудио-выходы слайсов — забота вызывающего). procedure RemovePan(PanId: Integer); // Смена rate живого пана: пересбор аккумулятора/анализатора + переоткрытие // WDSP-каналов его слайсов на новом входном rate (state слайсов сохраняется). function SetPanRate(PanId, RateHz: Integer): Boolean; function PanActive(PanId: Integer): Boolean; // --- Мультислайсы (софт-fan-out) --- // Добавляет слайс с логическим Id (уникальным). ShiftHz = TargetHz - центр // захвата ЕГО пана. APanId>0 требует активного пана (AddPan). // Возвращает False если нет свободных слотов или Id уже есть. function AddSlice(Id, Mode, FilterLo, FilterHi: Integer; ShiftHz, Volume: Double; AGC: TWDSPAGCMode; APanId: Integer = 0): 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); procedure SetSliceFMSquelch(Id: Integer; Enable: Boolean; Level: Integer); 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); // IQ доп. панадаптера (аппаратный DDC пана PanId, этап 3). Пока (3.1) // заглушка-дроп; 3.2 добавит per-pan аккумулятор + Spectrum0(PAN_DISP_BASE+ // PanId) + ProcessSlices по слайсам этого пана. procedure PushPanIQ(PanId: Integer; 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 SetRawFMDeviation(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); // 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal). // Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды // по 0.49 (сумма чуть ниже полной шкалы, как Thetis/pihpsdr). procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double); // Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX. // Для openHPSDR P2 всегда включён; для остальных бэкендов выключен. procedure SetTXCFIR(Enabled: Boolean); // ---- PureSignal (WDSP calcc на TXA_CHAN) ---- // Армирование подачи feedback в pscc (кнопка PS). Сама калибровка // управляется PSControl (state machine в контроллере). procedure SetPureSignalArmed(Armed: Boolean); // Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 = // RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО // потока (как Thetis/pihpsdr — pscc потокобезопасен относительно // fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока). procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer; TotalPairs: Integer); // Тонкие врапперы WDSP PS API (все на TXA_CHAN, guard FInitialized): procedure PSControl(Reset, Mancal, Automode, Turnon: Integer); procedure PSSetMox(Mox: Boolean); procedure PSGetInfo(var Info: array of Integer); // 16 int function PSGetMaxTX: Double; procedure PSSetParams(MoxDelay, LoopDelay: Double; TXDelayNs: Integer; RelaxPtol: Boolean; Pin, Map, Stabilize: Boolean; Ints, Spi: Integer; HWPeak: Double; OutlierSigma: 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); // Зум/пан доп. пана (span-clip его анализатора). Видимое окно считает // контроллер той же формулой (GetPanViewWindow). procedure SetPanZoomPan(PanId: Integer; 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 OnDemodAudio: TOnAudioReady read FOnDemodAudio write FOnDemodAudio; property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio; property OnSliceDemodAudio: TOnSliceAudio read FOnSliceDemodAudio write FOnSliceDemodAudio; // Офсет дисплея (дБ) на кадр — см. TOnDispCalQuery. nil = без коррекции. property OnDispCal: TOnDispCalQuery read FOnDispCal write FOnDispCal; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall; // Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3). property OnPanSpectrum: TOnPanPixels read FOnPanSpectrum write FOnPanSpectrum; property OnPanWaterfall: TOnPanPixels read FOnPanWaterfall write FOnPanWaterfall; 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]; if Item^.PanId = 0 then begin // Гейт смены rate главного (ChangeSampleRate): main-пакеты дропаем // НЕ блокируясь — пан-пакеты позади в очереди не должны ждать // пересборку RXA. Повторная проверка под локом закрывает гонку // с установкой гейта из UI-потока. if not FEngine.FMainGated then begin FEngine.FMainLock.Enter; try if not FEngine.FMainGated then FEngine.PushIQItemToDSP(Item^); finally FEngine.FMainLock.Leave; end; end; end else FEngine.PushPanItemToDSP(Item^.PanId, 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; MODE_DIGU: Result := WDSP_DIGU; MODE_DIGL: Result := WDSP_DIGL; MODE_WFM: Result := WDSP_RX_WFM; // The narrow-FM path always applies its 300..3000 Hz speech filters and // de-emphasis. Those filters collapse the four DMR discriminator levels. // WFMD is a plain quadrature discriminator and permits flat data audio. MODE_DMR: Result := WDSP_RX_WFM; MODE_FMRAW: Result := WDSP_RX_WFM; else Result := WDSP_USB; end; end; function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer; // txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM. begin // The narrow FM modulator has mandatory speech pre-emphasis. FM RAW uses // wfmmod even at NFM deviation because that path has an explicit flat mode. if Mode in [MODE_WFM, MODE_FMRAW] then Result := WDSP_TX_WFM else if Mode = MODE_DMR then Result := WDSP_FM else Result := ModeToWDSP(Mode); end; function TWDSPEngine.WantWideDSP(Mode: Integer): Boolean; // WFM демодулируется квадратурным дискриминатором на dsp_rate. При 48 кГц // полоса ±24 кГц — вещательный канал (±100 кГц) туда не влезает, поэтому в // WFM цепь RXA поднимается до WFMDSPRate. Если поднимать некуда — работаем // как есть (узкий WFM), но dsp_rate не трогаем. begin Result := (Mode = MODE_WFM) and (WFMDSPRate > FAudioRate); end; function TWDSPEngine.WFMDSPRateFor(InRate: Integer): Integer; // Наибольший rate вида FAudioRate * 2^k, который не выше WFM_DSP_RATE и делит // InRate нацело. Оба требования жёсткие: // * pre_main_build считает dsp_insize через ЦЕЛОЧИСЛЕННОЕ in_rate/dsp_rate, // поэтому отношение обязано быть точным; // * dsp_size = FAudioBufSize * (rate/FAudioRate) должен остаться степенью // двойки, иначе fircore планирует FFT нестандартного размера мимо wisdom. var R: Integer; begin R := WFM_DSP_RATE div FAudioRate; // 4 при 48 кГц аудио while R > 1 do begin if (FAudioRate * R <= InRate) and (InRate mod (FAudioRate * R) = 0) then Break; R := R div 2; end; Result := FAudioRate * R; end; function TWDSPEngine.WFMDSPRate: Integer; begin Result := WFMDSPRateFor(FSampleRate); end; function TWDSPEngine.SliceDSPRate(Mode: Integer): Integer; begin Result := SliceDSPRateFor(Mode, FSampleRate); end; function TWDSPEngine.SliceDSPRateFor(Mode, InRate: Integer): Integer; // Пан-слайс поднимается в широкий WFM-режим от rate СВОЕГО пана, а не главного. begin if (Mode = MODE_WFM) and (WFMDSPRateFor(InRate) > FAudioRate) then Result := WFMDSPRateFor(InRate) else Result := FAudioRate; end; procedure TWDSPEngine.ApplyDSPRate(Mode: Integer); // Переводит RXA/TXA между «узким» (dsp_rate = FAudioRate) и «широким» режимом. // dsp_size всегда = FAudioBufSize * (dsp_rate / FAudioRate), поэтому dsp_outsize // остаётся FAudioBufSize и буферы fexchange0 (FRXIn/FRXOut/FTXIn/FTXOut) // переаллокации не требуют: // RXA wide: dsp_size=4096 @192k -> out=FAudioBufSize // RXA narw: dsp_size=FAudioBufSize @48k -> out=FAudioBufSize // TXA wide: dsp_size=FTXOutBufSize @192k -> in=FAudioBufSize, out=FTXOutBufSize // TXA narw: dsp_size=FAudioBufSize @48k -> in=FAudioBufSize, out=FTXOutBufSize // Зовётся и из Open — до FInitialized, но уже после OpenChannel обоих каналов. // RXA и TXA гейтятся раздельно: у TXA рейты свои (FAudioRate вход, // FTXSampleRate выход) и от FSampleRate не зависят, поэтому wfmmod уходит // на широкий dsp_rate даже когда приёмник остался на 48 кГц. var Wide: Boolean; RXRate, RXSize, TXRate, TXSize, Old: Integer; begin Wide := (Mode = MODE_WFM); if Wide then RXRate := WFMDSPRate else RXRate := FAudioRate; RXSize := FAudioBufSize * (RXRate div FAudioRate); if RXRate <> FRXDSPRate then begin // rate, size и nc меняем под одной остановкой канала: между вызовами // dsp_insize не совпал бы с in_size, и fexchange0 прочитал бы мусор. // RXASetNC/TXASetNC обязательны — см. комментарий к WDSP_MIN_NC. Old := SetChannelState(RXA_CHAN, 0, 1); SetDSPSamplerate(RXA_CHAN, RXRate); SetDSPBuffsize (RXA_CHAN, RXSize); FRXDSPRate := RXRate; RXASetNC (RXA_CHAN, Max(WDSP_MIN_NC, RXSize)); SetChannelState (RXA_CHAN, Old, 0); end; if Wide then TXRate := FTXSampleRate else TXRate := FAudioRate; TXSize := FAudioBufSize * (TXRate div FAudioRate); if TXSize <> FTXDSPSize then begin Old := SetChannelState(TXA_CHAN, 0, 1); SetDSPSamplerate(TXA_CHAN, TXRate); SetDSPBuffsize (TXA_CHAN, TXSize); FTXDSPSize := TXSize; TXASetNC (TXA_CHAN, EffectiveTXNC); SetChannelState (TXA_CHAN, Old, 0); end; end; function TWDSPEngine.EffectiveTXNC: Integer; // Пользовательский FilterNC (1024/2048/4096) не может быть короче dsp_size // канала: иначе nfor = nc / dsp_size = 0. В WFM dsp_size = FTXOutBufSize. begin Result := Max(FTXFilterNC, FTXDSPSize); end; procedure TWDSPEngine.ApplyRXWFMToChan(Chan: Integer); // create_wfmd поднимается с американской постоянной 75 мкс. begin SetRXAWFMDeviation (Chan, WFM_DEVIATION); SetRXAWFMAFFilter (Chan, WFM_AF_LOW, WFM_AF_HIGH); SetRXAWFMDeemphRun (Chan, 1); SetRXAWFMDeemphTau (Chan, WFM_TAU_DE_US); end; procedure TWDSPEngine.ApplyRXDMRToChan(Chan: Integer); begin // DMR needs the flat 4FSK discriminator, not broadcast-FM deemphasis. SetRXAWFMDeviation (Chan, DMR_DEVIATION); SetRXAWFMAFFilter (Chan, 20.0, 7000.0); SetRXAWFMDeemphRun (Chan, 0); end; procedure TWDSPEngine.ApplyRXRawToChan(Chan: Integer); begin // Decoder-grade discriminator: no broadcast de-emphasis or limiter. SetRXAWFMDeviation (Chan, FRawFMDeviation); SetRXAWFMAFFilter (Chan, 20.0, 7000.0); SetRXAWFMDeemphRun (Chan, 0); SetRXAWFMLimRun (Chan, 0); end; procedure TWDSPEngine.ApplyWFMSettings; begin ApplyRXWFMToChan(RXA_CHAN); SetTXAWFMDeviation (TXA_CHAN, WFM_DEVIATION); SetTXAWFMAFFreqs (TXA_CHAN, WFM_AF_LOW, WFM_AF_HIGH); SetTXAWFMPreEmphRun (TXA_CHAN, 1); SetTXAWFMPreEmphTau (TXA_CHAN, WFM_TAU_DE_US); end; procedure TWDSPEngine.ApplyTXModeSettings(Mode: Integer); begin if not FInitialized then Exit; if Mode = MODE_FMRAW then begin // Decoder/modem-grade FM input. A full-scale PCM peak produces the // selected peak deviation. Keep only a broad linear-phase AF bandpass; // APRS and other external modems must not see voice EQ, dynamics or emphasis. SetTXAWFMDeviation (TXA_CHAN, FRawFMDeviation); SetTXAWFMAFFreqs (TXA_CHAN, 20.0, 7000.0); SetTXAWFMPreEmphRun (TXA_CHAN, 0); SetTXABandpassFreqs (TXA_CHAN, 20.0, 7000.0); SetTXABandpassRun (TXA_CHAN, 1); SetTXAPanelGain1 (TXA_CHAN, 1.0); SetTXACompressorRun (TXA_CHAN, 0); SetTXALevelerSt (TXA_CHAN, 0); SetTXAALCSt (TXA_CHAN, 0); SetTXAPHROTRun (TXA_CHAN, 0); SetTXAEQRun (TXA_CHAN, 0); SetTXACTCSSRun (TXA_CHAN, 0); end else begin // Restore the user's normal voice/data chain after leaving FM RAW. SetTXABandpassRun(TXA_CHAN, 1); ApplyTXChainSettings; if Mode = MODE_WFM then ApplyWFMSettings; end; end; function TWDSPEngine.ClampToNyquist(Hz, Rate: Integer): Integer; var Limit: Integer; begin Limit := Rate div 2 - 1; Result := Max(-Limit, Min(Limit, Hz)); 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); MODE_DIGU: SetFilter( 0, 3000); MODE_DIGL: SetFilter(-3000, 0); MODE_WFM: SetFilter(-80000, 80000); MODE_DMR: SetFilter(-6250, 6250); MODE_FMRAW: SetFilter(-6250, 6250); 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 FRXDSPRate := AudioRate; // «узкий» режим; поднимается только в WFM FTXDSPSize := BufSize; // dsp_size TXA в «узком» режиме FInitialized := False; FAnalyzerOpen := False; FMode := MODE_USB; FTXMode := 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; // Гейт главного тракта (хирургическая смена rate) FMainGated := False; FMainLock := TCriticalSection.Create; // Мультислайсы FSliceLock := TCriticalSection.Create; FillChar(FSlices, SizeOf(FSlices), 0); FOnSliceAudio := nil; FOnSliceDemodAudio := nil; AllocSliceBuffers; // TX инициализация FTXMicHead := 0; FTXMicTail := 0; FTXActive := False; FTXToneOn := False; FTXTTOn := False; FKeepRXDuringTX := False; // PureSignal — дефолты как Thetis PSForm / clsHardwareSpecific (P2) FPSArmed := False; FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки FPSTXDelayNs := 150; // нс — компенсация задержки TX-тракта FPSRelaxPtol := False; FPSPin := True; FPSMap := True; FPSStabilize := False; FPSInts := 16; FPSSpi := 256; FPSHWPeak := 0.2899; // протокол 2 (Saturn: 0.6121) FPSOutlierSigma := 0.0; // выкл. до ApplyPSSettingsToDSP (авто по плате) 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; FRawFMDeviation := 2500.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; FMainLock.Free; inherited; end; // --------------------------------------------------------------------------- // Analyzer display // --------------------------------------------------------------------------- procedure TWDSPEngine.OpenAnalyzer; var Success, p: 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; // Анализаторы активных доп. панов (пересоздание после ChangeSampleRate: // Close их разрушил, записи FPans живы). FSliceLock.Enter; try for p := 1 to MAX_PANS - 1 do if FPans[p].Active then CreatePanAnalyzer(p); finally FSliceLock.Leave; end; 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.ApplyPanAnalyzerSettings(PanId: Integer); // Параметризованная копия ApplyRXAnalyzerSettings для analyzer'а доп. пана: // rate пана + СВОЙ span-clip/zoom (ZoomFactor/PanSlider записи пана); // FFT/детектор/усреднение пока общие с главным. Вызывать под FSliceLock. var AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels, Disp: Integer; Backmult, WfBackmult: Double; EffFPS: Double; Bins, SpanClipL, SpanClipH, ZWidth: Integer; ZoomSlider: Double; const KEEP_TIME_SEC = 0.10; ZOOM_LIMIT = 100.0; // как у главного (ApplyRXAnalyzerSettings) begin if not FPans[PanId].DispOpen then Exit; Disp := PAN_DISP_BASE + PanId; AnalyzerFFT := CalcDisplayFFTSize; FPans[PanId].DispPos := 0; MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FPans[PanId].Rate, KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100))); CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffFPS); DisplayPixels := EnsureRange(FLastSpectrumW, 64, EnsureRange(FDisplayResLimit, 64, SPECTRUM_PIXELS)); AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult); AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult); // Span-clip зума пана — та же математика, что у главного (clp=0, зум только // клипует края FFT). Контроллер считает видимое окно той же формулой. Bins := AnalyzerFFT; ZoomSlider := Log10(9.0 * EnsureRange(FPans[PanId].ZoomFactor, 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(FPans[PanId].PanSlider, 0.0, 1.0) * (Bins - ZWidth)); SpanClipH := Bins - ZWidth - SpanClipL; SetAnalyzer(Disp, 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(Disp, 0, FSpecDetector); SetDisplayAverageMode (Disp, 0, FSpecAvgMode); SetDisplayNumAverage (Disp, 0, AvgCount); SetDisplayAvBackmult (Disp, 0, Backmult); SetDisplayNormOneHz (Disp, 0, UsePanNormOneHz(FSpecDetector)); SetDisplayDetectorMode(Disp, 1, FWfDetector); SetDisplayAverageMode (Disp, 1, FWfAvgMode); SetDisplayNumAverage (Disp, 1, WfAvgCount); SetDisplayAvBackmult (Disp, 1, WfBackmult); SetDisplayNormOneHz (Disp, 1, 0); SetDisplaySampleRate (Disp, FPans[PanId].Rate); end; procedure TWDSPEngine.CreatePanAnalyzer(PanId: Integer); // Вызывать под FSliceLock, при живом WDSP (OpenAnalyzer уже прошёл). var Success: Integer; begin if FPans[PanId].DispOpen then Exit; Success := -1; XCreateAnalyzer(PAN_DISP_BASE + PanId, @Success, 262144, 1, 1, nil); if Success <> 0 then begin FLastError := Format('XCreateAnalyzer(PAN%d) failed: %d', [PanId, Success]); Exit; // пан без спектра: слайсы работают, DispOpen=False гейтит Spectrum0 end; FPans[PanId].DispOpen := True; ApplyPanAnalyzerSettings(PanId); end; procedure TWDSPEngine.DestroyPanAnalyzer(PanId: Integer); // Вызывать под FSliceLock (display-поток читает DispOpen/зовёт GetPixels под ним). begin if not FPans[PanId].DispOpen then Exit; FPans[PanId].DispOpen := False; DestroyAnalyzer(PAN_DISP_BASE + PanId); end; procedure TWDSPEngine.ReapplyPanDisplaySettings; // Перекатывает общие настройки дисплея (FFT/детектор/усреднение/пиксели) на // анализаторы всех активных панов. var p: Integer; begin FSliceLock.Enter; try for p := 1 to MAX_PANS - 1 do if FPans[p].Active then ApplyPanAnalyzerSettings(p); finally FSliceLock.Leave; end; end; procedure TWDSPEngine.ApplyAnalyzerSettings; // Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам. begin ApplyRXAnalyzerSettings; ApplyTXAnalyzerSettings; ReapplyPanDisplaySettings; end; procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double); // Задаёт зум/пан и переармирует RX-анализатор (SetAnalyzer внутри WDSP // защищён собственной критической секцией, дисплей-поток не мешает). var NZ, NP: Double; begin NZ := EnsureRange(AZoom, 0.0, 1.0); NP := EnsureRange(APan, 0.0, 1.0); // Без изменений — выходим БЕЗ переарма: SetAnalyzer сбрасывает FFT-историю, // и спектр/водопад замирают на время наполнения окна (~0.7с @ 192к/131072). // RestoreBand зовёт SetZoomPan безусловно — это било по КАЖДОЙ смене // диапазона, даже когда зум обоих бандов одинаковый (обычно 0). if (Abs(NZ - FZoomFactor) < 1E-9) and (Abs(NP - FPanSlider) < 1E-9) then Exit; FZoomFactor := NZ; FPanSlider := NP; if FAnalyzerOpen then ApplyRXAnalyzerSettings; end; procedure TWDSPEngine.CloseAnalyzer; var p: Integer; 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); // Анализаторы доп. панов (записи FPans сохраняются — пересоздание в OpenAnalyzer). FSliceLock.Enter; try for p := 1 to MAX_PANS - 1 do DestroyPanAnalyzer(p); finally FSliceLock.Leave; end; 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; OpenMainRXChannel; // TXA только что создан узким; забываем «широкую» геометрию прошлой // сессии (RX-половину сбрасывает OpenMainRXChannel). FTXDSPSize := FAudioBufSize; // 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, 1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как // pihpsdr). bfo=0 давал async-чтение кольца r2 со сдвигом outidx → // изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок). ); FTXMode := FMode; SetTXAMode(TXA_CHAN, ModeToWDSPTX(FTXMode)); SetTXAPanelRun(TXA_CHAN, 1); // PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I. SetTXAPanelSelect(TXA_CHAN, 2); FInitialized := True; // Оба канала созданы — только теперь можно поднять dsp_rate под WFM. // SetMode переставит rate, режимы обоих каналов и параметры wfmd/wfmmod; // SetFilter пересчитает полосу уже под новый dsp_rate. if FMode = MODE_WFM then begin SetMode(FMode); SetFilter(FFilterLow, FFilterHigh); end; // Прокидываем все 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; // PureSignal: снять подачу feedback ДО закрытия TXA — pscc зовётся из // сетевого потока напрямую и не должен попасть в закрываемый канал. FPSArmed := False; // Останавливаем 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; CloseMainRXChannel; CloseChannel(TXA_CHAN); FInitialized := False; end; procedure TWDSPEngine.OpenMainRXChannel; // Открывает RXA_CHAN под текущие FBufSize/FSampleRate и восстанавливает его // состояние из полей движка. Канал создаётся «узким» (dsp_rate = FAudioRate); // широкий WFM поднимает вызывающий через SetMode (ApplyDSPRate). begin OpenChannel( RXA_CHAN, FBufSize, FAudioBufSize, FSampleRate, FAudioRate, FAudioRate, 0, 0, // state=0: создать но не запускать 0.010, 0.010, 0.010, 0.010, 0 ); FRXDSPRate := FAudioRate; 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, FShiftHz); 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); end; procedure TWDSPEngine.CloseMainRXChannel; // dmode=0: канал к этому моменту гарантированно idle (Close — DSP-поток уже // мёртв; ChangeSampleRate — FMainGated+барьер), а dmode=1 на простаивающем // канале зря ждёт флаш ~130мс. begin SetChannelState(RXA_CHAN, 0, 0); destroy_anbEXT(RXA_CHAN); destroy_nobEXT(RXA_CHAN); CloseChannel(RXA_CHAN); end; procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer); // Смена rate главного тракта ХИРУРГИЧЕСКИ: DSP-поток, display-поток, // анализаторы и паны N живут — закрываем/переоткрываем только RXA_CHAN и // слайс-каналы пана 0. Пакеты панов идут через общую очередь без пауз: // DSP-поток лишь дропает main-пакеты, пока стоит FMainGated. (Раньше здесь // был полный Close/Open движка — паны замирали на всё время пересборки.) var i: Integer; begin if NewRate = FSampleRate then Exit; if not FInitialized then begin // Движок не открыт: только запоминаем параметры — Open создаст всё сам. 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); FRXAccPos := 0; Exit; end; // Гейт: DSP-поток перестаёт брать main-пакеты (дропает их из очереди); // барьер Enter/Leave гарантирует, что текущий PushIQItemToDSP // (fexchange0 на RXA_CHAN) завершён. FMainGated := True; FMainLock.Enter; FMainLock.Leave; try try // Слайсы пана 0 открыты с InRate = главный rate — закрываем на пересборку. FSliceLock.Enter; try for i := 0 to MAX_SLICES - 1 do if FSlices[i].Opened and (FSlices[i].PanId = 0) then CloseSliceChannel(i); finally FSliceLock.Leave; end; CloseMainRXChannel; // Новый rate/буферы главного тракта. 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); FRXAccPos := 0; FDispPos := 0; AllocSliceBuffers; // Переоткрываем главный канал и слайсы пана 0 (состояние из полей/записей). OpenMainRXChannel; if FMode = MODE_WFM then begin // Канал создан узким — SetMode поднимет dsp_rate под новый FSampleRate, // SetFilter пересчитает полосу (как в Open). SetMode(FMode); SetFilter(FFilterLow, FFilterHigh); end; ApplyNoiseFilterState; FSliceLock.Enter; try for i := 0 to MAX_SLICES - 1 do if FSlices[i].Active and (not FSlices[i].Opened) and (FSlices[i].PanId = 0) then OpenSliceChannel(i); finally FSliceLock.Leave; end; // RX-анализатор не разрушался — переармируем под новый rate. FFT-история // главного дисплея сбрасывается (при смене rate это неизбежно), паны // не затронуты. if FAnalyzerOpen then ApplyRXAnalyzerSettings; // Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate. if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate); except on E: Exception do begin // Главный тракт в неопределённом состоянии — гасим FInitialized: // ProcessRXBlock/слайсы/паны выходят по нему, контроллер увидит // по Initialized и сбросит FWDSPReady. FLastError := E.ClassName + ': ' + E.Message; FInitialized := False; end; end; finally FMainGated := False; end; 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; Item^.PanId := 0; FQueueHead := NextHead; RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR) end; procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte; DataOffset: Integer; IQPairs: Integer); // Сетевой поток: пакет пан-DDC в ОБЩУЮ очередь с меткой пана (порядок прихода // сохраняется, один семафор). Active читаем без лока: Boolean, худший случай — // один лишний пакет в очереди, который DSP-поток отсеет уже под локом. var NextHead: Integer; Item: ^TIQQueueItem; DataBytes: Integer; begin if (PanId < 1) or (PanId >= MAX_PANS) then Exit; if not FPans[PanId].Active then Exit; 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; Item^.PanId := PanId; FQueueHead := NextHead; RTLEventSetEvent(FQueueSem); end; procedure TWDSPEngine.PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem); // DSP-поток: декод 24-бит IQ пан-DDC → feed анализатора пана + аккумулятор; // по накоплению блока — слайсы этого пана. Всё под FSliceLock: Add/RemovePan // и слайс-API меняют состояние из UI-потока. TX-гейты зеркалят главный тракт. var i, Pos: Integer; IR, QR: LongInt; DI, DQ: Double; P: PByte; Pan: ^TPanChan; const SCALE = 1.0 / 8388608.0; begin if FTXActive and not FKeepRXDuringTX then Exit; if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit; if not FInitialized then Exit; FSliceLock.Enter; try Pan := @FPans[PanId]; if not Pan^.Active then Exit; Pos := 0; P := @Item.Data[0]; for i := 0 to Item.IQPairs - 1 do begin if Pos + 5 >= Item.DataLen then Break; IR := (LongInt(P[0]) shl 16) or (LongInt(P[1]) shl 8) or LongInt(P[2]); QR := (LongInt(P[3]) shl 16) or (LongInt(P[4]) shl 8) or LongInt(P[5]); Inc(P, 6); Inc(Pos, 6); if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000); if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000); DI := IR * SCALE; DQ := QR * SCALE; if Pan^.DispOpen then begin Pan^.DispBuf[Pan^.DispPos * 2] := DI; Pan^.DispBuf[Pan^.DispPos * 2 + 1] := DQ; Inc(Pan^.DispPos); if Pan^.DispPos >= DISPLAY_BLOCK_SIZE then begin Spectrum0(1, PAN_DISP_BASE + PanId, 0, 0, @Pan^.DispBuf[0]); Pan^.DispPos := 0; end; end; Pan^.AccI[Pan^.AccPos] := DI; Pan^.AccQ[Pan^.AccPos] := DQ; Inc(Pan^.AccPos); if Pan^.AccPos >= Pan^.BufSize then begin Pan^.AccPos := 0; if Assigned(FOnSliceAudio) or Assigned(FOnSliceDemodAudio) then try ProcessSlicesFor(PanId, Pan^.AccI, Pan^.AccQ, Pan^.BufSize); except // Защита от падения libwdsp в потоке — пропускаем блок (как ProcessRXBlock) end; end; end; finally FSliceLock.Leave; end; 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; DI, DQ: Double; // сэмпл во float — считаем ОДИН раз на всех трёх потребителей AnOpen: Boolean; // FAnalyzerOpen и FBeaconDec внутри цикла не меняются: Bec: TBeaconDecoder; // держим локально, не перечитываем поле на каждый сэмпл P: PByte; // бегущий указатель вместо Pos := i * 6 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; AnOpen := FAnalyzerOpen; Bec := FBeaconDec; Pos := 0; P := @Item.Data[0]; for i := 0 to Item.IQPairs - 1 do begin if Pos + 5 >= Item.DataLen then Break; IR := (LongInt(P[0]) shl 16) or (LongInt(P[1]) shl 8) or LongInt(P[2]); QR := (LongInt(P[3]) shl 16) or (LongInt(P[4]) shl 8) or LongInt(P[5]); Inc(P, 6); Inc(Pos, 6); if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000); if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000); DI := IR * SCALE; DQ := QR * SCALE; // FeedDisplaySample развёрнут сюда: вызов метода на каждый сэмпл (576к/с) // стоил дороже самой работы — двух записей в буфер. if AnOpen then begin FDispBuf[FDispPos * 2] := DI; FDispBuf[FDispPos * 2 + 1] := DQ; Inc(FDispPos); if FDispPos >= DISPLAY_BLOCK_SIZE then begin Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]); FDispPos := 0; end; end; if Bec <> nil then Bec.Feed(DI, DQ); FRXAccI[FRXAccPos] := DI; FRXAccQ[FRXAccPos] := DQ; Inc(FRXAccPos); if FRXAccPos >= FBufSize then ProcessRXBlock; 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 (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)) and (FPostTXMuteUntil = 0) then begin // Decoder tap is independent from speaker mute and volume. This is the // actual demodulator output; digital sync must survive MUTE/volume=0. if Assigned(FOnDemodAudio) then begin for i := 0 to FAudioBufSize - 1 do begin FOutL[i] := FRXOut[i * 2]; FOutR[i] := FRXOut[i * 2 + 1]; end; FOnDemodAudio(FOutL, FOutR, FAudioBufSize); end; if Assigned(FOnAudio) and not FMuted 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; end; // Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы. ProcessSlices; except // Защита от падения libwdsp в потоке — просто пропускаем блок end; end; // --------------------------------------------------------------------------- // Мультислайсы (софт-fan-out) // --------------------------------------------------------------------------- procedure TWDSPEngine.AllocSliceBuffers; // FSliceIn/Out должны вмещать наибольший входной блок среди главного тракта // и активных панов (пан на 768к при главном 192к имеет блок больше FBufSize). var Need, p: Integer; begin Need := FBufSize; for p := 1 to MAX_PANS - 1 do if FPans[p].Active and (FPans[p].BufSize > Need) then Need := FPans[p].BufSize; SetLength(FSliceIn, Need * 2); SetLength(FSliceOut, Need * 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; function TWDSPEngine.SliceInRate(Idx: Integer): Integer; // Входной rate слайса = rate его пана (пан 0 = главный захват). var p: Integer; begin p := FSlices[Idx].PanId; if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then Result := FPans[p].Rate else Result := FSampleRate; end; function TWDSPEngine.SliceInSize(Idx: Integer): Integer; // Входной блок слайса = блок его пана. Выходной блок при этом всегда // FAudioBufSize (in_size/in_rate = const по построению BufSize панов). var p: Integer; begin p := FSlices[Idx].PanId; if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then Result := FPans[p].BufSize else Result := FBufSize; 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, DSPRate, DSPSize, InRate, InSize: Integer; begin if not FInitialized then Exit; if FSlices[Idx].Opened then Exit; Chan := FSlices[Idx].Chan; // Вход канала = rate/блок ПАНА слайса (пан 0 = главный захват). InRate := SliceInRate(Idx); InSize := SliceInSize(Idx); // WFM-слайс, как и главный канал, живёт на WFMDSPRate: rsmpin децимирует // вход, rsmpout сводит к FAudioRate, так что out_size остаётся FAudioBufSize. DSPRate := SliceDSPRateFor(FSlices[Idx].Mode, InRate); DSPSize := FAudioBufSize * (DSPRate div FAudioRate); OpenChannel(Chan, InSize, DSPSize, InRate, DSPRate, FAudioRate, 0, 0, 0.010, 0.010, 0.010, 0.010, 0); // NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open. create_anbEXT(Chan, 1, InSize, InRate, 0.0001, 0.0001, 0.0001, 0.05, 20); create_nobEXT(Chan, 1, 0, InSize, InRate, 0.0001, 0.0001, 0.0001, 0.05, 20); SetRXABandpassWindow(Chan, 1); SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan); if FSlices[Idx].Mode = MODE_DMR then ApplyRXDMRToChan(Chan); if FSlices[Idx].Mode = MODE_FMRAW then ApplyRXRawToChan(Chan); SetRXAShiftRun(Chan, 1); SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate), ClampToNyquist(FSlices[Idx].FilterHi, DSPRate)); 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); ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel); // Decoder input must not disappear or change scale with speaker volume. if FSlices[Idx].Mode in [MODE_DMR, MODE_FMRAW] then SetRXAPanelGain1(Chan, 1.0) else 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); // dmode=0: fexchange0 слайс-каналов зовётся ТОЛЬКО под FSliceLock (ProcessSlices/ // PushPanItemToDSP), и все вызовы CloseSliceChannel — под тем же локом, т.е. // канал заведомо idle. dmode=1 здесь зря ждал флаш ~130мс на канал — при смене // rate главного эта пауза (под локом) замораживала обработку панов. begin if not FSlices[Idx].Opened then Exit; SetChannelState(FSlices[Idx].Chan, 0, 0); destroy_anbEXT(FSlices[Idx].Chan); destroy_nobEXT(FSlices[Idx].Chan); CloseChannel(FSlices[Idx].Chan); FSlices[Idx].Opened := False; end; // Fan-out слайсов главного пана: вызывается из ProcessRXBlock (DSP-поток) // после основного канала. procedure TWDSPEngine.ProcessSlices; begin if not Assigned(FOnSliceAudio) and not Assigned(FOnSliceDemodAudio) then Exit; FSliceLock.Enter; try ProcessSlicesFor(0, FRXAccI, FRXAccQ, FBufSize); finally FSliceLock.Leave; end; end; // Прогоняет входной IQ-блок пана PanId через каждый его активный слайс-канал. // Вызывать ПОД FSliceLock (общая дисциплина слайсов/панов vs DSP). procedure TWDSPEngine.ProcessSlicesFor(PanId: Integer; const AccI, AccQ: array of Double; N: Integer); var s, i, k, Err: Integer; V: Double; begin for s := 0 to MAX_SLICES - 1 do begin if not (FSlices[s].Active and FSlices[s].Opened) then Continue; if FSlices[s].PanId <> PanId then Continue; // Muting a DMR speaker must not make its decoder lose synchronization. if FSlices[s].Muted and not (FSlices[s].Mode in [MODE_DMR, MODE_FMRAW]) then Continue; // Свежая копия входного IQ — fexchange0 портит вход in-place. for i := 0 to N - 1 do begin FSliceIn[i * 2] := AccI[i]; FSliceIn[i * 2 + 1] := AccQ[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); for k := 0 to FAudioBufSize - 1 do begin FSliceOutL[k] := FSliceOut[k * 2]; FSliceOutR[k] := FSliceOut[k * 2 + 1]; end; if FSlices[s].Mode in [MODE_DMR, MODE_FMRAW] then begin if Assigned(FOnSliceDemodAudio) then FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize); Continue; // decoder-grade signal uses the independent pre-volume route end; if Assigned(FOnSliceAudio) then begin V := FSlices[s].Volume; for k := 0 to FAudioBufSize - 1 do begin FSliceOutL[k] := FSliceOutL[k] * V; FSliceOutR[k] := FSliceOutR[k] * V; end; FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize); end; end; end; // --------------------------------------------------------------------------- // Панадаптеры на аппаратных DDC (этап 3.2) // --------------------------------------------------------------------------- function TWDSPEngine.AddPan(PanId, RateHz: Integer): Boolean; begin Result := False; if (PanId < 1) or (PanId >= MAX_PANS) then Exit; // Кратность rate: и BufSize (блок = FAudioBufSize*Rate/FAudioRate), и WDSP // pre_main_build (целочисленное in_rate/dsp_rate) требуют точного отношения. if (RateHz < FAudioRate) or (RateHz mod FAudioRate <> 0) then Exit; FSliceLock.Enter; try if FPans[PanId].Active then Exit; FPans[PanId].Rate := RateHz; FPans[PanId].BufSize := FAudioBufSize * (RateHz div FAudioRate); SetLength(FPans[PanId].AccI, FPans[PanId].BufSize); SetLength(FPans[PanId].AccQ, FPans[PanId].BufSize); SetLength(FPans[PanId].DispBuf, DISPLAY_BLOCK_SIZE * 2); FPans[PanId].AccPos := 0; FPans[PanId].DispPos := 0; FPans[PanId].DispOpen := False; FPans[PanId].ZoomFactor := 0.0; // без зума FPans[PanId].PanSlider := 0.5; FPans[PanId].Active := True; AllocSliceBuffers; // FSliceIn/Out под новый макс. блок if FAnalyzerOpen then CreatePanAnalyzer(PanId); Result := True; finally FSliceLock.Leave; end; end; procedure TWDSPEngine.RemovePan(PanId: Integer); var s: Integer; begin if (PanId < 1) or (PanId >= MAX_PANS) then Exit; FSliceLock.Enter; try if not FPans[PanId].Active then Exit; // Слайсы пана: обычно контроллер уже убрал их (вместе со звуковухами); // здесь — страховка, чтобы канал не остался кормиться из мёртвого пана. for s := 0 to MAX_SLICES - 1 do if FSlices[s].Active and (FSlices[s].PanId = PanId) then begin CloseSliceChannel(s); FSlices[s].Active := False; end; DestroyPanAnalyzer(PanId); FPans[PanId].Active := False; FPans[PanId].AccI := nil; FPans[PanId].AccQ := nil; FPans[PanId].DispBuf := nil; finally FSliceLock.Leave; end; end; function TWDSPEngine.SetPanRate(PanId, RateHz: Integer): Boolean; var s: Integer; begin Result := False; if (PanId < 1) or (PanId >= MAX_PANS) then Exit; // Те же требования кратности, что в AddPan. if (RateHz < FAudioRate) or (RateHz mod FAudioRate <> 0) then Exit; FSliceLock.Enter; try if not FPans[PanId].Active then Exit; if FPans[PanId].Rate = RateHz then Exit(True); // Анализатор держит старый rate — снести до пересбора аккумулятора. DestroyPanAnalyzer(PanId); FPans[PanId].Rate := RateHz; FPans[PanId].BufSize := FAudioBufSize * (RateHz div FAudioRate); SetLength(FPans[PanId].AccI, FPans[PanId].BufSize); SetLength(FPans[PanId].AccQ, FPans[PanId].BufSize); FPans[PanId].AccPos := 0; FPans[PanId].DispPos := 0; AllocSliceBuffers; // FSliceIn/Out под новый макс. блок if FAnalyzerOpen then CreatePanAnalyzer(PanId); // Слайсы пана: их каналы открыты с InRate/InSize пана — переоткрываем // (OpenSliceChannel восстанавливает mode/filter/AGC/DSP из записи слайса). for s := 0 to MAX_SLICES - 1 do if FSlices[s].Active and (FSlices[s].PanId = PanId) then begin CloseSliceChannel(s); OpenSliceChannel(s); end; Result := True; finally FSliceLock.Leave; end; end; procedure TWDSPEngine.SetPanZoomPan(PanId: Integer; AZoom, APan: Double); // Переармирует анализатор пана под новый span-clip. No-op guard обязателен: // SetAnalyzer сбрасывает FFT-историю (урок SetZoomPan главного, dae0c19). var NZ, NP: Double; begin if (PanId < 1) or (PanId >= MAX_PANS) then Exit; NZ := EnsureRange(AZoom, 0.0, 1.0); NP := EnsureRange(APan, 0.0, 1.0); FSliceLock.Enter; try if not FPans[PanId].Active then Exit; if (Abs(NZ - FPans[PanId].ZoomFactor) < 1E-9) and (Abs(NP - FPans[PanId].PanSlider) < 1E-9) then Exit; FPans[PanId].ZoomFactor := NZ; FPans[PanId].PanSlider := NP; ApplyPanAnalyzerSettings(PanId); // внутри гейт по DispOpen finally FSliceLock.Leave; end; end; function TWDSPEngine.PanActive(PanId: Integer): Boolean; begin Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active; end; function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer; ShiftHz, Volume: Double; AGC: TWDSPAGCMode; APanId: Integer): Boolean; var i, slot: Integer; begin Result := False; FSliceLock.Enter; try if FindSliceIdx(Id) >= 0 then Exit; // уже существует if (APanId < 0) or (APanId >= MAX_PANS) then Exit; if (APanId > 0) and not FPans[APanId].Active 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].PanId := APanId; 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; Rerate: Boolean; begin FSliceLock.Enter; try idx := FindSliceIdx(Id); if idx < 0 then Exit; // Уход в/из WFM меняет dsp_rate канала — проще пересоздать его целиком, // чем гонять SetDSPSamplerate: слайс всё равно перестраивает все фильтры. Rerate := FSlices[idx].Opened and (SliceDSPRateFor(Mode, SliceInRate(idx)) <> SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx))); FSlices[idx].Mode := Mode; if Rerate then begin CloseSliceChannel(idx); OpenSliceChannel(idx); end else if FSlices[idx].Opened then begin SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode)); if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan); if Mode = MODE_DMR then ApplyRXDMRToChan(FSlices[idx].Chan); if Mode = MODE_FMRAW then ApplyRXRawToChan(FSlices[idx].Chan); if Mode in [MODE_DMR, MODE_FMRAW] then SetRXAPanelGain1(FSlices[idx].Chan, 1.0) else SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume); end; finally FSliceLock.Leave; end; end; procedure TWDSPEngine.SetSliceFilter(Id, Low, High: Integer); var idx, Rate: 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 begin Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx)); RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate), ClampToNyquist(High, Rate)); end; 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 and not (FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW]) 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; procedure TWDSPEngine.SetSliceFMSquelch(Id: Integer; Enable: Boolean; Level: Integer); var idx: Integer; begin FSliceLock.Enter; try idx := FindSliceIdx(Id); if idx < 0 then Exit; FSlices[idx].FMSQOn := Enable; FSlices[idx].FMSQLevel := Level; if FSlices[idx].Opened then ApplyFMSQToChan(FSlices[idx].Chan, Enable, Level); 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; FTXMode := Mode; if not FInitialized then Exit; // Rate — до режима: setDSPSamplerate_rxa перестраивает всю цепь (в т.ч. wfmd), // а SetRXAMode затем пересчитает bpsnba под уже актуальный dsp_rate. ApplyDSPRate(Mode); SetRXAMode(RXA_CHAN, ModeToWDSP(Mode)); SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode)); if Mode = MODE_WFM then ApplyWFMSettings else if Mode = MODE_DMR then begin SetRXAWFMDeviation (RXA_CHAN, DMR_DEVIATION); SetRXAWFMAFFilter (RXA_CHAN, 20.0, 7000.0); SetRXAWFMDeemphRun (RXA_CHAN, 0); end else if Mode = MODE_FMRAW then ApplyRXRawToChan(RXA_CHAN); ApplyTXModeSettings(FTXMode); // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI end; procedure TWDSPEngine.SetFilter(Low, High: Integer); begin FFilterLow := Low; FFilterHigh := High; if not FInitialized then Exit; // Кромки не могут выходить за Найквист текущего dsp_rate: в WFM они доходят // до ±100 кГц, а dsp_rate зависит от SampleRate. Клампим только то, что // уходит в WDSP — запрошенные значения храним как есть, иначе понижение // SampleRate необратимо «съело» бы полосу. // RXASetPassband — правильный unified API, пересчитывает фильтр целиком RXASetPassband(RXA_CHAN, ClampToNyquist(Low, FRXDSPRate), ClampToNyquist(High, FRXDSPRate)); 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; // FM-squelch на произвольном канале (RXA или слайс). Порог 0..100 → 0.0..0.5, // как в SetFMSquelch для главного приёмника. procedure TWDSPEngine.ApplyFMSQToChan(Chan: Integer; Enable: Boolean; Level: Integer); begin if Enable then begin SetRXAFMSQThreshold(Chan, Level / 200.0); SetRXAFMSQRun(Chan, 1); end else SetRXAFMSQRun(Chan, 0); 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; if FMode = MODE_FMRAW then SetRawFMDeviation(Deviation) else if FMode = MODE_DMR then SetRXAWFMDeviation(RXA_CHAN, Deviation) else SetRXAFMDeviation(RXA_CHAN, Deviation); end; procedure TWDSPEngine.SetRawFMDeviation(Deviation: Double); var i: Integer; begin FRawFMDeviation := EnsureRange(Deviation, 500.0, 10000.0); if not FInitialized then Exit; if FMode = MODE_FMRAW then SetRXAWFMDeviation(RXA_CHAN, FRawFMDeviation); if FTXMode = MODE_FMRAW then SetTXAWFMDeviation(TXA_CHAN, FRawFMDeviation); // One calibration value applies to every active FM RAW decoder output. FSliceLock.Enter; try for i := 0 to MAX_SLICES - 1 do if FSlices[i].Active and FSlices[i].Opened and (FSlices[i].Mode = MODE_FMRAW) then SetRXAWFMDeviation(FSlices[i].Chan, FRawFMDeviation); finally FSliceLock.Leave; end; 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 FTXMode := Mode; if not FInitialized then Exit; SetTXAMode(TXA_CHAN, ModeToWDSPTX(FTXMode)); ApplyTXModeSettings(FTXMode); end; procedure TWDSPEngine.SetTXFilter(Low, High: Integer); begin if not FInitialized then Exit; // In FM RAW this is an RF-channel-width update mirrored from the RX UI, // not an audio passband request. The flat modem AF passband is fixed above. if FTXMode = MODE_FMRAW 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.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double); // 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal. // Амплитуды 0.49+0.49 — сумма чуть ниже полной шкалы (как Thetis/pihpsdr). var F1, F2: Double; begin if not FInitialized then Exit; FTXTTOn := Enabled; // мьют mic-входа (см. ProcessTXBlock) if Enabled then begin // Для нижней боковой тона должны лежать НИЖЕ несущей. if (FMode = MODE_LSB) or (FMode = MODE_CWL) or (FMode = MODE_DIGL) then begin F1 := -Abs(Freq1); F2 := -Abs(Freq2); end else begin F1 := Abs(Freq1); F2 := Abs(Freq2); end; SetTXAPostGenMode(TXA_CHAN, 1); // 1 = two-tone SetTXAPostGenTTFreq(TXA_CHAN, F1, F2); SetTXAPostGenTTMag(TXA_CHAN, 0.49, 0.49); SetTXAPostGenRun(TXA_CHAN, 1); end else SetTXAPostGenRun(TXA_CHAN, 0); end; procedure TWDSPEngine.SetTXCFIR(Enabled: Boolean); begin if not FInitialized then Exit; SetTXACFIRRun(TXA_CHAN, Ord(Enabled)); 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/2TON: ринг потребляем (иначе поток зациклится на «есть полный блок»), // но в TXA подаём тишину — тон PostGen должен идти без голоса поверх. if FTXToneOn or FTXTTOn 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, EffectiveTXNC); 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)); // PureSignal (calcc живёт внутри TXA-канала — после пересоздания канала // все его параметры надо прокинуть заново) ApplyPSParams; end; procedure TWDSPEngine.ApplyPSParams; begin if not FInitialized then Exit; // Feedback приходит с DDC0/1 @ 192 кС/с (фиксированный PS-rate протокола 2) SetPSFeedbackRate(TXA_CHAN, TX_SAMPLE_RATE); SetPSMoxDelay(TXA_CHAN, FPSMoxDelay); SetPSLoopDelay(TXA_CHAN, FPSLoopDelay); SetPSTXDelay(TXA_CHAN, FPSTXDelayNs * 1.0e-9); if FPSRelaxPtol then SetPSPtol(TXA_CHAN, 0.400) else SetPSPtol(TXA_CHAN, 0.800); SetPSPinMode(TXA_CHAN, Ord(FPSPin)); SetPSMapMode(TXA_CHAN, Ord(FPSMap)); SetPSStabilize(TXA_CHAN, Ord(FPSStabilize)); SetPSIntsAndSpi(TXA_CHAN, FPSInts, FPSSpi); SetPSHWPeak(TXA_CHAN, FPSHWPeak); SetPSOutlierSigma(TXA_CHAN, FPSOutlierSigma); end; procedure TWDSPEngine.PSSetParams(MoxDelay, LoopDelay: Double; TXDelayNs: Integer; RelaxPtol: Boolean; Pin, Map, Stabilize: Boolean; Ints, Spi: Integer; HWPeak: Double; OutlierSigma: Double); begin FPSMoxDelay := MoxDelay; FPSLoopDelay := LoopDelay; FPSTXDelayNs := TXDelayNs; FPSRelaxPtol := RelaxPtol; FPSPin := Pin; FPSMap := Map; FPSStabilize := Stabilize; if Ints > 0 then FPSInts := Ints; if Spi > 0 then FPSSpi := Spi; if HWPeak > 0 then FPSHWPeak := HWPeak; if OutlierSigma >= 0 then FPSOutlierSigma := OutlierSigma; ApplyPSParams; end; procedure TWDSPEngine.SetPureSignalArmed(Armed: Boolean); begin FPSArmed := Armed; end; procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte; DataOffset: Integer; TotalPairs: Integer); // Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... → // деинтерливим и кормим WDSP pscc (как pihpsdr process_ps_iq_data): // DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон). var i, n, Pos: Integer; IR, QR: LongInt; P: PByte; const SCALE = 1.0 / 8388608.0; begin if not (FPSArmed and FInitialized) then Exit; n := TotalPairs div 2; // пар на каждый поток if n > 256 then n := 256; // ёмкость FPSTx/RxBuf if n <= 0 then Exit; if (DataOffset + n * 2 * 6) > Length(Buf) then Exit; P := @Buf[DataOffset]; for i := 0 to n * 2 - 1 do begin IR := (LongInt(P[0]) shl 16) or (LongInt(P[1]) shl 8) or LongInt(P[2]); QR := (LongInt(P[3]) shl 16) or (LongInt(P[4]) shl 8) or LongInt(P[5]); Inc(P, 6); if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000); if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000); Pos := i shr 1; // индекс пары в потоке if (i and 1) = 0 then begin FPSRxBuf[Pos * 2] := IR * SCALE; // чётные = DDC0 (RX feedback) FPSRxBuf[Pos * 2 + 1] := QR * SCALE; end else begin FPSTxBuf[Pos * 2] := IR * SCALE; // нечётные = DDC1 (TX DAC) FPSTxBuf[Pos * 2 + 1] := QR * SCALE; end; end; pscc(TXA_CHAN, n, @FPSTxBuf[0], @FPSRxBuf[0]); end; procedure TWDSPEngine.PSControl(Reset, Mancal, Automode, Turnon: Integer); begin if not FInitialized then Exit; SetPSControl(TXA_CHAN, Reset, Mancal, Automode, Turnon); end; procedure TWDSPEngine.PSSetMox(Mox: Boolean); begin if not FInitialized then Exit; SetPSMox(TXA_CHAN, Ord(Mox)); end; procedure TWDSPEngine.PSGetInfo(var Info: array of Integer); begin if (not FInitialized) or (Length(Info) < 16) then Exit; GetPSInfo(TXA_CHAN, @Info[0]); end; function TWDSPEngine.PSGetMaxTX: Double; begin Result := 0.0; if not FInitialized then Exit; GetPSMaxTX(TXA_CHAN, @Result); 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, EffectiveTXNC); 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; WfBuf: array[0..SPECTRUM_PIXELS - 1] of Single; Flag: Integer; i, N, p: Integer; DispID: Integer; Cal: Single; GotSpec, GotWf: Boolean; begin if not FInitialized or not FAnalyzerOpen then Exit; N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive // Калибровка дисплея (дБ) — сдвиг всей трассы, спрашиваем раз в кадр. // На TX-дисплее коррекция входного тракта не при чём. if Assigned(FOnDispCal) and (DispID = RX_DISP_ID) then Cal := FOnDispCal(0) else Cal := 0; Flag := 0; GetPixels(DispID, 0, @PixBuf[0], @Flag); if Flag <> 0 then begin for i := 0 to N - 1 do FSpectrumPixels[i] := PixBuf[i] + Cal; 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] + Cal; if Assigned(FOnWaterfall) then FOnWaterfall(FWaterfallPixels, N); end; // Доп. паны: GetPixels под FSliceLock (гонка с DestroyPanAnalyzer из // RemovePan в UI-потоке), колбэки — уже без лока (UI в них может звать // slice/pan API). Пиксели пана не кэшируем (аналог FSpectrumPixels не нужен). if Assigned(FOnPanSpectrum) or Assigned(FOnPanWaterfall) then for p := 1 to MAX_PANS - 1 do begin GotSpec := False; GotWf := False; FSliceLock.Enter; try if FPans[p].Active and FPans[p].DispOpen then begin Flag := 0; GetPixels(PAN_DISP_BASE + p, 0, @PixBuf[0], @Flag); GotSpec := Flag <> 0; Flag := 0; GetPixels(PAN_DISP_BASE + p, 1, @WfBuf[0], @Flag); GotWf := Flag <> 0; end; finally FSliceLock.Leave; end; // Пан может стоять на своём диапазоне — офсет спрашиваем по его PanId. if GotSpec or GotWf then begin if Assigned(FOnDispCal) then Cal := FOnDispCal(p) else Cal := 0; if Cal <> 0 then begin if GotSpec then for i := 0 to N - 1 do PixBuf[i] := PixBuf[i] + Cal; if GotWf then for i := 0 to N - 1 do WfBuf[i] := WfBuf[i] + Cal; end; end; if GotSpec and Assigned(FOnPanSpectrum) then FOnPanSpectrum(p, PixBuf, N); if GotWf and Assigned(FOnPanWaterfall) then FOnPanWaterfall(p, WfBuf, 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. // Калибровочный офсет здесь НЕ прибавляется: он зависит от диапазона/ // трансвертера, о которых движок не знает — его добавляет TRadioController // (ReadSMeterDBm / SliceSMeter). 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)); ReapplyPanDisplaySettings; // детектор/усреднение общие с панами 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); ReapplyPanDisplaySettings; // детектор/усреднение общие с панами end; end.