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 DISP_ID = 0; // ID для XCreateAnalyzer 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; 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; { TWDSPEngine } TWDSPEngine = class private FInitialized: Boolean; FAnalyzerOpen: Boolean; FSampleRate: Integer; FAudioRate: Integer; FBufSize: Integer; // входной буфер @ FSampleRate FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate) // 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; // Аудио выходные буферы — аллоцируем один раз 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; 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 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 mic вход — вызывается из receive thread (thread-safe lock-free) // Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов procedure PushTXMicSamples16(const Src: array of SmallInt; 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 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; 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; // Обрабатываем накопившиеся блоки с реальными 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 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); SetLength(FTXOut, FBufSize * 2); SetLength(FRXAccI, FBufSize); SetLength(FRXAccQ, FBufSize); SetLength(FSpecBuf, FBufSize * 2); SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2); FDispPos := 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; FTXThread := nil; FTXMicSem := RTLEventCreate; FillChar(FTXMicRing, SizeOf(FTXMicRing), 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(DISP_ID, @Success, 262144, 1, 1, nil); if Success <> 0 then Exit; FFlp[0] := 0; ApplyAnalyzerSettings; FAnalyzerOpen := True; if FDisplayThread = nil then FDisplayThread := TDisplayThread.Create(Self); end; procedure TWDSPEngine.ApplyAnalyzerSettings; // Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров var AnalyzerFFT: Integer; AvgCount: Integer; Backmult: Double; WaterfallAvgCount: Integer; WaterfallBackmult: Double; MaxW: Integer; Overlap: Integer; const KEEP_TIME_SEC = 0.10; var DisplaySampleRate: Integer; DisplayPixels: Integer; begin // FFT Size пользователя управляет именно display-analyzer, как в Thetis, // но визуальная нормализация должна идти к ширине видимого окна, как в piHPSDR. 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; DisplaySampleRate := FSampleRate; // Раздельные выходы display: pixout=0 панорама, pixout=1 водопад. SetAnalyzer(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); AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult); AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WaterfallAvgCount, WaterfallBackmult); SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector); SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode); SetDisplayNumAverage(DISP_ID, 0, AvgCount); SetDisplayAvBackmult(DISP_ID, 0, Backmult); SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector)); SetDisplayDetectorMode(DISP_ID, 1, FWfDetector); SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode); SetDisplayNumAverage(DISP_ID, 1, WaterfallAvgCount); SetDisplayAvBackmult(DISP_ID, 1, WaterfallBackmult); SetDisplayNormOneHz(DISP_ID, 1, 0); // В Thetis сюда идёт реальный sample rate приёмника. SetDisplaySampleRate(DISP_ID, DisplaySampleRate); end; procedure TWDSPEngine.CloseAnalyzer; begin if not FAnalyzerOpen then Exit; if Assigned(FDisplayThread) then begin FDisplayThread.Terminate; FDisplayThread.WaitFor; FreeAndNil(FDisplayThread); end; DestroyAnalyzer(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 @ FSampleRate (DUC rate) // out_size = FAudioBufSize * FSampleRate/FAudioRate = FBufSize (4096 @ 192kHz) // FTXOut уже имеет размер FBufSize*2 — подходит идеально OpenChannel( TXA_CHAN, FAudioBufSize, FAudioBufSize, FAudioRate, FAudioRate, FSampleRate, 1, 0, 0.010, 0.010, 0.010, 0.010, 0 ); SetTXAMode(TXA_CHAN, ModeToWDSP(FMode)); SetTXABandpassFreqs(TXA_CHAN, 100, 2800); SetTXABandpassWindow(TXA_CHAN, 1); SetTXALevelerSt(TXA_CHAN, 1); SetTXALevelerTop(TXA_CHAN, 5.0); SetTXAALCSt(TXA_CHAN, 1); SetTXAALCDecay(TXA_CHAN, 10); SetTXACompressorRun(TXA_CHAN, 0); SetTXAPanelGain1(TXA_CHAN, 1.0); SetTXAPanelRun(TXA_CHAN, 1); SetTXAPanelSelect(TXA_CHAN, 1); FInitialized := True; 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; if FTXActive then Exit; FDispBuf[FDispPos * 2] := I; FDispBuf[FDispPos * 2 + 1] := Q; Inc(FDispPos); if FDispPos >= DISPLAY_BLOCK_SIZE then begin Spectrum0(1, DISP_ID, 0, 0, @FDispBuf[0]); FDispPos := 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) if Assigned(FOnAudio) and not FMuted then begin for i := 0 to FAudioBufSize - 1 do begin FOutL[i] := FRXOut[i * 2] * FVolume; FOutR[i] := FRXOut[i * 2 + 1] * FVolume; end; FOnAudio(FOutL, FOutR, FAudioBufSize); end; except // Защита от падения libwdsp в потоке — просто пропускаем блок end; end; // --------------------------------------------------------------------------- // RX управление // --------------------------------------------------------------------------- procedure TWDSPEngine.SetMode(Mode: Integer); begin FMode := Mode; if not FInitialized then Exit; SetRXAMode(RXA_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 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; 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, выдаёт FBufSize IQ пар @ FSampleRate в 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 сигнал → анализатор: спектр/водопад показывают переданный сигнал // FTXOut: FBufSize IQ пар @ FSampleRate — тот же формат что и FSpecBuf if FAnalyzerOpen then Spectrum0(1, DISP_ID, 0, 0, @FTXOut[0]); // FTXOut содержит FBufSize IQ пар @ FSampleRate (interleaved double) if Assigned(FOnTXIQ) then FOnTXIQ(FTXOut, FBufSize); 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; // --------------------------------------------------------------------------- // 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; begin if not FInitialized or not FAnalyzerOpen then Exit; N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); Flag := 0; GetPixels(DISP_ID, 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(DISP_ID, 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(DISP_ID, 0, FSpecDetector); SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode); SetDisplayNumAverage(DISP_ID, 0, AvgCount); SetDisplayAvBackmult(DISP_ID, 0, Backmult); SetDisplayNormOneHz(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(DISP_ID, 1, FWfDetector); SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode); SetDisplayNumAverage(DISP_ID, 1, AvgCount); SetDisplayAvBackmult(DISP_ID, 1, Backmult); SetDisplayNormOneHz(DISP_ID, 1, 0); end; end.