start work tx

This commit is contained in:
2026-03-26 22:50:55 +03:00
parent 7664907b3e
commit ae532a33cd
9 changed files with 1373 additions and 149 deletions
+327 -55
View File
@@ -103,6 +103,9 @@ const
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
@@ -118,6 +121,11 @@ type
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
@@ -140,8 +148,8 @@ type
FRXAccQ: array of Double;
FRXAccPos: Integer;
// Snapshot буфер для Spectrum0
FSnapBuf: array of Double;
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSpecBuf: array of Double;
// Аудио выходные буферы — аллоцируем один раз
FOutL: array of Single;
FOutR: array of Single;
@@ -152,14 +160,36 @@ type
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
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
FOnAudio: TOnAudioReady;
FOnSpectrum: TOnSpectrumReady;
// Настройки анализатора
FFFTSize: Integer;
FWindowType: Integer;
FSpecDetector: Integer;
FSpecAvgMode: Integer;
FSpecAvgCount: Integer;
FSpecBackmult: Double;
FWfDetector: Integer;
FWfAvgMode: Integer;
FWfAvgCount: Integer;
FWfBackmult: 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;
@@ -187,11 +217,13 @@ type
procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer;
procedure CloseAnalyzer;
procedure ApplyAnalyzerSettings;
procedure ApplyNRState;
procedure ApplyNBState;
procedure ApplySNBState;
procedure ApplyANFState;
procedure ApplyNoiseFilterState;
procedure ProcessTXBlock; // вызывается из TTXDSPThread
public
constructor Create(SampleRate: Integer = 192000;
@@ -236,6 +268,15 @@ type
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 SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
procedure SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
// Spectrum — вызывать из таймера (~20 fps)
procedure UpdateSpectrum;
procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);
@@ -259,12 +300,69 @@ type
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;
property FFTSize: Integer read FFFTSize;
property WindowType: Integer read FWindowType;
property SpecDetector: Integer read FSpecDetector;
property SpecAvgMode: Integer read FSpecAvgMode;
property SpecAvgCount: Integer read FSpecAvgCount;
property SpecBackmult: Double read FSpecBackmult;
property WfDetector: Integer read FWfDetector;
property WfAvgMode: Integer read FWfAvgMode;
property WfAvgCount: Integer read FWfAvgCount;
property WfBackmult: Double read FWfBackmult;
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
TTXDSPThread = class(TThread)
private
FEngine: TWDSPEngine;
protected
procedure Execute; override;
public
constructor Create(AEngine: TWDSPEngine);
end;
constructor TTXDSPThread.Create(AEngine: TWDSPEngine);
begin
FEngine := AEngine;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TTXDSPThread.Execute;
var
Avail: Integer;
begin
while not Terminated do
begin
RTLEventWaitFor(FEngine.FTXMicSem, 50);
if Terminated then Break;
if not FEngine.FTXActive then Continue;
// Обрабатываем все накопившиеся блоки
repeat
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1);
if Avail >= FEngine.FAudioBufSize then
FEngine.ProcessTXBlock
else
Break;
until Terminated;
end;
end;
// ===========================================================================
// DSP поток — обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
@@ -367,6 +465,17 @@ begin
FSNBEnabled := False;
FANFEnabled := False;
FRXAccPos := 0;
// Analyzer defaults
FFFTSize := 4096;
FWindowType := 5; // Kaiser
FSpecDetector := 0; // Peak
FSpecAvgMode := 3; // Log Recursive
FSpecAvgCount := 2;
FSpecBackmult := 0.45;
FWfDetector := 2; // Average
FWfAvgMode := 3; // Log Recursive
FWfAvgCount := 2;
FWfBackmult := 0.45;
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
@@ -374,10 +483,9 @@ begin
SetLength(FTXOut, FBufSize * 2);
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
SetLength(FSnapBuf, FBufSize * 2);
SetLength(FSpecBuf, FBufSize * 2);
SetLength(FOutL, FAudioBufSize);
SetLength(FOutR, FAudioBufSize);
SetLength(FSpecBuf, FBufSize * 2);
// Очередь и DSP поток
FQueueHead := 0;
@@ -385,12 +493,22 @@ begin
FDSPRunning := False;
FQueueSem := RTLEventCreate;
FDSPThread := 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;
@@ -400,10 +518,7 @@ end;
procedure TWDSPEngine.OpenAnalyzer;
var
Success: Integer;
MaxW: Integer;
Ovrlp: Integer;
FRAME_RATE: Integer;
Success: Integer;
begin
if FAnalyzerOpen then Exit;
@@ -411,30 +526,41 @@ begin
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);
ApplyAnalyzerSettings;
FAnalyzerOpen := True;
end;
procedure TWDSPEngine.ApplyAnalyzerSettings;
// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
var
MaxW: Integer;
begin
// MaxW = FFTSize — минимально допустимо (piHPSDR approach)
MaxW := FFFTSize;
// n_pixout=2: pixout=0 — спектр, pixout=1 — водопад
SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], FFFTSize, FAudioBufSize, FWindowType, 14.0,
0, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW);
// Спектр (pixout=0)
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount);
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult);
SetDisplayNormOneHz(DISP_ID, 0, 0);
// Водопад (pixout=1)
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
SetDisplayNormOneHz(DISP_ID, 1, 0);
SetDisplaySampleRate(DISP_ID, FSampleRate);
end;
procedure TWDSPEngine.CloseAnalyzer;
begin
if not FAnalyzerOpen then Exit;
@@ -501,13 +627,16 @@ begin
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,
FAudioRate,
FSampleRate,
1,
0,
0.010, 0.010, 0.010, 0.010,
@@ -668,18 +797,19 @@ begin
FRXIn[i * 2 + 1] := FRXAccQ[i];
end;
// Копируем входные IQ ДО fexchange0 для Spectrum0
// fexchange0 in-place перезапишет FRXIn выходными данными @ 48kHz
// Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn
if FAnalyzerOpen then
for i := 0 to FBufSize * 2 - 1 do
FSpecBuf[i] := FRXIn[i]; // Single→Double, входные IQ @ 192kHz
begin
Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double));
end;
// DSP обработка
Err := 0;
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
// Spectrum0 с входными данными (скопированными до fexchange0)
if FAnalyzerOpen then
// Подаём RX IQ в анализатор только когда не передаём.
// Во время TX анализатор кормит ProcessTXBlock (TX сигнал на той же частоте).
if FAnalyzerOpen and not FTXActive then
Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]);
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
@@ -973,11 +1103,92 @@ end;
procedure TWDSPEngine.SetTXRun(Run: Boolean);
begin
if not FInitialized then Exit;
FTXActive := Run;
if Run then
SetChannelState(TXA_CHAN, 1, 0) // run, no delay
else
SetChannelState(TXA_CHAN, 0, 1); // stop, with slew
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
var
i, tail: Integer;
Err: Integer;
begin
if not FInitialized then Exit;
// Читаем 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)
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;
// ---------------------------------------------------------------------------
@@ -985,9 +1196,12 @@ end;
// ---------------------------------------------------------------------------
procedure TWDSPEngine.UpdateSpectrum;
// Вызывается из таймера главного потока (~20fps)
// Spectrum0 уже вызван в сетевом потоке после каждого fexchange0
// Здесь только читаем готовые пиксели
// Вызывается из таймера главного потока (настраиваемый FPS).
// Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
// буфера (который fexchange0 непрерывно заполняет из DSP-потока).
// Это обеспечивает равномерный FPS независимо от размера FFT:
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
// буфера, а не ждёт следующего полного блока.
var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer;
@@ -995,15 +1209,27 @@ var
begin
if not FInitialized or not FAnalyzerOpen then Exit;
// Спектр (pixout=0)
Flag := 0;
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
if Flag = 0 then Exit; // нет нового кадра — ждём следующего тика
if Flag <> 0 then
begin
for i := 0 to SPECTRUM_PIXELS - 1 do
FSpectrumPixels[i] := PixBuf[i];
if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
end;
for i := 0 to SPECTRUM_PIXELS - 1 do
FSpectrumPixels[i] := PixBuf[i];
if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
// Водопад (pixout=1)
Flag := 0;
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
if Flag <> 0 then
begin
for i := 0 to SPECTRUM_PIXELS - 1 do
FWaterfallPixels[i] := PixBuf[i];
if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS);
end;
end;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
@@ -1024,4 +1250,50 @@ begin
Result := FSMeter;
end;
// ---------------------------------------------------------------------------
// Настройки анализатора (применяются немедленно)
// ---------------------------------------------------------------------------
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
begin
// Ограничения: FFT size должен быть >= bf_sz (FAudioBufSize = 1024)
// и степенью двойки, не превышать m_size=16384 (из XCreateAnalyzer)
if FFTSize < FAudioBufSize then FFTSize := FAudioBufSize;
if FFTSize > 16384 then FFTSize := 16384;
if WinType < 0 then WinType := 0;
if WinType > 6 then WinType := 6;
FFFTSize := FFTSize;
FWindowType := WinType;
if FAnalyzerOpen then
ApplyAnalyzerSettings;
end;
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer;
Backmult: Double);
begin
FSpecDetector := Detector;
FSpecAvgMode := AvgMode;
FSpecAvgCount := AvgCount;
FSpecBackmult := Backmult;
if not FAnalyzerOpen then Exit;
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount);
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult);
end;
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer;
Backmult: Double);
begin
FWfDetector := Detector;
FWfAvgMode := AvgMode;
FWfAvgCount := AvgCount;
FWfBackmult := Backmult;
if not FAnalyzerOpen then Exit;
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
end;
end.