Files
ewsdr/WDSPEngine.pas
T
2026-03-06 12:29:36 +03:00

913 lines
31 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;
// 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.