unit WDSPEngine;

{
  WDSP DSP Engine for OpenHPSDR Transceiver
  Correct WDSP API usage based on actual WDSP.pas bindings:

  Spectrum pipeline:
    XCreateAnalyzer(disp, ...) — создать analyzer display
    SetDisplay*(disp, ...) — настроить detector/average/rate
    SetRXASpectrum(ch, 1, disp, 0, 0) — подключить RXA к display
    fexchange0() в цикле → WDSP внутри пишет данные в display буфер
    Spectrum0(1, disp, 0, 0, nil) — тригер snapshot
    GetPixels(disp, 0, pix, flag) — забрать пиксели

  OpenChannel сигнатура (13 параметров):
    channel, in_size, dsp_size, in_rate, dsp_rate, out_rate,
    atype, state, tdelayup, tslewup, tdelaydown, tslewdown, bfo
}

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

interface

uses
  Classes, SysUtils, Math, SyncObjs,
  WDSP;

const
  // WDSP mode integers (нет именованных констант в WDSP.pas)
  WDSP_LSB = 0;
  WDSP_USB = 1;
  WDSP_DSB = 2;
  WDSP_CWL = 3;
  WDSP_CWU = 4;
  WDSP_FM  = 5;
  WDSP_AM  = 6;
  WDSP_SAM = 12;

  // Наши индексы режимов (совпадают с кнопками MainForm)
  MODE_LSB = 0;
  MODE_USB = 1;
  MODE_DSB = 2;
  MODE_CWL = 3;
  MODE_CWU = 4;
  MODE_FM  = 5;
  MODE_AM  = 6;
  MODE_SAM = 7;

  // AGC modes
  AGC_OFF    = 0;
  AGC_LONG   = 1;
  AGC_SLOW   = 2;
  AGC_MEDIUM = 3;
  AGC_FAST   = 4;

  // RX filter modes
  NR_MODE_OFF = 0;
  NR_MODE_1   = 1;
  NR_MODE_2   = 2;
  NR_MODE_3   = 3;
  NR_MODE_4   = 4;

  NB_MODE_OFF = 0;
  NB_MODE_1   = 1;
  NB_MODE_2   = 2;

  FILTER_NR_AGC_POSITION         = 1;
  FILTER_NB_TAU                  = 0.00001;
  FILTER_NB_ADVTIME              = 0.00001;
  FILTER_NB_HANGTIME             = 0.00001;
  FILTER_NB_THRESHOLD            = 4.95;
  FILTER_NB2_MODE                = 0;
  FILTER_NR2_GAIN_METHOD         = 2;
  FILTER_NR2_NPE_METHOD          = 0;
  FILTER_NR2_POST_RUN            = 0;
  FILTER_NR2_POST_TAPER          = 12;
  FILTER_NR2_POST_NLEVEL         = 15.0;
  FILTER_NR2_POST_FACTOR         = 15.0;
  FILTER_NR2_POST_RATE           = 5.0;
  FILTER_NR2_TRAINED_THRESHOLD   = -0.5;
  FILTER_NR2_TRAINED_T2          = 0.2;
  FILTER_NR4_REDUCTION_AMOUNT    = 10.0;
  FILTER_NR4_SMOOTHING_FACTOR    = 20.0;
  FILTER_NR4_WHITENING_FACTOR    = 0.0;
  FILTER_NR4_NOISE_RESCALE       = 2.0;
  FILTER_NR4_POST_THRESHOLD      = -3.0;
  FILTER_NR4_NOISE_SCALING_TYPE  = 0;

  // S-meter types для GetRXAMeter
  RXA_S_PK = 0;   // peak, dBm
  RXA_S_AV = 1;   // average, dBm

  RXA_CHAN = 0;
  TXA_CHAN = 1;   // используем разные каналы для RX и TX

  DISP_ID         = 0;    // ID для XCreateAnalyzer
  DSP_BUFSIZE     = 1024;
  SPECTRUM_PIXELS = 1024;
  DISPLAY_BLOCK_SIZE = 64;

  // Очередь IQ пакетов между сетевым и DSP потоком
  IQ_QUEUE_SIZE   = 64;    // кол-во слотов (степень двойки для AND-маски)
  IQ_PKT_MAXBYTES = 1428;  // max байт IQ данных в пакете (238*6)

  // TX mic ring buffer — должен быть степенью двойки
  TX_MIC_RING     = 8192;

type
  // Пакет в очереди между сетевым и DSP потоком
  TIQQueueItem = record
    Data:    array[0..IQ_PKT_MAXBYTES - 1] of Byte;
    DataLen: Integer;   // реальная длина данных
    IQPairs: Integer;   // количество IQ пар
  end;

  TWDSPAGCMode = (agcOff = 0, agcLong = 1, agcSlow = 2,
                  agcMedium = 3, agcFast = 4);

  TOnAudioReady    = procedure(const Left, Right: array of Single;
                                Count: Integer) of object;
  TOnSpectrumReady = procedure(const Pixels: array of Single;
                                Count: Integer) of object;
  // TX IQ callback — вызывается из TTXDSPThread при готовности блока
  // Buf: interleaved [I0,Q0,I1,Q1,...] doubles, Count — кол-во IQ пар
  TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
  TOnWaterfallReady = procedure(const Pixels: array of Single;
                                Count: Integer) of object;

  { TWDSPEngine }
  TWDSPEngine = class
  private
    FInitialized:   Boolean;
    FAnalyzerOpen:  Boolean;
    FSampleRate:    Integer;
    FAudioRate:     Integer;
    FBufSize:       Integer;   // входной буфер @ FSampleRate
    FAudioBufSize:  Integer;   // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate)

    // Interleaved double I/Q буферы для fexchange0
    FRXIn:    array of Double;
    FRXOut:   array of Double;
    FTXIn:    array of Double;
    FTXOut:   array of Double;

    // Накопитель входного буфера (DDC пакеты могут быть мельче FBufSize)
    FRXAccI:  array of Double;
    FRXAccQ:  array of Double;
    FRXAccPos: Integer;

    // Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
    FSpecBuf:  array of Double;
    // Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
    FDispBuf:  array of Double;
    FDispPos:  Integer;
    // Аудио выходные буферы — аллоцируем один раз
    FOutL:   array of Single;
    FOutR:   array of Single;
    // DSP поток + очередь пакетов
    FDSPThread:    TThread;
    FDisplayThread:TThread;
    FQueue:        array[0..IQ_QUEUE_SIZE - 1] of TIQQueueItem;
    FQueueHead:    Integer;   // пишет сетевой поток
    FQueueTail:    Integer;   // читает DSP поток
    FQueueSem:     PRTLEvent;  // сигнал: есть новые данные (RTLEvent)
    FDSPRunning:   Boolean;

    FSpectrumPixels:   array[0..SPECTRUM_PIXELS - 1] of Single;
    FWaterfallPixels:  array[0..SPECTRUM_PIXELS - 1] of Single;
    FFlp:              array[0..0] of Integer;  // for SetAnalyzer flp parameter

    // Настройки анализатора
    FFFTSize:      Integer;
    FWindowType:   Integer;
    FDisplayFPS:   Integer;
    FEffectiveDisplayFPS: Double;
    FDisplayPixelCount: Integer;
    FSpecDetector: Integer;
    FSpecAvgMode:  Integer;
    FSpecAvgTimeMS: Double;
    FWfDetector:   Integer;
    FWfAvgMode:    Integer;
    FWfAvgTimeMS:  Double;

    FOnAudio:     TOnAudioReady;
    FOnSpectrum:  TOnSpectrumReady;
    FOnWaterfall: TOnWaterfallReady;
    FOnTXIQ:      TOnTXIQReady;

    // TX mic ring buffer (lock-free: один writer — receive thread,
    // один reader — TX DSP thread)
    FTXMicRing:  array[0..TX_MIC_RING-1] of Double;
    FTXMicHead:  Integer;   // пишет receive thread
    FTXMicTail:  Integer;   // читает TX DSP thread
    FTXMicSem:   PRTLEvent; // сигнал: есть новые mic сэмплы
    FTXThread:   TThread;
    FTXActive:   Boolean;

    FMode:       Integer;
    FSMeter:     Double;
    FFilterLow:  Integer;
    FFilterHigh: Integer;
    FAGCMode:          TWDSPAGCMode;
    FAGCTop:           Double;   // gain (dB) = agc_gain в piHPSDR
    FAGCSlope:         Integer;  // наклон АРУ в дБ (default 0)
    FAGCHangThreshold: Integer;  // порог hang 0..100
    FAGCHangLevel:     Double;   // читается из WDSP (GetRXAAGCHangLevel)
    FAGCThresh:        Double;   // читается из WDSP (GetRXAAGCThresh)
    FLastSpectrumW:    Integer;  // последняя известная ширина спектра (пикс)
    FShiftHz:          Double;   // NCO shift для CTUN
    FNRMode:     Integer;
    FNBMode:     Integer;
    FSNBEnabled: Boolean;
    FANFEnabled: Boolean;
    FMuted:      Boolean;
    FVolume:     Double;
    FLastError:  string;

    function  ModeToWDSP(Mode: Integer): Integer;
    procedure ApplyDefaultFilter;
    procedure FeedDisplaySample(const I, Q: Double);
    procedure ProcessRXBlock;
    procedure PushIQItemToDSP(const Item: TIQQueueItem);
    procedure OpenAnalyzer;
    procedure CloseAnalyzer;
    procedure ApplyAnalyzerSettings;
    function  CalcDisplayFFTSize: Integer;
    procedure CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
      out EffectiveFPS: Double);
    function  NormalizeFFTSize(FFTSize: Integer): Integer;
    procedure NormalizeDisplayParams(var Detector, AvgMode: Integer;
      var AvgTimeMS: Double; ForWaterfall: Boolean);
    procedure AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
      out AvgCount: Integer; out Backmult: Double);
    procedure ApplyNRState;
    procedure ApplyNBState;
    procedure ApplySNBState;
    procedure ApplyANFState;
    procedure ApplyNoiseFilterState;
    procedure ProcessTXBlock;   // вызывается из TTXDSPThread

  public
    constructor Create(SampleRate: Integer = 192000;
                       AudioRate:  Integer = 48000;
                       BufSize:    Integer = DSP_BUFSIZE);
    destructor Destroy; override;

    // Открыть DSP каналы
    function  Open: Boolean;
    procedure Close;
    procedure ChangeSampleRate(NewRate: Integer);

    // Подача 24-bit big-endian IQ из DDC пакета
    // Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
    procedure PushDDCPacket(const Buf: array of Byte;
                             DataOffset: Integer;
                             IQPairs:    Integer);

    // RX управление
    procedure SetMode(Mode: Integer);
    procedure SetFilter(Low, High: Integer);
    procedure SetAGC(Mode: TWDSPAGCMode; FixedGain: Double = 0.0);
    procedure SetAGCTop(TopDBm: Double);
    procedure SetAGCSlope(Slope: Integer);
    procedure SetAGCHangThreshold(Threshold: Integer);
    procedure UpdateAGCLines(SpectrumW: Integer);  // читает hang/thresh из WDSP для линии на спектре
    procedure SetSpectrumWidth(W: Integer);         // обновляет FLastSpectrumW для корректных AGC линий
    procedure SetShift(ShiftHz: Double);  // CTUN NCO сдвиг
    procedure SetNRMode(Mode: Integer);
    procedure SetNR(Enable: Boolean);
    procedure SetNBMode(Mode: Integer);
    procedure SetNB(Enable: Boolean);
    procedure SetSNB(Enable: Boolean);
    procedure SetANF(Enable: Boolean);
    procedure SetVolume(Vol: Double);
    procedure SetMute(Mute: Boolean);

    // TX управление
    procedure SetTXMode(Mode: Integer);
    procedure SetTXFilter(Low, High: Integer);
    procedure SetDriveLevel(Level: Double);
    procedure SetMicGain(GainDB: Double);
    procedure SetTXRun(Run: Boolean);

    // TX mic вход — вызывается из receive thread (thread-safe lock-free)
    // Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов
    procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);

    // Настройки анализатора (применяются немедленно если открыт)
    procedure SetFFTParams(FFTSize, WinType: Integer);
    procedure SetDisplayFPS(FPS: Integer);
    procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
    procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);

    // Spectrum — вызывать из таймера (~20 fps)
    procedure UpdateSpectrum;
    procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);

    // S-meter
    function GetSMeterDBm: Double;

    property Initialized: Boolean          read FInitialized;
    property SampleRate:  Integer          read FSampleRate;
    property AudioBufSize: Integer         read FAudioBufSize;
    property AGCTop:           Double  read FAGCTop;
    property AGCSlope:         Integer read FAGCSlope;
    property AGCHangThreshold: Integer read FAGCHangThreshold;
    property AGCHangLevel:     Double  read FAGCHangLevel;
    property AGCThresh:        Double  read FAGCThresh;
    property Mode:        Integer          read FMode;
    property FilterLow:   Integer          read FFilterLow;
    property FilterHigh:  Integer          read FFilterHigh;
    property NRMode:      Integer          read FNRMode;
    property NBMode:      Integer          read FNBMode;
    property SNBEnabled:  Boolean          read FSNBEnabled;
    property LastError:   string           read FLastError;
    property SMeter:      Double           read FSMeter;
    property FFTSize:      Integer          read FFFTSize;
    property WindowType:   Integer          read FWindowType;
    property SpecDetector: Integer          read FSpecDetector;
    property SpecAvgMode:  Integer          read FSpecAvgMode;
    property SpecAvgTimeMS: Double          read FSpecAvgTimeMS;
    property WfDetector:   Integer          read FWfDetector;
    property WfAvgMode:    Integer          read FWfAvgMode;
    property WfAvgTimeMS:  Double           read FWfAvgTimeMS;
    property OnAudio:      TOnAudioReady    read FOnAudio    write FOnAudio;
    property OnSpectrum:   TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
    property OnWaterfall:  TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
    property OnTXIQ:       TOnTXIQReady     read FOnTXIQ     write FOnTXIQ;
    property TXActive:     Boolean           read FTXActive;
  end;

implementation

// ===========================================================================
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
// Аналог tx_thread в piHPSDR/transmitter.c
// ===========================================================================
type
  TDisplayThread = class(TThread)
  private
    FEngine: TWDSPEngine;
  protected
    procedure Execute; override;
  public
    constructor Create(AEngine: TWDSPEngine);
  end;

  TTXDSPThread = class(TThread)
  private
    FEngine: TWDSPEngine;
  protected
    procedure Execute; override;
  public
    constructor Create(AEngine: TWDSPEngine);
  end;

function UsePanNormOneHz(Detector: Integer): Integer;
begin
  // В Thetis нормализация к 1 Гц включается только для panadapter
  // и только для detector modes 2/3/4.
  if Detector in [2, 3, 4] then
    Result := 1
  else
    Result := 0;
end;

constructor TDisplayThread.Create(AEngine: TWDSPEngine);
begin
  FEngine := AEngine;
  FreeOnTerminate := False;
  inherited Create(False);
end;

procedure TDisplayThread.Execute;
var
  NextTick: QWord;
  NowTick: QWord;
  DelayMS: Integer;
begin
  NextTick := GetTickCount64;
  while not Terminated do
  begin
    if FEngine.FInitialized and FEngine.FAnalyzerOpen then
      FEngine.UpdateSpectrum;

    Inc(NextTick, QWord(Max(1, Round(1000.0 / Max(1, FEngine.FDisplayFPS)))));
    NowTick := GetTickCount64;
    if NextTick > NowTick then
    begin
      DelayMS := Integer(NextTick - NowTick);
      if DelayMS > 0 then
        Sleep(DelayMS);
    end
    else
      NextTick := NowTick;
  end;
end;

constructor TTXDSPThread.Create(AEngine: TWDSPEngine);
begin
  FEngine := AEngine;
  FreeOnTerminate := False;
  inherited Create(False);
end;

procedure TTXDSPThread.Execute;
// Аналог tx_thread в piHPSDR/transmitter.c:
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
var
  Avail:    Integer;
  PeriodMs: Integer;
begin
  // 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс
  PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate);

  while not Terminated do
  begin
    // Ждём сигнала от mic-данных ИЛИ истечения периода —
    // в piHPSDR timing идёт от receive thread; здесь — от таймаута
    RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
    if Terminated then Break;
    if not FEngine.FTXActive then Continue;

    // Обрабатываем накопившиеся блоки с реальными mic-данными
    repeat
      Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
                and (TX_MIC_RING - 1);
      if (Avail >= FEngine.FAudioBufSize) and not Terminated then
        FEngine.ProcessTXBlock
      else
        Break;
    until False;

    if Terminated then Break;

    // Если mic-данных не было — всё равно обрабатываем один блок тишины.
    // Аналог piHPSDR: если mic_ring пуст, tx_add_mic_sample получает 0.0.
    // Это держит DUC IQ поток и обновляет спектр/водопад TX-сигналом.
    Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
              and (TX_MIC_RING - 1);
    if Avail < FEngine.FAudioBufSize then
      FEngine.ProcessTXBlock;
  end;
end;

// ===========================================================================
// DSP поток — обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
// Аналог iq_thread в piHPSDR/new_protocol.c
// ===========================================================================
type
  TDSPThread = class(TThread)
  private
    FEngine: TWDSPEngine;
  protected
    procedure Execute; override;
  public
    constructor Create(AEngine: TWDSPEngine);
  end;

constructor TDSPThread.Create(AEngine: TWDSPEngine);
begin
  FEngine := AEngine;
  FreeOnTerminate := False;
  inherited Create(False);
end;

procedure TDSPThread.Execute;
var
  Item: ^TIQQueueItem;
begin
  while not Terminated do
  begin
    // Ждём сигнала от сетевого потока (как sem_wait в piHPSDR)
    RTLEventWaitFor(FEngine.FQueueSem, 100);
    if Terminated then Break;

    // Разбираем все накопившиеся пакеты
    while FEngine.FQueueTail <> FEngine.FQueueHead do
    begin
      Item := @FEngine.FQueue[FEngine.FQueueTail];
      FEngine.PushIQItemToDSP(Item^);
      FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1);
    end;
  end;
end;

// ---------------------------------------------------------------------------

function TWDSPEngine.ModeToWDSP(Mode: Integer): Integer;
begin
  case Mode of
    MODE_LSB: Result := WDSP_LSB;
    MODE_USB: Result := WDSP_USB;
    MODE_DSB: Result := WDSP_DSB;
    MODE_CWL: Result := WDSP_CWL;
    MODE_CWU: Result := WDSP_CWU;
    MODE_FM:  Result := WDSP_FM;
    MODE_AM:  Result := WDSP_AM;
    MODE_SAM: Result := WDSP_SAM;
    else      Result := WDSP_USB;
  end;
end;

procedure TWDSPEngine.ApplyDefaultFilter;
begin
  case FMode of
    MODE_LSB: SetFilter(-2400, -100);
    MODE_USB: SetFilter(  100, 2400);
    MODE_DSB: SetFilter(-2400, 2400);
    MODE_CWL: SetFilter( -800, -200);
    MODE_CWU: SetFilter(  200,  800);
    MODE_FM:  SetFilter(-5000, 5000);
    MODE_AM:  SetFilter(-4000, 4000);
    MODE_SAM: SetFilter(-4000, 4000);
  end;
end;

// ---------------------------------------------------------------------------
// Constructor / Destructor
// ---------------------------------------------------------------------------

constructor TWDSPEngine.Create(SampleRate, AudioRate, BufSize: Integer);
begin
  inherited Create;
  FSampleRate   := SampleRate;
  FAudioRate    := AudioRate;
  // BufSize = dsp_size @ AudioRate (внутренний DSP буфер, piHPSDR buffer_size = 1024)
  // FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
  // piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
  FAudioBufSize := BufSize;                            // dsp_size = out_size @ AudioRate
  FBufSize      := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
  FInitialized  := False;
  FAnalyzerOpen := False;
  FMode         := MODE_USB;
  FFilterLow    :=  100;
  FFilterHigh   := 2400;
  FAGCMode      := agcMedium;
  FShiftHz      := 0.0;
  FAGCTop       := -90.0;
  FVolume       := 0.7;
  FSMeter       := -130.0;
  FNRMode       := NR_MODE_OFF;
  FNBMode       := NB_MODE_OFF;
  FSNBEnabled   := False;
  FANFEnabled   := False;
  FRXAccPos     := 0;
  // Analyzer defaults
  FFFTSize      := 131072;
  FWindowType   := 2;     // Hann
  FDisplayFPS   := 60;
  FEffectiveDisplayFPS := 60.0;
  FDisplayPixelCount := SPECTRUM_PIXELS;
  FSpecDetector := 0;     // Peak
  FSpecAvgMode  := 3;     // Log Recursive
  FSpecAvgTimeMS := 30.0;
  FWfDetector   := 0;     // Peak
  FWfAvgMode    := 3;     // Log Recursive
  FWfAvgTimeMS  := 120.0;

  SetLength(FRXIn,   FBufSize * 2);       // in_size пар @ FSampleRate (4096*2)
  SetLength(FRXOut,  FAudioBufSize * 2);  // out_size пар @ FAudioRate  (1024*2)
  SetLength(FTXIn,   FAudioBufSize * 2);
  SetLength(FTXOut,  FBufSize * 2);
  SetLength(FRXAccI, FBufSize);
  SetLength(FRXAccQ, FBufSize);
  SetLength(FSpecBuf, FBufSize * 2);
  SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
  FDispPos := 0;
  SetLength(FOutL, FAudioBufSize);
  SetLength(FOutR, FAudioBufSize);

  // Очередь и DSP поток
  FQueueHead  := 0;
  FQueueTail  := 0;
  FDSPRunning := False;
  FQueueSem   := RTLEventCreate;
  FDSPThread  := nil;
  FDisplayThread := nil;

  // TX инициализация
  FTXMicHead  := 0;
  FTXMicTail  := 0;
  FTXActive   := False;
  FTXThread   := nil;
  FTXMicSem   := RTLEventCreate;
  FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
end;

destructor TWDSPEngine.Destroy;
begin
  SetTXRun(False);  // останавливаем TX поток если запущен
  Close;
  RTLEventDestroy(FQueueSem);
  RTLEventDestroy(FTXMicSem);
  inherited;
end;

// ---------------------------------------------------------------------------
// Analyzer display
// ---------------------------------------------------------------------------

procedure TWDSPEngine.OpenAnalyzer;
var
  Success: Integer;
begin
  if FAnalyzerOpen then Exit;

  Success := -1;
  XCreateAnalyzer(DISP_ID, @Success, 262144, 1, 1, nil);
  if Success <> 0 then Exit;

  FFlp[0] := 0;
  ApplyAnalyzerSettings;

  FAnalyzerOpen := True;
  if FDisplayThread = nil then
    FDisplayThread := TDisplayThread.Create(Self);
end;

procedure TWDSPEngine.ApplyAnalyzerSettings;
// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
var
  AnalyzerFFT: Integer;
  AvgCount: Integer;
  Backmult: Double;
  WaterfallAvgCount: Integer;
  WaterfallBackmult: Double;
  MaxW: Integer;
  Overlap: Integer;
const
  KEEP_TIME_SEC = 0.10;
var
  DisplaySampleRate: Integer;
  DisplayPixels: Integer;
begin
  // FFT Size пользователя управляет именно display-analyzer, как в Thetis,
  // но визуальная нормализация должна идти к ширине видимого окна, как в piHPSDR.
  AnalyzerFFT := CalcDisplayFFTSize;
  FDispPos := 0;
  MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
                                  KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
  CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
  DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
  FDisplayPixelCount := DisplayPixels;
  DisplaySampleRate := FSampleRate;

  // Раздельные выходы display: pixout=0 панорама, pixout=1 водопад.
  SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE, FWindowType, 14.0,
              Overlap, 0, 0.0, 0.0, DisplayPixels, 1, 0, 0.0, 0.0, MaxW);

  AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
  AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WaterfallAvgCount, WaterfallBackmult);
  SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
  SetDisplayAverageMode(DISP_ID,  0, FSpecAvgMode);
  SetDisplayNumAverage(DISP_ID,   0, AvgCount);
  SetDisplayAvBackmult(DISP_ID,   0, Backmult);
  SetDisplayNormOneHz(DISP_ID,    0, UsePanNormOneHz(FSpecDetector));

  SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
  SetDisplayAverageMode(DISP_ID,  1, FWfAvgMode);
  SetDisplayNumAverage(DISP_ID,   1, WaterfallAvgCount);
  SetDisplayAvBackmult(DISP_ID,   1, WaterfallBackmult);
  SetDisplayNormOneHz(DISP_ID,    1, 0);

  // В Thetis сюда идёт реальный sample rate приёмника.
  SetDisplaySampleRate(DISP_ID, DisplaySampleRate);
end;

procedure TWDSPEngine.CloseAnalyzer;
begin
  if not FAnalyzerOpen then Exit;
  if Assigned(FDisplayThread) then
  begin
    FDisplayThread.Terminate;
    FDisplayThread.WaitFor;
    FreeAndNil(FDisplayThread);
  end;
  DestroyAnalyzer(DISP_ID);
  FAnalyzerOpen := False;
end;

// ---------------------------------------------------------------------------
// Open / Close
// ---------------------------------------------------------------------------

function TWDSPEngine.Open: Boolean;
var
  DllPath: string;
begin
  Result := False;
  FLastError := '';
  if FInitialized then Exit;

  if not Assigned(@OpenChannel) then
  begin
    FInitialized := False;
    DllPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + WDSP_LIB;
    if FileExists(DllPath) then
      FLastError := Format('Symbols from %s are not resolved. Check architecture (x64/x86) and dependencies.', [DllPath])
    else
      FLastError := Format('WDSP library not found: %s', [DllPath]);
    Exit;
  end;

  try
    // Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
    OpenAnalyzer;
    if not FAnalyzerOpen then
    begin
      FLastError := 'XCreateAnalyzer failed (WDSP init failed).';
      Exit;
    end;

    OpenChannel(
      RXA_CHAN,
      FBufSize,
      FAudioBufSize,
      FSampleRate,
      FAudioRate,
      FAudioRate,
      0,
      0,             // state=0: создать но не запускать
      0.010, 0.010, 0.010, 0.010,
      0
    );

    create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
    create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);

    SetRXABandpassWindow(RXA_CHAN, 1);
    SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
    SetRXAShiftRun(RXA_CHAN, 1);
    SetRXAShiftFreq(RXA_CHAN, 0.0);
    RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
    SetAGC(FAGCMode, 50.0);
    SetRXAPanelGain1(RXA_CHAN, FVolume);
    SetRXAPanelSelect(RXA_CHAN, 3);
    SetRXAPanelRun(RXA_CHAN, 1);
    SetChannelState(RXA_CHAN, 1, 0);

    // TXA: mic @ FAudioRate → IQ @ FSampleRate (DUC rate)
    // out_size = FAudioBufSize * FSampleRate/FAudioRate = FBufSize (4096 @ 192kHz)
    // FTXOut уже имеет размер FBufSize*2 — подходит идеально
    OpenChannel(
      TXA_CHAN,
      FAudioBufSize,
      FAudioBufSize,
      FAudioRate,
      FAudioRate,
      FSampleRate,
      1,
      0,
      0.010, 0.010, 0.010, 0.010,
      0
    );

    SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
    SetTXABandpassFreqs(TXA_CHAN, 100, 2800);
    SetTXABandpassWindow(TXA_CHAN, 1);
    SetTXALevelerSt(TXA_CHAN, 1);
    SetTXALevelerTop(TXA_CHAN, 5.0);
    SetTXAALCSt(TXA_CHAN, 1);
    SetTXAALCDecay(TXA_CHAN, 10);
    SetTXACompressorRun(TXA_CHAN, 0);
    SetTXAPanelGain1(TXA_CHAN, 1.0);
    SetTXAPanelRun(TXA_CHAN, 1);
    SetTXAPanelSelect(TXA_CHAN, 1);

    FInitialized := True;
    ApplyNoiseFilterState;
    Result := True;

    // Запускаем DSP поток после успешной инициализации WDSP
    // Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
    FDSPThread := TDSPThread.Create(Self);
    FDSPThread.Priority := tpHighest;  // DSP критический поток

  except
    on E: Exception do
    begin
      FLastError := E.ClassName + ': ' + E.Message;
      FInitialized := False;
      Result := False;
    end;
  end;
end;

procedure TWDSPEngine.Close;
begin
  if not FInitialized then Exit;

  // Останавливаем DSP поток перед закрытием WDSP каналов
  if Assigned(FDSPThread) then
  begin
    FDSPThread.Terminate;
    RTLEventSetEvent(FQueueSem);   // разбудить чтобы увидел Terminated
    FDSPThread.WaitFor;
    FreeAndNil(FDSPThread);
  end;

  CloseAnalyzer;
  destroy_anbEXT(RXA_CHAN);
  destroy_nobEXT(RXA_CHAN);
  CloseChannel(RXA_CHAN);
  CloseChannel(TXA_CHAN);
  FInitialized := False;
end;

procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open.
begin
  if NewRate = FSampleRate then Exit;

  // Останавливаем текущий канал
  Close;

  // Сбрасываем очередь IQ пакетов — старые пакеты не совместимы с новым rate.
  // Без этого DSP поток тратит секунды на разбор накопившихся пакетов.
  FQueueHead := 0;
  FQueueTail := 0;
  FRXAccPos  := 0;

  // Обновляем параметры
  FSampleRate   := NewRate;
  FBufSize      := FAudioBufSize * NewRate div FAudioRate;

  // Перераспределяем буферы под новый размер
  SetLength(FRXIn,    FBufSize * 2);
  SetLength(FRXOut,   FBufSize * 2);
  SetLength(FSpecBuf, FBufSize * 2);
  SetLength(FRXAccI,  FBufSize);
  SetLength(FRXAccQ,  FBufSize);
  SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
  FDispPos := 0;

  // Переинициализируем WDSP с новым rate
  Open;
end;

procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
  DataOffset: Integer; IQPairs: Integer);
// Вызывается из СЕТЕВОГО потока — только кладём в очередь и возвращаемся немедленно
// DSP поток разбудится семафором и займётся обработкой
var
  NextHead: Integer;
  Item:     ^TIQQueueItem;
  DataBytes: Integer;
begin
  NextHead := (FQueueHead + 1) and (IQ_QUEUE_SIZE - 1);
  if NextHead = FQueueTail then Exit;  // очередь полна — пропускаем пакет

  Item := @FQueue[FQueueHead];
  DataBytes := IQPairs * 6;
  if DataBytes > IQ_PKT_MAXBYTES then DataBytes := IQ_PKT_MAXBYTES;

  Move(Buf[DataOffset], Item^.Data[0], DataBytes);
  Item^.DataLen := DataBytes;
  Item^.IQPairs := IQPairs;

  FQueueHead := NextHead;
  RTLEventSetEvent(FQueueSem);  // будим DSP поток (как sem_post в piHPSDR)
end;

procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem);
// Вызывается из DSP потока — декодирует IQ и накапливает до FBufSize
var
  i, Pos: Integer;
  IR, QR: LongInt;
const
  SCALE = 1.0 / 8388608.0;
begin
  for i := 0 to Item.IQPairs - 1 do
  begin
    Pos := i * 6;
    if Pos + 5 >= Item.DataLen then Break;

    IR := (LongInt(Item.Data[Pos])   shl 16) or
          (LongInt(Item.Data[Pos+1]) shl  8) or
           LongInt(Item.Data[Pos+2]);
    QR := (LongInt(Item.Data[Pos+3]) shl 16) or
          (LongInt(Item.Data[Pos+4]) shl  8) or
           LongInt(Item.Data[Pos+5]);

    if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
    if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);

    FeedDisplaySample(IR * SCALE, QR * SCALE);

    FRXAccI[FRXAccPos] := IR * SCALE;
    FRXAccQ[FRXAccPos] := QR * SCALE;
    Inc(FRXAccPos);

    if FRXAccPos >= FBufSize then
      ProcessRXBlock;
  end;
end;

procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
begin
  if not FAnalyzerOpen then Exit;
  if FTXActive then Exit;

  FDispBuf[FDispPos * 2]     := I;
  FDispBuf[FDispPos * 2 + 1] := Q;
  Inc(FDispPos);

  if FDispPos >= DISPLAY_BLOCK_SIZE then
  begin
    Spectrum0(1, DISP_ID, 0, 0, @FDispBuf[0]);
    FDispPos := 0;
  end;
end;

procedure TWDSPEngine.ProcessRXBlock;
var
  i:    Integer;
  Err:  Integer;
  // OutL/OutR теперь поля класса — не аллоцируем каждый раз
begin
  FRXAccPos := 0;
  if not FInitialized then Exit;

  try
    // Упаковываем в interleaved буфер для fexchange0
    for i := 0 to FBufSize - 1 do
    begin
      FRXIn[i * 2]     := FRXAccI[i];
      FRXIn[i * 2 + 1] := FRXAccQ[i];
    end;

    // DSP обработка
    Err := 0;
    fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);

    // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
    // GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
    // вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
    FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);

    // Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1)
    if Assigned(FOnAudio) and not FMuted then
    begin
      for i := 0 to FAudioBufSize - 1 do
      begin
        FOutL[i] := FRXOut[i * 2]     * FVolume;
        FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
      end;
      FOnAudio(FOutL, FOutR, FAudioBufSize);
    end;

  except
    // Защита от падения libwdsp в потоке — просто пропускаем блок
  end;
end;

// ---------------------------------------------------------------------------
// RX управление
// ---------------------------------------------------------------------------

procedure TWDSPEngine.SetMode(Mode: Integer);
begin
  FMode := Mode;
  if not FInitialized then Exit;
  SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
  // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
end;

procedure TWDSPEngine.SetFilter(Low, High: Integer);
begin
  FFilterLow  := Low;
  FFilterHigh := High;
  if not FInitialized then Exit;
  // RXASetPassband — правильный unified API, пересчитывает фильтр целиком
  RXASetPassband(RXA_CHAN, Low, High);
end;

procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double);
begin
  FAGCMode := Mode;
  if not FInitialized then Exit;

  // Точно по piHPSDR receiver.c: set_agc()
  SetRXAAGCMode(RXA_CHAN, Ord(Mode));
  SetRXAAGCSlope(RXA_CHAN, FAGCSlope);
  SetRXAAGCTop(RXA_CHAN, FAGCTop);

  case Mode of
    agcOff:
      SetRXAAGCFixed(RXA_CHAN, FixedGain);

    agcLong: begin
      SetRXAAGCAttack(RXA_CHAN, 2);
      SetRXAAGCHang(RXA_CHAN, 2000);
      SetRXAAGCDecay(RXA_CHAN, 2000);
      SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold);
    end;

    agcSlow: begin
      SetRXAAGCAttack(RXA_CHAN, 2);
      SetRXAAGCHang(RXA_CHAN, 1000);
      SetRXAAGCDecay(RXA_CHAN, 500);
      SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold);
    end;

    agcMedium: begin
      SetRXAAGCAttack(RXA_CHAN, 2);
      SetRXAAGCHang(RXA_CHAN, 0);
      SetRXAAGCDecay(RXA_CHAN, 250);
      SetRXAAGCHangThreshold(RXA_CHAN, 100);
    end;

    agcFast: begin
      SetRXAAGCAttack(RXA_CHAN, 2);
      SetRXAAGCHang(RXA_CHAN, 0);
      SetRXAAGCDecay(RXA_CHAN, 50);
      SetRXAAGCHangThreshold(RXA_CHAN, 100);
    end;
  end;

  // Читаем обратно hang level и thresh для линии на спектре
  UpdateAGCLines(FLastSpectrumW);
end;

procedure TWDSPEngine.SetShift(ShiftHz: Double);
begin
  FShiftHz := ShiftHz;
  if not FInitialized then Exit;
  SetRXAShiftFreq(RXA_CHAN, ShiftHz);
end;

procedure TWDSPEngine.SetAGCTop(TopDBm: Double);
begin
  FAGCTop := TopDBm;
  if not FInitialized then Exit;
  SetRXAAGCTop(RXA_CHAN, TopDBm);
  UpdateAGCLines(FLastSpectrumW);
end;

procedure TWDSPEngine.SetAGCSlope(Slope: Integer);
begin
  FAGCSlope := Slope;
  if not FInitialized then Exit;
  SetRXAAGCSlope(RXA_CHAN, Slope);
end;

procedure TWDSPEngine.SetAGCHangThreshold(Threshold: Integer);
begin
  FAGCHangThreshold := Threshold;
  if not FInitialized then Exit;
  SetRXAAGCHangThreshold(RXA_CHAN, Threshold);
  UpdateAGCLines(FLastSpectrumW);
end;

procedure TWDSPEngine.SetSpectrumWidth(W: Integer);
begin
  if W <= 0 then Exit;
  if FLastSpectrumW = W then Exit;
  FLastSpectrumW := W;
  if FAnalyzerOpen then
    ApplyAnalyzerSettings;
end;

procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer);
begin
  if not FInitialized then Exit;
  if SpectrumW <= 0 then Exit;  // окно ещё не показано — не читаем
  GetRXAAGCHangLevel(RXA_CHAN, @FAGCHangLevel);
  GetRXAAGCThresh(RXA_CHAN, @FAGCThresh,
                  SpectrumW,     // реальная ширина дисплея в пикселях
                  FSampleRate);
end;

procedure TWDSPEngine.ApplyNRState;
begin
  if not FInitialized then Exit;

  SetRXAANRVals(RXA_CHAN, 64, 16, 16e-4, 10e-7);
  SetRXAANRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
  SetRXAANRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_1));

  SetRXAEMNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
  SetRXAEMNRgainMethod(RXA_CHAN, FILTER_NR2_GAIN_METHOD);
  SetRXAEMNRnpeMethod(RXA_CHAN, FILTER_NR2_NPE_METHOD);
  SetRXAEMNRtrainZetaThresh(RXA_CHAN, FILTER_NR2_TRAINED_THRESHOLD);
  SetRXAEMNRtrainT2(RXA_CHAN, FILTER_NR2_TRAINED_T2);
  SetRXAEMNRpost2Taper(RXA_CHAN, FILTER_NR2_POST_TAPER);
  SetRXAEMNRpost2Nlevel(RXA_CHAN, FILTER_NR2_POST_NLEVEL);
  SetRXAEMNRpost2Factor(RXA_CHAN, FILTER_NR2_POST_FACTOR);
  SetRXAEMNRpost2Rate(RXA_CHAN, FILTER_NR2_POST_RATE);
  SetRXAEMNRaeRun(RXA_CHAN, 1);
  SetRXAEMNRpost2Run(RXA_CHAN, FILTER_NR2_POST_RUN);
  SetRXAEMNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_2));

  SetRXARNNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
  SetRXARNNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_3));

  SetRXASBNRreductionAmount(RXA_CHAN, FILTER_NR4_REDUCTION_AMOUNT);
  SetRXASBNRsmoothingFactor(RXA_CHAN, FILTER_NR4_SMOOTHING_FACTOR);
  SetRXASBNRwhiteningFactor(RXA_CHAN, FILTER_NR4_WHITENING_FACTOR);
  SetRXASBNRnoiseRescale(RXA_CHAN, FILTER_NR4_NOISE_RESCALE);
  SetRXASBNRpostFilterThreshold(RXA_CHAN, FILTER_NR4_POST_THRESHOLD);
  SetRXASBNRnoiseScalingType(RXA_CHAN, FILTER_NR4_NOISE_SCALING_TYPE);
  SetRXASBNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
  SetRXASBNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_4));
end;

procedure TWDSPEngine.ApplyNBState;
begin
  if not FInitialized then Exit;

  SetEXTANBTau(RXA_CHAN,                  FILTER_NB_TAU);
  SetEXTANBHangtime(RXA_CHAN,             FILTER_NB_HANGTIME);
  SetEXTANBAdvtime(RXA_CHAN,              FILTER_NB_ADVTIME);
  SetEXTANBThreshold(RXA_CHAN,            FILTER_NB_THRESHOLD);
  SetEXTANBRun(RXA_CHAN,                  Ord(FNBMode = NB_MODE_1));

  SetEXTNOBMode(RXA_CHAN,                 FILTER_NB2_MODE);
  SetEXTNOBTau(RXA_CHAN,                  FILTER_NB_TAU);
  SetEXTNOBHangtime(RXA_CHAN,             FILTER_NB_HANGTIME);
  SetEXTNOBAdvtime(RXA_CHAN,              FILTER_NB_ADVTIME);
  SetEXTNOBThreshold(RXA_CHAN,            FILTER_NB_THRESHOLD);
  SetEXTNOBRun(RXA_CHAN,                  Ord(FNBMode = NB_MODE_2));
end;

procedure TWDSPEngine.ApplySNBState;
begin
  if not FInitialized then Exit;
  SetRXASNBARun(RXA_CHAN, Ord(FSNBEnabled));
end;

procedure TWDSPEngine.ApplyANFState;
begin
  if not FInitialized then Exit;
  SetRXAANFPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
  SetRXAANFRun(RXA_CHAN, Ord(FANFEnabled));
end;

procedure TWDSPEngine.ApplyNoiseFilterState;
begin
  ApplyNRState;
  ApplyNBState;
  ApplySNBState;
  ApplyANFState;
end;

procedure TWDSPEngine.SetNRMode(Mode: Integer);
begin
  if Mode < NR_MODE_OFF then Mode := NR_MODE_OFF;
  if Mode > NR_MODE_4 then Mode := NR_MODE_4;
  FNRMode := Mode;
  ApplyNRState;
end;

procedure TWDSPEngine.SetNR(Enable: Boolean);
begin
  if Enable then SetNRMode(NR_MODE_1)
  else SetNRMode(NR_MODE_OFF);
end;

procedure TWDSPEngine.SetNBMode(Mode: Integer);
begin
  if Mode < NB_MODE_OFF then Mode := NB_MODE_OFF;
  if Mode > NB_MODE_2 then Mode := NB_MODE_2;
  FNBMode := Mode;
  ApplyNBState;
end;

procedure TWDSPEngine.SetNB(Enable: Boolean);
begin
  if Enable then SetNBMode(NB_MODE_1)
  else SetNBMode(NB_MODE_OFF);
end;

procedure TWDSPEngine.SetSNB(Enable: Boolean);
begin
  FSNBEnabled := Enable;
  ApplySNBState;
end;

procedure TWDSPEngine.SetANF(Enable: Boolean);
begin
  FANFEnabled := Enable;
  ApplyANFState;
end;

procedure TWDSPEngine.SetVolume(Vol: Double);
begin
  FVolume := Max(0.0, Min(1.0, Vol));
  if not FInitialized then Exit;
  SetRXAPanelGain1(RXA_CHAN, FVolume);
end;

procedure TWDSPEngine.SetMute(Mute: Boolean);
begin
  FMuted := Mute;
  if not FInitialized then Exit;
  SetRXAPanelRun(RXA_CHAN, Ord(not Mute));
end;

// ---------------------------------------------------------------------------
// TX управление
// ---------------------------------------------------------------------------

procedure TWDSPEngine.SetTXMode(Mode: Integer);
begin
  if not FInitialized then Exit;
  SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
end;

procedure TWDSPEngine.SetTXFilter(Low, High: Integer);
begin
  if not FInitialized then Exit;
  SetTXABandpassFreqs(TXA_CHAN, Low, High);
end;

procedure TWDSPEngine.SetDriveLevel(Level: Double);
begin
  if not FInitialized then Exit;
  SetTXAALCMaxGain(TXA_CHAN, Max(0.0, Min(1.0, Level)));
end;

procedure TWDSPEngine.SetMicGain(GainDB: Double);
begin
  if not FInitialized then Exit;
  SetTXAPanelGain1(TXA_CHAN, Power(10.0, GainDB / 20.0));
end;

procedure TWDSPEngine.SetTXRun(Run: Boolean);
begin
  FTXActive := Run;
  FDispPos := 0;

  if Run then
  begin
    if FInitialized then
      SetChannelState(TXA_CHAN, 1, 0);   // run, no delay
    // Сброс mic ring buffer и запуск TX потока
    FTXMicHead := 0;
    FTXMicTail := 0;
    if not Assigned(FTXThread) then
    begin
      FTXThread := TTXDSPThread.Create(Self);
      FTXThread.Priority := tpHighest;
    end;
  end else begin
    if FInitialized then
      SetChannelState(TXA_CHAN, 0, 1);  // stop, with slew
    // Останавливаем TX поток
    if Assigned(FTXThread) then
    begin
      FTXThread.Terminate;
      RTLEventSetEvent(FTXMicSem);
      FTXThread.WaitFor;
      FreeAndNil(FTXThread);
    end;
    FTXMicHead := 0;
    FTXMicTail := 0;
  end;
end;

procedure TWDSPEngine.ProcessTXBlock;
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback.
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
//   если mic ring buffer содержит достаточно данных — берём их,
//   иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
var
  i, tail: Integer;
  Err:     Integer;
  Avail:   Integer;
begin
  if not FInitialized then Exit;

  Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);

  if Avail >= FAudioBufSize then
  begin
    // Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
    tail := FTXMicTail;
    for i := 0 to FAudioBufSize - 1 do
    begin
      FTXIn[i * 2]     := FTXMicRing[tail];
      FTXIn[i * 2 + 1] := 0.0;   // Q = 0, mic — монофонический сигнал
      tail := (tail + 1) and (TX_MIC_RING - 1);
    end;
    FTXMicTail := tail;  // Атомарно обновляем tail (один writer — TX thread)
  end else
  begin
    // Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
    FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
  end;

  Err := 0;
  fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);

  // TX сигнал → анализатор: спектр/водопад показывают переданный сигнал
  // FTXOut: FBufSize IQ пар @ FSampleRate — тот же формат что и FSpecBuf
  if FAnalyzerOpen then
    Spectrum0(1, DISP_ID, 0, 0, @FTXOut[0]);

  // FTXOut содержит FBufSize IQ пар @ FSampleRate (interleaved double)
  if Assigned(FOnTXIQ) then
    FOnTXIQ(FTXOut, FBufSize);
end;

procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
// Вызывается из receive thread — lock-free, кладёт сэмплы в ring buffer
// 16-bit big-endian samples из аппаратного микрофона
var
  i, cnt, newHead: Integer;
  S: SmallInt;
begin
  cnt := N;
  if cnt > Length(Src) then cnt := Length(Src);

  for i := 0 to cnt - 1 do
  begin
    // Аппаратный mic приходит в big-endian, нужно swap байт
    S := SmallInt(((Src[i] and $FF) shl 8) or ((Src[i] shr 8) and $FF));
    newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
    if newHead <> FTXMicTail then  // буфер не полон
    begin
      FTXMicRing[FTXMicHead] := S / 32768.0;
      FTXMicHead := newHead;
    end;
    // если буфер полон — сэмпл отбрасывается (overrun)
  end;

  RTLEventSetEvent(FTXMicSem);  // будим TX поток
end;

// ---------------------------------------------------------------------------
// Spectrum
// ---------------------------------------------------------------------------

procedure TWDSPEngine.UpdateSpectrum;
// Вызывается из таймера главного потока (настраиваемый FPS).
// Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
// буфера (который fexchange0 непрерывно заполняет из DSP-потока).
// Это обеспечивает равномерный FPS независимо от размера FFT:
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
// буфера, а не ждёт следующего полного блока.
var
  PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
  Flag:   Integer;
  i, N:   Integer;
begin
  if not FInitialized or not FAnalyzerOpen then Exit;
  N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);

  Flag := 0;
  GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
  if Flag <> 0 then
  begin
    for i := 0 to N - 1 do
      FSpectrumPixels[i] := PixBuf[i];
    if Assigned(FOnSpectrum) then
      FOnSpectrum(FSpectrumPixels, N);
  end;

  Flag := 0;
  GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
  if Flag <> 0 then
  begin
    for i := 0 to N - 1 do
      FWaterfallPixels[i] := PixBuf[i];
    if Assigned(FOnWaterfall) then
      FOnWaterfall(FWaterfallPixels, N);
  end;
end;

procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
  var Count: Integer);
var
  N: Integer;
begin
  N := Min(SPECTRUM_PIXELS, Length(Pixels));
  if N > 0 then
    Move(FSpectrumPixels[0], Pixels[0], N * SizeOf(Single));
  Count := N;
end;

function TWDSPEngine.GetSMeterDBm: Double;
begin
  // НЕ вызываем GetRXAMeter здесь повторно — значение уже обновлено
  // в DSP-колбэке. Повторный вызов сбрасывает аккумулятор WDSP → 0.
  Result := FSMeter;
end;

// ---------------------------------------------------------------------------
// Настройки анализатора (применяются немедленно)
// ---------------------------------------------------------------------------

function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
begin
  // Thetis-style display FFT range.
  if FFTSize < 4096 then FFTSize := 4096;
  Result := 4096;
  while Result < FFTSize do
    Result := Result shl 1;
  if Result > 262144 then
    Result := 262144;
end;

function TWDSPEngine.CalcDisplayFFTSize: Integer;
begin
  // FFT Size теперь снова управляет display-analyzer как в Thetis,
  // но не влияет на аудио DSP цепочку.
  Result := FFFTSize;
end;

procedure TWDSPEngine.CalcAnalyzerTiming(AnalyzerFFT: Integer; out Overlap: Integer;
  out EffectiveFPS: Double);
var
  TargetFPS: Double;
begin
  TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
  // Thetis/piHPSDR-style timing:
  // целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
  Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
  EffectiveFPS := TargetFPS;
end;

procedure TWDSPEngine.NormalizeDisplayParams(var Detector, AvgMode: Integer;
  var AvgTimeMS: Double; ForWaterfall: Boolean);
begin
  Detector := EnsureRange(Detector, 0, 4);

  if ForWaterfall then
  begin
    // Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
    if AvgMode < 0 then AvgMode := 1;
  end
  else
    if AvgMode < -1 then AvgMode := -1;

  if AvgMode > 3 then AvgMode := 3;

  if AvgMode <= 0 then
    AvgTimeMS := 0.0
  else
    AvgTimeMS := EnsureRange(AvgTimeMS, 10.0, 5000.0);
end;

procedure TWDSPEngine.AverageTimeToParams(AvgMode: Integer; AvgTimeMS: Double;
  out AvgCount: Integer; out Backmult: Double);
var
  TimeSec: Double;
  Frames: Double;
  FPSv: Double;
begin
  if AvgMode <= 0 then
  begin
    AvgCount := 1;
    Backmult := 0.0;
    Exit;
  end;

  TimeSec := EnsureRange(AvgTimeMS, 10.0, 5000.0) * 0.001;
  FPSv := Max(1.0, FEffectiveDisplayFPS);
  Frames := FPSv * TimeSec;
  AvgCount := EnsureRange(Round(Frames), 2, 60);
  Backmult := Exp(-1.0 / Max(1E-6, FPSv * TimeSec));
  Backmult := EnsureRange(Backmult, 0.0, 0.9999);
end;

procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
var
  NewFFT: Integer;
begin
  NewFFT := NormalizeFFTSize(FFTSize);
  if WinType < 0 then WinType := 0;
  if WinType > 6 then WinType := 6;

  if (FFFTSize = NewFFT) and (FWindowType = WinType) then Exit;

  FFFTSize    := NewFFT;
  FWindowType := WinType;
  if FAnalyzerOpen then
    ApplyAnalyzerSettings;
end;

procedure TWDSPEngine.SetDisplayFPS(FPS: Integer);
begin
  FPS := EnsureRange(FPS, 1, 100);
  if FDisplayFPS = FPS then Exit;
  FDisplayFPS := FPS;
  if FAnalyzerOpen then
    ApplyAnalyzerSettings;
end;

procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode: Integer;
  AvgTimeMS: Double);
var
  AvgCount: Integer;
  Backmult: Double;
begin
  NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);

  if (FSpecDetector = Detector) and
     (FSpecAvgMode = AvgMode) and
     (Abs(FSpecAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;

  FSpecDetector := Detector;
  FSpecAvgMode  := AvgMode;
  FSpecAvgTimeMS := AvgTimeMS;
  if not FAnalyzerOpen then Exit;
  AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
  SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
  SetDisplayAverageMode(DISP_ID,  0, FSpecAvgMode);
  SetDisplayNumAverage(DISP_ID,   0, AvgCount);
  SetDisplayAvBackmult(DISP_ID,   0, Backmult);
  SetDisplayNormOneHz(DISP_ID,    0, UsePanNormOneHz(FSpecDetector));
end;

procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
  AvgTimeMS: Double);
var
  AvgCount: Integer;
  Backmult: Double;
begin
  NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);

  if (FWfDetector = Detector) and
     (FWfAvgMode = AvgMode) and
     (Abs(FWfAvgTimeMS - AvgTimeMS) < 1E-6) then Exit;

  FWfDetector := Detector;
  FWfAvgMode  := AvgMode;
  FWfAvgTimeMS := AvgTimeMS;
  if not FAnalyzerOpen then Exit;
  AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
  SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
  SetDisplayAverageMode(DISP_ID,  1, FWfAvgMode);
  SetDisplayNumAverage(DISP_ID,   1, AvgCount);
  SetDisplayAvBackmult(DISP_ID,   1, Backmult);
  SetDisplayNormOneHz(DISP_ID,    1, 0);
end;

end.
