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; // 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; // Очередь IQ пакетов между сетевым и DSP потоком IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски) IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6) 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; { 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; // Snapshot буфер для Spectrum0 FSnapBuf: array of Double; // Аудио выходные буферы — аллоцируем один раз FOutL: array of Single; FOutR: array of Single; // DSP поток + очередь пакетов FDSPThread: TThread; FQueue: array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem; FQueueHead: Integer; // пишет сетевой поток FQueueTail: Integer; // читает DSP поток FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent) FDSPRunning: Boolean; // Double буфер для Spectrum0 (принимает PDouble, не PdINREAL) FSpecBuf: array of Double; // Double буфер для Spectrum0 (PDouble) FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single; FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter FOnAudio: TOnAudioReady; FOnSpectrum: TOnSpectrumReady; 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 FNREnabled: Boolean; FNBEnabled: Boolean; FANFEnabled: Boolean; FMuted: Boolean; FVolume: Double; function ModeToWDSP(Mode: Integer): Integer; procedure ApplyDefaultFilter; procedure ProcessRXBlock; procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure OpenAnalyzer; procedure CloseAnalyzer; 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 SetNR(Enable: Boolean); procedure SetNB(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); // 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 SMeter: Double read FSMeter; property OnAudio: TOnAudioReady read FOnAudio write FOnAudio; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; end; implementation // =========================================================================== // 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; FRXAccPos := 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(FSnapBuf, FBufSize * 2); SetLength(FOutL, FAudioBufSize); SetLength(FOutR, FAudioBufSize); SetLength(FSpecBuf, FBufSize * 2); // Очередь и DSP поток FQueueHead := 0; FQueueTail := 0; FDSPRunning := False; FQueueSem := RTLEventCreate; FDSPThread := nil; end; destructor TWDSPEngine.Destroy; begin Close; RTLEventDestroy(FQueueSem); inherited; end; // --------------------------------------------------------------------------- // Analyzer display // --------------------------------------------------------------------------- procedure TWDSPEngine.OpenAnalyzer; var Success: Integer; MaxW: Integer; Ovrlp: Integer; FRAME_RATE: Integer; begin if FAnalyzerOpen then Exit; // ---- Шаг 1: создать анализатор ---- // success = 0 означает УСПЕХ (WDSP Guide p.119) // m_size = максимальный FFT размер (должен быть степенью 2, ≤ 262144) // ОПТИМИЗАЦИЯ: 262144 → 16384. WDSP аллоцирует и инициализирует twiddle-таблицы // для ВСЕХ степеней 2 вплоть до m_size при каждом вызове XCreateAnalyzer. // Реальный рабочий FFT = 4096, запас ×4 = 16384 достаточен для будущих апгрейдов. // Эффект: время открытия WDSP ~10 сек → ~1 сек. // m_LO = 1 (нет spur elimination) // m_stitch = 1 (один sub-span) Success := -1; XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil); if Success <> 0 then begin // Анализатор не создан — работаем без спектра, краша не будет Exit; end; // ---- Шаг 2: SetAnalyzer ---- // Рассчитываем параметры по документации (WDSP Guide p.120-121) FRAME_RATE := 20; // 20 fps // max_w = fft_size + min(KEEP_TIME*sample_rate, KEEP_TIME*fft_size*frame_rate) // KEEP_TIME = 0.1 // min(0.1*192000, 0.1*4096*20) = min(19200, 8192) = 8192 MaxW := 4096 + 8192; // = 12288 // ovrlp = max(0, ceil(fft_size - sample_rate/frame_rate)) // = max(0, ceil(4096 - 192000/20)) = max(0, 4096-9600) = 0 Ovrlp := 0; FFlp[0] := 0; // LO на нижней стороне, нет spur elimination SetAnalyzer( DISP_ID, 1, // n_pixout: 1 (один выход — панадаптер и водопад одинаковые) 1, // n_fft: 1 (без spur elimination) 1, // typ: 1 = COMPLEX (I+Q) @FFlp[0], // flp: [0] — LO ниже сигнала 4096, // sz: размер FFT FAudioBufSize, // bf_sz: блок данных @ AudioRate = dsp_size (1024 @ 48kHz) 1, // win_type: 1 = 4-term Blackman-Harris 14.0, // pi: Kaiser beta (не используется при BH) Ovrlp, // ovrlp: перекрытие = 0 0, // clp: обрезка краёв = 0 0.0, // fscLin: pan left = 0 (полный диапазон) 0.0, // fscHin: pan right = 0 (полный диапазон) SPECTRUM_PIXELS, // n_pix: пикселей на выходе GetPixels 1, // n_stch: 1 sub-span (нет stitching) 0, // calset: нет калибровки 0.0, // fmin 0.0, // fmax MaxW // max_w: рассчитан выше = 12288 ); // ---- Шаг 3: настройка детектора и усреднения ---- // DETECTOR_MODE_AVERAGE + LOG_RECURSIVE — плавный, как в piHPSDR/Thetis // backmult ~0.45 = инерция ~2.2 кадра → плавно, без смазывания быстрых сигналов // NumAverage = FRAME_RATE * 0.12 ≈ 2..3 при 20fps — достаточно для LOG_RECURSIVE SetDisplayDetectorMode(DISP_ID, 0, DETECTOR_MODE_AVERAGE); SetDisplayAverageMode(DISP_ID, 0, AVERAGE_MODE_LOG_RECURSIVE); SetDisplayNumAverage(DISP_ID, 0, Max(2, Trunc(FRAME_RATE * 0.12))); SetDisplayAvBackmult(DISP_ID, 0, 0.45); SetDisplaySampleRate(DISP_ID, FSampleRate); SetDisplayNormOneHz(DISP_ID, 0, 0); FAnalyzerOpen := True; end; procedure TWDSPEngine.CloseAnalyzer; begin if not FAnalyzerOpen then Exit; DestroyAnalyzer(DISP_ID); FAnalyzerOpen := False; end; // --------------------------------------------------------------------------- // Open / Close // --------------------------------------------------------------------------- function TWDSPEngine.Open: Boolean; begin Result := False; if FInitialized then Exit; // Проверяем что libwdsp загружена (WEAKEXTERNALSYMBOLS: nil если нет DLL) if not Assigned(@OpenChannel) then begin // wdsp.dll / libwdsp.so не найдена — демо-режим FInitialized := False; Exit; end; try // ОПТИМИЗАЦИЯ: Анализатор создаём ПЕРВЫМ — до OpenChannel. // Это соответствует порядку piHPSDR и гарантирует что DISP_ID готов // до того как DSP поток начнёт вызывать Spectrum0. OpenAnalyzer; // НЕ используем SetRXASpectrum — кормим анализатор вручную // через Spectrum0 с Double буфером, чтобы избежать SIGSEGV // (fexchange0 работает с Single, а внутри WDSP Spectrum2 ждёт Double) // ---- RXA channel 0 ---- // ОПТИМИЗАЦИЯ: state=0 (не запускать сразу) — настраиваем все параметры // вхолостую, только потом стартуем через SetChannelState. // Канал не тратит CPU на обработку "мусорных" данных в процессе настройки. OpenChannel( RXA_CHAN, FBufSize, // in_size @ FSampleRate (4096 @ 192kHz) FAudioBufSize, // dsp_size @ AudioRate (1024 @ 48kHz) — как piHPSDR buffer_size FSampleRate, FAudioRate, // dsp_rate = 48kHz FAudioRate, // out_rate = 48kHz 0, // atype: 0=RX 0, // state: 0=hold (запустим после настройки всех параметров) 0.010, 0.010, 0.010, 0.010, 0 ); SetRXAMode(RXA_CHAN, ModeToWDSP(FMode)); SetRXAShiftRun(RXA_CHAN, 1); // включаем NCO шифтер (нужен для CTUN) SetRXAShiftFreq(RXA_CHAN, 0.0); // по умолчанию сдвига нет RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh); SetAGC(FAGCMode, 50.0); // настраиваем AGC с полными параметрами piHPSDR SetRXAPanelGain1(RXA_CHAN, FVolume); SetRXAPanelSelect(RXA_CHAN, 3); SetRXAPanelRun(RXA_CHAN, 1); // Запускаем RXA канал только после полной настройки SetChannelState(RXA_CHAN, 1, 0); // run, no delay-wait // ---- TXA channel 1 ---- OpenChannel( TXA_CHAN, FBufSize, FBufSize, FAudioRate, 48000, FSampleRate, 1, // atype: 1=TX 0, // state: 0=hold 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; Result := True; // Запускаем DSP поток после успешной инициализации WDSP // Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR FDSPThread := TDSPThread.Create(Self); FDSPThread.Priority := tpHighest; // DSP критический поток except FInitialized := False; Result := False; 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; 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); // Переинициализируем 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); FRXAccI[FRXAccPos] := IR * SCALE; FRXAccQ[FRXAccPos] := QR * SCALE; Inc(FRXAccPos); if FRXAccPos >= FBufSize then ProcessRXBlock; 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; // Копируем входные IQ ДО fexchange0 для Spectrum0 // fexchange0 in-place перезапишет FRXIn выходными данными @ 48kHz if FAnalyzerOpen then for i := 0 to FBufSize * 2 - 1 do FSpecBuf[i] := FRXIn[i]; // Single→Double, входные IQ @ 192kHz // DSP обработка Err := 0; fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); // Spectrum0 с входными данными (скопированными до fexchange0) if FAnalyzerOpen then Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]); // 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 FLastSpectrumW := W; 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.SetNR(Enable: Boolean); begin FNREnabled := Enable; if not FInitialized then Exit; SetRXAEMNRRun(RXA_CHAN, Ord(Enable)); end; procedure TWDSPEngine.SetNB(Enable: Boolean); begin FNBEnabled := Enable; if not FInitialized then Exit; SetEXTANBRun(RXA_CHAN, Ord(Enable)); end; procedure TWDSPEngine.SetANF(Enable: Boolean); begin FANFEnabled := Enable; if not FInitialized then Exit; SetRXAANFRun(RXA_CHAN, Ord(Enable)); 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 if not FInitialized then Exit; if Run then SetChannelState(TXA_CHAN, 1, 0) // run, no delay else SetChannelState(TXA_CHAN, 0, 1); // stop, with slew end; // --------------------------------------------------------------------------- // Spectrum // --------------------------------------------------------------------------- procedure TWDSPEngine.UpdateSpectrum; // Вызывается из таймера главного потока (~20fps) // Spectrum0 уже вызван в сетевом потоке после каждого fexchange0 // Здесь только читаем готовые пиксели var PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single; Flag: Integer; i: Integer; begin if not FInitialized or not FAnalyzerOpen then Exit; Flag := 0; GetPixels(DISP_ID, 0, @PixBuf[0], @Flag); if Flag = 0 then Exit; // нет нового кадра — ждём следующего тика for i := 0 to SPECTRUM_PIXELS - 1 do FSpectrumPixels[i] := PixBuf[i]; if Assigned(FOnSpectrum) then FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS); 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; end.