mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
fix fft, waterfall
This commit is contained in:
+332
-85
@@ -98,6 +98,7 @@ const
|
||||
DISP_ID = 0; // ID для XCreateAnalyzer
|
||||
DSP_BUFSIZE = 1024;
|
||||
SPECTRUM_PIXELS = 1024;
|
||||
DISPLAY_BLOCK_SIZE = 64;
|
||||
|
||||
// Очередь IQ пакетов между сетевым и DSP потоком
|
||||
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
|
||||
@@ -150,11 +151,15 @@ type
|
||||
|
||||
// Копия входных 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 поток
|
||||
@@ -168,14 +173,15 @@ type
|
||||
// Настройки анализатора
|
||||
FFFTSize: Integer;
|
||||
FWindowType: Integer;
|
||||
FDisplayFPS: Integer;
|
||||
FEffectiveDisplayFPS: Double;
|
||||
FDisplayPixelCount: Integer;
|
||||
FSpecDetector: Integer;
|
||||
FSpecAvgMode: Integer;
|
||||
FSpecAvgCount: Integer;
|
||||
FSpecBackmult: Double;
|
||||
FSpecAvgTimeMS: Double;
|
||||
FWfDetector: Integer;
|
||||
FWfAvgMode: Integer;
|
||||
FWfAvgCount: Integer;
|
||||
FWfBackmult: Double;
|
||||
FWfAvgTimeMS: Double;
|
||||
|
||||
FOnAudio: TOnAudioReady;
|
||||
FOnSpectrum: TOnSpectrumReady;
|
||||
@@ -213,11 +219,20 @@ type
|
||||
|
||||
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;
|
||||
@@ -274,8 +289,9 @@ type
|
||||
|
||||
// Настройки анализатора (применяются немедленно если открыт)
|
||||
procedure SetFFTParams(FFTSize, WinType: Integer);
|
||||
procedure SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
|
||||
procedure SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
|
||||
procedure SetDisplayFPS(FPS: Integer);
|
||||
procedure SetSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||
procedure SetWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||
|
||||
// Spectrum — вызывать из таймера (~20 fps)
|
||||
procedure UpdateSpectrum;
|
||||
@@ -304,12 +320,10 @@ type
|
||||
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 SpecAvgTimeMS: Double read FSpecAvgTimeMS;
|
||||
property WfDetector: Integer read FWfDetector;
|
||||
property WfAvgMode: Integer read FWfAvgMode;
|
||||
property WfAvgCount: Integer read FWfAvgCount;
|
||||
property WfBackmult: Double read FWfBackmult;
|
||||
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;
|
||||
@@ -325,6 +339,15 @@ implementation
|
||||
// Аналог 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;
|
||||
@@ -334,6 +357,48 @@ type
|
||||
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;
|
||||
@@ -342,24 +407,44 @@ begin
|
||||
end;
|
||||
|
||||
procedure TTXDSPThread.Execute;
|
||||
// Аналог tx_thread в piHPSDR/transmitter.c:
|
||||
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
|
||||
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
|
||||
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
|
||||
var
|
||||
Avail: Integer;
|
||||
Avail: Integer;
|
||||
PeriodMs: Integer;
|
||||
begin
|
||||
// 512 сэмплов / 48000 Гц = ~10.67 мс → округляем вниз до 10 мс
|
||||
PeriodMs := Max(1, FEngine.FAudioBufSize * 1000 div FEngine.FAudioRate);
|
||||
|
||||
while not Terminated do
|
||||
begin
|
||||
RTLEventWaitFor(FEngine.FTXMicSem, 50);
|
||||
// Ждём сигнала от 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 then
|
||||
if (Avail >= FEngine.FAudioBufSize) and not Terminated then
|
||||
FEngine.ProcessTXBlock
|
||||
else
|
||||
Break;
|
||||
until Terminated;
|
||||
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;
|
||||
|
||||
@@ -467,15 +552,16 @@ begin
|
||||
FRXAccPos := 0;
|
||||
// Analyzer defaults
|
||||
FFFTSize := 4096;
|
||||
FWindowType := 5; // Kaiser
|
||||
FWindowType := 2; // Hann
|
||||
FDisplayFPS := 20;
|
||||
FEffectiveDisplayFPS := 20.0;
|
||||
FDisplayPixelCount := SPECTRUM_PIXELS;
|
||||
FSpecDetector := 0; // Peak
|
||||
FSpecAvgMode := 3; // Log Recursive
|
||||
FSpecAvgCount := 2;
|
||||
FSpecBackmult := 0.45;
|
||||
FWfDetector := 2; // Average
|
||||
FSpecAvgTimeMS := 30.0;
|
||||
FWfDetector := 0; // Peak
|
||||
FWfAvgMode := 3; // Log Recursive
|
||||
FWfAvgCount := 2;
|
||||
FWfBackmult := 0.45;
|
||||
FWfAvgTimeMS := 120.0;
|
||||
|
||||
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
||||
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
||||
@@ -484,6 +570,8 @@ begin
|
||||
SetLength(FRXAccI, FBufSize);
|
||||
SetLength(FRXAccQ, FBufSize);
|
||||
SetLength(FSpecBuf, FBufSize * 2);
|
||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
FDispPos := 0;
|
||||
SetLength(FOutL, FAudioBufSize);
|
||||
SetLength(FOutR, FAudioBufSize);
|
||||
|
||||
@@ -493,6 +581,7 @@ begin
|
||||
FDSPRunning := False;
|
||||
FQueueSem := RTLEventCreate;
|
||||
FDSPThread := nil;
|
||||
FDisplayThread := nil;
|
||||
|
||||
// TX инициализация
|
||||
FTXMicHead := 0;
|
||||
@@ -523,47 +612,75 @@ begin
|
||||
if FAnalyzerOpen then Exit;
|
||||
|
||||
Success := -1;
|
||||
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
|
||||
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
|
||||
// MaxW = FFTSize — минимально допустимо (piHPSDR approach)
|
||||
MaxW := FFFTSize;
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
// Раздельные выходы 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);
|
||||
|
||||
// Спектр (pixout=0)
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WaterfallAvgCount, WaterfallBackmult);
|
||||
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);
|
||||
SetDisplayNumAverage(DISP_ID, 0, AvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 0, Backmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||
|
||||
// Водопад (pixout=1)
|
||||
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
|
||||
SetDisplayNumAverage(DISP_ID, 1, WaterfallAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, WaterfallBackmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
||||
|
||||
SetDisplaySampleRate(DISP_ID, FSampleRate);
|
||||
// В 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;
|
||||
@@ -719,6 +836,8 @@ begin
|
||||
SetLength(FSpecBuf, FBufSize * 2);
|
||||
SetLength(FRXAccI, FBufSize);
|
||||
SetLength(FRXAccQ, FBufSize);
|
||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
FDispPos := 0;
|
||||
|
||||
// Переинициализируем WDSP с новым rate
|
||||
Open;
|
||||
@@ -771,6 +890,8 @@ begin
|
||||
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);
|
||||
@@ -780,6 +901,22 @@ begin
|
||||
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;
|
||||
@@ -797,21 +934,10 @@ begin
|
||||
FRXIn[i * 2 + 1] := FRXAccQ[i];
|
||||
end;
|
||||
|
||||
// Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn
|
||||
if FAnalyzerOpen then
|
||||
begin
|
||||
Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double));
|
||||
end;
|
||||
|
||||
// DSP обработка
|
||||
Err := 0;
|
||||
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
|
||||
|
||||
// Подаём RX IQ в анализатор только когда не передаём.
|
||||
// Во время TX анализатор кормит ProcessTXBlock (TX сигнал на той же частоте).
|
||||
if FAnalyzerOpen and not FTXActive then
|
||||
Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]);
|
||||
|
||||
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
|
||||
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
|
||||
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
|
||||
@@ -933,7 +1059,11 @@ end;
|
||||
|
||||
procedure TWDSPEngine.SetSpectrumWidth(W: Integer);
|
||||
begin
|
||||
if W > 0 then FLastSpectrumW := W;
|
||||
if W <= 0 then Exit;
|
||||
if FLastSpectrumW = W then Exit;
|
||||
FLastSpectrumW := W;
|
||||
if FAnalyzerOpen then
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.UpdateAGCLines(SpectrumW: Integer);
|
||||
@@ -1104,6 +1234,7 @@ end;
|
||||
procedure TWDSPEngine.SetTXRun(Run: Boolean);
|
||||
begin
|
||||
FTXActive := Run;
|
||||
FDispPos := 0;
|
||||
|
||||
if Run then
|
||||
begin
|
||||
@@ -1135,22 +1266,35 @@ end;
|
||||
|
||||
procedure TWDSPEngine.ProcessTXBlock;
|
||||
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
||||
// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback
|
||||
// через 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;
|
||||
|
||||
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
|
||||
tail := FTXMicTail;
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||||
|
||||
if Avail >= FAudioBufSize then
|
||||
begin
|
||||
FTXIn[i * 2] := FTXMicRing[tail];
|
||||
FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал
|
||||
tail := (tail + 1) and (TX_MIC_RING - 1);
|
||||
// Читаем 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;
|
||||
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
|
||||
|
||||
Err := 0;
|
||||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||||
@@ -1205,30 +1349,29 @@ procedure TWDSPEngine.UpdateSpectrum;
|
||||
var
|
||||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
Flag: Integer;
|
||||
i: Integer;
|
||||
i, N: Integer;
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||||
|
||||
// Спектр (pixout=0)
|
||||
Flag := 0;
|
||||
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to SPECTRUM_PIXELS - 1 do
|
||||
for i := 0 to N - 1 do
|
||||
FSpectrumPixels[i] := PixBuf[i];
|
||||
if Assigned(FOnSpectrum) then
|
||||
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
|
||||
FOnSpectrum(FSpectrumPixels, N);
|
||||
end;
|
||||
|
||||
// Водопад (pixout=1)
|
||||
Flag := 0;
|
||||
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to SPECTRUM_PIXELS - 1 do
|
||||
for i := 0 to N - 1 do
|
||||
FWaterfallPixels[i] := PixBuf[i];
|
||||
if Assigned(FOnWaterfall) then
|
||||
FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS);
|
||||
FOnWaterfall(FWaterfallPixels, N);
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -1254,46 +1397,150 @@ end;
|
||||
// Настройки анализатора (применяются немедленно)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
|
||||
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
|
||||
begin
|
||||
// Ограничения: FFT size должен быть >= bf_sz (FAudioBufSize = 1024)
|
||||
// и степенью двойки, не превышать m_size=16384 (из XCreateAnalyzer)
|
||||
if FFTSize < FAudioBufSize then FFTSize := FAudioBufSize;
|
||||
if FFTSize > 16384 then FFTSize := 16384;
|
||||
// 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;
|
||||
FFFTSize := FFTSize;
|
||||
|
||||
if (FFFTSize = NewFFT) and (FWindowType = WinType) then Exit;
|
||||
|
||||
FFFTSize := NewFFT;
|
||||
FWindowType := WinType;
|
||||
if FAnalyzerOpen then
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer;
|
||||
Backmult: Double);
|
||||
procedure TWDSPEngine.SetDisplayFPS(FPS: Integer);
|
||||
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);
|
||||
FPS := EnsureRange(FPS, 1, 100);
|
||||
if FDisplayFPS = FPS then Exit;
|
||||
FDisplayFPS := FPS;
|
||||
if FAnalyzerOpen then
|
||||
ApplyAnalyzerSettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer;
|
||||
Backmult: Double);
|
||||
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;
|
||||
FWfAvgCount := AvgCount;
|
||||
FWfBackmult := Backmult;
|
||||
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, FWfAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
|
||||
SetDisplayNumAverage(DISP_ID, 1, AvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, Backmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
Reference in New Issue
Block a user