Files
ewsdr/WDSPEngine.pas
T
ew8bakandClaude Opus 4.7 96d7e0c759 TX: фиксированный sample rate, per-device grid, CTUN-aware overlay, alpha-blend filter bands
Декаплинг TX от RX sample rate (как в Thetis):
- WDSPEngine.pas: константа TX_SAMPLE_RATE=192000, поля FTXSampleRate/FTXOutBufSize.
  TXA OpenChannel теперь использует FTXSampleRate как out_rate, TX-анализатор
  через SetDisplaySampleRate(TX_DISP_ID, FTXSampleRate). ProcessTXBlock работает
  с FTXOutBufSize вместо FBufSize. Public TXSampleRate для MainForm.
- MainForm.SendDUCSpecificFromSettings: DUC0Rate теперь берётся из
  FDSPEngine.TXSampleRate, а не из RX-настроек — TX всегда 192k независимо от
  выбранного RX-rate (48/96/192/384k).

DUC Specific cache + keepalive resend (HPSDRNetwork.pas):
- FCachedDUCSpec/FCachedDUCValid параллельно DDC-кэшу.
- KeepAlive thread в первые 5s после Run=1 переотправляет и DDC, и DUC
  Specific — устраняет гонку, когда DUC настройки терялись железом.
- Disconnect сбрасывает оба valid-флага.

Per-device TX grid settings (Thetis-style):
- Settings.pas: TXSpecRefLevel/TXSpecRange/TXSpecGridStep в TTXSettings
  (defaults 0.0/80.0/10.0). LoadTX/SaveTX — ключи tx_spec_ref_level/range/grid_step.
- SettingsForm.pas: новая группа \"TX Grid\" во вкладке Transmit
  (RefLevel edit, Range edit, GridStep combo). CollectTXFromUI читает значения,
  LoadTXSettings заполняет UI.
- MainForm.pas: поля FTXSpecRefLevel/Range/GridStep, ApplySpecViewGridFromState
  пушит в FSpecView либо TX, либо RX параметры в зависимости от FTransmitting.
  ApplyGridParams (RX-путь) уважает FTransmitting и не перебивает TX-grid.
  OnTXSettingsChange при активном TX моментально применяет новый grid.

TX-aware spectrum/waterfall с CTUN-выравниванием (SpectrumView.pas):
- Поля FTXMode/FTXFreq/FTXSpanHz/FTXVfoIndex. TXSpanHz=192000 при создании.
- DrawSpectrum/DrawWaterfall в FTXMode мапят данные TX-анализатора (192k вокруг
  FTXFreq) в RX-координаты FCenterFreq/FSpanHz. Формула:
    freq = FCenterFreq - FSpanHz/2 + x*FSpanHz/W
    idx  = (freq-FTXFreq + TXSpanHz/2)/TXSpanHz * (SrcCount-1)
  Вне диапазона — спектр уходит в -200 dB; водопад копирует пиксель из
  предыдущей строки (FWfPixels[X+W]) — вместо чёрной полосы продолжает
  скроллиться старая RX-картинка.
- Эффект: при CTUN ON TX-сигнал рисуется на полосе фильтра (на VFO), а не в
  центре. При RX rate ≠ 192k частотная сетка совпадает между RX и TX
  автоматически (ruler всегда в RX-координатах).
- MainForm.SyncSpecViewFreq/ApplyMOX обновляют TXFreq/TXVfoIndex/TXMode при
  каждом PTT и смене VFO; учитывается split (FSplitTxB → TXVfoIndex=1).

Alpha-blended filter bands (визуальный feedback TX/RX):
- SpectrumView: PixelFormat:=pf32bit, процедура BlendBand через ScanLine[Y]
  внутри BeginUpdate/EndUpdate. Альфа-смешение по формуле
  (a*src + (255-a)*dst) / 255 для каждого канала BGR.
- В FTXMode полоса фильтра становится полупрозрачно красной. Если split
  (FTXVfoIndex<>FActiveVfo): на RX-частоте полоса зелёная, на TX-частоте —
  красная (CalcFilterBandX вычисляет границы для произвольной VFO-freq).
- При FTXMode=False — старое поведение (сплошная заливка SpecFilter).

Build: lazbuild --ws=qt6 ewsdr.lpi — clean, 0 errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 14:17:49 +03:00

2021 lines
70 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit WDSPEngine;
{
WDSP DSP Engine for OpenHPSDR Transceiver
Correct WDSP API usage based on actual WDSP.pas bindings:
Spectrum pipeline:
XCreateAnalyzer(disp, ...) — создать analyzer display
SetDisplay*(disp, ...) — настроить detector/average/rate
SetRXASpectrum(ch, 1, disp, 0, 0) — подключить RXA к display
fexchange0() в цикле → WDSP внутри пишет данные в display буфер
Spectrum0(1, disp, 0, 0, nil) — тригер snapshot
GetPixels(disp, 0, pix, flag) — забрать пиксели
OpenChannel сигнатура (13 параметров):
channel, in_size, dsp_size, in_rate, dsp_rate, out_rate,
atype, state, tdelayup, tslewup, tdelaydown, tslewdown, bfo
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math, SyncObjs,
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);
{ 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;
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);
// 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 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);
// Расширенное 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 пути.
// Должен вызываться из ОДНОГО потока (PortAudio callback).
procedure PushTXMicSampleD(const S: Double);
// Настройки анализатора (применяются немедленно если открыт)
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;
// Активный 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;
begin
// 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс
PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate);
while not Terminated do
begin
// Ждём сигнала от 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-данными
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;
// Если mic-данных не было — всё равно обрабатываем один блок тишины.
// Аналог piHPSDR: если mic_ring пуст, tx_add_mic_sample получает 0.0.
// Это держит DUC IQ поток и обновляет спектр/водопад TX-сигналом.
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1);
if Avail < FEngine.FAudioBufSize then
FEngine.ProcessTXBlock;
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;
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.PushIQItemToDSP(const Item: TIQQueueItem);
// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize
var
i, Pos: Integer;
IR, QR: LongInt;
const
SCALE = 1.0 / 8388608.0;
begin
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.
if Assigned(FOnAudio) and not FMuted and not FTXActive 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.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.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;
// ---------------------------------------------------------------------------
// 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.