Files
ewsdr/WDSPEngine.pas
T
2026-03-09 20:55:19 +03:00

1028 lines
33 KiB
ObjectPascal

unit WDSPEngine;
{
WDSP DSP Engine for OpenHPSDR Transceiver
Correct WDSP API usage based on actual WDSP.pas bindings:
Spectrum pipeline:
XCreateAnalyzer(disp, ...) — создать analyzer display
SetDisplay*(disp, ...) — настроить detector/average/rate
SetRXASpectrum(ch, 1, disp, 0, 0) — подключить RXA к display
fexchange0() в цикле → WDSP внутри пишет данные в display буфер
Spectrum0(1, disp, 0, 0, nil) — тригер snapshot
GetPixels(disp, 0, pix, flag) — забрать пиксели
OpenChannel сигнатура (13 параметров):
channel, in_size, dsp_size, in_rate, dsp_rate, out_rate,
atype, state, tdelayup, tslewup, tdelaydown, tslewdown, bfo
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math, SyncObjs,
WDSP;
const
// WDSP mode integers (нет именованных констант в WDSP.pas)
WDSP_LSB = 0;
WDSP_USB = 1;
WDSP_DSB = 2;
WDSP_CWL = 3;
WDSP_CWU = 4;
WDSP_FM = 5;
WDSP_AM = 6;
WDSP_SAM = 12;
// Наши индексы режимов (совпадают с кнопками MainForm)
MODE_LSB = 0;
MODE_USB = 1;
MODE_DSB = 2;
MODE_CWL = 3;
MODE_CWU = 4;
MODE_FM = 5;
MODE_AM = 6;
MODE_SAM = 7;
// AGC modes
AGC_OFF = 0;
AGC_LONG = 1;
AGC_SLOW = 2;
AGC_MEDIUM = 3;
AGC_FAST = 4;
// RX filter modes
NR_MODE_OFF = 0;
NR_MODE_1 = 1;
NR_MODE_2 = 2;
NR_MODE_3 = 3;
NR_MODE_4 = 4;
NB_MODE_OFF = 0;
NB_MODE_1 = 1;
NB_MODE_2 = 2;
FILTER_NR_AGC_POSITION = 1;
FILTER_NB_TAU = 0.00001;
FILTER_NB_ADVTIME = 0.00001;
FILTER_NB_HANGTIME = 0.00001;
FILTER_NB_THRESHOLD = 4.95;
FILTER_NB2_MODE = 0;
FILTER_NR2_GAIN_METHOD = 2;
FILTER_NR2_NPE_METHOD = 0;
FILTER_NR2_POST_RUN = 0;
FILTER_NR2_POST_TAPER = 12;
FILTER_NR2_POST_NLEVEL = 15.0;
FILTER_NR2_POST_FACTOR = 15.0;
FILTER_NR2_POST_RATE = 5.0;
FILTER_NR2_TRAINED_THRESHOLD = -0.5;
FILTER_NR2_TRAINED_T2 = 0.2;
FILTER_NR4_REDUCTION_AMOUNT = 10.0;
FILTER_NR4_SMOOTHING_FACTOR = 20.0;
FILTER_NR4_WHITENING_FACTOR = 0.0;
FILTER_NR4_NOISE_RESCALE = 2.0;
FILTER_NR4_POST_THRESHOLD = -3.0;
FILTER_NR4_NOISE_SCALING_TYPE = 0;
// S-meter types для GetRXAMeter
RXA_S_PK = 0; // peak, dBm
RXA_S_AV = 1; // average, dBm
RXA_CHAN = 0;
TXA_CHAN = 1; // используем разные каналы для RX и TX
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
FNRMode: Integer;
FNBMode: Integer;
FSNBEnabled: Boolean;
FANFEnabled: Boolean;
FMuted: Boolean;
FVolume: Double;
FLastError: string;
function ModeToWDSP(Mode: Integer): Integer;
procedure ApplyDefaultFilter;
procedure ProcessRXBlock;
procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer;
procedure CloseAnalyzer;
procedure ApplyNRState;
procedure ApplyNBState;
procedure ApplySNBState;
procedure ApplyANFState;
procedure ApplyNoiseFilterState;
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);
// 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 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;
FNRMode := NR_MODE_OFF;
FNBMode := NB_MODE_OFF;
FSNBEnabled := False;
FANFEnabled := False;
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;
Success := -1;
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
if Success <> 0 then Exit;
FRAME_RATE := 20;
// max_w: WDSP аллоцирует ring-buffer и FFT планы на max_w сэмплов.
// Формула: fft_size + min(0.1*samplerate, 0.1*fft_size*fps)
// = 4096 + min(19200, 8192) = 12288 → 1700ms инициализации!
// Реально нам нужен только один FFT-кадр без усреднения по времени.
// MaxW = sz = 4096 — минимально допустимое значение (должно быть ≥ sz).
// piHPSDR использует именно sz без добавки keep_time.
MaxW := 4096;
Ovrlp := 0;
FFlp[0] := 0;
SetAnalyzer(DISP_ID, 1, 1, 1, @FFlp[0], 4096, FAudioBufSize, 1, 14.0,
Ovrlp, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW);
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;
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);
OpenChannel(
TXA_CHAN,
FAudioBufSize,
FAudioBufSize,
FAudioRate,
FAudioRate,
FAudioRate,
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);
// Переинициализируем 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.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
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.