mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
913 lines
31 KiB
ObjectPascal
913 lines
31 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;
|
||
|
||
// 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.
|