mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Pre-roll bug: when MIC_PREROLL_SAMP is an exact multiple of OPUS_FRAME_SAMP (1920/960=2), the N<=Want branch filled the buffer but never flushed it, losing the first 40ms of audio and leaving the ring empty long enough for silence blocks to be injected into WDSP. StallCount threshold raised from 2 to 4 (~43ms) to cover OS scheduler jitter of up to 22ms, preventing false silence injection when an Opus frame arrives slightly late relative to the DSP tick. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2211 lines
79 KiB
ObjectPascal
2211 lines
79 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,
|
||
WDSP;
|
||
|
||
const
|
||
// WDSP mode integers (нет именованных констант в WDSP.pas)
|
||
WDSP_LSB = 0;
|
||
WDSP_USB = 1;
|
||
WDSP_DSB = 2;
|
||
WDSP_CWL = 3;
|
||
WDSP_CWU = 4;
|
||
WDSP_FM = 5;
|
||
WDSP_AM = 6;
|
||
WDSP_SAM = 12;
|
||
|
||
// Наши индексы режимов (совпадают с кнопками MainForm)
|
||
MODE_LSB = 0;
|
||
MODE_USB = 1;
|
||
MODE_DSB = 2;
|
||
MODE_CWL = 3;
|
||
MODE_CWU = 4;
|
||
MODE_FM = 5;
|
||
MODE_AM = 6;
|
||
MODE_SAM = 7;
|
||
|
||
// AGC modes
|
||
AGC_OFF = 0;
|
||
AGC_LONG = 1;
|
||
AGC_SLOW = 2;
|
||
AGC_MEDIUM = 3;
|
||
AGC_FAST = 4;
|
||
|
||
// RX filter modes
|
||
NR_MODE_OFF = 0;
|
||
NR_MODE_1 = 1;
|
||
NR_MODE_2 = 2;
|
||
NR_MODE_3 = 3;
|
||
NR_MODE_4 = 4;
|
||
|
||
NB_MODE_OFF = 0;
|
||
NB_MODE_1 = 1;
|
||
NB_MODE_2 = 2;
|
||
|
||
FILTER_NR_AGC_POSITION = 1;
|
||
FILTER_NB_TAU = 0.00001;
|
||
FILTER_NB_ADVTIME = 0.00001;
|
||
FILTER_NB_HANGTIME = 0.00001;
|
||
FILTER_NB_THRESHOLD = 4.95;
|
||
FILTER_NB2_MODE = 0;
|
||
FILTER_NR2_GAIN_METHOD = 2;
|
||
FILTER_NR2_NPE_METHOD = 0;
|
||
FILTER_NR2_POST_RUN = 0;
|
||
FILTER_NR2_POST_TAPER = 12;
|
||
FILTER_NR2_POST_NLEVEL = 15.0;
|
||
FILTER_NR2_POST_FACTOR = 15.0;
|
||
FILTER_NR2_POST_RATE = 5.0;
|
||
FILTER_NR2_TRAINED_THRESHOLD = -0.5;
|
||
FILTER_NR2_TRAINED_T2 = 0.2;
|
||
FILTER_NR4_REDUCTION_AMOUNT = 10.0;
|
||
FILTER_NR4_SMOOTHING_FACTOR = 20.0;
|
||
FILTER_NR4_WHITENING_FACTOR = 0.0;
|
||
FILTER_NR4_NOISE_RESCALE = 2.0;
|
||
FILTER_NR4_POST_THRESHOLD = -3.0;
|
||
FILTER_NR4_NOISE_SCALING_TYPE = 0;
|
||
|
||
// S-meter types для GetRXAMeter
|
||
RXA_S_PK = 0; // peak, dBm
|
||
RXA_S_AV = 1; // average, dBm
|
||
|
||
RXA_CHAN = 0;
|
||
TXA_CHAN = 1; // используем разные каналы для RX и TX
|
||
|
||
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||
DSP_BUFSIZE = 1024;
|
||
SPECTRUM_PIXELS = 1024;
|
||
DISPLAY_BLOCK_SIZE = 64;
|
||
|
||
// Очередь IQ пакетов между сетевым и DSP потоком
|
||
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
|
||
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
|
||
|
||
// TX mic ring buffer — должен быть степенью двойки
|
||
TX_MIC_RING = 8192;
|
||
|
||
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
|
||
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0),
|
||
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
|
||
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
|
||
TX_SAMPLE_RATE = 192000;
|
||
|
||
type
|
||
// Пакет в очереди между сетевым и DSP потоком
|
||
TIQQueueItem = record
|
||
Data: array[0..IQ_PKT_MAXBYTES - 1] of Byte;
|
||
DataLen: Integer; // реальная длина данных
|
||
IQPairs: Integer; // количество IQ пар
|
||
end;
|
||
|
||
TWDSPAGCMode = (agcOff = 0, agcLong = 1, agcSlow = 2,
|
||
agcMedium = 3, agcFast = 4);
|
||
|
||
TOnAudioReady = procedure(const Left, Right: array of Single;
|
||
Count: Integer) of object;
|
||
TOnSpectrumReady = procedure(const Pixels: array of Single;
|
||
Count: Integer) of object;
|
||
// TX IQ callback — вызывается из TTXDSPThread при готовности блока
|
||
// Buf: interleaved [I0,Q0,I1,Q1,...] doubles, Count — кол-во IQ пар
|
||
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
||
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
||
Count: Integer) of object;
|
||
// Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком,
|
||
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
|
||
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
|
||
TPullMicSamplesFunc = function(Max: Integer): Integer of object;
|
||
|
||
// TX mic source: откуда получаем сэмплы для модулятора.
|
||
TTXMicSource = (txmsRadio, txmsSoundCard, txmsWeb);
|
||
|
||
{ TWDSPEngine }
|
||
TWDSPEngine = class
|
||
private
|
||
FInitialized: Boolean;
|
||
FAnalyzerOpen: Boolean;
|
||
FSampleRate: Integer;
|
||
FAudioRate: Integer;
|
||
FBufSize: Integer; // входной буфер @ FSampleRate
|
||
FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate)
|
||
FTXSampleRate: Integer; // фиксированный rate TX/DUC (TX_SAMPLE_RATE), не меняется при ChangeSampleRate
|
||
FTXOutBufSize: Integer; // выходной буфер TXA в IQ парах @ FTXSampleRate (= FAudioBufSize * FTXSampleRate / FAudioRate)
|
||
|
||
// Interleaved double I/Q буферы для fexchange0
|
||
FRXIn: array of Double;
|
||
FRXOut: array of Double;
|
||
FTXIn: array of Double;
|
||
FTXOut: array of Double;
|
||
|
||
// Накопитель входного буфера (DDC пакеты могут быть мельче FBufSize)
|
||
FRXAccI: array of Double;
|
||
FRXAccQ: array of Double;
|
||
FRXAccPos: Integer;
|
||
|
||
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
|
||
FSpecBuf: array of Double;
|
||
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
|
||
FDispBuf: array of Double;
|
||
FDispPos: Integer;
|
||
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
|
||
// по DISPLAY_BLOCK_SIZE из FTXOut в ProcessTXBlock.
|
||
FTXDispBuf: array of Double;
|
||
FTXDispPos: Integer;
|
||
// Аудио выходные буферы — аллоцируем один раз
|
||
FOutL: array of Single;
|
||
FOutR: array of Single;
|
||
// DSP поток + очередь пакетов
|
||
FDSPThread: TThread;
|
||
FDisplayThread:TThread;
|
||
FQueue: array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem;
|
||
FQueueHead: Integer; // пишет сетевой поток
|
||
FQueueTail: Integer; // читает DSP поток
|
||
FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent)
|
||
FDSPRunning: Boolean;
|
||
|
||
FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||
FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||
FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
|
||
|
||
// Настройки анализатора
|
||
FFFTSize: Integer;
|
||
FWindowType: Integer;
|
||
FDisplayFPS: Integer;
|
||
FEffectiveDisplayFPS: Double;
|
||
FDisplayPixelCount: Integer;
|
||
FSpecDetector: Integer;
|
||
FSpecAvgMode: Integer;
|
||
FSpecAvgTimeMS: Double;
|
||
FWfDetector: Integer;
|
||
FWfAvgMode: Integer;
|
||
FWfAvgTimeMS: Double;
|
||
|
||
FOnAudio: TOnAudioReady;
|
||
FOnSpectrum: TOnSpectrumReady;
|
||
FOnWaterfall: TOnWaterfallReady;
|
||
FOnTXIQ: TOnTXIQReady;
|
||
|
||
// TX mic ring buffer (lock-free: один writer — receive thread,
|
||
// один reader — TX DSP thread)
|
||
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
|
||
FTXMicHead: Integer; // пишет receive thread
|
||
FTXMicTail: Integer; // читает TX DSP thread
|
||
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
|
||
FTXThread: TThread;
|
||
FTXActive: Boolean;
|
||
// DUP-флаг от UI. True — RX-анализатор и RXA-канал продолжают работать
|
||
// во время TX (full-duplex). False — RX-IQ пакеты дропаются на входе DSP
|
||
// потока и не кормят RXA/анализатор. Это не трогает SetChannelState
|
||
// (мы выяснили на практике что стоп/старт RXA добавляет задержки на
|
||
// Windows без видимого выигрыша).
|
||
FKeepRXDuringTX: Boolean;
|
||
// Дедлайн (GetTickCount64), до которого RX-аудио не пишется в FOnAudio.
|
||
// Используется на Windows для маскировки «хвоста» TX leakage (radio
|
||
// FPGA TX-buffer + PA slew-down → RX1 IQ → audio) после MOX-off.
|
||
// На Linux выставляется в 0 (UI не зовёт BeginPostTXMute) — поведение
|
||
// не меняется.
|
||
FPostTXMuteUntil: QWord;
|
||
FActiveDisplayID: Integer; // RX_DISP_ID или TX_DISP_ID
|
||
FTXMicSource: TTXMicSource;
|
||
FOnPullMic: TPullMicSamplesFunc;
|
||
|
||
// Кэшированные TX-параметры — применяются после OpenChannel(TXA) и при изменении.
|
||
// Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS.
|
||
FTXFilterLow: Integer;
|
||
FTXFilterHigh: Integer;
|
||
FTXFilterNC: Integer;
|
||
FTXFilterMP: Integer;
|
||
FTXFilterWindow: Integer;
|
||
FTXMicGainDB: Double;
|
||
FTXCompressorOn: Boolean;
|
||
FTXCompressorGain:Double;
|
||
FTXLevelerOn: Boolean;
|
||
FTXLevelerTop: Double;
|
||
FTXLevelerDecay: Integer;
|
||
FTXALCOn: Boolean;
|
||
FTXALCMaxGain: Double;
|
||
FTXALCDecay: Integer;
|
||
FTXPhaseRotOn: Boolean;
|
||
FTXPhaseRotStages:Integer;
|
||
FTXPhaseRotFreq: Double;
|
||
FTXEQOn: Boolean;
|
||
FTXEQNumBands: Integer;
|
||
FTXEQGains: array[0..10] of Double;
|
||
FTXEQFreqs: array[0..10] of Double;
|
||
FTXAMCarrier: Double;
|
||
FTXFMDeviation: Double;
|
||
FTXFMLowCut: Integer;
|
||
FTXFMHighCut: Integer;
|
||
FTXFMEmphPos: Integer;
|
||
FTXCTCSSOn: Boolean;
|
||
FTXCTCSSFreq: Double;
|
||
|
||
// TX-display параметры (отдельные от RX) — применяются к TX_DISP_ID.
|
||
FTXFFTSize: Integer;
|
||
FTXWindowType: Integer;
|
||
FTXSpecDetector: Integer;
|
||
FTXSpecAvgMode: Integer;
|
||
FTXSpecAvgTimeMS: Double;
|
||
FTXWfDetector: Integer;
|
||
FTXWfAvgMode: Integer;
|
||
FTXWfAvgTimeMS: Double;
|
||
|
||
FMode: Integer;
|
||
FSMeter: Double;
|
||
FFilterLow: Integer;
|
||
FFilterHigh: Integer;
|
||
FAGCMode: TWDSPAGCMode;
|
||
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR
|
||
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
|
||
FAGCHangThreshold: Integer; // порог hang 0..100
|
||
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
|
||
FAGCThresh: Double; // читается из WDSP (GetRXAAGCThresh)
|
||
FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс)
|
||
FShiftHz: Double; // NCO shift для CTUN
|
||
FNRMode: Integer;
|
||
FNBMode: Integer;
|
||
FSNBEnabled: Boolean;
|
||
FANFEnabled: Boolean;
|
||
FMuted: Boolean;
|
||
FVolume: Double;
|
||
FLastError: string;
|
||
|
||
function ModeToWDSP(Mode: Integer): Integer;
|
||
procedure ApplyDefaultFilter;
|
||
procedure FeedDisplaySample(const I, Q: Double);
|
||
procedure ProcessRXBlock;
|
||
procedure PushIQItemToDSP(const Item: TIQQueueItem);
|
||
procedure OpenAnalyzer;
|
||
procedure CloseAnalyzer;
|
||
procedure ApplyAnalyzerSettings;
|
||
function CalcDisplayFFTSize: Integer;
|
||
procedure CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
|
||
out EffectiveFPS: Double);
|
||
function NormalizeFFTSize(FFTSize: Integer): Integer;
|
||
procedure NormalizeDisplayParams(var Detector, AvgMode: Integer;
|
||
var AvgTimeMS: Double; ForWaterfall: Boolean);
|
||
procedure AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
|
||
out AvgCount: Integer; out Backmult: Double);
|
||
procedure ApplyNRState;
|
||
procedure ApplyNBState;
|
||
procedure ApplySNBState;
|
||
procedure ApplyANFState;
|
||
procedure ApplyNoiseFilterState;
|
||
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
||
procedure FeedTXDisplaySample(const I, Q: Double);
|
||
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
|
||
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
||
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
|
||
|
||
public
|
||
constructor Create(SampleRate: Integer = 192000;
|
||
AudioRate: Integer = 48000;
|
||
BufSize: Integer = DSP_BUFSIZE);
|
||
destructor Destroy; override;
|
||
|
||
// Открыть DSP каналы
|
||
function Open: Boolean;
|
||
procedure Close;
|
||
procedure ChangeSampleRate(NewRate: Integer);
|
||
|
||
// Подача 24-bit big-endian IQ из DDC пакета
|
||
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
|
||
procedure PushDDCPacket(const Buf: array of Byte;
|
||
DataOffset: Integer;
|
||
IQPairs: Integer);
|
||
procedure FlushRX;
|
||
// Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это
|
||
// на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio
|
||
// буфер удерживает «хвост» с TX leakage из радио, который AudioOut.Clear
|
||
// не достаёт. На Linux буфер маленький и проблемы нет, поэтому метод
|
||
// не вызывается.
|
||
procedure BeginPostTXMute(DurationMs: Integer);
|
||
|
||
// RX управление
|
||
procedure SetMode(Mode: Integer);
|
||
procedure SetFilter(Low, High: Integer);
|
||
procedure SetAGC(Mode: TWDSPAGCMode; FixedGain: Double = 0.0);
|
||
procedure SetAGCTop(TopDBm: Double);
|
||
procedure SetAGCSlope(Slope: Integer);
|
||
procedure SetAGCHangThreshold(Threshold: Integer);
|
||
procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре
|
||
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
|
||
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг
|
||
procedure SetNRMode(Mode: Integer);
|
||
procedure SetNR(Enable: Boolean);
|
||
procedure SetNBMode(Mode: Integer);
|
||
procedure SetNB(Enable: Boolean);
|
||
procedure SetSNB(Enable: Boolean);
|
||
procedure SetANF(Enable: Boolean);
|
||
procedure SetRXFMDeviation(Deviation: Double);
|
||
procedure SetFMSquelch(Enable: Boolean; Level: Integer);
|
||
procedure SetVolume(Vol: Double);
|
||
procedure SetMute(Mute: Boolean);
|
||
|
||
// TX управление
|
||
procedure SetTXMode(Mode: Integer);
|
||
procedure SetTXFilter(Low, High: Integer);
|
||
procedure SetDriveLevel(Level: Double);
|
||
procedure SetMicGain(GainDB: Double);
|
||
procedure SetTXRun(Run: Boolean);
|
||
// DUP: переключить ИСТОЧНИК пикселей display'а независимо от TX-аудио
|
||
// (SetTXRun меняет и аудио, и источник; этот метод позволяет вернуть
|
||
// RX-источник пока TX-аудио продолжает работать).
|
||
procedure SetDisplaySourceTX(UseTX: Boolean);
|
||
// TUN: WDSP TXA PostGen — гонит чистый тон в DSP-цепь после всей
|
||
// обработки. Mag — амплитуда в [0..1], Freq — Hz (audio).
|
||
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
|
||
|
||
// Расширенное TX управление (применяются мгновенно если канал открыт)
|
||
procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||
procedure SetTXCompressor(Enable: Boolean; GainDB: Double);
|
||
procedure SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||
procedure SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||
procedure SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
|
||
procedure SetTXEQ(Enable: Boolean; NumBands: Integer;
|
||
const Gains, Freqs: array of Double);
|
||
procedure SetTXAMCarrierLevel(Level: Double);
|
||
procedure SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
|
||
procedure SetTXCTCSS(Enable: Boolean; FreqHz: Double);
|
||
// Источник микрофона: Radio (HW mic-stream) или SoundCard (PortAudio).
|
||
procedure SetTXMicSource(Source: TTXMicSource);
|
||
|
||
// TX display: отдельные FFT/window/detector/avg для TX-analyzer (TX_DISP_ID).
|
||
// Аналог RX-сеттеров, но применяются только к TX-anaylzer.
|
||
procedure SetTXFFTParams(FFTSize, WinType: Integer);
|
||
procedure SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
procedure SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
|
||
// TX mic вход — вызывается из receive thread (thread-safe lock-free).
|
||
// Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов.
|
||
// Если MicSource=SoundCard, эти сэмплы игнорируются.
|
||
procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
||
// Прямой push double-сэмпла (-1..+1), для sound-card пути.
|
||
// Должен вызываться из ОДНОГО потока.
|
||
procedure PushTXMicSampleD(const S: Double);
|
||
// Batch-вариант: пушит N сэмплов одним вызовом, будит TX-поток один раз.
|
||
procedure PushTXMicSamplesD(const Src: array of Double; N: Integer);
|
||
|
||
// Настройки анализатора (применяются немедленно если открыт)
|
||
procedure SetFFTParams(FFTSize, WinType: Integer);
|
||
procedure SetDisplayFPS(FPS: Integer);
|
||
procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
|
||
// Spectrum — вызывать из таймера (~20 fps)
|
||
procedure UpdateSpectrum;
|
||
procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);
|
||
|
||
// S-meter
|
||
function GetSMeterDBm: Double;
|
||
|
||
property Initialized: Boolean read FInitialized;
|
||
property SampleRate: Integer read FSampleRate;
|
||
property TXSampleRate: Integer read FTXSampleRate;
|
||
property AudioBufSize: Integer read FAudioBufSize;
|
||
property AGCTop: Double read FAGCTop;
|
||
property AGCSlope: Integer read FAGCSlope;
|
||
property AGCHangThreshold: Integer read FAGCHangThreshold;
|
||
property AGCHangLevel: Double read FAGCHangLevel;
|
||
property AGCThresh: Double read FAGCThresh;
|
||
property Mode: Integer read FMode;
|
||
property FilterLow: Integer read FFilterLow;
|
||
property FilterHigh: Integer read FFilterHigh;
|
||
property NRMode: Integer read FNRMode;
|
||
property NBMode: Integer read FNBMode;
|
||
property SNBEnabled: Boolean read FSNBEnabled;
|
||
property LastError: string read FLastError;
|
||
property SMeter: Double read FSMeter;
|
||
property FFTSize: Integer read FFFTSize;
|
||
property WindowType: Integer read FWindowType;
|
||
property SpecDetector: Integer read FSpecDetector;
|
||
property SpecAvgMode: Integer read FSpecAvgMode;
|
||
property SpecAvgTimeMS: Double read FSpecAvgTimeMS;
|
||
property WfDetector: Integer read FWfDetector;
|
||
property WfAvgMode: Integer read FWfAvgMode;
|
||
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
||
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
||
property TXActive: Boolean read FTXActive;
|
||
// Включает full-duplex поведение RX-тракта: RXA не останавливается на TX,
|
||
// RX-анализатор продолжает получать IQ. Должен ставиться UI ДО SetTXRun.
|
||
property KeepRXDuringTX: Boolean read FKeepRXDuringTX
|
||
write FKeepRXDuringTX;
|
||
// Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive).
|
||
// SpectrumView/Waterfall должны читать пиксели именно из этого ID.
|
||
property ActiveDisplayID: Integer read FActiveDisplayID;
|
||
property TXMicSource: TTXMicSource read FTXMicSource;
|
||
property OnPullMicSamples: TPullMicSamplesFunc
|
||
read FOnPullMic write FOnPullMic;
|
||
|
||
// Диагностика mic ring buffer: сколько сэмплов накоплено TX-цепью.
|
||
function TXMicRingAvail: Integer;
|
||
|
||
property TXFFTSize: Integer read FTXFFTSize;
|
||
property TXWindowType: Integer read FTXWindowType;
|
||
property TXSpecDetector: Integer read FTXSpecDetector;
|
||
property TXSpecAvgMode: Integer read FTXSpecAvgMode;
|
||
property TXSpecAvgTimeMS: Double read FTXSpecAvgTimeMS;
|
||
property TXWfDetector: Integer read FTXWfDetector;
|
||
property TXWfAvgMode: Integer read FTXWfAvgMode;
|
||
property TXWfAvgTimeMS: Double read FTXWfAvgTimeMS;
|
||
end;
|
||
|
||
implementation
|
||
|
||
// ===========================================================================
|
||
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
|
||
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
|
||
// Аналог tx_thread в piHPSDR/transmitter.c
|
||
// ===========================================================================
|
||
type
|
||
TDisplayThread = class(TThread)
|
||
private
|
||
FEngine: TWDSPEngine;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(AEngine: TWDSPEngine);
|
||
end;
|
||
|
||
TTXDSPThread = class(TThread)
|
||
private
|
||
FEngine: TWDSPEngine;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(AEngine: TWDSPEngine);
|
||
end;
|
||
|
||
function UsePanNormOneHz(Detector: Integer): Integer;
|
||
begin
|
||
// В Thetis нормализация к 1 Гц включается только для panadapter
|
||
// и только для detector modes 2/3/4.
|
||
if Detector in [2, 3, 4] then
|
||
Result := 1
|
||
else
|
||
Result := 0;
|
||
end;
|
||
|
||
constructor TDisplayThread.Create(AEngine: TWDSPEngine);
|
||
begin
|
||
FEngine := AEngine;
|
||
FreeOnTerminate := False;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TDisplayThread.Execute;
|
||
var
|
||
NextTick: QWord;
|
||
NowTick: QWord;
|
||
DelayMS: Integer;
|
||
begin
|
||
NextTick := GetTickCount64;
|
||
while not Terminated do
|
||
begin
|
||
if FEngine.FInitialized and FEngine.FAnalyzerOpen then
|
||
FEngine.UpdateSpectrum;
|
||
|
||
Inc(NextTick, QWord(Max(1, Round(1000.0 / Max(1, FEngine.FDisplayFPS)))));
|
||
NowTick := GetTickCount64;
|
||
if NextTick > NowTick then
|
||
begin
|
||
DelayMS := Integer(NextTick - NowTick);
|
||
if DelayMS > 0 then
|
||
Sleep(DelayMS);
|
||
end
|
||
else
|
||
NextTick := NowTick;
|
||
end;
|
||
end;
|
||
|
||
constructor TTXDSPThread.Create(AEngine: TWDSPEngine);
|
||
begin
|
||
FEngine := AEngine;
|
||
FreeOnTerminate := False;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TTXDSPThread.Execute;
|
||
// Аналог tx_thread в piHPSDR/transmitter.c:
|
||
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
|
||
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
|
||
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
|
||
var
|
||
Avail: Integer;
|
||
PeriodMs: Integer;
|
||
PeriodUs: Integer;
|
||
AccUs: Integer;
|
||
DidBlock: Boolean;
|
||
StallCount: Integer;
|
||
begin
|
||
// 512 samples / 48000 Hz = 10.666 ms. Do not floor this to 10 ms:
|
||
// that over-produces TX IQ by ~6.7%, eventually forcing DUC queue
|
||
// corrections/drops and producing periodic sidebands on a pure TUN tone.
|
||
PeriodUs := Max(1000, FEngine.FAudioBufSize * 1000000 div FEngine.FAudioRate);
|
||
AccUs := 0;
|
||
StallCount := 0;
|
||
|
||
while not Terminated do
|
||
begin
|
||
Inc(AccUs, PeriodUs);
|
||
PeriodMs := Max(1, AccUs div 1000);
|
||
Dec(AccUs, PeriodMs * 1000);
|
||
|
||
// Ждём сигнала от mic-данных ИЛИ истечения периода —
|
||
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
|
||
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
|
||
if Terminated then Break;
|
||
if not FEngine.FTXActive then Continue;
|
||
|
||
// Если выбран sound-card mic — тянем сэмплы из внешнего источника
|
||
// (PortAudio через FOnPullMic в MainForm) ПЕРЕД проверкой ring buffer.
|
||
// Callback сам пушит сэмплы в FTXMicRing через PushTXMicSampleD.
|
||
if (FEngine.FTXMicSource = txmsSoundCard) and
|
||
Assigned(FEngine.FOnPullMic) then
|
||
FEngine.FOnPullMic(FEngine.FAudioBufSize * 2);
|
||
|
||
// Обрабатываем накопившиеся блоки с реальными mic-данными
|
||
DidBlock := False;
|
||
repeat
|
||
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
||
and (TX_MIC_RING - 1);
|
||
if (Avail >= FEngine.FAudioBufSize) and not Terminated then
|
||
begin
|
||
FEngine.ProcessTXBlock;
|
||
DidBlock := True;
|
||
end
|
||
else
|
||
Break;
|
||
until False;
|
||
|
||
if Terminated then Break;
|
||
|
||
// Если на этом тике не было ни одного полного mic-блока и mic-данных
|
||
// вообще нет — обрабатываем один блок тишины, чтобы DUC IQ поток не
|
||
// прерывался и спектр/водопад обновлялись.
|
||
// Важно: не добавлять silence-block сразу после реального блока. Для TUN
|
||
// этот "silence" тоже содержит PostGen tone, поэтому лишний блок ломает
|
||
// темп DUC IQ и даёт периодические боковые/тр-р-р.
|
||
// Если же в ring уже есть неполный блок, нельзя подменять его тишиной:
|
||
// radio mic приходит пакетами по 64 сэмпла, sound-card часто по 128.
|
||
// Иначе каждый mic packet будит поток раньше срока, а мы генерируем
|
||
// лишний TX-блок тишины вместо ожидания полного 512-sample блока.
|
||
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
||
and (TX_MIC_RING - 1);
|
||
// Тишину генерируем только после 4 подряд пустых тиков (~43 ms).
|
||
// Порог 43 ms даёт 22 ms запаса относительно периода Opus-фрейма (20 ms),
|
||
// что покрывает джиттер планировщика ОС (~10 ms) и сетевой джиттер.
|
||
// При реальной потере пакета тишина всё равно инжектируется.
|
||
if not DidBlock then
|
||
begin
|
||
if Avail = 0 then
|
||
begin
|
||
Inc(StallCount);
|
||
if StallCount >= 4 then
|
||
FEngine.ProcessTXBlock;
|
||
end
|
||
else
|
||
StallCount := 0;
|
||
end
|
||
else
|
||
StallCount := 0;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// DSP поток — обрабатывает IQ пакеты из очереди
|
||
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
|
||
// Аналог iq_thread в piHPSDR/new_protocol.c
|
||
// ===========================================================================
|
||
type
|
||
TDSPThread = class(TThread)
|
||
private
|
||
FEngine: TWDSPEngine;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(AEngine: TWDSPEngine);
|
||
end;
|
||
|
||
constructor TDSPThread.Create(AEngine: TWDSPEngine);
|
||
begin
|
||
FEngine := AEngine;
|
||
FreeOnTerminate := False;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TDSPThread.Execute;
|
||
var
|
||
Item: ^TIQQueueItem;
|
||
begin
|
||
while not Terminated do
|
||
begin
|
||
// Ждём сигнала от сетевого потока (как sem_wait в piHPSDR)
|
||
RTLEventWaitFor(FEngine.FQueueSem, 100);
|
||
if Terminated then Break;
|
||
|
||
// Разбираем все накопившиеся пакеты
|
||
while FEngine.FQueueTail <> FEngine.FQueueHead do
|
||
begin
|
||
Item := @FEngine.FQueue[FEngine.FQueueTail];
|
||
FEngine.PushIQItemToDSP(Item^);
|
||
FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TWDSPEngine.ModeToWDSP(Mode: Integer): Integer;
|
||
begin
|
||
case Mode of
|
||
MODE_LSB: Result := WDSP_LSB;
|
||
MODE_USB: Result := WDSP_USB;
|
||
MODE_DSB: Result := WDSP_DSB;
|
||
MODE_CWL: Result := WDSP_CWL;
|
||
MODE_CWU: Result := WDSP_CWU;
|
||
MODE_FM: Result := WDSP_FM;
|
||
MODE_AM: Result := WDSP_AM;
|
||
MODE_SAM: Result := WDSP_SAM;
|
||
else Result := WDSP_USB;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyDefaultFilter;
|
||
begin
|
||
case FMode of
|
||
MODE_LSB: SetFilter(-2400, -100);
|
||
MODE_USB: SetFilter( 100, 2400);
|
||
MODE_DSB: SetFilter(-2400, 2400);
|
||
MODE_CWL: SetFilter( -800, -200);
|
||
MODE_CWU: SetFilter( 200, 800);
|
||
MODE_FM: SetFilter(-5000, 5000);
|
||
MODE_AM: SetFilter(-4000, 4000);
|
||
MODE_SAM: SetFilter(-4000, 4000);
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Constructor / Destructor
|
||
// ---------------------------------------------------------------------------
|
||
|
||
constructor TWDSPEngine.Create(SampleRate, AudioRate, BufSize: Integer);
|
||
begin
|
||
inherited Create;
|
||
FSampleRate := SampleRate;
|
||
FAudioRate := AudioRate;
|
||
// BufSize = dsp_size @ AudioRate (внутренний DSP буфер, piHPSDR buffer_size = 1024)
|
||
// FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
|
||
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
|
||
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
|
||
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
|
||
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
|
||
FTXSampleRate := TX_SAMPLE_RATE;
|
||
FTXOutBufSize := BufSize * FTXSampleRate div AudioRate; // = 4096 @ 48k audio, 192k tx
|
||
FInitialized := False;
|
||
FAnalyzerOpen := False;
|
||
FMode := MODE_USB;
|
||
FFilterLow := 100;
|
||
FFilterHigh := 2400;
|
||
FAGCMode := agcMedium;
|
||
FShiftHz := 0.0;
|
||
FAGCTop := -90.0;
|
||
FVolume := 0.7;
|
||
FSMeter := -130.0;
|
||
FNRMode := NR_MODE_OFF;
|
||
FNBMode := NB_MODE_OFF;
|
||
FSNBEnabled := False;
|
||
FANFEnabled := False;
|
||
FRXAccPos := 0;
|
||
// Analyzer defaults
|
||
FFFTSize := 131072;
|
||
FWindowType := 2; // Hann
|
||
FDisplayFPS := 60;
|
||
FEffectiveDisplayFPS := 60.0;
|
||
FDisplayPixelCount := SPECTRUM_PIXELS;
|
||
FSpecDetector := 0; // Peak
|
||
FSpecAvgMode := 3; // Log Recursive
|
||
FSpecAvgTimeMS := 30.0;
|
||
FWfDetector := 0; // Peak
|
||
FWfAvgMode := 3; // Log Recursive
|
||
FWfAvgTimeMS := 120.0;
|
||
|
||
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
||
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
||
SetLength(FTXIn, FAudioBufSize * 2); // in_size пар @ FAudioRate
|
||
SetLength(FTXOut, FTXOutBufSize * 2); // out_size пар @ FTXSampleRate (фикс)
|
||
SetLength(FRXAccI, FBufSize);
|
||
SetLength(FRXAccQ, FBufSize);
|
||
SetLength(FSpecBuf, FBufSize * 2);
|
||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||
SetLength(FTXDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||
FDispPos := 0;
|
||
FTXDispPos := 0;
|
||
SetLength(FOutL, FAudioBufSize);
|
||
SetLength(FOutR, FAudioBufSize);
|
||
|
||
// Очередь и DSP поток
|
||
FQueueHead := 0;
|
||
FQueueTail := 0;
|
||
FDSPRunning := False;
|
||
FQueueSem := RTLEventCreate;
|
||
FDSPThread := nil;
|
||
FDisplayThread := nil;
|
||
|
||
// TX инициализация
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
FTXActive := False;
|
||
FKeepRXDuringTX := False;
|
||
FPostTXMuteUntil := 0;
|
||
FActiveDisplayID := RX_DISP_ID;
|
||
FTXMicSource := txmsRadio;
|
||
FOnPullMic := nil;
|
||
FTXThread := nil;
|
||
FTXMicSem := RTLEventCreate;
|
||
FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
|
||
|
||
// TX-цепь — дефолты, согласуются с Settings.DefaultTX
|
||
FTXFilterLow := 100;
|
||
FTXFilterHigh := 2800;
|
||
FTXFilterNC := 1024;
|
||
FTXFilterMP := 1;
|
||
FTXFilterWindow := 1;
|
||
FTXMicGainDB := 10.0;
|
||
FTXCompressorOn := False;
|
||
FTXCompressorGain:= 5.0;
|
||
FTXLevelerOn := True;
|
||
FTXLevelerTop := 5.0;
|
||
FTXLevelerDecay := 500;
|
||
FTXALCOn := True;
|
||
FTXALCMaxGain := 0.0;
|
||
FTXALCDecay := 10;
|
||
FTXPhaseRotOn := False;
|
||
FTXPhaseRotStages:= 8;
|
||
FTXPhaseRotFreq := 338.0;
|
||
FTXEQOn := False;
|
||
FTXEQNumBands := 10;
|
||
FillChar(FTXEQGains, SizeOf(FTXEQGains), 0);
|
||
// Дефолтные центры полос EQ (Hz). Должны быть валидными ДАЖЕ при EQOn=False —
|
||
// SetTXAEQProfile вызывается в ApplyTXChainSettings всегда, и нулевые freqs
|
||
// могут сломать TX-фильтры внутри WDSP.
|
||
FTXEQFreqs[0] := 0;
|
||
FTXEQFreqs[1] := 100;
|
||
FTXEQFreqs[2] := 200;
|
||
FTXEQFreqs[3] := 400;
|
||
FTXEQFreqs[4] := 700;
|
||
FTXEQFreqs[5] := 1100;
|
||
FTXEQFreqs[6] := 1500;
|
||
FTXEQFreqs[7] := 2000;
|
||
FTXEQFreqs[8] := 2500;
|
||
FTXEQFreqs[9] := 3000;
|
||
FTXEQFreqs[10] := 3500;
|
||
FTXAMCarrier := 0.5;
|
||
FTXFMDeviation := 5000.0;
|
||
FTXFMLowCut := 300;
|
||
FTXFMHighCut := 3000;
|
||
FTXFMEmphPos := 1;
|
||
FTXCTCSSOn := False;
|
||
FTXCTCSSFreq := 100.0;
|
||
|
||
// TX-display defaults
|
||
FTXFFTSize := 16384;
|
||
FTXWindowType := 2;
|
||
FTXSpecDetector := 0;
|
||
FTXSpecAvgMode := 3;
|
||
FTXSpecAvgTimeMS := 30.0;
|
||
FTXWfDetector := 0;
|
||
FTXWfAvgMode := 3;
|
||
FTXWfAvgTimeMS := 120.0;
|
||
end;
|
||
|
||
destructor TWDSPEngine.Destroy;
|
||
begin
|
||
SetTXRun(False); // останавливаем TX поток если запущен
|
||
Close;
|
||
RTLEventDestroy(FQueueSem);
|
||
RTLEventDestroy(FTXMicSem);
|
||
inherited;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Analyzer display
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.OpenAnalyzer;
|
||
var
|
||
Success: Integer;
|
||
begin
|
||
if FAnalyzerOpen then Exit;
|
||
|
||
Success := -1;
|
||
XCreateAnalyzer(RX_DISP_ID, @Success, 262144, 1, 1, nil);
|
||
if Success <> 0 then
|
||
begin
|
||
FLastError := Format('XCreateAnalyzer(RX) failed: %d', [Success]);
|
||
Exit;
|
||
end;
|
||
|
||
// Отдельный analyzer для TX-спектра/водопада. Ошибку логируем в LastError,
|
||
// но не считаем фатальной — RX без TX-display продолжает работать.
|
||
Success := -1;
|
||
XCreateAnalyzer(TX_DISP_ID, @Success, 262144, 1, 1, nil);
|
||
if Success <> 0 then
|
||
FLastError := Format('XCreateAnalyzer(TX) failed: %d', [Success]);
|
||
|
||
FFlp[0] := 0;
|
||
ApplyAnalyzerSettings;
|
||
|
||
FAnalyzerOpen := True;
|
||
if FDisplayThread = nil then
|
||
FDisplayThread := TDisplayThread.Create(Self);
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyRXAnalyzerSettings;
|
||
// Применяет FFFTSize / FWindowType / FSpec* / FWf* к RX_DISP_ID.
|
||
var
|
||
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
|
||
Backmult, WfBackmult: Double;
|
||
const KEEP_TIME_SEC = 0.10;
|
||
begin
|
||
AnalyzerFFT := CalcDisplayFFTSize;
|
||
FDispPos := 0;
|
||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
|
||
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
|
||
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||
FDisplayPixelCount := DisplayPixels;
|
||
|
||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||
|
||
SetAnalyzer(RX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
||
FWindowType, 14.0, Overlap, 0, 0.0, 0.0, DisplayPixels,
|
||
1, 0, 0.0, 0.0, MaxW);
|
||
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
|
||
SetDisplayAverageMode (RX_DISP_ID, 0, FSpecAvgMode);
|
||
SetDisplayNumAverage (RX_DISP_ID, 0, AvgCount);
|
||
SetDisplayAvBackmult (RX_DISP_ID, 0, Backmult);
|
||
SetDisplayNormOneHz (RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||
SetDisplayDetectorMode(RX_DISP_ID, 1, FWfDetector);
|
||
SetDisplayAverageMode (RX_DISP_ID, 1, FWfAvgMode);
|
||
SetDisplayNumAverage (RX_DISP_ID, 1, WfAvgCount);
|
||
SetDisplayAvBackmult (RX_DISP_ID, 1, WfBackmult);
|
||
SetDisplayNormOneHz (RX_DISP_ID, 1, 0);
|
||
SetDisplaySampleRate (RX_DISP_ID, FSampleRate);
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyTXAnalyzerSettings;
|
||
// Применяет FTXFFTSize / FTXWindowType / FTXSpec* / FTXWf* к TX_DISP_ID.
|
||
// FFT/Window/Det/Avg отдельные от RX — TX-спектр обычно нужен мельче по FFT
|
||
// (TX-полоса узкая) и с другим усреднением (быстрее реагировать на голос).
|
||
var
|
||
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
|
||
Backmult, WfBackmult: Double;
|
||
EffectiveFPS: Double;
|
||
const KEEP_TIME_SEC = 0.10;
|
||
begin
|
||
AnalyzerFFT := NormalizeFFTSize(FTXFFTSize);
|
||
FTXDispPos := 0;
|
||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FTXSampleRate,
|
||
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffectiveFPS);
|
||
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||
|
||
AverageTimeToParams(FTXSpecAvgMode, FTXSpecAvgTimeMS, AvgCount, Backmult);
|
||
AverageTimeToParams(FTXWfAvgMode, FTXWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||
|
||
SetAnalyzer(TX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
||
FTXWindowType, 14.0, Overlap, 0, 0.0, 0.0, DisplayPixels,
|
||
1, 0, 0.0, 0.0, MaxW);
|
||
SetDisplayDetectorMode(TX_DISP_ID, 0, FTXSpecDetector);
|
||
SetDisplayAverageMode (TX_DISP_ID, 0, FTXSpecAvgMode);
|
||
SetDisplayNumAverage (TX_DISP_ID, 0, AvgCount);
|
||
SetDisplayAvBackmult (TX_DISP_ID, 0, Backmult);
|
||
SetDisplayNormOneHz (TX_DISP_ID, 0, UsePanNormOneHz(FTXSpecDetector));
|
||
SetDisplayDetectorMode(TX_DISP_ID, 1, FTXWfDetector);
|
||
SetDisplayAverageMode (TX_DISP_ID, 1, FTXWfAvgMode);
|
||
SetDisplayNumAverage (TX_DISP_ID, 1, WfAvgCount);
|
||
SetDisplayAvBackmult (TX_DISP_ID, 1, WfBackmult);
|
||
SetDisplayNormOneHz (TX_DISP_ID, 1, 0);
|
||
SetDisplaySampleRate (TX_DISP_ID, FTXSampleRate);
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у.
|
||
begin
|
||
ApplyRXAnalyzerSettings;
|
||
ApplyTXAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.CloseAnalyzer;
|
||
begin
|
||
if not FAnalyzerOpen then Exit;
|
||
if Assigned(FDisplayThread) then
|
||
begin
|
||
FDisplayThread.Terminate;
|
||
FDisplayThread.WaitFor;
|
||
FreeAndNil(FDisplayThread);
|
||
end;
|
||
DestroyAnalyzer(RX_DISP_ID);
|
||
DestroyAnalyzer(TX_DISP_ID);
|
||
FAnalyzerOpen := False;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Open / Close
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TWDSPEngine.Open: Boolean;
|
||
var
|
||
DllPath: string;
|
||
begin
|
||
Result := False;
|
||
FLastError := '';
|
||
if FInitialized then Exit;
|
||
|
||
if not Assigned(@OpenChannel) 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 (x64/x86) and dependencies.', [DllPath])
|
||
else
|
||
FLastError := Format('WDSP library not found: %s', [DllPath]);
|
||
Exit;
|
||
end;
|
||
|
||
try
|
||
// Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
|
||
OpenAnalyzer;
|
||
if not FAnalyzerOpen then
|
||
begin
|
||
FLastError := 'XCreateAnalyzer failed (WDSP init failed).';
|
||
Exit;
|
||
end;
|
||
|
||
OpenChannel(
|
||
RXA_CHAN,
|
||
FBufSize,
|
||
FAudioBufSize,
|
||
FSampleRate,
|
||
FAudioRate,
|
||
FAudioRate,
|
||
0,
|
||
0, // state=0: создать но не запускать
|
||
0.010, 0.010, 0.010, 0.010,
|
||
0
|
||
);
|
||
|
||
create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||
create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||
|
||
SetRXABandpassWindow(RXA_CHAN, 1);
|
||
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
|
||
SetRXAShiftRun(RXA_CHAN, 1);
|
||
SetRXAShiftFreq(RXA_CHAN, 0.0);
|
||
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
|
||
SetAGC(FAGCMode, 50.0);
|
||
SetRXAPanelGain1(RXA_CHAN, FVolume);
|
||
SetRXAPanelSelect(RXA_CHAN, 3);
|
||
SetRXAPanelRun(RXA_CHAN, 1);
|
||
SetChannelState(RXA_CHAN, 1, 0);
|
||
|
||
// TXA: mic @ FAudioRate → IQ @ FTXSampleRate (фиксированный DUC rate, не зависит от RX).
|
||
// out_size = FAudioBufSize * FTXSampleRate/FAudioRate = FTXOutBufSize (4096 @ 192k tx).
|
||
// FTXOut уже имеет размер FTXOutBufSize*2.
|
||
OpenChannel(
|
||
TXA_CHAN,
|
||
FAudioBufSize,
|
||
FAudioBufSize,
|
||
FAudioRate,
|
||
FAudioRate,
|
||
FTXSampleRate,
|
||
1,
|
||
0,
|
||
0.010, 0.010, 0.010, 0.010,
|
||
0
|
||
);
|
||
|
||
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
|
||
SetTXAPanelRun(TXA_CHAN, 1);
|
||
// PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I.
|
||
SetTXAPanelSelect(TXA_CHAN, 2);
|
||
|
||
FInitialized := True;
|
||
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
|
||
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
||
ApplyTXChainSettings;
|
||
ApplyNoiseFilterState;
|
||
Result := True;
|
||
|
||
// Запускаем DSP поток после успешной инициализации WDSP
|
||
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
|
||
FDSPThread := TDSPThread.Create(Self);
|
||
FDSPThread.Priority := tpHighest; // DSP критический поток
|
||
|
||
except
|
||
on E: Exception do
|
||
begin
|
||
FLastError := E.ClassName + ': ' + E.Message;
|
||
FInitialized := False;
|
||
Result := False;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.Close;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
// Останавливаем DSP поток перед закрытием WDSP каналов
|
||
if Assigned(FDSPThread) then
|
||
begin
|
||
FDSPThread.Terminate;
|
||
RTLEventSetEvent(FQueueSem); // разбудить чтобы увидел Terminated
|
||
FDSPThread.WaitFor;
|
||
FreeAndNil(FDSPThread);
|
||
end;
|
||
|
||
CloseAnalyzer;
|
||
destroy_anbEXT(RXA_CHAN);
|
||
destroy_nobEXT(RXA_CHAN);
|
||
CloseChannel(RXA_CHAN);
|
||
CloseChannel(TXA_CHAN);
|
||
FInitialized := False;
|
||
end;
|
||
|
||
procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
|
||
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open.
|
||
begin
|
||
if NewRate = FSampleRate then Exit;
|
||
|
||
// Останавливаем текущий канал
|
||
Close;
|
||
|
||
// Сбрасываем очередь IQ пакетов — старые пакеты не совместимы с новым rate.
|
||
// Без этого DSP поток тратит секунды на разбор накопившихся пакетов.
|
||
FQueueHead := 0;
|
||
FQueueTail := 0;
|
||
FRXAccPos := 0;
|
||
|
||
// Обновляем параметры
|
||
FSampleRate := NewRate;
|
||
FBufSize := FAudioBufSize * NewRate div FAudioRate;
|
||
|
||
// Перераспределяем буферы под новый размер
|
||
SetLength(FRXIn, FBufSize * 2);
|
||
SetLength(FRXOut, FBufSize * 2);
|
||
SetLength(FSpecBuf, FBufSize * 2);
|
||
SetLength(FRXAccI, FBufSize);
|
||
SetLength(FRXAccQ, FBufSize);
|
||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||
FDispPos := 0;
|
||
|
||
// Переинициализируем WDSP с новым rate
|
||
Open;
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
|
||
DataOffset: Integer; IQPairs: Integer);
|
||
// Вызывается из СЕТЕВОГО потока — только кладём в очередь и возвращаемся немедленно
|
||
// DSP поток разбудится семафором и займётся обработкой
|
||
var
|
||
NextHead: Integer;
|
||
Item: ^TIQQueueItem;
|
||
DataBytes: Integer;
|
||
begin
|
||
NextHead := (FQueueHead + 1) and (IQ_QUEUE_SIZE - 1);
|
||
if NextHead = FQueueTail then Exit; // очередь полна — пропускаем пакет
|
||
|
||
Item := @FQueue[FQueueHead];
|
||
DataBytes := IQPairs * 6;
|
||
if DataBytes > IQ_PKT_MAXBYTES then DataBytes := IQ_PKT_MAXBYTES;
|
||
|
||
Move(Buf[DataOffset], Item^.Data[0], DataBytes);
|
||
Item^.DataLen := DataBytes;
|
||
Item^.IQPairs := IQPairs;
|
||
|
||
FQueueHead := NextHead;
|
||
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
|
||
end;
|
||
|
||
procedure TWDSPEngine.FlushRX;
|
||
begin
|
||
FQueueHead := FQueueTail;
|
||
FRXAccPos := 0;
|
||
FDispPos := 0;
|
||
if Length(FRXAccI) > 0 then FillChar(FRXAccI[0], Length(FRXAccI) * SizeOf(Double), 0);
|
||
if Length(FRXAccQ) > 0 then FillChar(FRXAccQ[0], Length(FRXAccQ) * SizeOf(Double), 0);
|
||
if Length(FRXIn) > 0 then FillChar(FRXIn[0], Length(FRXIn) * SizeOf(Double), 0);
|
||
if Length(FRXOut) > 0 then FillChar(FRXOut[0], Length(FRXOut) * SizeOf(Double), 0);
|
||
end;
|
||
|
||
procedure TWDSPEngine.BeginPostTXMute(DurationMs: Integer);
|
||
begin
|
||
if DurationMs <= 0 then
|
||
FPostTXMuteUntil := 0
|
||
else
|
||
FPostTXMuteUntil := GetTickCount64 + QWord(DurationMs);
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem);
|
||
// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize.
|
||
// В non-DUP TX (FTXActive and not FKeepRXDuringTX) полностью пропускаем
|
||
// IQ-пакет: не кормим RX-анализатор и не накапливаем для fexchange0.
|
||
// Иначе TX leakage из DDC оседает в RXA pipeline / RX_DISP_ID FFT-истории
|
||
// и звучит/виден на водопаде первые секунды после возврата на RX.
|
||
var
|
||
i, Pos: Integer;
|
||
IR, QR: LongInt;
|
||
const
|
||
SCALE = 1.0 / 8388608.0;
|
||
begin
|
||
if FTXActive and not FKeepRXDuringTX then Exit;
|
||
// Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта.
|
||
// Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не
|
||
// кормится), и аудио (RXA не запускается, FOnAudio не вызывается).
|
||
// Радио продолжает дотравливать FPGA TX-buffer + PA slew-down первые
|
||
// ~50–200мс после MOX-off; этот хвост попадает в RX1 как leak. Дропаем
|
||
// его на входе — single-source-of-truth.
|
||
if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit;
|
||
for i := 0 to Item.IQPairs - 1 do
|
||
begin
|
||
Pos := i * 6;
|
||
if Pos + 5 >= Item.DataLen then Break;
|
||
|
||
IR := (LongInt(Item.Data[Pos]) shl 16) or
|
||
(LongInt(Item.Data[Pos+1]) shl 8) or
|
||
LongInt(Item.Data[Pos+2]);
|
||
QR := (LongInt(Item.Data[Pos+3]) shl 16) or
|
||
(LongInt(Item.Data[Pos+4]) shl 8) or
|
||
LongInt(Item.Data[Pos+5]);
|
||
|
||
if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
|
||
if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);
|
||
|
||
FeedDisplaySample(IR * SCALE, QR * SCALE);
|
||
|
||
FRXAccI[FRXAccPos] := IR * SCALE;
|
||
FRXAccQ[FRXAccPos] := QR * SCALE;
|
||
Inc(FRXAccPos);
|
||
|
||
if FRXAccPos >= FBufSize then
|
||
ProcessRXBlock;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
|
||
begin
|
||
if not FAnalyzerOpen then Exit;
|
||
|
||
FDispBuf[FDispPos * 2] := I;
|
||
FDispBuf[FDispPos * 2 + 1] := Q;
|
||
Inc(FDispPos);
|
||
|
||
if FDispPos >= DISPLAY_BLOCK_SIZE then
|
||
begin
|
||
Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]);
|
||
FDispPos := 0;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.FeedTXDisplaySample(const I, Q: Double);
|
||
begin
|
||
if not FAnalyzerOpen then Exit;
|
||
|
||
FTXDispBuf[FTXDispPos * 2] := I;
|
||
FTXDispBuf[FTXDispPos * 2 + 1] := Q;
|
||
Inc(FTXDispPos);
|
||
|
||
if FTXDispPos >= DISPLAY_BLOCK_SIZE then
|
||
begin
|
||
Spectrum0(1, TX_DISP_ID, 0, 0, @FTXDispBuf[0]);
|
||
FTXDispPos := 0;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.ProcessRXBlock;
|
||
var
|
||
i: Integer;
|
||
Err: Integer;
|
||
// OutL/OutR теперь поля класса — не аллоцируем каждый раз
|
||
begin
|
||
FRXAccPos := 0;
|
||
if not FInitialized then Exit;
|
||
|
||
try
|
||
// Упаковываем в interleaved буфер для fexchange0
|
||
for i := 0 to FBufSize - 1 do
|
||
begin
|
||
FRXIn[i * 2] := FRXAccI[i];
|
||
FRXIn[i * 2 + 1] := FRXAccQ[i];
|
||
end;
|
||
|
||
// DSP обработка
|
||
Err := 0;
|
||
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
|
||
|
||
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
|
||
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
|
||
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
|
||
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
||
|
||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
||
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
||
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
||
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
|
||
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
|
||
// через unsigned-сравнение, GetTickCount64 не зовётся когда дедлайн = 0.
|
||
if (FPostTXMuteUntil <> 0) and (GetTickCount64 >= FPostTXMuteUntil) then
|
||
FPostTXMuteUntil := 0;
|
||
if Assigned(FOnAudio) and not FMuted and not FTXActive
|
||
and (FPostTXMuteUntil = 0) then
|
||
begin
|
||
for i := 0 to FAudioBufSize - 1 do
|
||
begin
|
||
FOutL[i] := FRXOut[i * 2] * FVolume;
|
||
FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
|
||
end;
|
||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||
end;
|
||
|
||
except
|
||
// Защита от падения libwdsp в потоке — просто пропускаем блок
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// RX управление
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.SetMode(Mode: Integer);
|
||
begin
|
||
FMode := Mode;
|
||
if not FInitialized then Exit;
|
||
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
|
||
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
|
||
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetFilter(Low, High: Integer);
|
||
begin
|
||
FFilterLow := Low;
|
||
FFilterHigh := High;
|
||
if not FInitialized then Exit;
|
||
// RXASetPassband — правильный unified API, пересчитывает фильтр целиком
|
||
RXASetPassband(RXA_CHAN, Low, High);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double);
|
||
begin
|
||
FAGCMode := Mode;
|
||
if not FInitialized then Exit;
|
||
|
||
// Точно по piHPSDR receiver.c: set_agc()
|
||
SetRXAAGCMode(RXA_CHAN, Ord(Mode));
|
||
SetRXAAGCSlope(RXA_CHAN, FAGCSlope);
|
||
SetRXAAGCTop(RXA_CHAN, FAGCTop);
|
||
|
||
case Mode of
|
||
agcOff:
|
||
SetRXAAGCFixed(RXA_CHAN, FixedGain);
|
||
|
||
agcLong: begin
|
||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||
SetRXAAGCHang(RXA_CHAN, 2000);
|
||
SetRXAAGCDecay(RXA_CHAN, 2000);
|
||
SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold);
|
||
end;
|
||
|
||
agcSlow: begin
|
||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||
SetRXAAGCHang(RXA_CHAN, 1000);
|
||
SetRXAAGCDecay(RXA_CHAN, 500);
|
||
SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold);
|
||
end;
|
||
|
||
agcMedium: begin
|
||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||
SetRXAAGCHang(RXA_CHAN, 0);
|
||
SetRXAAGCDecay(RXA_CHAN, 250);
|
||
SetRXAAGCHangThreshold(RXA_CHAN, 100);
|
||
end;
|
||
|
||
agcFast: begin
|
||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||
SetRXAAGCHang(RXA_CHAN, 0);
|
||
SetRXAAGCDecay(RXA_CHAN, 50);
|
||
SetRXAAGCHangThreshold(RXA_CHAN, 100);
|
||
end;
|
||
end;
|
||
|
||
// Читаем обратно hang level и thresh для линии на спектре
|
||
UpdateAGCLines(FLastSpectrumW);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetShift(ShiftHz: Double);
|
||
begin
|
||
FShiftHz := ShiftHz;
|
||
if not FInitialized then Exit;
|
||
SetRXAShiftFreq(RXA_CHAN, ShiftHz);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetAGCTop(TopDBm: Double);
|
||
begin
|
||
FAGCTop := TopDBm;
|
||
if not FInitialized then Exit;
|
||
SetRXAAGCTop(RXA_CHAN, TopDBm);
|
||
UpdateAGCLines(FLastSpectrumW);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetAGCSlope(Slope: Integer);
|
||
begin
|
||
FAGCSlope := Slope;
|
||
if not FInitialized then Exit;
|
||
SetRXAAGCSlope(RXA_CHAN, Slope);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetAGCHangThreshold(Threshold: Integer);
|
||
begin
|
||
FAGCHangThreshold := Threshold;
|
||
if not FInitialized then Exit;
|
||
SetRXAAGCHangThreshold(RXA_CHAN, Threshold);
|
||
UpdateAGCLines(FLastSpectrumW);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSpectrumWidth(W: Integer);
|
||
begin
|
||
if W <= 0 then Exit;
|
||
if FLastSpectrumW = W then Exit;
|
||
FLastSpectrumW := W;
|
||
if FAnalyzerOpen then
|
||
ApplyAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
if SpectrumW <= 0 then Exit; // окно ещё не показано — не читаем
|
||
GetRXAAGCHangLevel(RXA_CHAN, @FAGCHangLevel);
|
||
GetRXAAGCThresh(RXA_CHAN, @FAGCThresh,
|
||
SpectrumW, // реальная ширина дисплея в пикселях
|
||
FSampleRate);
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyNRState;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
SetRXAANRVals(RXA_CHAN, 64, 16, 16e-4, 10e-7);
|
||
SetRXAANRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||
SetRXAANRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_1));
|
||
|
||
SetRXAEMNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||
SetRXAEMNRgainMethod(RXA_CHAN, FILTER_NR2_GAIN_METHOD);
|
||
SetRXAEMNRnpeMethod(RXA_CHAN, FILTER_NR2_NPE_METHOD);
|
||
SetRXAEMNRtrainZetaThresh(RXA_CHAN, FILTER_NR2_TRAINED_THRESHOLD);
|
||
SetRXAEMNRtrainT2(RXA_CHAN, FILTER_NR2_TRAINED_T2);
|
||
SetRXAEMNRpost2Taper(RXA_CHAN, FILTER_NR2_POST_TAPER);
|
||
SetRXAEMNRpost2Nlevel(RXA_CHAN, FILTER_NR2_POST_NLEVEL);
|
||
SetRXAEMNRpost2Factor(RXA_CHAN, FILTER_NR2_POST_FACTOR);
|
||
SetRXAEMNRpost2Rate(RXA_CHAN, FILTER_NR2_POST_RATE);
|
||
SetRXAEMNRaeRun(RXA_CHAN, 1);
|
||
SetRXAEMNRpost2Run(RXA_CHAN, FILTER_NR2_POST_RUN);
|
||
SetRXAEMNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_2));
|
||
|
||
SetRXARNNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||
SetRXARNNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_3));
|
||
|
||
SetRXASBNRreductionAmount(RXA_CHAN, FILTER_NR4_REDUCTION_AMOUNT);
|
||
SetRXASBNRsmoothingFactor(RXA_CHAN, FILTER_NR4_SMOOTHING_FACTOR);
|
||
SetRXASBNRwhiteningFactor(RXA_CHAN, FILTER_NR4_WHITENING_FACTOR);
|
||
SetRXASBNRnoiseRescale(RXA_CHAN, FILTER_NR4_NOISE_RESCALE);
|
||
SetRXASBNRpostFilterThreshold(RXA_CHAN, FILTER_NR4_POST_THRESHOLD);
|
||
SetRXASBNRnoiseScalingType(RXA_CHAN, FILTER_NR4_NOISE_SCALING_TYPE);
|
||
SetRXASBNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||
SetRXASBNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_4));
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyNBState;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
SetEXTANBTau(RXA_CHAN, FILTER_NB_TAU);
|
||
SetEXTANBHangtime(RXA_CHAN, FILTER_NB_HANGTIME);
|
||
SetEXTANBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME);
|
||
SetEXTANBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD);
|
||
SetEXTANBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_1));
|
||
|
||
SetEXTNOBMode(RXA_CHAN, FILTER_NB2_MODE);
|
||
SetEXTNOBTau(RXA_CHAN, FILTER_NB_TAU);
|
||
SetEXTNOBHangtime(RXA_CHAN, FILTER_NB_HANGTIME);
|
||
SetEXTNOBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME);
|
||
SetEXTNOBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD);
|
||
SetEXTNOBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_2));
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplySNBState;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetRXASNBARun(RXA_CHAN, Ord(FSNBEnabled));
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyANFState;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetRXAANFPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||
SetRXAANFRun(RXA_CHAN, Ord(FANFEnabled));
|
||
end;
|
||
|
||
procedure TWDSPEngine.ApplyNoiseFilterState;
|
||
begin
|
||
ApplyNRState;
|
||
ApplyNBState;
|
||
ApplySNBState;
|
||
ApplyANFState;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetNRMode(Mode: Integer);
|
||
begin
|
||
if Mode < NR_MODE_OFF then Mode := NR_MODE_OFF;
|
||
if Mode > NR_MODE_4 then Mode := NR_MODE_4;
|
||
FNRMode := Mode;
|
||
ApplyNRState;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetNR(Enable: Boolean);
|
||
begin
|
||
if Enable then SetNRMode(NR_MODE_1)
|
||
else SetNRMode(NR_MODE_OFF);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetNBMode(Mode: Integer);
|
||
begin
|
||
if Mode < NB_MODE_OFF then Mode := NB_MODE_OFF;
|
||
if Mode > NB_MODE_2 then Mode := NB_MODE_2;
|
||
FNBMode := Mode;
|
||
ApplyNBState;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetNB(Enable: Boolean);
|
||
begin
|
||
if Enable then SetNBMode(NB_MODE_1)
|
||
else SetNBMode(NB_MODE_OFF);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSNB(Enable: Boolean);
|
||
begin
|
||
FSNBEnabled := Enable;
|
||
ApplySNBState;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetANF(Enable: Boolean);
|
||
begin
|
||
FANFEnabled := Enable;
|
||
ApplyANFState;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetRXAFMDeviation(RXA_CHAN, Deviation);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer);
|
||
var Threshold: Double;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
if Enable then
|
||
begin
|
||
Threshold := Level / 200.0; // 0..100 → 0.0..0.5
|
||
SetRXAFMSQThreshold(RXA_CHAN, Threshold);
|
||
SetRXAFMSQRun(RXA_CHAN, 1);
|
||
end
|
||
else
|
||
SetRXAFMSQRun(RXA_CHAN, 0);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetVolume(Vol: Double);
|
||
begin
|
||
FVolume := Max(0.0, Min(1.0, Vol));
|
||
if not FInitialized then Exit;
|
||
SetRXAPanelGain1(RXA_CHAN, FVolume);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetMute(Mute: Boolean);
|
||
begin
|
||
FMuted := Mute;
|
||
if not FInitialized then Exit;
|
||
SetRXAPanelRun(RXA_CHAN, Ord(not Mute));
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX управление
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.SetTXMode(Mode: Integer);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFilter(Low, High: Integer);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetTXABandpassFreqs(TXA_CHAN, Low, High);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetDriveLevel(Level: Double);
|
||
begin
|
||
if not FInitialized then Exit;
|
||
SetTXAALCMaxGain(TXA_CHAN, Max(0.0, Min(1.0, Level)));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetMicGain(GainDB: Double);
|
||
begin
|
||
FTXMicGainDB := GainDB;
|
||
if not FInitialized then Exit;
|
||
SetTXAPanelGain1(TXA_CHAN, Power(10.0, GainDB / 20.0));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXRun(Run: Boolean);
|
||
begin
|
||
FTXActive := Run;
|
||
FDispPos := 0;
|
||
FTXDispPos := 0;
|
||
if Run then
|
||
FActiveDisplayID := TX_DISP_ID
|
||
else
|
||
FActiveDisplayID := RX_DISP_ID;
|
||
|
||
if Run then
|
||
begin
|
||
if FInitialized then
|
||
SetChannelState(TXA_CHAN, 1, 0); // run, no delay
|
||
// Сброс mic ring buffer и запуск TX потока
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
if not Assigned(FTXThread) then
|
||
begin
|
||
FTXThread := TTXDSPThread.Create(Self);
|
||
FTXThread.Priority := tpHighest;
|
||
end;
|
||
end else begin
|
||
if FInitialized then
|
||
SetChannelState(TXA_CHAN, 0, 1); // stop, with slew
|
||
// Останавливаем TX поток
|
||
if Assigned(FTXThread) then
|
||
begin
|
||
FTXThread.Terminate;
|
||
RTLEventSetEvent(FTXMicSem);
|
||
FTXThread.WaitFor;
|
||
FreeAndNil(FTXThread);
|
||
end;
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
|
||
var
|
||
SignedFreq: Double;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
if Enabled then
|
||
begin
|
||
// gen1 (PostGen) стоит после всех модуляторов и бандпасса.
|
||
// Положительная частота даёт тон ВЫШЕ несущей (USB-сторона).
|
||
// Для LSB и CWL нужен тон НИЖЕ несущей — инвертируем знак.
|
||
if (FMode = MODE_LSB) or (FMode = MODE_CWL) then
|
||
SignedFreq := -Abs(FreqHz)
|
||
else
|
||
SignedFreq := Abs(FreqHz);
|
||
SetTXAPostGenMode(TXA_CHAN, 0); // 0 = tone
|
||
SetTXAPostGenToneFreq(TXA_CHAN, SignedFreq);
|
||
SetTXAPostGenToneMag(TXA_CHAN, Max(0.0, Min(1.0, Mag)));
|
||
SetTXAPostGenRun(TXA_CHAN, 1);
|
||
end else
|
||
SetTXAPostGenRun(TXA_CHAN, 0);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetDisplaySourceTX(UseTX: Boolean);
|
||
begin
|
||
// Тот же сброс позиций, что и в SetTXRun — analyzer-буферы у обоих DispID
|
||
// независимы, но мы переключаем источник, и старые байты в накапливающем
|
||
// буфере на новой стороне могут повести себя как «всплеск», если их не
|
||
// обнулить.
|
||
FDispPos := 0;
|
||
FTXDispPos := 0;
|
||
if UseTX then
|
||
FActiveDisplayID := TX_DISP_ID
|
||
else
|
||
FActiveDisplayID := RX_DISP_ID;
|
||
end;
|
||
|
||
procedure TWDSPEngine.ProcessTXBlock;
|
||
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
||
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
|
||
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
|
||
// если mic ring buffer содержит достаточно данных — берём их,
|
||
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
|
||
var
|
||
i, tail: Integer;
|
||
Err: Integer;
|
||
Avail: Integer;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||
|
||
if Avail >= FAudioBufSize then
|
||
begin
|
||
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
|
||
tail := FTXMicTail;
|
||
for i := 0 to FAudioBufSize - 1 do
|
||
begin
|
||
FTXIn[i * 2] := FTXMicRing[tail];
|
||
FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал
|
||
tail := (tail + 1) and (TX_MIC_RING - 1);
|
||
end;
|
||
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
|
||
end else
|
||
begin
|
||
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
|
||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||
end;
|
||
|
||
Err := 0;
|
||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||
|
||
// TX сигнал → отдельный TX-analyzer (TX_DISP_ID).
|
||
// Подаём чанками по DISPLAY_BLOCK_SIZE — analyzer ожидает именно такой
|
||
// размер блока (см. SetAnalyzer). Если подавать FTXOut целиком
|
||
// (FTXOutBufSize пар), analyzer прочитает только первые DISPLAY_BLOCK_SIZE
|
||
// и проигнорирует остальное → спектр будет пустой.
|
||
if FAnalyzerOpen then
|
||
for i := 0 to FTXOutBufSize - 1 do
|
||
FeedTXDisplaySample(FTXOut[i * 2], FTXOut[i * 2 + 1]);
|
||
|
||
// FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double)
|
||
if Assigned(FOnTXIQ) then
|
||
FOnTXIQ(FTXOut, FTXOutBufSize);
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
||
// Вызывается из receive thread — lock-free, кладёт сэмплы в ring buffer
|
||
// 16-bit big-endian samples из аппаратного микрофона
|
||
var
|
||
i, cnt, newHead: Integer;
|
||
S: SmallInt;
|
||
begin
|
||
cnt := N;
|
||
if cnt > Length(Src) then cnt := Length(Src);
|
||
|
||
for i := 0 to cnt - 1 do
|
||
begin
|
||
// Аппаратный mic приходит в big-endian, нужно swap байт
|
||
S := SmallInt(((Src[i] and $FF) shl 8) or ((Src[i] shr 8) and $FF));
|
||
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
|
||
if newHead <> FTXMicTail then // буфер не полон
|
||
begin
|
||
FTXMicRing[FTXMicHead] := S / 32768.0;
|
||
FTXMicHead := newHead;
|
||
end;
|
||
// если буфер полон — сэмпл отбрасывается (overrun)
|
||
end;
|
||
|
||
RTLEventSetEvent(FTXMicSem); // будим TX поток
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushTXMicSampleD(const S: Double);
|
||
var newHead: Integer;
|
||
begin
|
||
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
|
||
if newHead <> FTXMicTail then
|
||
begin
|
||
FTXMicRing[FTXMicHead] := S;
|
||
FTXMicHead := newHead;
|
||
end;
|
||
// overrun → дроп, как в HW path
|
||
RTLEventSetEvent(FTXMicSem);
|
||
end;
|
||
|
||
procedure TWDSPEngine.PushTXMicSamplesD(const Src: array of Double; N: Integer);
|
||
// Batch-вариант: одна будилка семафора на N сэмплов.
|
||
var
|
||
i, cnt, newHead: Integer;
|
||
begin
|
||
cnt := N;
|
||
if cnt > Length(Src) then cnt := Length(Src);
|
||
|
||
for i := 0 to cnt - 1 do
|
||
begin
|
||
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
|
||
if newHead <> FTXMicTail then
|
||
begin
|
||
FTXMicRing[FTXMicHead] := Src[i];
|
||
FTXMicHead := newHead;
|
||
end;
|
||
// overrun → дроп
|
||
end;
|
||
|
||
if cnt > 0 then
|
||
RTLEventSetEvent(FTXMicSem);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX chain: применение всех TX-параметров к WDSP-каналу
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.ApplyTXChainSettings;
|
||
// Вызывается после OpenChannel(TXA) и при старте TX. Прокидывает все
|
||
// FTX*-поля в WDSP. Идемпотентна, ничего не меняет если канал не открыт.
|
||
var
|
||
i: Integer;
|
||
Gains, Freqs, Qs: array[0..10] of Double;
|
||
begin
|
||
if not FInitialized then Exit;
|
||
|
||
// Bandpass
|
||
if (FTXFilterLow > 0) and (FTXFilterHigh > FTXFilterLow) then
|
||
SetTXABandpassFreqs(TXA_CHAN, FTXFilterLow, FTXFilterHigh);
|
||
if FTXFilterWindow >= 0 then
|
||
SetTXABandpassWindow(TXA_CHAN, FTXFilterWindow);
|
||
if FTXFilterNC > 0 then
|
||
TXASetNC(TXA_CHAN, FTXFilterNC);
|
||
if FTXFilterMP >= 0 then
|
||
TXASetMP(TXA_CHAN, FTXFilterMP);
|
||
|
||
// Mic gain (panel gain1)
|
||
SetTXAPanelGain1(TXA_CHAN, Power(10.0, FTXMicGainDB / 20.0));
|
||
|
||
// Compressor
|
||
SetTXACompressorRun(TXA_CHAN, Ord(FTXCompressorOn));
|
||
SetTXACompressorGain(TXA_CHAN, FTXCompressorGain);
|
||
|
||
// Leveler
|
||
SetTXALevelerSt(TXA_CHAN, Ord(FTXLevelerOn));
|
||
SetTXALevelerTop(TXA_CHAN, FTXLevelerTop);
|
||
SetTXALevelerDecay(TXA_CHAN, FTXLevelerDecay);
|
||
|
||
// ALC
|
||
SetTXAALCSt(TXA_CHAN, Ord(FTXALCOn));
|
||
SetTXAALCMaxGain(TXA_CHAN, FTXALCMaxGain);
|
||
SetTXAALCDecay(TXA_CHAN, FTXALCDecay);
|
||
|
||
// Phase Rotator
|
||
SetTXAPHROTRun(TXA_CHAN, Ord(FTXPhaseRotOn));
|
||
SetTXAPHROTNstages(TXA_CHAN, FTXPhaseRotStages);
|
||
SetTXAPHROTCorner(TXA_CHAN, FTXPhaseRotFreq);
|
||
|
||
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
|
||
for i := 0 to 10 do
|
||
begin
|
||
Gains[i] := FTXEQGains[i];
|
||
Freqs[i] := FTXEQFreqs[i];
|
||
Qs[i] := 0.707; // умолчание Q
|
||
end;
|
||
SetTXAEQProfile(TXA_CHAN, FTXEQNumBands, @Freqs[0], @Gains[0]);
|
||
SetTXAEQRun(TXA_CHAN, Ord(FTXEQOn));
|
||
|
||
// AM
|
||
SetTXAAMCarrierLevel(TXA_CHAN, FTXAMCarrier);
|
||
|
||
// FM
|
||
SetTXAFMDeviation(TXA_CHAN, FTXFMDeviation);
|
||
if (FTXFMLowCut < FTXFMHighCut) then
|
||
SetTXAFMAFFilter(TXA_CHAN, FTXFMLowCut, FTXFMHighCut);
|
||
SetTXAFMEmphPosition(TXA_CHAN, FTXFMEmphPos);
|
||
|
||
// CTCSS
|
||
SetTXACTCSSFreq(TXA_CHAN, FTXCTCSSFreq);
|
||
SetTXACTCSSRun(TXA_CHAN, Ord(FTXCTCSSOn));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||
begin
|
||
FTXFilterLow := Low;
|
||
FTXFilterHigh := High;
|
||
FTXFilterNC := NC;
|
||
FTXFilterMP := MP;
|
||
FTXFilterWindow := Window;
|
||
FFilterLow := Low;
|
||
FFilterHigh := High;
|
||
if not FInitialized then Exit;
|
||
SetTXABandpassFreqs(TXA_CHAN, Low, High);
|
||
SetTXABandpassWindow(TXA_CHAN, Window);
|
||
if NC > 0 then TXASetNC(TXA_CHAN, NC);
|
||
if MP >= 0 then TXASetMP(TXA_CHAN, MP);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXCompressor(Enable: Boolean; GainDB: Double);
|
||
begin
|
||
FTXCompressorOn := Enable;
|
||
FTXCompressorGain := GainDB;
|
||
if not FInitialized then Exit;
|
||
SetTXACompressorGain(TXA_CHAN, GainDB);
|
||
SetTXACompressorRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||
begin
|
||
FTXLevelerOn := Enable;
|
||
FTXLevelerTop := TopDB;
|
||
FTXLevelerDecay := DecayMS;
|
||
if not FInitialized then Exit;
|
||
SetTXALevelerTop(TXA_CHAN, TopDB);
|
||
SetTXALevelerDecay(TXA_CHAN, DecayMS);
|
||
SetTXALevelerSt(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||
begin
|
||
FTXALCOn := Enable;
|
||
FTXALCMaxGain := MaxGainDB;
|
||
FTXALCDecay := DecayMS;
|
||
if not FInitialized then Exit;
|
||
SetTXAALCMaxGain(TXA_CHAN, MaxGainDB);
|
||
SetTXAALCDecay(TXA_CHAN, DecayMS);
|
||
SetTXAALCSt(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
|
||
begin
|
||
FTXPhaseRotOn := Enable;
|
||
FTXPhaseRotStages := Stages;
|
||
FTXPhaseRotFreq := FreqHz;
|
||
if not FInitialized then Exit;
|
||
SetTXAPHROTNstages(TXA_CHAN, Stages);
|
||
SetTXAPHROTCorner(TXA_CHAN, FreqHz);
|
||
SetTXAPHROTRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXEQ(Enable: Boolean; NumBands: Integer;
|
||
const Gains, Freqs: array of Double);
|
||
var
|
||
i, n: Integer;
|
||
G, F: array[0..10] of Double;
|
||
begin
|
||
FTXEQOn := Enable;
|
||
FTXEQNumBands := NumBands;
|
||
n := Min(11, Min(Length(Gains), Length(Freqs)));
|
||
for i := 0 to n - 1 do
|
||
begin
|
||
FTXEQGains[i] := Gains[i];
|
||
FTXEQFreqs[i] := Freqs[i];
|
||
G[i] := Gains[i];
|
||
F[i] := Freqs[i];
|
||
end;
|
||
if not FInitialized then Exit;
|
||
SetTXAEQProfile(TXA_CHAN, NumBands, @F[0], @G[0]);
|
||
SetTXAEQRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXAMCarrierLevel(Level: Double);
|
||
begin
|
||
FTXAMCarrier := Level;
|
||
if not FInitialized then Exit;
|
||
WDSP.SetTXAAMCarrierLevel(TXA_CHAN, Level);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
|
||
begin
|
||
FTXFMDeviation := Deviation;
|
||
FTXFMLowCut := LowCut;
|
||
FTXFMHighCut := HighCut;
|
||
FTXFMEmphPos := EmphPos;
|
||
if not FInitialized then Exit;
|
||
SetTXAFMDeviation(TXA_CHAN, Deviation);
|
||
if LowCut < HighCut then
|
||
SetTXAFMAFFilter(TXA_CHAN, LowCut, HighCut);
|
||
SetTXAFMEmphPosition(TXA_CHAN, EmphPos);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXCTCSS(Enable: Boolean; FreqHz: Double);
|
||
begin
|
||
FTXCTCSSOn := Enable;
|
||
FTXCTCSSFreq := FreqHz;
|
||
if not FInitialized then Exit;
|
||
SetTXACTCSSFreq(TXA_CHAN, FreqHz);
|
||
SetTXACTCSSRun(TXA_CHAN, Ord(Enable));
|
||
end;
|
||
|
||
function TWDSPEngine.TXMicRingAvail: Integer;
|
||
begin
|
||
Result := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXMicSource(Source: TTXMicSource);
|
||
begin
|
||
if FTXMicSource = Source then Exit;
|
||
FTXMicSource := Source;
|
||
// При смене источника сбрасываем ring, чтобы не подмешивать остатки
|
||
// от предыдущего producer'а (race window игнорируется — TX обычно неактивен).
|
||
FTXMicHead := 0;
|
||
FTXMicTail := 0;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXFFTParams(FFTSize, WinType: Integer);
|
||
begin
|
||
FTXFFTSize := NormalizeFFTSize(FFTSize);
|
||
FTXWindowType := EnsureRange(WinType, 0, 6);
|
||
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
begin
|
||
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);
|
||
FTXSpecDetector := Detector;
|
||
FTXSpecAvgMode := AvgMode;
|
||
FTXSpecAvgTimeMS := AvgTimeMS;
|
||
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||
begin
|
||
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);
|
||
FTXWfDetector := Detector;
|
||
FTXWfAvgMode := AvgMode;
|
||
FTXWfAvgTimeMS := AvgTimeMS;
|
||
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Spectrum
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TWDSPEngine.UpdateSpectrum;
|
||
// Вызывается из таймера главного потока (настраиваемый FPS).
|
||
// Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
|
||
// буфера (который fexchange0 непрерывно заполняет из DSP-потока).
|
||
// Это обеспечивает равномерный FPS независимо от размера FFT:
|
||
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
|
||
// буфера, а не ждёт следующего полного блока.
|
||
var
|
||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||
Flag: Integer;
|
||
i, N: Integer;
|
||
DispID: Integer;
|
||
begin
|
||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
|
||
|
||
Flag := 0;
|
||
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||
if Flag <> 0 then
|
||
begin
|
||
for i := 0 to N - 1 do
|
||
FSpectrumPixels[i] := PixBuf[i];
|
||
if Assigned(FOnSpectrum) then
|
||
FOnSpectrum(FSpectrumPixels, N);
|
||
end;
|
||
|
||
Flag := 0;
|
||
GetPixels(DispID, 1, @PixBuf[0], @Flag);
|
||
if Flag <> 0 then
|
||
begin
|
||
for i := 0 to N - 1 do
|
||
FWaterfallPixels[i] := PixBuf[i];
|
||
if Assigned(FOnWaterfall) then
|
||
FOnWaterfall(FWaterfallPixels, N);
|
||
end;
|
||
end;
|
||
|
||
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
||
var Count: Integer);
|
||
var
|
||
N: Integer;
|
||
begin
|
||
N := Min(SPECTRUM_PIXELS, Length(Pixels));
|
||
if N > 0 then
|
||
Move(FSpectrumPixels[0], Pixels[0], N * SizeOf(Single));
|
||
Count := N;
|
||
end;
|
||
|
||
function TWDSPEngine.GetSMeterDBm: Double;
|
||
begin
|
||
// НЕ вызываем GetRXAMeter здесь повторно — значение уже обновлено
|
||
// в DSP-колбэке. Повторный вызов сбрасывает аккумулятор WDSP → 0.
|
||
Result := FSMeter;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Настройки анализатора (применяются немедленно)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
|
||
begin
|
||
// Thetis-style display FFT range.
|
||
if FFTSize < 4096 then FFTSize := 4096;
|
||
Result := 4096;
|
||
while Result < FFTSize do
|
||
Result := Result shl 1;
|
||
if Result > 262144 then
|
||
Result := 262144;
|
||
end;
|
||
|
||
function TWDSPEngine.CalcDisplayFFTSize: Integer;
|
||
begin
|
||
// FFT Size теперь снова управляет display-analyzer как в Thetis,
|
||
// но не влияет на аудио DSP цепочку.
|
||
Result := FFFTSize;
|
||
end;
|
||
|
||
procedure TWDSPEngine.CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
|
||
out EffectiveFPS: Double);
|
||
var
|
||
TargetFPS: Double;
|
||
begin
|
||
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
|
||
// Thetis/piHPSDR-style timing:
|
||
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
|
||
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
|
||
EffectiveFPS := TargetFPS;
|
||
end;
|
||
|
||
procedure TWDSPEngine.NormalizeDisplayParams(var Detector, AvgMode: Integer;
|
||
var AvgTimeMS: Double; ForWaterfall: Boolean);
|
||
begin
|
||
Detector := EnsureRange(Detector, 0, 4);
|
||
|
||
if ForWaterfall then
|
||
begin
|
||
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
|
||
if AvgMode < 0 then AvgMode := 1;
|
||
end
|
||
else
|
||
if AvgMode < -1 then AvgMode := -1;
|
||
|
||
if AvgMode > 3 then AvgMode := 3;
|
||
|
||
if AvgMode <= 0 then
|
||
AvgTimeMS := 0.0
|
||
else
|
||
AvgTimeMS := EnsureRange(AvgTimeMS, 10.0, 5000.0);
|
||
end;
|
||
|
||
procedure TWDSPEngine.AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
|
||
out AvgCount: Integer; out Backmult: Double);
|
||
var
|
||
TimeSec: Double;
|
||
Frames: Double;
|
||
FPSv: Double;
|
||
begin
|
||
if AvgMode <= 0 then
|
||
begin
|
||
AvgCount := 1;
|
||
Backmult := 0.0;
|
||
Exit;
|
||
end;
|
||
|
||
TimeSec := EnsureRange(AvgTimeMS, 10.0, 5000.0) * 0.001;
|
||
FPSv := Max(1.0, FEffectiveDisplayFPS);
|
||
Frames := FPSv * TimeSec;
|
||
AvgCount := EnsureRange(Round(Frames), 2, 60);
|
||
Backmult := Exp(-1.0 / Max(1E-6, FPSv * TimeSec));
|
||
Backmult := EnsureRange(Backmult, 0.0, 0.9999);
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
|
||
var
|
||
NewFFT: Integer;
|
||
begin
|
||
NewFFT := NormalizeFFTSize(FFTSize);
|
||
if WinType < 0 then WinType := 0;
|
||
if WinType > 6 then WinType := 6;
|
||
|
||
if (FFFTSize = NewFFT) and (FWindowType = WinType) then Exit;
|
||
|
||
FFFTSize := NewFFT;
|
||
FWindowType := WinType;
|
||
if FAnalyzerOpen then
|
||
ApplyAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetDisplayFPS(FPS: Integer);
|
||
begin
|
||
FPS := EnsureRange(FPS, 1, 100);
|
||
if FDisplayFPS = FPS then Exit;
|
||
FDisplayFPS := FPS;
|
||
if FAnalyzerOpen then
|
||
ApplyAnalyzerSettings;
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode: Integer;
|
||
AvgTimeMS: Double);
|
||
var
|
||
AvgCount: Integer;
|
||
Backmult: Double;
|
||
begin
|
||
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);
|
||
|
||
if (FSpecDetector = Detector) and
|
||
(FSpecAvgMode = AvgMode) and
|
||
(Abs(FSpecAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;
|
||
|
||
FSpecDetector := Detector;
|
||
FSpecAvgMode := AvgMode;
|
||
FSpecAvgTimeMS := AvgTimeMS;
|
||
if not FAnalyzerOpen then Exit;
|
||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
|
||
SetDisplayAverageMode(RX_DISP_ID, 0, FSpecAvgMode);
|
||
SetDisplayNumAverage(RX_DISP_ID, 0, AvgCount);
|
||
SetDisplayAvBackmult(RX_DISP_ID, 0, Backmult);
|
||
SetDisplayNormOneHz(RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||
end;
|
||
|
||
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
|
||
AvgTimeMS: Double);
|
||
var
|
||
AvgCount: Integer;
|
||
Backmult: Double;
|
||
begin
|
||
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);
|
||
|
||
if (FWfDetector = Detector) and
|
||
(FWfAvgMode = AvgMode) and
|
||
(Abs(FWfAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;
|
||
|
||
FWfDetector := Detector;
|
||
FWfAvgMode := AvgMode;
|
||
FWfAvgTimeMS := AvgTimeMS;
|
||
if not FAnalyzerOpen then Exit;
|
||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
|
||
SetDisplayDetectorMode(RX_DISP_ID, 1, FWfDetector);
|
||
SetDisplayAverageMode(RX_DISP_ID, 1, FWfAvgMode);
|
||
SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount);
|
||
SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult);
|
||
SetDisplayNormOneHz(RX_DISP_ID, 1, 0);
|
||
end;
|
||
|
||
end.
|