mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
SetZoomPan вызывался из RestoreBand безусловно и переармировал RX-анализатор (SetAnalyzer = сброс FFT-истории) даже когда zoom/pan не менялись — дисплей замирал на время наполнения окна. Теперь no-op guard: без изменений выходим без переарма. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3478 lines
136 KiB
ObjectPascal
3478 lines
136 KiB
ObjectPascal
unit WDSPEngine;
|
||
|
||
{
|
||
WDSP DSP Engine for OpenHPSDR Transceiver
|
||
Correct WDSP API usage based on actual WDSP.pas bindings:
|
||
|
||
Spectrum pipeline:
|
||
XCreateAnalyzer(disp, ...) — создать analyzer display
|
||
SetDisplay*(disp, ...) — настроить detector/average/rate
|
||
SetRXASpectrum(ch, 1, disp, 0, 0) — подключить RXA к display
|
||
fexchange0() в цикле → WDSP внутри пишет данные в display буфер
|
||
Spectrum0(1, disp, 0, 0, nil) — тригер snapshot
|
||
GetPixels(disp, 0, pix, flag) — забрать пиксели
|
||
|
||
OpenChannel сигнатура (13 параметров):
|
||
channel, in_size, dsp_size, in_rate, dsp_rate, out_rate,
|
||
atype, state, tdelayup, tslewup, tdelaydown, tslewdown, bfo
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, Math, SyncObjs,
|
||
BeaconDecoder,
|
||
WDSP;
|
||
|
||
const
|
||
// WDSP mode integers (нет именованных констант в WDSP.pas).
|
||
// Значения — 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 = 0;
|
||
MODE_USB = 1;
|
||
MODE_DSB = 2;
|
||
MODE_CWL = 3;
|
||
MODE_CWU = 4;
|
||
MODE_FM = 5;
|
||
MODE_AM = 6;
|
||
MODE_SAM = 7;
|
||
MODE_DIGU = 8;
|
||
MODE_DIGL = 9;
|
||
MODE_WFM = 10;
|
||
|
||
// Минимальная длина 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 мкс)
|
||
// Потолок 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;
|
||
// Пиксели спектра/водопада доп. пана — из 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) жив
|
||
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;
|
||
|
||
FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||
FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||
FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
|
||
|
||
// Настройки анализатора
|
||
FFFTSize: Integer;
|
||
FWindowType: Integer;
|
||
FDisplayFPS: Integer;
|
||
FEffectiveDisplayFPS: Double;
|
||
FDisplayPixelCount: Integer;
|
||
FSpecDetector: Integer;
|
||
FSpecAvgMode: Integer;
|
||
FSpecAvgTimeMS: Double;
|
||
FWfDetector: Integer;
|
||
FWfAvgMode: Integer;
|
||
FWfAvgTimeMS: Double;
|
||
|
||
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
|
||
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
|
||
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
|
||
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
|
||
// пересчитываются в ApplyRXAnalyzerSettings.
|
||
FZoomFactor: Double;
|
||
FPanSlider: Double;
|
||
FViewLowHz: Double;
|
||
FViewHighHz: Double;
|
||
|
||
FOnAudio: TOnAudioReady;
|
||
FOnSpectrum: TOnSpectrumReady;
|
||
FOnWaterfall: TOnWaterfallReady;
|
||
FOnTXIQ: TOnTXIQReady;
|
||
|
||
// --- Мультислайсы ---
|
||
FSlices: array[0..MAX_SLICES-1] of TSliceChan;
|
||
FSliceLock: TCriticalSection; // сериализует add/remove/set vs DSP-обработку
|
||
FSliceIn: array of Double; // interleaved вход fexchange0 (переиспользуем)
|
||
FSliceOut: array of Double; // interleaved выход fexchange0
|
||
FSliceOutL: array of Single; // деинтерливленный L для callback
|
||
FSliceOutR: array of Single; // R
|
||
FOnSliceAudio: TOnSliceAudio;
|
||
|
||
// --- Панадаптеры на аппаратных 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;
|
||
|
||
// Кэшированные TX-параметры — применяются после OpenChannel(TXA) и при изменении.
|
||
// Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS.
|
||
FTXFilterLow: Integer;
|
||
FTXFilterHigh: Integer;
|
||
FTXFilterNC: Integer;
|
||
FTXFilterMP: Integer;
|
||
FTXFilterWindow: Integer;
|
||
FTXMicGainDB: Double;
|
||
FTXCompressorOn: Boolean;
|
||
FTXCompressorGain:Double;
|
||
FTXLevelerOn: Boolean;
|
||
FTXLevelerTop: Double;
|
||
FTXLevelerDecay: Integer;
|
||
FTXALCOn: Boolean;
|
||
FTXALCMaxGain: Double;
|
||
FTXALCDecay: Integer;
|
||
FTXPhaseRotOn: Boolean;
|
||
FTXPhaseRotStages:Integer;
|
||
FTXPhaseRotFreq: Double;
|
||
FTXEQOn: Boolean;
|
||
FTXEQNumBands: Integer;
|
||
FTXEQGains: array[0..10] of Double;
|
||
FTXEQFreqs: array[0..10] of Double;
|
||
FTXAMCarrier: Double;
|
||
FTXFMDeviation: Double;
|
||
FTXFMLowCut: Integer;
|
||
FTXFMHighCut: Integer;
|
||
FTXFMEmphPos: Integer;
|
||
FTXCTCSSOn: Boolean;
|
||
FTXCTCSSFreq: Double;
|
||
|
||
// TX-display параметры (отдельные от RX) — применяются к TX_DISP_ID.
|
||
FTXFFTSize: Integer;
|
||
FTXWindowType: Integer;
|
||
FTXSpecDetector: Integer;
|
||
FTXSpecAvgMode: Integer;
|
||
FTXSpecAvgTimeMS: Double;
|
||
FTXWfDetector: Integer;
|
||
FTXWfAvgMode: Integer;
|
||
FTXWfAvgTimeMS: Double;
|
||
|
||
FMode: Integer;
|
||
FSMeter: Double;
|
||
FFilterLow: Integer;
|
||
FFilterHigh: Integer;
|
||
FAGCMode: TWDSPAGCMode;
|
||
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR
|
||
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
|
||
FAGCHangThreshold: Integer; // порог hang 0..100
|
||
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
|
||
FAGCThresh: Double; // читается из WDSP (GetRXAAGCThresh)
|
||
FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс)
|
||
FDisplayResLimit: Integer; // потолок точек анализатора (настройка display_pixels)
|
||
FShiftHz: Double; // NCO shift для CTUN
|
||
FNRMode: Integer;
|
||
FNBMode: Integer;
|
||
FSNBEnabled: Boolean;
|
||
FANFEnabled: Boolean;
|
||
FMuted: Boolean;
|
||
FVolume: Double;
|
||
FLastError: string;
|
||
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
|
||
|
||
function ModeToWDSP(Mode: Integer): Integer;
|
||
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 ApplyWFMSettings;
|
||
// --- Мультислайсы ---
|
||
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 OpenAnalyzer;
|
||
procedure CloseAnalyzer;
|
||
procedure ApplyAnalyzerSettings;
|
||
function CalcDisplayFFTSize: Integer;
|
||
procedure CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
|
||
out EffectiveFPS: Double);
|
||
function NormalizeFFTSize(FFTSize: Integer): Integer;
|
||
procedure NormalizeDisplayParams(var Detector, AvgMode: Integer;
|
||
var AvgTimeMS: Double; ForWaterfall: Boolean);
|
||
procedure AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
|
||
out AvgCount: Integer; out Backmult: Double);
|
||
procedure ApplyNRState;
|
||
procedure ApplyNBState;
|
||
procedure ApplySNBState;
|
||
procedure ApplyANFState;
|
||
procedure ApplyNRToChan(Chan, NRMode: Integer);
|
||
procedure ApplyNBToChan(Chan, NBMode: Integer);
|
||
procedure ApplySNBToChan(Chan: Integer; On_: Boolean);
|
||
procedure ApplyANFToChan(Chan: Integer; On_: Boolean);
|
||
procedure ApplyFMSQToChan(Chan: Integer; Enable: Boolean; Level: Integer);
|
||
procedure ApplyNoiseFilterState;
|
||
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
||
procedure FeedTXDisplaySample(const I, Q: Double);
|
||
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
|
||
procedure 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);
|
||
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 SetFMSquelch(Enable: Boolean; Level: Integer);
|
||
procedure SetVolume(Vol: Double);
|
||
procedure SetMute(Mute: Boolean);
|
||
|
||
// TX управление
|
||
procedure SetTXMode(Mode: Integer);
|
||
procedure SetTXFilter(Low, High: Integer);
|
||
procedure SetDriveLevel(Level: Double);
|
||
procedure SetMicGain(GainDB: Double);
|
||
procedure SetTXRun(Run: Boolean);
|
||
// DUP: переключить ИСТОЧНИК пикселей display'а независимо от TX-аудио
|
||
// (SetTXRun меняет и аудио, и источник; этот метод позволяет вернуть
|
||
// RX-источник пока TX-аудио продолжает работать).
|
||
procedure SetDisplaySourceTX(UseTX: Boolean);
|
||
// TUN: WDSP TXA PostGen — гонит чистый тон в DSP-цепь после всей
|
||
// обработки. Mag — амплитуда в [0..1], Freq — Hz (audio).
|
||
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
|
||
|
||
// Расширенное TX управление (применяются мгновенно если канал открыт)
|
||
procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||
procedure SetTXCompressor(Enable: Boolean; GainDB: Double);
|
||
procedure SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||
procedure SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||
procedure SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
|
||
procedure SetTXEQ(Enable: Boolean; NumBands: Integer;
|
||
const Gains, Freqs: array of Double);
|
||
procedure SetTXAMCarrierLevel(Level: Double);
|
||
procedure SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
|
||
procedure SetTXCTCSS(Enable: Boolean; FreqHz: Double);
|
||
// Источник микрофона: Radio (HW mic-stream) или SoundCard (PortAudio).
|
||
procedure SetTXMicSource(Source: TTXMicSource);
|
||
|
||
// TX display: отдельные FFT/window/detector/avg для TX-analyzer (TX_DISP_ID).
|
||
// Аналог RX-сеттеров, но применяются только к TX-anaylzer.
|
||
procedure SetTXFFTParams(FFTSize, WinType: Integer);
|
||
procedure SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
procedure SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
|
||
// TX mic вход — вызывается из receive thread (thread-safe lock-free).
|
||
// Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов.
|
||
// Если MicSource=SoundCard, эти сэмплы игнорируются.
|
||
procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
||
// Прямой push double-сэмпла (-1..+1), для sound-card пути.
|
||
// Должен вызываться из ОДНОГО потока.
|
||
procedure PushTXMicSampleD(const S: Double);
|
||
// Batch-вариант: пушит N сэмплов одним вызовом, будит TX-поток один раз.
|
||
procedure PushTXMicSamplesD(const Src: array of Double; N: Integer);
|
||
|
||
// Настройки анализатора (применяются немедленно если открыт)
|
||
procedure SetFFTParams(FFTSize, WinType: Integer);
|
||
procedure SetDisplayFPS(FPS: Integer);
|
||
procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
|
||
// Spectrum — вызывать из таймера (~20 fps)
|
||
procedure UpdateSpectrum;
|
||
procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);
|
||
|
||
// S-meter
|
||
function GetSMeterDBm: Double;
|
||
|
||
// Zoom/pan: задать коэффициент зума и положение пана и переармировать
|
||
// RX-анализатор. View* — границы видимого окна (Гц отн. DDC-центра).
|
||
procedure SetZoomPan(AZoom, APan: Double);
|
||
property ZoomFactor: Double read FZoomFactor;
|
||
property PanSlider: Double read FPanSlider;
|
||
property ViewLowHz: Double read FViewLowHz;
|
||
property ViewHighHz: Double read FViewHighHz;
|
||
|
||
property Initialized: Boolean read FInitialized;
|
||
property SampleRate: Integer read FSampleRate;
|
||
property TXSampleRate: Integer read FTXSampleRate;
|
||
property AudioBufSize: Integer read FAudioBufSize;
|
||
property AGCTop: Double read FAGCTop;
|
||
property AGCSlope: Integer read FAGCSlope;
|
||
property AGCHangThreshold: Integer read FAGCHangThreshold;
|
||
property AGCHangLevel: Double read FAGCHangLevel;
|
||
property AGCThresh: Double read FAGCThresh;
|
||
property Mode: Integer read FMode;
|
||
property FilterLow: Integer read FFilterLow;
|
||
property FilterHigh: Integer read FFilterHigh;
|
||
property NRMode: Integer read FNRMode;
|
||
property NBMode: Integer read FNBMode;
|
||
property SNBEnabled: Boolean read FSNBEnabled;
|
||
property LastError: string read FLastError;
|
||
property SMeter: Double read FSMeter;
|
||
property FFTSize: Integer read FFFTSize;
|
||
property WindowType: Integer read FWindowType;
|
||
property SpecDetector: Integer read FSpecDetector;
|
||
property SpecAvgMode: Integer read FSpecAvgMode;
|
||
property SpecAvgTimeMS: Double read FSpecAvgTimeMS;
|
||
property WfDetector: Integer read FWfDetector;
|
||
property WfAvgMode: Integer read FWfAvgMode;
|
||
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
||
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
||
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||
// Пиксели доп. панов (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
|
||
FEngine.PushIQItemToDSP(Item^)
|
||
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;
|
||
else Result := WDSP_USB;
|
||
end;
|
||
end;
|
||
|
||
function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer;
|
||
// txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM.
|
||
begin
|
||
if Mode = MODE_WFM then Result := WDSP_TX_WFM
|
||
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.ApplyWFMSettings;
|
||
begin
|
||
ApplyRXWFMToChan(RXA_CHAN);
|
||
SetTXAWFMDeviation (TXA_CHAN, WFM_DEVIATION);
|
||
SetTXAWFMAFFreqs (TXA_CHAN, WFM_AF_LOW, WFM_AF_HIGH);
|
||
SetTXAWFMPreEmphRun (TXA_CHAN, 1);
|
||
SetTXAWFMPreEmphTau (TXA_CHAN, WFM_TAU_DE_US);
|
||
end;
|
||
|
||
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);
|
||
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;
|
||
FFilterLow := 100;
|
||
FFilterHigh := 2400;
|
||
FAGCMode := agcMedium;
|
||
FShiftHz := 0.0;
|
||
FAGCTop := -90.0;
|
||
FVolume := 0.7;
|
||
FSMeter := -130.0;
|
||
FNRMode := NR_MODE_OFF;
|
||
FNBMode := NB_MODE_OFF;
|
||
FSNBEnabled := False;
|
||
FANFEnabled := False;
|
||
FRXAccPos := 0;
|
||
// Analyzer defaults
|
||
FFFTSize := 131072;
|
||
FWindowType := 2; // Hann
|
||
FDisplayFPS := 60;
|
||
FEffectiveDisplayFPS := 60.0;
|
||
FDisplayPixelCount := SPECTRUM_PIXELS;
|
||
FDisplayResLimit := SPECTRUM_PIXELS;
|
||
FSpecDetector := 0; // Peak
|
||
FSpecAvgMode := 3; // Log Recursive
|
||
FSpecAvgTimeMS := 30.0;
|
||
FWfDetector := 0; // Peak
|
||
FWfAvgMode := 3; // Log Recursive
|
||
FWfAvgTimeMS := 120.0;
|
||
FZoomFactor := 0.0; // 0.0 = весь span, без зума
|
||
FPanSlider := 0.5; // окно по центру
|
||
FViewLowHz := -FSampleRate / 2.0;
|
||
FViewHighHz := FSampleRate / 2.0;
|
||
|
||
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
||
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
||
SetLength(FTXIn, FAudioBufSize * 2); // in_size пар @ FAudioRate
|
||
SetLength(FTXOut, FTXOutBufSize * 2); // out_size пар @ FTXSampleRate (фикс)
|
||
SetLength(FRXAccI, FBufSize);
|
||
SetLength(FRXAccQ, FBufSize);
|
||
SetLength(FSpecBuf, FBufSize * 2);
|
||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||
SetLength(FTXDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||
FDispPos := 0;
|
||
FTXDispPos := 0;
|
||
SetLength(FOutL, FAudioBufSize);
|
||
SetLength(FOutR, FAudioBufSize);
|
||
|
||
// Очередь и DSP поток
|
||
FQueueHead := 0;
|
||
FQueueTail := 0;
|
||
FDSPRunning := False;
|
||
FQueueSem := RTLEventCreate;
|
||
FDSPThread := nil;
|
||
FDisplayThread := nil;
|
||
|
||
// Мультислайсы
|
||
FSliceLock := TCriticalSection.Create;
|
||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||
FOnSliceAudio := nil;
|
||
AllocSliceBuffers;
|
||
|
||
// TX инициализация
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
FTXActive := False;
|
||
FTXToneOn := False;
|
||
FKeepRXDuringTX := False;
|
||
FMonitorRXDuringTX := False;
|
||
FPostTXMuteUntil := 0;
|
||
FActiveDisplayID := RX_DISP_ID;
|
||
FTXMicSource := txmsRadio;
|
||
FOnPullMic := nil;
|
||
FTXThread := nil;
|
||
FTXMicSem := RTLEventCreate;
|
||
FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
|
||
|
||
// TX-цепь — дефолты, согласуются с Settings.DefaultTX
|
||
FTXFilterLow := 100;
|
||
FTXFilterHigh := 2800;
|
||
FTXFilterNC := 1024;
|
||
FTXFilterMP := 1;
|
||
FTXFilterWindow := 1;
|
||
FTXMicGainDB := 10.0;
|
||
FTXCompressorOn := False;
|
||
FTXCompressorGain:= 5.0;
|
||
FTXLevelerOn := True;
|
||
FTXLevelerTop := 5.0;
|
||
FTXLevelerDecay := 500;
|
||
FTXALCOn := True;
|
||
FTXALCMaxGain := 0.0;
|
||
FTXALCDecay := 10;
|
||
FTXPhaseRotOn := False;
|
||
FTXPhaseRotStages:= 8;
|
||
FTXPhaseRotFreq := 338.0;
|
||
FTXEQOn := False;
|
||
FTXEQNumBands := 10;
|
||
FillChar(FTXEQGains, SizeOf(FTXEQGains), 0);
|
||
// Дефолтные центры полос EQ (Hz). Должны быть валидными ДАЖЕ при EQOn=False —
|
||
// SetTXAEQProfile вызывается в ApplyTXChainSettings всегда, и нулевые freqs
|
||
// могут сломать TX-фильтры внутри WDSP.
|
||
FTXEQFreqs[0] := 0;
|
||
FTXEQFreqs[1] := 100;
|
||
FTXEQFreqs[2] := 200;
|
||
FTXEQFreqs[3] := 400;
|
||
FTXEQFreqs[4] := 700;
|
||
FTXEQFreqs[5] := 1100;
|
||
FTXEQFreqs[6] := 1500;
|
||
FTXEQFreqs[7] := 2000;
|
||
FTXEQFreqs[8] := 2500;
|
||
FTXEQFreqs[9] := 3000;
|
||
FTXEQFreqs[10] := 3500;
|
||
FTXAMCarrier := 0.5;
|
||
FTXFMDeviation := 5000.0;
|
||
FTXFMLowCut := 300;
|
||
FTXFMHighCut := 3000;
|
||
FTXFMEmphPos := 1;
|
||
FTXCTCSSOn := False;
|
||
FTXCTCSSFreq := 100.0;
|
||
|
||
// TX-display defaults
|
||
FTXFFTSize := 16384;
|
||
FTXWindowType := 2;
|
||
FTXSpecDetector := 0;
|
||
FTXSpecAvgMode := 3;
|
||
FTXSpecAvgTimeMS := 30.0;
|
||
FTXWfDetector := 0;
|
||
FTXWfAvgMode := 3;
|
||
FTXWfAvgTimeMS := 120.0;
|
||
end;
|
||
|
||
destructor TWDSPEngine.Destroy;
|
||
begin
|
||
SetTXRun(False); // останавливаем TX поток если запущен
|
||
Close;
|
||
RTLEventDestroy(FQueueSem);
|
||
RTLEventDestroy(FTXMicSem);
|
||
FSliceLock.Free;
|
||
inherited;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Analyzer display
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.OpenAnalyzer;
|
||
var
|
||
Success, 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 (пан-зум — этап 3.3+), FFT/детектор/усреднение
|
||
// пока общие с главным. Вызывать под FSliceLock.
|
||
var
|
||
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels, Disp: Integer;
|
||
Backmult, WfBackmult: Double;
|
||
EffFPS: Double;
|
||
const
|
||
KEEP_TIME_SEC = 0.10;
|
||
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);
|
||
|
||
SetAnalyzer(Disp, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
||
FWindowType, 14.0, Overlap, 0, 0, 0, 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;
|
||
|
||
OpenChannel(
|
||
RXA_CHAN,
|
||
FBufSize,
|
||
FAudioBufSize,
|
||
FSampleRate,
|
||
FAudioRate,
|
||
FAudioRate,
|
||
0,
|
||
0, // state=0: создать но не запускать
|
||
0.010, 0.010, 0.010, 0.010,
|
||
0
|
||
);
|
||
|
||
// Каналы только что созданы с dsp_rate = FAudioRate; забываем «широкую»
|
||
// геометрию прошлой сессии (Close/Open через ChangeSampleRate).
|
||
FRXDSPRate := FAudioRate;
|
||
FTXDSPSize := FAudioBufSize;
|
||
|
||
create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||
create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||
|
||
SetRXABandpassWindow(RXA_CHAN, 1);
|
||
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
|
||
SetRXAShiftRun(RXA_CHAN, 1);
|
||
SetRXAShiftFreq(RXA_CHAN, 0.0);
|
||
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
|
||
SetAGC(FAGCMode, 50.0);
|
||
SetRXAPanelGain1(RXA_CHAN, FVolume);
|
||
SetRXAPanelSelect(RXA_CHAN, 3);
|
||
SetRXAPanelRun(RXA_CHAN, 1);
|
||
SetChannelState(RXA_CHAN, 1, 0);
|
||
|
||
// TXA: mic @ FAudioRate → IQ @ FTXSampleRate (фиксированный DUC rate, не зависит от RX).
|
||
// out_size = FAudioBufSize * FTXSampleRate/FAudioRate = FTXOutBufSize (4096 @ 192k tx).
|
||
// FTXOut уже имеет размер FTXOutBufSize*2.
|
||
OpenChannel(
|
||
TXA_CHAN,
|
||
FAudioBufSize,
|
||
FAudioBufSize,
|
||
FAudioRate,
|
||
FAudioRate,
|
||
FTXSampleRate,
|
||
1,
|
||
0,
|
||
0.010, 0.010, 0.010, 0.010,
|
||
1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как
|
||
// pihpsdr). bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
|
||
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
|
||
);
|
||
|
||
SetTXAMode(TXA_CHAN, ModeToWDSPTX(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);
|
||
SetFilter(FFilterLow, FFilterHigh);
|
||
end;
|
||
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
|
||
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
||
ApplyTXChainSettings;
|
||
ApplyNoiseFilterState;
|
||
|
||
// Переоткрываем WDSP-каналы активных слайсов (например после ChangeSampleRate,
|
||
// где Close закрыл их, но записи FSlices сохранились).
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and not FSlices[i].Opened then
|
||
OpenSliceChannel(i);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
|
||
Result := True;
|
||
|
||
// Запускаем DSP поток после успешной инициализации WDSP
|
||
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
|
||
FDSPThread := TDSPThread.Create(Self);
|
||
FDSPThread.Priority := tpHighest; // DSP критический поток
|
||
|
||
except
|
||
on E: Exception do
|
||
begin
|
||
FLastError := E.ClassName + ': ' + E.Message;
|
||
FInitialized := False;
|
||
Result := False;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.Close;
|
||
var
|
||
i: Integer;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
// Останавливаем DSP поток перед закрытием WDSP каналов
|
||
if Assigned(FDSPThread) then
|
||
begin
|
||
FDSPThread.Terminate;
|
||
RTLEventSetEvent(FQueueSem); // разбудить чтобы увидел Terminated
|
||
FDSPThread.WaitFor;
|
||
FreeAndNil(FDSPThread);
|
||
end;
|
||
|
||
// TX-DSP поток живёт весь сеанс (не пересоздаётся на T/R) — гасим его здесь,
|
||
// до закрытия WDSP-каналов (он зовёт fexchange0 на TXA_CHAN).
|
||
FTXActive := False;
|
||
if Assigned(FTXThread) then
|
||
begin
|
||
FTXThread.Terminate;
|
||
RTLEventSetEvent(FTXMicSem);
|
||
FTXThread.WaitFor;
|
||
FreeAndNil(FTXThread);
|
||
end;
|
||
|
||
// Закрываем WDSP-каналы слайсов (записи FSlices сохраняем — переоткроются в Open).
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Opened then CloseSliceChannel(i);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
|
||
CloseAnalyzer;
|
||
destroy_anbEXT(RXA_CHAN);
|
||
destroy_nobEXT(RXA_CHAN);
|
||
CloseChannel(RXA_CHAN);
|
||
CloseChannel(TXA_CHAN);
|
||
FInitialized := False;
|
||
end;
|
||
|
||
procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
|
||
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open.
|
||
// Панов не трогает: их rate свой; аккумуляторы сбрасываем (очередь общая,
|
||
// их пакеты умерли вместе с ней), анализаторы пересоздаст OpenAnalyzer.
|
||
var
|
||
p: Integer;
|
||
begin
|
||
if NewRate = FSampleRate then Exit;
|
||
|
||
// Останавливаем текущий канал
|
||
Close;
|
||
|
||
// Сбрасываем очередь IQ пакетов — старые пакеты не совместимы с новым rate.
|
||
// Без этого DSP поток тратит секунды на разбор накопившихся пакетов.
|
||
FQueueHead := 0;
|
||
FQueueTail := 0;
|
||
FRXAccPos := 0;
|
||
for p := 1 to MAX_PANS - 1 do
|
||
if FPans[p].Active then
|
||
begin
|
||
FPans[p].AccPos := 0;
|
||
FPans[p].DispPos := 0;
|
||
end;
|
||
|
||
// Обновляем параметры
|
||
FSampleRate := NewRate;
|
||
FBufSize := FAudioBufSize * NewRate div FAudioRate;
|
||
|
||
// Перераспределяем буферы под новый размер
|
||
SetLength(FRXIn, FBufSize * 2);
|
||
SetLength(FRXOut, FBufSize * 2);
|
||
SetLength(FSpecBuf, FBufSize * 2);
|
||
SetLength(FRXAccI, FBufSize);
|
||
SetLength(FRXAccQ, FBufSize);
|
||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||
FDispPos := 0;
|
||
AllocSliceBuffers;
|
||
|
||
// Переинициализируем WDSP с новым rate
|
||
Open;
|
||
|
||
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
|
||
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetBeaconDecoder(D: TBeaconDecoder);
|
||
begin
|
||
FBeaconDec := D;
|
||
if (D <> nil) and (FSampleRate > 0) then D.Configure(FSampleRate);
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
|
||
DataOffset: Integer; IQPairs: Integer);
|
||
// Вызывается из СЕТЕВОГО потока — только кладём в очередь и возвращаемся немедленно
|
||
// DSP поток разбудится семафором и займётся обработкой
|
||
var
|
||
NextHead: Integer;
|
||
Item: ^TIQQueueItem;
|
||
DataBytes: Integer;
|
||
begin
|
||
NextHead := (FQueueHead + 1) and (IQ_QUEUE_SIZE - 1);
|
||
if NextHead = FQueueTail then Exit; // очередь полна — пропускаем пакет
|
||
|
||
Item := @FQueue[FQueueHead];
|
||
DataBytes := IQPairs * 6;
|
||
if DataBytes > IQ_PKT_MAXBYTES then DataBytes := IQ_PKT_MAXBYTES;
|
||
|
||
Move(Buf[DataOffset], Item^.Data[0], DataBytes);
|
||
Item^.DataLen := DataBytes;
|
||
Item^.IQPairs := IQPairs;
|
||
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) then
|
||
try
|
||
ProcessSlicesFor(PanId, Pan^.AccI, Pan^.AccQ, Pan^.BufSize);
|
||
except
|
||
// Защита от падения libwdsp в потоке — пропускаем блок (как ProcessRXBlock)
|
||
end;
|
||
end;
|
||
end;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.FlushRX;
|
||
begin
|
||
FQueueHead := FQueueTail;
|
||
FRXAccPos := 0;
|
||
FDispPos := 0;
|
||
if Length(FRXAccI) > 0 then FillChar(FRXAccI[0], Length(FRXAccI) * SizeOf(Double), 0);
|
||
if Length(FRXAccQ) > 0 then FillChar(FRXAccQ[0], Length(FRXAccQ) * SizeOf(Double), 0);
|
||
if Length(FRXIn) > 0 then FillChar(FRXIn[0], Length(FRXIn) * SizeOf(Double), 0);
|
||
if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0);
|
||
end;
|
||
|
||
procedure TWDSPEngine.BeginPostTXMute(DurationMs: Integer);
|
||
begin
|
||
if DurationMs <= 0 then
|
||
FPostTXMuteUntil := 0
|
||
else
|
||
FPostTXMuteUntil := GetTickCount64 + QWord(DurationMs);
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem);
|
||
// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize.
|
||
// В non-DUP TX (FTXActive and not FKeepRXDuringTX) полностью пропускаем
|
||
// IQ-пакет: не кормим RX-анализатор и не накапливаем для fexchange0.
|
||
// Иначе TX leakage из DDC оседает в RXA pipeline / RX_DISP_ID FFT-истории
|
||
// и звучит/виден на водопаде первые секунды после возврата на RX.
|
||
var
|
||
i, Pos: Integer;
|
||
IR, QR: LongInt;
|
||
DI, DQ: Double; // сэмпл во float — считаем ОДИН раз на всех трёх потребителей
|
||
AnOpen: Boolean; // FAnalyzerOpen и FBeaconDec внутри цикла не меняются:
|
||
Bec: TBeaconDecoder; // держим локально, не перечитываем поле на каждый сэмпл
|
||
P: PByte; // бегущий указатель вместо Pos := i * 6
|
||
const
|
||
SCALE = 1.0 / 8388608.0;
|
||
begin
|
||
if FTXActive and not FKeepRXDuringTX then Exit;
|
||
// Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта.
|
||
// Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не
|
||
// кормится), и аудио (RXA не запускается, FOnAudio не вызывается).
|
||
// Радио продолжает дотравливать FPGA TX-buffer + PA slew-down первые
|
||
// ~50–200мс после MOX-off; этот хвост попадает в RX1 как leak. Дропаем
|
||
// его на входе — single-source-of-truth.
|
||
if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit;
|
||
AnOpen := FAnalyzerOpen;
|
||
Bec := FBeaconDec;
|
||
Pos := 0;
|
||
P := @Item.Data[0];
|
||
for i := 0 to Item.IQPairs - 1 do
|
||
begin
|
||
if Pos + 5 >= Item.DataLen then Break;
|
||
|
||
IR := (LongInt(P[0]) shl 16) or (LongInt(P[1]) shl 8) or LongInt(P[2]);
|
||
QR := (LongInt(P[3]) shl 16) or (LongInt(P[4]) shl 8) or LongInt(P[5]);
|
||
Inc(P, 6);
|
||
Inc(Pos, 6);
|
||
|
||
if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
|
||
if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);
|
||
|
||
DI := IR * SCALE;
|
||
DQ := QR * SCALE;
|
||
|
||
// FeedDisplaySample развёрнут сюда: вызов метода на каждый сэмпл (576к/с)
|
||
// стоил дороже самой работы — двух записей в буфер.
|
||
if AnOpen then
|
||
begin
|
||
FDispBuf[FDispPos * 2] := DI;
|
||
FDispBuf[FDispPos * 2 + 1] := DQ;
|
||
Inc(FDispPos);
|
||
if FDispPos >= DISPLAY_BLOCK_SIZE then
|
||
begin
|
||
Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]);
|
||
FDispPos := 0;
|
||
end;
|
||
end;
|
||
|
||
if Bec <> nil then Bec.Feed(DI, DQ);
|
||
|
||
FRXAccI[FRXAccPos] := DI;
|
||
FRXAccQ[FRXAccPos] := DQ;
|
||
Inc(FRXAccPos);
|
||
|
||
if FRXAccPos >= FBufSize then
|
||
ProcessRXBlock;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.FeedTXDisplaySample(const I, Q: Double);
|
||
begin
|
||
if not FAnalyzerOpen then Exit;
|
||
|
||
FTXDispBuf[FTXDispPos * 2] := I;
|
||
FTXDispBuf[FTXDispPos * 2 + 1] := Q;
|
||
Inc(FTXDispPos);
|
||
|
||
if FTXDispPos >= DISPLAY_BLOCK_SIZE then
|
||
begin
|
||
Spectrum0(1, TX_DISP_ID, 0, 0, @FTXDispBuf[0]);
|
||
FTXDispPos := 0;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.ProcessRXBlock;
|
||
var
|
||
i: Integer;
|
||
Err: Integer;
|
||
// OutL/OutR теперь поля класса — не аллоцируем каждый раз
|
||
begin
|
||
FRXAccPos := 0;
|
||
if not FInitialized then Exit;
|
||
|
||
try
|
||
// Упаковываем в interleaved буфер для fexchange0
|
||
for i := 0 to FBufSize - 1 do
|
||
begin
|
||
FRXIn[i * 2] := FRXAccI[i];
|
||
FRXIn[i * 2 + 1] := FRXAccQ[i];
|
||
end;
|
||
|
||
// DSP обработка
|
||
Err := 0;
|
||
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
|
||
|
||
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
|
||
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
|
||
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
|
||
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
||
|
||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
||
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
||
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
||
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
|
||
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
|
||
// через unsigned-сравнение, GetTickCount64 не зовётся когда дедлайн = 0.
|
||
if (FPostTXMuteUntil <> 0) and (GetTickCount64 >= FPostTXMuteUntil) then
|
||
FPostTXMuteUntil := 0;
|
||
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
|
||
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
|
||
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
|
||
if Assigned(FOnAudio) and not FMuted
|
||
and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
||
and (FPostTXMuteUntil = 0) then
|
||
begin
|
||
for i := 0 to FAudioBufSize - 1 do
|
||
begin
|
||
FOutL[i] := FRXOut[i * 2] * FVolume;
|
||
FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
|
||
end;
|
||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||
end;
|
||
|
||
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
|
||
ProcessSlices;
|
||
|
||
except
|
||
// Защита от падения libwdsp в потоке — просто пропускаем блок
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Мультислайсы (софт-fan-out)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.AllocSliceBuffers;
|
||
// FSliceIn/Out должны вмещать наибольший входной блок среди главного тракта
|
||
// и активных панов (пан на 768к при главном 192к имеет блок больше FBufSize).
|
||
var
|
||
Need, p: Integer;
|
||
begin
|
||
Need := FBufSize;
|
||
for p := 1 to MAX_PANS - 1 do
|
||
if FPans[p].Active and (FPans[p].BufSize > Need) then Need := FPans[p].BufSize;
|
||
SetLength(FSliceIn, Need * 2);
|
||
SetLength(FSliceOut, Need * 2); // с запасом (>= FAudioBufSize*2)
|
||
SetLength(FSliceOutL, FAudioBufSize);
|
||
SetLength(FSliceOutR, FAudioBufSize);
|
||
end;
|
||
|
||
function TWDSPEngine.FindSliceIdx(Id: Integer): Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := -1;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
|
||
end;
|
||
|
||
function TWDSPEngine.SliceInRate(Idx: Integer): Integer;
|
||
// Входной rate слайса = rate его пана (пан 0 = главный захват).
|
||
var p: Integer;
|
||
begin
|
||
p := FSlices[Idx].PanId;
|
||
if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then
|
||
Result := FPans[p].Rate
|
||
else
|
||
Result := FSampleRate;
|
||
end;
|
||
|
||
function TWDSPEngine.SliceInSize(Idx: Integer): Integer;
|
||
// Входной блок слайса = блок его пана. Выходной блок при этом всегда
|
||
// FAudioBufSize (in_size/in_rate = const по построению BufSize панов).
|
||
var p: Integer;
|
||
begin
|
||
p := FSlices[Idx].PanId;
|
||
if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then
|
||
Result := FPans[p].BufSize
|
||
else
|
||
Result := FBufSize;
|
||
end;
|
||
|
||
// Применяет AGC к произвольному каналу — вынесено из SetAGC чтобы делить с слайсами.
|
||
procedure TWDSPEngine.ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode;
|
||
FixedGain: Double);
|
||
begin
|
||
SetRXAAGCMode(Chan, Ord(Mode));
|
||
SetRXAAGCSlope(Chan, FAGCSlope);
|
||
SetRXAAGCTop(Chan, FAGCTop);
|
||
case Mode of
|
||
agcOff:
|
||
SetRXAAGCFixed(Chan, FixedGain);
|
||
agcLong: begin
|
||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 2000);
|
||
SetRXAAGCDecay(Chan, 2000); SetRXAAGCHangThreshold(Chan, FAGCHangThreshold);
|
||
end;
|
||
agcSlow: begin
|
||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 1000);
|
||
SetRXAAGCDecay(Chan, 500); SetRXAAGCHangThreshold(Chan, FAGCHangThreshold);
|
||
end;
|
||
agcMedium: begin
|
||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 0);
|
||
SetRXAAGCDecay(Chan, 250); SetRXAAGCHangThreshold(Chan, 100);
|
||
end;
|
||
agcFast: begin
|
||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 0);
|
||
SetRXAAGCDecay(Chan, 50); SetRXAAGCHangThreshold(Chan, 100);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
|
||
// под FSliceLock и только при FInitialized.
|
||
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
|
||
var Chan, DSPRate, DSPSize, InRate, InSize: Integer;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
if FSlices[Idx].Opened then Exit;
|
||
Chan := FSlices[Idx].Chan;
|
||
// Вход канала = rate/блок ПАНА слайса (пан 0 = главный захват).
|
||
InRate := SliceInRate(Idx);
|
||
InSize := SliceInSize(Idx);
|
||
// WFM-слайс, как и главный канал, живёт на WFMDSPRate: rsmpin децимирует
|
||
// вход, rsmpout сводит к FAudioRate, так что out_size остаётся FAudioBufSize.
|
||
DSPRate := SliceDSPRateFor(FSlices[Idx].Mode, InRate);
|
||
DSPSize := FAudioBufSize * (DSPRate div FAudioRate);
|
||
OpenChannel(Chan, InSize, DSPSize, InRate, DSPRate, FAudioRate,
|
||
0, 0, 0.010, 0.010, 0.010, 0.010, 0);
|
||
// NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open.
|
||
create_anbEXT(Chan, 1, InSize, InRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||
create_nobEXT(Chan, 1, 0, InSize, InRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||
SetRXABandpassWindow(Chan, 1);
|
||
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
|
||
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
|
||
SetRXAShiftRun(Chan, 1);
|
||
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz);
|
||
RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate),
|
||
ClampToNyquist(FSlices[Idx].FilterHi, DSPRate));
|
||
ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0);
|
||
// NR/NB/SNB/ANF из сохранённого состояния слайса.
|
||
ApplyNRToChan(Chan, FSlices[Idx].NRMode);
|
||
ApplyNBToChan(Chan, FSlices[Idx].NBMode);
|
||
ApplySNBToChan(Chan, FSlices[Idx].SNBOn);
|
||
ApplyANFToChan(Chan, FSlices[Idx].ANFOn);
|
||
ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel);
|
||
SetRXAPanelGain1(Chan, FSlices[Idx].Volume);
|
||
SetRXAPanelSelect(Chan, 3);
|
||
SetRXAPanelRun(Chan, 1);
|
||
SetChannelState(Chan, 1, 0);
|
||
FSlices[Idx].Opened := True;
|
||
end;
|
||
|
||
procedure TWDSPEngine.CloseSliceChannel(Idx: Integer);
|
||
begin
|
||
if not FSlices[Idx].Opened then Exit;
|
||
SetChannelState(FSlices[Idx].Chan, 0, 1);
|
||
destroy_anbEXT(FSlices[Idx].Chan);
|
||
destroy_nobEXT(FSlices[Idx].Chan);
|
||
CloseChannel(FSlices[Idx].Chan);
|
||
FSlices[Idx].Opened := False;
|
||
end;
|
||
|
||
// Fan-out слайсов главного пана: вызывается из ProcessRXBlock (DSP-поток)
|
||
// после основного канала.
|
||
procedure TWDSPEngine.ProcessSlices;
|
||
begin
|
||
if not Assigned(FOnSliceAudio) 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;
|
||
if FSlices[s].Muted 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);
|
||
V := FSlices[s].Volume;
|
||
for k := 0 to FAudioBufSize - 1 do
|
||
begin
|
||
FSliceOutL[k] := FSliceOut[k * 2] * V;
|
||
FSliceOutR[k] := FSliceOut[k * 2 + 1] * V;
|
||
end;
|
||
FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize);
|
||
end;
|
||
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].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.PanActive(PanId: Integer): Boolean;
|
||
begin
|
||
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;
|
||
end;
|
||
|
||
function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
|
||
ShiftHz, Volume: Double; AGC: TWDSPAGCMode; APanId: Integer): Boolean;
|
||
var i, slot: Integer;
|
||
begin
|
||
Result := False;
|
||
FSliceLock.Enter;
|
||
try
|
||
if FindSliceIdx(Id) >= 0 then Exit; // уже существует
|
||
if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
|
||
if (APanId > 0) and not FPans[APanId].Active then Exit; // пан не создан
|
||
slot := -1;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if not FSlices[i].Active then begin slot := i; Break; end;
|
||
if slot < 0 then Exit; // нет свободных слотов
|
||
FSlices[slot].Active := True;
|
||
FSlices[slot].Id := Id;
|
||
FSlices[slot].Chan := SLICE_CHAN_BASE + slot;
|
||
FSlices[slot].PanId := APanId;
|
||
FSlices[slot].Mode := Mode;
|
||
FSlices[slot].FilterLo := FilterLo;
|
||
FSlices[slot].FilterHi := FilterHi;
|
||
FSlices[slot].ShiftHz := ShiftHz;
|
||
FSlices[slot].Volume := Volume;
|
||
FSlices[slot].AGCMode := AGC;
|
||
FSlices[slot].Muted := False;
|
||
FSlices[slot].NRMode := NR_MODE_OFF;
|
||
FSlices[slot].NBMode := NB_MODE_OFF;
|
||
FSlices[slot].SNBOn := False;
|
||
FSlices[slot].ANFOn := False;
|
||
FSlices[slot].SMeter := -130.0;
|
||
FSlices[slot].Opened := False;
|
||
if FInitialized then OpenSliceChannel(slot);
|
||
Result := True;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.RemoveSlice(Id: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id);
|
||
if idx < 0 then Exit;
|
||
CloseSliceChannel(idx);
|
||
FSlices[idx].Active := False;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceShift(Id: Integer; ShiftHz: Double);
|
||
var idx: Integer;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
FSlices[idx].ShiftHz := ShiftHz;
|
||
if FSlices[idx].Opened then SetRXAShiftFreq(FSlices[idx].Chan, ShiftHz);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceMode(Id, Mode: Integer);
|
||
var idx: Integer; Rerate: Boolean;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
// Уход в/из WFM меняет dsp_rate канала — проще пересоздать его целиком,
|
||
// чем гонять SetDSPSamplerate: слайс всё равно перестраивает все фильтры.
|
||
Rerate := FSlices[idx].Opened and
|
||
(SliceDSPRateFor(Mode, SliceInRate(idx)) <>
|
||
SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx)));
|
||
FSlices[idx].Mode := Mode;
|
||
if Rerate then
|
||
begin
|
||
CloseSliceChannel(idx);
|
||
OpenSliceChannel(idx);
|
||
end
|
||
else if FSlices[idx].Opened then
|
||
begin
|
||
SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode));
|
||
if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan);
|
||
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 then SetRXAPanelGain1(FSlices[idx].Chan, Vol);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceMute(Id: Integer; Mute: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
FSlices[idx].Muted := Mute;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceNRMode(Id, Mode: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
if Mode < NR_MODE_OFF then Mode := NR_MODE_OFF;
|
||
if Mode > NR_MODE_4 then Mode := NR_MODE_4;
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
FSlices[idx].NRMode := Mode;
|
||
if FSlices[idx].Opened then ApplyNRToChan(FSlices[idx].Chan, Mode);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceNBMode(Id, Mode: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
if Mode < NB_MODE_OFF then Mode := NB_MODE_OFF;
|
||
if Mode > NB_MODE_2 then Mode := NB_MODE_2;
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
FSlices[idx].NBMode := Mode;
|
||
if FSlices[idx].Opened then ApplyNBToChan(FSlices[idx].Chan, Mode);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceSNB(Id: Integer; On_: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
FSlices[idx].SNBOn := On_;
|
||
if FSlices[idx].Opened then ApplySNBToChan(FSlices[idx].Chan, On_);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceANF(Id: Integer; On_: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
FSlices[idx].ANFOn := On_;
|
||
if FSlices[idx].Opened then ApplyANFToChan(FSlices[idx].Chan, On_);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSliceFMSquelch(Id: Integer; Enable: Boolean; Level: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||
FSlices[idx].FMSQOn := Enable;
|
||
FSlices[idx].FMSQLevel := Level;
|
||
if FSlices[idx].Opened then
|
||
ApplyFMSQToChan(FSlices[idx].Chan, Enable, Level);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TWDSPEngine.GetSliceSMeter(Id: Integer): Double;
|
||
var idx: Integer;
|
||
begin
|
||
Result := -130.0;
|
||
FSliceLock.Enter;
|
||
try
|
||
idx := FindSliceIdx(Id);
|
||
if idx >= 0 then Result := FSlices[idx].SMeter;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TWDSPEngine.SliceCount: Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := 0;
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then Inc(Result);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TWDSPEngine.HasSlice(Id: Integer): Boolean;
|
||
begin
|
||
FSliceLock.Enter;
|
||
try
|
||
Result := FindSliceIdx(Id) >= 0;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// RX управление
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.SetMode(Mode: Integer);
|
||
begin
|
||
FMode := Mode;
|
||
if not FInitialized then Exit;
|
||
// Rate — до режима: setDSPSamplerate_rxa перестраивает всю цепь (в т.ч. wfmd),
|
||
// а SetRXAMode затем пересчитает bpsnba под уже актуальный dsp_rate.
|
||
ApplyDSPRate(Mode);
|
||
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
|
||
SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
|
||
if Mode = MODE_WFM then ApplyWFMSettings;
|
||
// НЕ вызываем 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;
|
||
SetRXAFMDeviation(RXA_CHAN, Deviation);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer);
|
||
var Threshold: Double;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
if Enable then
|
||
begin
|
||
Threshold := Level / 200.0; // 0..100 → 0.0..0.5
|
||
SetRXAFMSQThreshold(RXA_CHAN, Threshold);
|
||
SetRXAFMSQRun(RXA_CHAN, 1);
|
||
end
|
||
else
|
||
SetRXAFMSQRun(RXA_CHAN, 0);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetVolume(Vol: Double);
|
||
begin
|
||
FVolume := Max(0.0, Min(1.0, Vol));
|
||
if not FInitialized then Exit;
|
||
SetRXAPanelGain1(RXA_CHAN, FVolume);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetMute(Mute: Boolean);
|
||
begin
|
||
FMuted := Mute;
|
||
if not FInitialized then Exit;
|
||
SetRXAPanelRun(RXA_CHAN, Ord(not Mute));
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX управление
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.SetTXMode(Mode: Integer);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFilter(Low, High: Integer);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetTXABandpassFreqs(TXA_CHAN, Low, High);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetDriveLevel(Level: Double);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetTXAALCMaxGain(TXA_CHAN, Max(0.0, Min(1.0, Level)));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetMicGain(GainDB: Double);
|
||
begin
|
||
FTXMicGainDB := GainDB;
|
||
if not FInitialized then Exit;
|
||
SetTXAPanelGain1(TXA_CHAN, Power(10.0, GainDB / 20.0));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXRun(Run: Boolean);
|
||
var
|
||
k, Err: Integer;
|
||
begin
|
||
FDispPos := 0;
|
||
FTXDispPos := 0;
|
||
if Run then
|
||
begin
|
||
FActiveDisplayID := TX_DISP_ID;
|
||
// Сброс mic ring ДО активации, чтобы простаивавший поток не прочитал старое.
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
if FInitialized then
|
||
begin
|
||
SetChannelState(TXA_CHAN, 1, 0); // run, no delay
|
||
// Продувка канала: страховка от остатка прошлой TX-сессии в буферах/
|
||
// фильтрах TXA (без неё огрызок — после TUN это полноразмахный тон —
|
||
// выплёвывался в эфир первым же блоком нового TX). Основной слив теперь
|
||
// на стопе (ниже), но нештатные пути останова канал не чистят. 4 нулевых
|
||
// блока через fexchange0, выход в мусор (не в FOnTXIQ/display); поток
|
||
// ещё idle (FTXActive=False) — гонки нет.
|
||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||
for k := 1 to 4 do
|
||
begin
|
||
Err := 0;
|
||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||
end;
|
||
// Сброс TX-анализатора: его усредняющая история хранит спектр ПРОШЛОЙ
|
||
// сессии (после TUN — тон), и при переключении на TX-дисплей первые
|
||
// кадры показывали «всплеск» старого тона (виден в non-DUP, где на
|
||
// экране TX-спектр; эфир при этом чист — в DUP старт чистый).
|
||
// ResetPixelBuffers — лёгкое зануление pixel/average буферов (MW0LGE);
|
||
// НЕ ApplyTXAnalyzerSettings: тот зовёт SetAnalyzer (перепланирование
|
||
// FFT в UI-потоке) — давал видимую задержку отрисовки на старте TX.
|
||
if FAnalyzerOpen then
|
||
ResetPixelBuffers(TX_DISP_ID);
|
||
end;
|
||
// TX-DSP поток создаётся ОДИН раз и живёт весь сеанс. НЕ пересоздаём на каждом
|
||
// T/R: join (WaitFor) давал фиксированные ~100 мс фриза GUI на отпускании PTT.
|
||
// При FTXActive=False поток простаивает (см. TTXDSPThread.Execute: Continue).
|
||
if not Assigned(FTXThread) then
|
||
begin
|
||
FTXThread := TTXDSPThread.Create(Self);
|
||
FTXThread.Priority := tpHighest;
|
||
end;
|
||
FTXActive := True; // активируем последним
|
||
end else begin
|
||
FTXActive := False; // поток уходит в простой (не трогает канал/ring)
|
||
FActiveDisplayID := RX_DISP_ID;
|
||
if FInitialized then
|
||
begin
|
||
// Мгновенный стоп + синхронный слив. Раньше здесь был dmode=1: он
|
||
// спинует Sleep(1)×100 в ожидании фейда, который должен прокрутить
|
||
// fexchange — а продюсер уже остановлен, fexchange никто не зовёт →
|
||
// чистый таймаут ~106мс (остаточный фриз GUI на отпускании PTT) и
|
||
// канал, брошенный с недобранным сигналом в буферах. dmode=0 лишь
|
||
// взводит downflag/flushflag и возвращается сразу; down-slew (фейд
|
||
// 10мс) и штатный flush буферов исполняют наши же fexchange0 с нулевым
|
||
// входом — суммарно <1мс, выход в мусор (в эфир не идёт: DUC-очередь
|
||
// чистится контроллером на T/R).
|
||
SetChannelState(TXA_CHAN, 0, 0); // stop, без ожидания
|
||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||
for k := 1 to 4 do
|
||
begin
|
||
Err := 0;
|
||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||
end;
|
||
end;
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
|
||
var
|
||
SignedFreq: Double;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
FTXToneOn := Enabled; // мьют mic-входа на TUN (см. ProcessTXBlock)
|
||
if Enabled then
|
||
begin
|
||
// gen1 (PostGen) стоит после всех модуляторов и бандпасса.
|
||
// Положительная частота даёт тон ВЫШЕ несущей (USB-сторона).
|
||
// Для LSB и CWL нужен тон НИЖЕ несущей — инвертируем знак.
|
||
if (FMode = MODE_LSB) or (FMode = MODE_CWL) then
|
||
SignedFreq := -Abs(FreqHz)
|
||
else
|
||
SignedFreq := Abs(FreqHz);
|
||
SetTXAPostGenMode(TXA_CHAN, 0); // 0 = tone
|
||
SetTXAPostGenToneFreq(TXA_CHAN, SignedFreq);
|
||
SetTXAPostGenToneMag(TXA_CHAN, Max(0.0, Min(1.0, Mag)));
|
||
SetTXAPostGenRun(TXA_CHAN, 1);
|
||
end else
|
||
SetTXAPostGenRun(TXA_CHAN, 0);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetDisplaySourceTX(UseTX: Boolean);
|
||
begin
|
||
// Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID
|
||
// независимы, но мы переключаем источник, и старые байты в накапливающем
|
||
// буфере на новой стороне могут повести себя как «всплеск», если их не
|
||
// обнулить.
|
||
FDispPos := 0;
|
||
FTXDispPos := 0;
|
||
if UseTX then
|
||
FActiveDisplayID := TX_DISP_ID
|
||
else
|
||
FActiveDisplayID := RX_DISP_ID;
|
||
end;
|
||
|
||
procedure TWDSPEngine.ProcessTXBlock;
|
||
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
||
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
|
||
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
|
||
// если mic ring buffer содержит достаточно данных — берём их,
|
||
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
|
||
var
|
||
i, tail: Integer;
|
||
Err: Integer;
|
||
Avail: Integer;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||
|
||
if Avail >= FAudioBufSize then
|
||
begin
|
||
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
|
||
tail := FTXMicTail;
|
||
for i := 0 to FAudioBufSize - 1 do
|
||
begin
|
||
FTXIn[i * 2] := FTXMicRing[tail];
|
||
FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал
|
||
tail := (tail + 1) and (TX_MIC_RING - 1);
|
||
end;
|
||
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
|
||
// TUN: ринг потребляем (иначе поток зациклится на «есть полный блок»),
|
||
// но в TXA подаём тишину — тон PostGen должен идти без голоса поверх.
|
||
if FTXToneOn then
|
||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||
end else
|
||
begin
|
||
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
|
||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||
end;
|
||
|
||
Err := 0;
|
||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||
|
||
// TX сигнал → отдельный TX-analyzer (TX_DISP_ID).
|
||
// Подаём чанками по DISPLAY_BLOCK_SIZE — analyzer ожидает именно такой
|
||
// размер блока (см. SetAnalyzer). Если подавать FTXOut целиком
|
||
// (FTXOutBufSize пар), analyzer прочитает только первые DISPLAY_BLOCK_SIZE
|
||
// и проигнорирует остальное → спектр будет пустой.
|
||
if FAnalyzerOpen then
|
||
for i := 0 to FTXOutBufSize - 1 do
|
||
FeedTXDisplaySample(FTXOut[i * 2], FTXOut[i * 2 + 1]);
|
||
|
||
// FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double)
|
||
if Assigned(FOnTXIQ) then
|
||
FOnTXIQ(FTXOut, FTXOutBufSize);
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
||
// Вызывается из receive thread — lock-free, кладёт сэмплы в ring buffer
|
||
// 16-bit big-endian samples из аппаратного микрофона
|
||
var
|
||
i, cnt, newHead: Integer;
|
||
S: SmallInt;
|
||
begin
|
||
cnt := N;
|
||
if cnt > Length(Src) then cnt := Length(Src);
|
||
|
||
for i := 0 to cnt - 1 do
|
||
begin
|
||
// Аппаратный mic приходит в big-endian, нужно swap байт
|
||
S := SmallInt(((Src[i] and $FF) shl 8) or ((Src[i] shr 8) and $FF));
|
||
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
|
||
if newHead <> FTXMicTail then // буфер не полон
|
||
begin
|
||
FTXMicRing[FTXMicHead] := S / 32768.0;
|
||
FTXMicHead := newHead;
|
||
end;
|
||
// если буфер полон — сэмпл отбрасывается (overrun)
|
||
end;
|
||
|
||
RTLEventSetEvent(FTXMicSem); // будим TX поток
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushTXMicSampleD(const S: Double);
|
||
var newHead: Integer;
|
||
begin
|
||
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
|
||
if newHead <> FTXMicTail then
|
||
begin
|
||
FTXMicRing[FTXMicHead] := S;
|
||
FTXMicHead := newHead;
|
||
end;
|
||
// overrun → дроп, как в HW path
|
||
RTLEventSetEvent(FTXMicSem);
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushTXMicSamplesD(const Src: array of Double; N: Integer);
|
||
// Batch-вариант: одна будилка семафора на N сэмплов.
|
||
var
|
||
i, cnt, newHead: Integer;
|
||
begin
|
||
cnt := N;
|
||
if cnt > Length(Src) then cnt := Length(Src);
|
||
|
||
for i := 0 to cnt - 1 do
|
||
begin
|
||
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
|
||
if newHead <> FTXMicTail then
|
||
begin
|
||
FTXMicRing[FTXMicHead] := Src[i];
|
||
FTXMicHead := newHead;
|
||
end;
|
||
// overrun → дроп
|
||
end;
|
||
|
||
if cnt > 0 then
|
||
RTLEventSetEvent(FTXMicSem);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX chain: применение всех TX-параметров к WDSP-каналу
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.ApplyTXChainSettings;
|
||
// Вызывается после OpenChannel(TXA) и при старте TX. Прокидывает все
|
||
// FTX*-поля в WDSP. Идемпотентна, ничего не меняет если канал не открыт.
|
||
var
|
||
i: Integer;
|
||
Gains, Freqs, Qs: array[0..10] of Double;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
// Bandpass
|
||
if (FTXFilterLow > 0) and (FTXFilterHigh > FTXFilterLow) then
|
||
SetTXABandpassFreqs(TXA_CHAN, FTXFilterLow, FTXFilterHigh);
|
||
if FTXFilterWindow >= 0 then
|
||
SetTXABandpassWindow(TXA_CHAN, FTXFilterWindow);
|
||
if FTXFilterNC > 0 then
|
||
TXASetNC(TXA_CHAN, EffectiveTXNC);
|
||
if FTXFilterMP >= 0 then
|
||
TXASetMP(TXA_CHAN, FTXFilterMP);
|
||
|
||
// Mic gain (panel gain1)
|
||
SetTXAPanelGain1(TXA_CHAN, Power(10.0, FTXMicGainDB / 20.0));
|
||
|
||
// Compressor
|
||
SetTXACompressorRun(TXA_CHAN, Ord(FTXCompressorOn));
|
||
SetTXACompressorGain(TXA_CHAN, FTXCompressorGain);
|
||
|
||
// Leveler
|
||
SetTXALevelerSt(TXA_CHAN, Ord(FTXLevelerOn));
|
||
SetTXALevelerTop(TXA_CHAN, FTXLevelerTop);
|
||
SetTXALevelerDecay(TXA_CHAN, FTXLevelerDecay);
|
||
|
||
// ALC
|
||
SetTXAALCSt(TXA_CHAN, Ord(FTXALCOn));
|
||
SetTXAALCMaxGain(TXA_CHAN, FTXALCMaxGain);
|
||
SetTXAALCDecay(TXA_CHAN, FTXALCDecay);
|
||
|
||
// Phase Rotator
|
||
SetTXAPHROTRun(TXA_CHAN, Ord(FTXPhaseRotOn));
|
||
SetTXAPHROTNstages(TXA_CHAN, FTXPhaseRotStages);
|
||
SetTXAPHROTCorner(TXA_CHAN, FTXPhaseRotFreq);
|
||
|
||
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
|
||
for i := 0 to 10 do
|
||
begin
|
||
Gains[i] := FTXEQGains[i];
|
||
Freqs[i] := FTXEQFreqs[i];
|
||
Qs[i] := 0.707; // умолчание Q
|
||
end;
|
||
SetTXAEQProfile(TXA_CHAN, FTXEQNumBands, @Freqs[0], @Gains[0]);
|
||
SetTXAEQRun(TXA_CHAN, Ord(FTXEQOn));
|
||
|
||
// AM
|
||
SetTXAAMCarrierLevel(TXA_CHAN, FTXAMCarrier);
|
||
|
||
// FM
|
||
SetTXAFMDeviation(TXA_CHAN, FTXFMDeviation);
|
||
if (FTXFMLowCut < FTXFMHighCut) then
|
||
SetTXAFMAFFilter(TXA_CHAN, FTXFMLowCut, FTXFMHighCut);
|
||
SetTXAFMEmphPosition(TXA_CHAN, FTXFMEmphPos);
|
||
|
||
// CTCSS
|
||
SetTXACTCSSFreq(TXA_CHAN, FTXCTCSSFreq);
|
||
SetTXACTCSSRun(TXA_CHAN, Ord(FTXCTCSSOn));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||
begin
|
||
FTXFilterLow := Low;
|
||
FTXFilterHigh := High;
|
||
FTXFilterNC := NC;
|
||
FTXFilterMP := MP;
|
||
FTXFilterWindow := Window;
|
||
FFilterLow := Low;
|
||
FFilterHigh := High;
|
||
if not FInitialized then Exit;
|
||
SetTXABandpassFreqs(TXA_CHAN, Low, High);
|
||
SetTXABandpassWindow(TXA_CHAN, Window);
|
||
if NC > 0 then TXASetNC(TXA_CHAN, EffectiveTXNC);
|
||
if MP >= 0 then TXASetMP(TXA_CHAN, MP);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXCompressor(Enable: Boolean; GainDB: Double);
|
||
begin
|
||
FTXCompressorOn := Enable;
|
||
FTXCompressorGain := GainDB;
|
||
if not FInitialized then Exit;
|
||
SetTXACompressorGain(TXA_CHAN, GainDB);
|
||
SetTXACompressorRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||
begin
|
||
FTXLevelerOn := Enable;
|
||
FTXLevelerTop := TopDB;
|
||
FTXLevelerDecay := DecayMS;
|
||
if not FInitialized then Exit;
|
||
SetTXALevelerTop(TXA_CHAN, TopDB);
|
||
SetTXALevelerDecay(TXA_CHAN, DecayMS);
|
||
SetTXALevelerSt(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||
begin
|
||
FTXALCOn := Enable;
|
||
FTXALCMaxGain := MaxGainDB;
|
||
FTXALCDecay := DecayMS;
|
||
if not FInitialized then Exit;
|
||
SetTXAALCMaxGain(TXA_CHAN, MaxGainDB);
|
||
SetTXAALCDecay(TXA_CHAN, DecayMS);
|
||
SetTXAALCSt(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
|
||
begin
|
||
FTXPhaseRotOn := Enable;
|
||
FTXPhaseRotStages := Stages;
|
||
FTXPhaseRotFreq := FreqHz;
|
||
if not FInitialized then Exit;
|
||
SetTXAPHROTNstages(TXA_CHAN, Stages);
|
||
SetTXAPHROTCorner(TXA_CHAN, FreqHz);
|
||
SetTXAPHROTRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXEQ(Enable: Boolean; NumBands: Integer;
|
||
const Gains, Freqs: array of Double);
|
||
var
|
||
i, n: Integer;
|
||
G, F: array[0..10] of Double;
|
||
begin
|
||
FTXEQOn := Enable;
|
||
FTXEQNumBands := NumBands;
|
||
n := Min(11, Min(Length(Gains), Length(Freqs)));
|
||
for i := 0 to n - 1 do
|
||
begin
|
||
FTXEQGains[i] := Gains[i];
|
||
FTXEQFreqs[i] := Freqs[i];
|
||
G[i] := Gains[i];
|
||
F[i] := Freqs[i];
|
||
end;
|
||
if not FInitialized then Exit;
|
||
SetTXAEQProfile(TXA_CHAN, NumBands, @F[0], @G[0]);
|
||
SetTXAEQRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXAMCarrierLevel(Level: Double);
|
||
begin
|
||
FTXAMCarrier := Level;
|
||
if not FInitialized then Exit;
|
||
WDSP.SetTXAAMCarrierLevel(TXA_CHAN, Level);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
|
||
begin
|
||
FTXFMDeviation := Deviation;
|
||
FTXFMLowCut := LowCut;
|
||
FTXFMHighCut := HighCut;
|
||
FTXFMEmphPos := EmphPos;
|
||
if not FInitialized then Exit;
|
||
SetTXAFMDeviation(TXA_CHAN, Deviation);
|
||
if LowCut < HighCut then
|
||
SetTXAFMAFFilter(TXA_CHAN, LowCut, HighCut);
|
||
SetTXAFMEmphPosition(TXA_CHAN, EmphPos);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXCTCSS(Enable: Boolean; FreqHz: Double);
|
||
begin
|
||
FTXCTCSSOn := Enable;
|
||
FTXCTCSSFreq := FreqHz;
|
||
if not FInitialized then Exit;
|
||
SetTXACTCSSFreq(TXA_CHAN, FreqHz);
|
||
SetTXACTCSSRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
function TWDSPEngine.TXMicRingAvail: Integer;
|
||
begin
|
||
Result := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXMicSource(Source: TTXMicSource);
|
||
begin
|
||
if FTXMicSource = Source then Exit;
|
||
FTXMicSource := Source;
|
||
// При смене источника сбрасываем ring, чтобы не подмешивать остатки
|
||
// от предыдущего producer'а (race window игнорируется — TX обычно неактивен).
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFFTParams(FFTSize, WinType: Integer);
|
||
begin
|
||
FTXFFTSize := NormalizeFFTSize(FFTSize);
|
||
FTXWindowType := EnsureRange(WinType, 0, 6);
|
||
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
begin
|
||
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);
|
||
FTXSpecDetector := Detector;
|
||
FTXSpecAvgMode := AvgMode;
|
||
FTXSpecAvgTimeMS := AvgTimeMS;
|
||
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
begin
|
||
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);
|
||
FTXWfDetector := Detector;
|
||
FTXWfAvgMode := AvgMode;
|
||
FTXWfAvgTimeMS := AvgTimeMS;
|
||
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Spectrum
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.UpdateSpectrum;
|
||
// Вызывается из таймера главного потока (настраиваемый FPS).
|
||
// Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
|
||
// буфера (который fexchange0 непрерывно заполняет из DSP-потока).
|
||
// Это обеспечивает равномерный FPS независимо от размера FFT:
|
||
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
|
||
// буфера, а не ждёт следующего полного блока.
|
||
var
|
||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||
WfBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||
Flag: Integer;
|
||
i, N, p: Integer;
|
||
DispID: Integer;
|
||
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
|
||
|
||
Flag := 0;
|
||
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||
if Flag <> 0 then
|
||
begin
|
||
for i := 0 to N - 1 do
|
||
FSpectrumPixels[i] := PixBuf[i];
|
||
if Assigned(FOnSpectrum) then
|
||
FOnSpectrum(FSpectrumPixels, N);
|
||
end;
|
||
|
||
Flag := 0;
|
||
GetPixels(DispID, 1, @PixBuf[0], @Flag);
|
||
if Flag <> 0 then
|
||
begin
|
||
for i := 0 to N - 1 do
|
||
FWaterfallPixels[i] := PixBuf[i];
|
||
if Assigned(FOnWaterfall) then
|
||
FOnWaterfall(FWaterfallPixels, N);
|
||
end;
|
||
|
||
// Доп. паны: 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;
|
||
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.
|
||
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.
|