Files
ewsdr/WDSPEngine.pas
ew8bakandClaude Opus 5 6195c80ce3 fix(tx): полоса ЧМ на передаче — занимаемая полоса, а не кромки bp0
TXFilterEdgesHz была тонкой обёрткой над TXSignedEdges, то есть отдавала
в отрисовку кромки аудио-полосовика перед модулятором. У SSB/CW/DIGI/AM
это то же самое, что полоса вокруг несущей, а у ЧМ — нет: FM RAW рисовался
как (20..7000), WFM как (20..15000), то есть полоса уезжала вверх от
несущей и визуально «переключалась на USB».

Теперь у экранных кромок своя семантика — Карсон ±(девиация + верхний срез
аудио) для FM/WFM/FM RAW, остальные режимы как были. TXSignedEdges и
SetTXABandpassFreqs не тронуты: bp0 остаётся портом Thetis, в эфире ничего
не меняется. Попутно 20.0/7000.0 из трёх мест FM RAW сведены в
RAW_AF_LOW/RAW_AF_HIGH.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:09:31 +03:00

4196 lines
171 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit WDSPEngine;
{
WDSP DSP Engine for OpenHPSDR Transceiver
Correct WDSP API usage based on actual WDSP.pas bindings:
Spectrum pipeline:
XCreateAnalyzer(disp, ...) — создать analyzer display
SetDisplay*(disp, ...) — настроить detector/average/rate
SetRXASpectrum(ch, 1, disp, 0, 0) — подключить RXA к display
fexchange0() в цикле → WDSP внутри пишет данные в display буфер
Spectrum0(1, disp, 0, 0, nil) — тригер snapshot
GetPixels(disp, 0, pix, flag) — забрать пиксели
OpenChannel сигнатура (13 параметров):
channel, in_size, dsp_size, in_rate, dsp_rate, out_rate,
atype, state, tdelayup, tslewup, tdelaydown, tslewdown, bfo
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math, SyncObjs,
BeaconDecoder, 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;
// FM RAW: плоский baseband для внешних декодеров — одна величина на RX-AF,
// TX-AF и bp0 (раньше 20/7000 стояли числами в трёх местах).
RAW_AF_LOW = 20.0;
RAW_AF_HIGH = 7000.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 ApplyTXWFMSettings;
// Единая RX-режимная надстройка канала (WFM/DMR/FM RAW). Одна точка для
// главного RXA и для слайсов — иначе дублирование по трём местам.
procedure ApplyRXModeSettings(Chan, Mode: Integer);
// Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима.
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака,
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// 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);
// ЕДИНАЯ точка перевода TX-тракта в режим (модулятор + режимная цепь).
// Через неё идут ВСЕ переходы: Open, SetMode, SetTXMode (в т.ч. слайс-TX).
procedure ApplyTXMode(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 ApplySliceFMSQ(Idx: Integer); // squelch слайса с гейтом по режиму
procedure ApplyNoiseFilterState;
procedure ProcessTXBlock; // вызывается из TTXDSPThread
procedure FeedTXDisplaySample(const I, Q: Double);
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
procedure PushTXEQProfile; // кривая EQ в WDSP (узлы + legacy 3 полосы)
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 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);
// Знаковые края TX-полосы текущего режима передачи — ровно то, что стоит
// в bp0. Нужны спектру, чтобы на передаче рисовать TX-ширину, а не RX.
procedure TXFilterEdgesHz(out Lo, Hi: 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, RAW_AF_LOW, RAW_AF_HIGH);
SetRXAWFMDeemphRun (Chan, 0);
SetRXAWFMLimRun (Chan, 0);
end;
procedure TWDSPEngine.ApplyTXWFMSettings;
// TX-часть широкого ЧМ. RX-сторона WFM ставится через ApplyRXModeSettings.
begin
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.ApplyRXModeSettings(Chan, Mode: Integer);
// Режимная RX-надстройка канала. Для «нормальных» режимов ничего не нужно —
// всё делает SetRXAMode; спец-обвязку требуют только FM-семейства.
begin
if Mode = MODE_WFM then ApplyRXWFMToChan(Chan)
else if Mode = MODE_DMR then ApplyRXDMRToChan(Chan)
else if Mode = MODE_FMRAW then ApplyRXRawToChan(Chan);
end;
procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// Порт Thetis UpdateTXLowHighFilterForMode. Вход — магнитуда TX-аудио-полосы
// (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под
// боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты.
var
L, H, Half: Double;
begin
L := FTXFilterLow;
H := FTXFilterHigh;
case Mode of
MODE_LSB, MODE_CWL, MODE_DIGL:
begin Lo := -H; Hi := -L; end; // нижняя боковая
MODE_USB, MODE_CWU, MODE_DIGU:
begin Lo := L; Hi := H; end; // верхняя боковая
MODE_DSB, MODE_AM, MODE_SAM:
begin Lo := -H; Hi := H; end; // симметрично
MODE_FM:
begin // Карсон: ±(девиация + верхний срез)
Half := FTXFMDeviation + FTXFMHighCut;
Lo := -Half; Hi := Half;
end;
MODE_WFM:
// Широкий ЧМ: bp0 — это АУДИО-полосовик перед модулятором, а не РЧ-полоса
// канала (FILT_WFM_BW=120..200 кГц — селективность RX, другая величина).
// Открываем его в аудио-полосу вещательного FM = RX AF-фильтр WFM
// (ApplyRXWFMToChan). Без этого else-ветка душила бы аудио до (200,3100).
begin Lo := WFM_AF_LOW; Hi := WFM_AF_HIGH; end;
MODE_FMRAW:
// Плоский baseband для внешних декодеров (совпадает с RX AF в ApplyRXRawToChan
// и с явным SetTXABandpassFreqs в ApplyTXModeSettings). РЧ-ширина FILT_RAW_BW
// (6..25 кГц) — селективность RX, к аудио-полосовику отношения не имеет.
begin Lo := RAW_AF_LOW; Hi := RAW_AF_HIGH; end;
else
begin Lo := L; Hi := H; end; // прочее — как есть
end;
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, RAW_AF_LOW, RAW_AF_HIGH);
SetTXAWFMPreEmphRun (TXA_CHAN, 0);
SetTXABandpassFreqs (TXA_CHAN, RAW_AF_LOW, RAW_AF_HIGH);
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.
// bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через
// TXASetupBPFilters (как в Thetis). bp0 всегда активен сам.
ApplyTXChainSettings;
if Mode = MODE_WFM then ApplyTXWFMSettings;
end;
end;
procedure TWDSPEngine.ApplyTXMode(Mode: Integer);
// ЕДИНАЯ точка перевода TX-тракта в режим. SetTXAMode пересобирает модулятор и
// bp0/bp1 под режим (TXASetupBPFilters), ApplyTXModeSettings кладёт режимную цепь.
begin
if not FInitialized then Exit;
SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
ApplyTXModeSettings(Mode);
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 := 200;
FTXFilterHigh := 3100;
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;
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); // внутри: ApplyRXModeSettings + ApplyTXMode
SetFilter(FFilterLow, FFilterHigh);
end;
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
ApplyTXChainSettings;
// Перевод TX-тракта в режим через единую точку (для WFM уже сделал SetMode).
if FMode <> MODE_WFM then ApplyTXMode(FTXMode);
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 первые
// ~50200мс после 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));
ApplyRXModeSettings(Chan, FSlices[Idx].Mode);
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 and (FSlices[Idx].Mode = MODE_FM),
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));
ApplyRXModeSettings(FSlices[idx].Chan, Mode);
if Mode in [MODE_DMR, MODE_FMRAW] then
SetRXAPanelGain1(FSlices[idx].Chan, 1.0)
else
SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume);
// Ушли из FM — снять squelch (иначе слайс остаётся заглушенным, а
// шумодава в не-FM режимах нет); вернулись в FM — восстановить выбор.
ApplySliceFMSQ(idx);
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;
ApplySliceFMSQ(idx);
finally
FSliceLock.Leave;
end;
end;
// Squelch слайса с гейтом по режиму — как у главного приёмника
// (ApplyModeFilter: включён только в MODE_FM). Блок fmsq WDSP стоит в тракте
// и глушит звук по шумовому выходу FM-дискриминатора: оставшись включённым
// после ухода из FM, он затыкает слайс намертво, а органов управления
// шумодавом в не-FM режимах нет. Пользовательский выбор при этом сохраняется
// в FMSQOn и возвращается вместе с режимом FM.
procedure TWDSPEngine.ApplySliceFMSQ(Idx: Integer);
begin
if (Idx < 0) or (Idx > High(FSlices)) or not FSlices[Idx].Opened then Exit;
ApplyFMSQToChan(FSlices[Idx].Chan,
FSlices[Idx].FMSQOn and (FSlices[Idx].Mode = MODE_FM),
FSlices[Idx].FMSQLevel);
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));
ApplyRXModeSettings(RXA_CHAN, Mode);
ApplyTXMode(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;
ApplyTXMode(FTXMode);
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
BpLo, BpHi: Double;
begin
if not FInitialized then Exit;
// Bandpass — магнитуду TX-полосы прогоняем через знак боковой режима
// (иначе LSB/DIGL уходят в USB, когда этот пуш оказывается последним, напр. MOX).
if (FTXFilterLow > 0) and (FTXFilterHigh > FTXFilterLow) then
begin
TXSignedEdges(FTXMode, BpLo, BpHi);
SetTXABandpassFreqs(TXA_CHAN, BpLo, BpHi);
end;
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 ([0]=preamp, [1..10]=точки кривой)
PushTXEQProfile;
// 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.TXFilterEdgesHz(out Lo, Hi: Integer);
// ЭКРАННЫЕ кромки — занимаемая полоса вокруг несущей, а НЕ bp0. У SSB/CW/DIGI/
// AM/DSB это одно и то же, поэтому здесь и стояла обёртка над TXSignedEdges.
// Но у ЧМ bp0 — АУДИО-полосовик ПЕРЕД модулятором: у FM RAW он (20..7000), у
// WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально
// «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез
// аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту
// величину и без нас (порт Thetis), но пусть формула будет в одном месте.
var L, H, Half: Double;
begin
Half := 0.0;
case FTXMode of
MODE_FM: Half := FTXFMDeviation + FTXFMHighCut;
MODE_WFM: Half := WFM_DEVIATION + WFM_AF_HIGH;
MODE_FMRAW: Half := FRawFMDeviation + RAW_AF_HIGH;
end;
if Half > 0.0 then
begin
Lo := -Round(Half);
Hi := Round(Half);
Exit;
end;
TXSignedEdges(FTXMode, L, H);
Lo := Round(L);
Hi := Round(H);
end;
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer);
var BpLo, BpHi: Double;
begin
FTXFilterLow := Low;
FTXFilterHigh := High;
FTXFilterNC := NC;
FTXFilterMP := MP;
FTXFilterWindow := Window;
FFilterLow := Low;
FFilterHigh := High;
if not FInitialized then Exit;
// Магнитуда диалога → знаковые края под боковую режима (см. TXSignedEdges).
TXSignedEdges(FTXMode, BpLo, BpHi);
SetTXABandpassFreqs(TXA_CHAN, BpLo, BpHi);
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.PushTXEQProfile;
// Единственная точка, отдающая кривую EQ в WDSP.
// F[1..n] — УЗЛЫ ломаной АЧХ (eq.c/eq_impulse), между ними интерполяция по
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
// см. заводские ESSB-профили в Settings.DefaultTXProfiles);
// • 3 полосы — legacy-раскладка самого WDSP (SetTXAGrphEQ): 150/400/1500/
// 6000, ручка Low держит полку 150..400, High тянет до 6 кГц. Отдавать
// здесь первые три узла 10-полосной сетки (100/200/400) нельзя — весь
// голос выше 400 Гц ушёл бы в ноль.
var
F, G: array[0..10] of Double;
i, NF: Integer;
begin
if not FInitialized then Exit;
if FTXEQNumBands = 3 then
begin
NF := 4;
F[0] := 0.0; G[0] := FTXEQGains[0]; // preamp
F[1] := 150.0; G[1] := FTXEQGains[1]; // Low
F[2] := 400.0; G[2] := FTXEQGains[1];
F[3] := 1500.0; G[3] := FTXEQGains[2]; // Mid
F[4] := 6000.0; G[4] := FTXEQGains[3]; // High
end
else
begin
NF := 10;
for i := 0 to 10 do
begin
F[i] := FTXEQFreqs[i];
G[i] := FTXEQGains[i];
end;
end;
SetTXAEQProfile(TXA_CHAN, NF, @F[0], @G[0]);
SetTXAEQRun(TXA_CHAN, Ord(FTXEQOn));
end;
procedure TWDSPEngine.SetTXEQ(Enable: Boolean; NumBands: Integer;
const Gains, Freqs: array of Double);
var
i, n: Integer;
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];
end;
PushTXEQProfile;
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.