mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(slices): этап 3.2 — WDSPEngine per-pan (аккумулятор, анализаторы, слайсы в панах)
- общая IQ-очередь с TIQQueueItem.PanId (64→256 слотов); DSP-поток диспетчеризует пан-пакеты в PushPanItemToDSP: декод, feed анализатора PAN_DISP_BASE+PanId, аккумулятор, ProcessSlicesFor(PanId) под FSliceLock - пан-анализаторы: Create/DestroyPanAnalyzer + ApplyPanAnalyzerSettings (rate пана, без зума — пан-зум в 3.3+), живут в OpenAnalyzer/CloseAnalyzer, общие настройки дисплея перекатываются (ReapplyPanDisplaySettings) - слайсы в панах: TSliceChan.PanId, AddSlice(..., APanId), OpenSliceChannel от rate/блока своего пана, SliceDSPRateFor (WFM от rate пана); блок пана = FAudioBufSize*Rate/FAudioRate => аудио-блок слайсов неизменен - выдача пикселей: OnPanSpectrum/OnPanWaterfall(PanId,...) из display-потока, GetPixels пана под FSliceLock, колбэки вне лока - MAX_PANS=4 переехал в WDSPEngine (единый движок/контроллер/UI) - контроллер: AddPanDDC создаёт пан в движке до включения DDC, RemovePanDDC убирает слайсы (со звуковухами)+движок+DDC, гейт/NCO слайсов от центра своего пана, ретюн пана двигает его слайсы - смоук-тест на libwdsp без железа ALL PASS: пик спектра пана точно на месте, слайс пана выдал 1 кГц RMS 0.68, главный тракт жив, RemovePan чисто Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+480
-56
@@ -132,12 +132,23 @@ const
|
||||
|
||||
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||
|
||||
// --- Панадаптеры на аппаратных DDC (этап 3) ---
|
||||
// Пан 0 = главный тракт (RXA/RX_DISP_ID), доп. паны 1..MAX_PANS-1 живут на
|
||||
// своих DDC. Единый потолок для движка/контроллера/UI.
|
||||
MAX_PANS = 4;
|
||||
// Analyzer id пана N = PAN_DISP_BASE + N (9..11). ID 4/5 заняты веткой
|
||||
// feature/qo100-zoom-panel — не пересекаемся при будущем merge.
|
||||
PAN_DISP_BASE = 8;
|
||||
|
||||
DSP_BUFSIZE = 1024;
|
||||
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
|
||||
DISPLAY_BLOCK_SIZE = 64;
|
||||
|
||||
// Очередь IQ пакетов между сетевым и DSP потоком
|
||||
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
|
||||
// Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны).
|
||||
// 256: главный 1.536М (~6.5к пкт/с) + 3 пана по 768к (~10к пкт/с) должны
|
||||
// переживать scheduling-паузы ~15мс без дропов.
|
||||
IQ_QUEUE_SIZE = 256; // кол-во слотов (степень двойки для AND-маски)
|
||||
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
|
||||
|
||||
// TX mic ring buffer — должен быть степенью двойки
|
||||
@@ -155,6 +166,7 @@ type
|
||||
Data: array[0..IQ_PKT_MAXBYTES - 1] of Byte;
|
||||
DataLen: Integer; // реальная длина данных
|
||||
IQPairs: Integer; // количество IQ пар
|
||||
PanId: Integer; // 0 = главный тракт, 1..MAX_PANS-1 = доп. пан
|
||||
end;
|
||||
|
||||
TWDSPAGCMode = (agcOff = 0, agcLong = 1, agcSlow = 2,
|
||||
@@ -173,6 +185,10 @@ type
|
||||
// SliceId — логический id (из RadioController), не WDSP-канал.
|
||||
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
|
||||
Count: Integer) of object;
|
||||
// Пиксели спектра/водопада доп. пана — из display-потока (как FOnSpectrum/
|
||||
// FOnWaterfall главного). Внутри колбэка НЕ звать slice/pan API движка.
|
||||
TOnPanPixels = procedure(PanId: Integer; const Pixels: array of Single;
|
||||
Count: Integer) of object;
|
||||
// Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком,
|
||||
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
|
||||
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
|
||||
@@ -188,6 +204,7 @@ type
|
||||
Active: Boolean; // слот занят
|
||||
Id: Integer; // логический id слайса
|
||||
Chan: Integer; // WDSP-канал (SLICE_CHAN_BASE + слот)
|
||||
PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан
|
||||
ShiftHz: Double; // NCO-сдвиг = TargetHz - CapturedCenterHz
|
||||
Mode: Integer; // индекс режима (MODE_*)
|
||||
FilterLo: Integer;
|
||||
@@ -205,6 +222,21 @@ type
|
||||
Opened: Boolean; // WDSP-канал реально открыт (после Open движка)
|
||||
end;
|
||||
|
||||
// Доп. панадаптер (этап 3): свой DDC-поток (rate 192-768к), свой analyzer
|
||||
// (PAN_DISP_BASE+PanId), свои слайсы (TSliceChan.PanId). Доступ к записи —
|
||||
// только под FSliceLock (кроме read-only Active в PushPanIQ).
|
||||
TPanChan = record
|
||||
Active: Boolean;
|
||||
Rate: Integer; // входной rate пан-DDC, Гц (кратен FAudioRate)
|
||||
BufSize: Integer; // блок @ Rate = FAudioBufSize * Rate / FAudioRate
|
||||
AccI: array of Double; // аккумулятор входных IQ до BufSize
|
||||
AccQ: array of Double;
|
||||
AccPos: Integer;
|
||||
DispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар)
|
||||
DispPos: Integer;
|
||||
DispOpen: Boolean; // XCreateAnalyzer(PAN_DISP_BASE+PanId) жив
|
||||
end;
|
||||
|
||||
{ TWDSPEngine }
|
||||
TWDSPEngine = class
|
||||
private
|
||||
@@ -294,6 +326,11 @@ type
|
||||
FSliceOutR: array of Single; // R
|
||||
FOnSliceAudio: TOnSliceAudio;
|
||||
|
||||
// --- Панадаптеры на аппаратных DDC (этап 3.2) ---
|
||||
FPans: array[1..MAX_PANS-1] of TPanChan;
|
||||
FOnPanSpectrum: TOnPanPixels;
|
||||
FOnPanWaterfall: TOnPanPixels;
|
||||
|
||||
// TX mic ring buffer (lock-free: один writer — receive thread,
|
||||
// один reader — TX DSP thread)
|
||||
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
|
||||
@@ -395,7 +432,10 @@ type
|
||||
// WFM требует dsp_rate >= 192k (девиация 75 кГц не влезает в 48 кГц).
|
||||
function WantWideDSP(Mode: Integer): Boolean;
|
||||
function WFMDSPRate: Integer;
|
||||
function WFMDSPRateFor(InRate: Integer): Integer; // WFMDSPRate для произвольного входа
|
||||
function SliceDSPRate(Mode: Integer): Integer;
|
||||
// dsp_rate слайса от режима и ЕГО входного rate (у пан-слайсов вход = rate пана)
|
||||
function SliceDSPRateFor(Mode, InRate: Integer): Integer;
|
||||
function ClampToNyquist(Hz, Rate: Integer): Integer;
|
||||
function EffectiveTXNC: Integer;
|
||||
procedure ApplyDSPRate(Mode: Integer);
|
||||
@@ -403,11 +443,22 @@ type
|
||||
procedure ApplyWFMSettings;
|
||||
// --- Мультислайсы ---
|
||||
function FindSliceIdx(Id: Integer): Integer; // -1 если нет
|
||||
function SliceInRate(Idx: Integer): Integer; // входной rate слайса (его пан)
|
||||
function SliceInSize(Idx: Integer): Integer; // входной блок слайса (его пан)
|
||||
procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки
|
||||
procedure CloseSliceChannel(Idx: Integer); // CloseChannel (запись хранит)
|
||||
procedure ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; FixedGain: Double);
|
||||
procedure ProcessSlices; // fan-out в ProcessRXBlock
|
||||
// Прогон блока IQ пана через его слайсы. Вызывать ПОД FSliceLock.
|
||||
procedure ProcessSlicesFor(PanId: Integer;
|
||||
const AccI, AccQ: array of Double; N: Integer);
|
||||
procedure AllocSliceBuffers; // (пере)аллокация буферов
|
||||
// --- Панадаптеры (этап 3.2) ---
|
||||
procedure CreatePanAnalyzer(PanId: Integer); // XCreateAnalyzer + настройки
|
||||
procedure DestroyPanAnalyzer(PanId: Integer);
|
||||
procedure ApplyPanAnalyzerSettings(PanId: Integer);
|
||||
procedure ReapplyPanDisplaySettings; // все активные паны, под локом
|
||||
procedure PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem);
|
||||
procedure ProcessRXBlock;
|
||||
procedure PushIQItemToDSP(const Item: TIQQueueItem);
|
||||
procedure OpenAnalyzer;
|
||||
@@ -451,11 +502,22 @@ type
|
||||
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
|
||||
procedure SetBeaconDecoder(D: TBeaconDecoder);
|
||||
|
||||
// --- Панадаптеры на аппаратных DDC (этап 3.2) ---
|
||||
// Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer
|
||||
// PAN_DISP_BASE+PanId. RateHz — rate его DDC, кратен FAudioRate.
|
||||
// Слайсы пана создаются отдельно (AddSlice с APanId).
|
||||
function AddPan(PanId, RateHz: Integer): Boolean;
|
||||
// Убирает пан вместе с его слайсами (аудио-выходы слайсов — забота вызывающего).
|
||||
procedure RemovePan(PanId: Integer);
|
||||
function PanActive(PanId: Integer): Boolean;
|
||||
|
||||
// --- Мультислайсы (софт-fan-out) ---
|
||||
// Добавляет слайс с логическим Id (уникальным). ShiftHz = TargetHz - центр
|
||||
// захвата. Возвращает False если нет свободных слотов или Id уже есть.
|
||||
// захвата ЕГО пана. APanId>0 требует активного пана (AddPan).
|
||||
// Возвращает False если нет свободных слотов или Id уже есть.
|
||||
function AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
|
||||
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean;
|
||||
ShiftHz, Volume: Double; AGC: TWDSPAGCMode;
|
||||
APanId: Integer = 0): Boolean;
|
||||
procedure RemoveSlice(Id: Integer);
|
||||
procedure SetSliceShift(Id: Integer; ShiftHz: Double);
|
||||
procedure SetSliceMode(Id, Mode: Integer);
|
||||
@@ -606,6 +668,9 @@ type
|
||||
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||
// Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3).
|
||||
property OnPanSpectrum: TOnPanPixels read FOnPanSpectrum write FOnPanSpectrum;
|
||||
property OnPanWaterfall: TOnPanPixels read FOnPanWaterfall write FOnPanWaterfall;
|
||||
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
||||
property TXActive: Boolean read FTXActive;
|
||||
// Включает full-duplex поведение RX-тракта: RXA не останавливается на TX,
|
||||
@@ -804,11 +869,14 @@ begin
|
||||
RTLEventWaitFor(FEngine.FQueueSem, 100);
|
||||
if Terminated then Break;
|
||||
|
||||
// Разбираем все накопившиеся пакеты
|
||||
// Разбираем все накопившиеся пакеты (общая очередь: главный + паны)
|
||||
while FEngine.FQueueTail <> FEngine.FQueueHead do
|
||||
begin
|
||||
Item := @FEngine.FQueue[FEngine.FQueueTail];
|
||||
FEngine.PushIQItemToDSP(Item^);
|
||||
if Item^.PanId = 0 then
|
||||
FEngine.PushIQItemToDSP(Item^)
|
||||
else
|
||||
FEngine.PushPanItemToDSP(Item^.PanId, Item^);
|
||||
FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1);
|
||||
end;
|
||||
end;
|
||||
@@ -850,9 +918,9 @@ begin
|
||||
Result := (Mode = MODE_WFM) and (WFMDSPRate > FAudioRate);
|
||||
end;
|
||||
|
||||
function TWDSPEngine.WFMDSPRate: Integer;
|
||||
function TWDSPEngine.WFMDSPRateFor(InRate: Integer): Integer;
|
||||
// Наибольший rate вида FAudioRate * 2^k, который не выше WFM_DSP_RATE и делит
|
||||
// FSampleRate нацело. Оба требования жёсткие:
|
||||
// InRate нацело. Оба требования жёсткие:
|
||||
// * pre_main_build считает dsp_insize через ЦЕЛОЧИСЛЕННОЕ in_rate/dsp_rate,
|
||||
// поэтому отношение обязано быть точным;
|
||||
// * dsp_size = FAudioBufSize * (rate/FAudioRate) должен остаться степенью
|
||||
@@ -863,17 +931,30 @@ begin
|
||||
R := WFM_DSP_RATE div FAudioRate; // 4 при 48 кГц аудио
|
||||
while R > 1 do
|
||||
begin
|
||||
if (FAudioRate * R <= FSampleRate) and (FSampleRate mod (FAudioRate * R) = 0) then
|
||||
if (FAudioRate * R <= InRate) and (InRate mod (FAudioRate * R) = 0) then
|
||||
Break;
|
||||
R := R div 2;
|
||||
end;
|
||||
Result := FAudioRate * R;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.WFMDSPRate: Integer;
|
||||
begin
|
||||
Result := WFMDSPRateFor(FSampleRate);
|
||||
end;
|
||||
|
||||
function TWDSPEngine.SliceDSPRate(Mode: Integer): Integer;
|
||||
begin
|
||||
if WantWideDSP(Mode) then Result := WFMDSPRate
|
||||
else Result := FAudioRate;
|
||||
Result := SliceDSPRateFor(Mode, FSampleRate);
|
||||
end;
|
||||
|
||||
function TWDSPEngine.SliceDSPRateFor(Mode, InRate: Integer): Integer;
|
||||
// Пан-слайс поднимается в широкий WFM-режим от rate СВОЕГО пана, а не главного.
|
||||
begin
|
||||
if (Mode = MODE_WFM) and (WFMDSPRateFor(InRate) > FAudioRate) then
|
||||
Result := WFMDSPRateFor(InRate)
|
||||
else
|
||||
Result := FAudioRate;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyDSPRate(Mode: Integer);
|
||||
@@ -1137,7 +1218,7 @@ end;
|
||||
|
||||
procedure TWDSPEngine.OpenAnalyzer;
|
||||
var
|
||||
Success: Integer;
|
||||
Success, p: Integer;
|
||||
begin
|
||||
if FAnalyzerOpen then Exit;
|
||||
|
||||
@@ -1160,6 +1241,17 @@ begin
|
||||
ApplyAnalyzerSettings;
|
||||
|
||||
FAnalyzerOpen := True;
|
||||
|
||||
// Анализаторы активных доп. панов (пересоздание после ChangeSampleRate:
|
||||
// Close их разрушил, записи FPans живы).
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for p := 1 to MAX_PANS - 1 do
|
||||
if FPans[p].Active then CreatePanAnalyzer(p);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
|
||||
if FDisplayThread = nil then
|
||||
FDisplayThread := TDisplayThread.Create(Self);
|
||||
end;
|
||||
@@ -1257,11 +1349,91 @@ begin
|
||||
SetDisplaySampleRate (TX_DISP_ID, FTXSampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyPanAnalyzerSettings(PanId: Integer);
|
||||
// Параметризованная копия ApplyRXAnalyzerSettings для analyzer'а доп. пана:
|
||||
// rate пана, БЕЗ span-clip/zoom (пан-зум — этап 3.3+), FFT/детектор/усреднение
|
||||
// пока общие с главным. Вызывать под FSliceLock.
|
||||
var
|
||||
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels, Disp: Integer;
|
||||
Backmult, WfBackmult: Double;
|
||||
EffFPS: Double;
|
||||
const
|
||||
KEEP_TIME_SEC = 0.10;
|
||||
begin
|
||||
if not FPans[PanId].DispOpen then Exit;
|
||||
Disp := PAN_DISP_BASE + PanId;
|
||||
AnalyzerFFT := CalcDisplayFFTSize;
|
||||
FPans[PanId].DispPos := 0;
|
||||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FPans[PanId].Rate,
|
||||
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffFPS);
|
||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64,
|
||||
EnsureRange(FDisplayResLimit, 64, SPECTRUM_PIXELS));
|
||||
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||
|
||||
SetAnalyzer(Disp, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
||||
FWindowType, 14.0, Overlap, 0, 0, 0, DisplayPixels,
|
||||
1, 0, 0.0, 0.0, MaxW);
|
||||
SetDisplayDetectorMode(Disp, 0, FSpecDetector);
|
||||
SetDisplayAverageMode (Disp, 0, FSpecAvgMode);
|
||||
SetDisplayNumAverage (Disp, 0, AvgCount);
|
||||
SetDisplayAvBackmult (Disp, 0, Backmult);
|
||||
SetDisplayNormOneHz (Disp, 0, UsePanNormOneHz(FSpecDetector));
|
||||
SetDisplayDetectorMode(Disp, 1, FWfDetector);
|
||||
SetDisplayAverageMode (Disp, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage (Disp, 1, WfAvgCount);
|
||||
SetDisplayAvBackmult (Disp, 1, WfBackmult);
|
||||
SetDisplayNormOneHz (Disp, 1, 0);
|
||||
SetDisplaySampleRate (Disp, FPans[PanId].Rate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.CreatePanAnalyzer(PanId: Integer);
|
||||
// Вызывать под FSliceLock, при живом WDSP (OpenAnalyzer уже прошёл).
|
||||
var
|
||||
Success: Integer;
|
||||
begin
|
||||
if FPans[PanId].DispOpen then Exit;
|
||||
Success := -1;
|
||||
XCreateAnalyzer(PAN_DISP_BASE + PanId, @Success, 262144, 1, 1, nil);
|
||||
if Success <> 0 then
|
||||
begin
|
||||
FLastError := Format('XCreateAnalyzer(PAN%d) failed: %d', [PanId, Success]);
|
||||
Exit; // пан без спектра: слайсы работают, DispOpen=False гейтит Spectrum0
|
||||
end;
|
||||
FPans[PanId].DispOpen := True;
|
||||
ApplyPanAnalyzerSettings(PanId);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.DestroyPanAnalyzer(PanId: Integer);
|
||||
// Вызывать под FSliceLock (display-поток читает DispOpen/зовёт GetPixels под ним).
|
||||
begin
|
||||
if not FPans[PanId].DispOpen then Exit;
|
||||
FPans[PanId].DispOpen := False;
|
||||
DestroyAnalyzer(PAN_DISP_BASE + PanId);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ReapplyPanDisplaySettings;
|
||||
// Перекатывает общие настройки дисплея (FFT/детектор/усреднение/пиксели) на
|
||||
// анализаторы всех активных панов.
|
||||
var p: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for p := 1 to MAX_PANS - 1 do
|
||||
if FPans[p].Active then ApplyPanAnalyzerSettings(p);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у.
|
||||
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам.
|
||||
begin
|
||||
ApplyRXAnalyzerSettings;
|
||||
ApplyTXAnalyzerSettings;
|
||||
ReapplyPanDisplaySettings;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
|
||||
@@ -1275,6 +1447,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.CloseAnalyzer;
|
||||
var p: Integer;
|
||||
begin
|
||||
if not FAnalyzerOpen then Exit;
|
||||
if Assigned(FDisplayThread) then
|
||||
@@ -1285,6 +1458,14 @@ begin
|
||||
end;
|
||||
DestroyAnalyzer(RX_DISP_ID);
|
||||
DestroyAnalyzer(TX_DISP_ID);
|
||||
// Анализаторы доп. панов (записи FPans сохраняются — пересоздание в OpenAnalyzer).
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for p := 1 to MAX_PANS - 1 do
|
||||
DestroyPanAnalyzer(p);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
FAnalyzerOpen := False;
|
||||
end;
|
||||
|
||||
@@ -1465,6 +1646,10 @@ end;
|
||||
|
||||
procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
|
||||
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open.
|
||||
// Панов не трогает: их rate свой; аккумуляторы сбрасываем (очередь общая,
|
||||
// их пакеты умерли вместе с ней), анализаторы пересоздаст OpenAnalyzer.
|
||||
var
|
||||
p: Integer;
|
||||
begin
|
||||
if NewRate = FSampleRate then Exit;
|
||||
|
||||
@@ -1476,6 +1661,12 @@ begin
|
||||
FQueueHead := 0;
|
||||
FQueueTail := 0;
|
||||
FRXAccPos := 0;
|
||||
for p := 1 to MAX_PANS - 1 do
|
||||
if FPans[p].Active then
|
||||
begin
|
||||
FPans[p].AccPos := 0;
|
||||
FPans[p].DispPos := 0;
|
||||
end;
|
||||
|
||||
// Обновляем параметры
|
||||
FSampleRate := NewRate;
|
||||
@@ -1523,6 +1714,7 @@ begin
|
||||
Move(Buf[DataOffset], Item^.Data[0], DataBytes);
|
||||
Item^.DataLen := DataBytes;
|
||||
Item^.IQPairs := IQPairs;
|
||||
Item^.PanId := 0;
|
||||
|
||||
FQueueHead := NextHead;
|
||||
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
|
||||
@@ -1530,9 +1722,102 @@ end;
|
||||
|
||||
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
|
||||
DataOffset: Integer; IQPairs: Integer);
|
||||
// Сетевой поток. Этап 3.2 положит пакет в аккумулятор пана PanId; до тех пор
|
||||
// поток доп. пан-DDC отбрасывается (панов ещё никто не создаёт).
|
||||
// Сетевой поток: пакет пан-DDC в ОБЩУЮ очередь с меткой пана (порядок прихода
|
||||
// сохраняется, один семафор). Active читаем без лока: Boolean, худший случай —
|
||||
// один лишний пакет в очереди, который DSP-поток отсеет уже под локом.
|
||||
var
|
||||
NextHead: Integer;
|
||||
Item: ^TIQQueueItem;
|
||||
DataBytes: Integer;
|
||||
begin
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
if not FPans[PanId].Active then Exit;
|
||||
|
||||
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;
|
||||
Item^.PanId := PanId;
|
||||
|
||||
FQueueHead := NextHead;
|
||||
RTLEventSetEvent(FQueueSem);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem);
|
||||
// DSP-поток: декод 24-бит IQ пан-DDC → feed анализатора пана + аккумулятор;
|
||||
// по накоплению блока — слайсы этого пана. Всё под FSliceLock: Add/RemovePan
|
||||
// и слайс-API меняют состояние из UI-потока. TX-гейты зеркалят главный тракт.
|
||||
var
|
||||
i, Pos: Integer;
|
||||
IR, QR: LongInt;
|
||||
DI, DQ: Double;
|
||||
P: PByte;
|
||||
Pan: ^TPanChan;
|
||||
const
|
||||
SCALE = 1.0 / 8388608.0;
|
||||
begin
|
||||
if FTXActive and not FKeepRXDuringTX then Exit;
|
||||
if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit;
|
||||
if not FInitialized then Exit;
|
||||
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
Pan := @FPans[PanId];
|
||||
if not Pan^.Active then Exit;
|
||||
|
||||
Pos := 0;
|
||||
P := @Item.Data[0];
|
||||
for i := 0 to Item.IQPairs - 1 do
|
||||
begin
|
||||
if Pos + 5 >= Item.DataLen then Break;
|
||||
|
||||
IR := (LongInt(P[0]) shl 16) or (LongInt(P[1]) shl 8) or LongInt(P[2]);
|
||||
QR := (LongInt(P[3]) shl 16) or (LongInt(P[4]) shl 8) or LongInt(P[5]);
|
||||
Inc(P, 6);
|
||||
Inc(Pos, 6);
|
||||
|
||||
if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
|
||||
if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);
|
||||
|
||||
DI := IR * SCALE;
|
||||
DQ := QR * SCALE;
|
||||
|
||||
if Pan^.DispOpen then
|
||||
begin
|
||||
Pan^.DispBuf[Pan^.DispPos * 2] := DI;
|
||||
Pan^.DispBuf[Pan^.DispPos * 2 + 1] := DQ;
|
||||
Inc(Pan^.DispPos);
|
||||
if Pan^.DispPos >= DISPLAY_BLOCK_SIZE then
|
||||
begin
|
||||
Spectrum0(1, PAN_DISP_BASE + PanId, 0, 0, @Pan^.DispBuf[0]);
|
||||
Pan^.DispPos := 0;
|
||||
end;
|
||||
end;
|
||||
|
||||
Pan^.AccI[Pan^.AccPos] := DI;
|
||||
Pan^.AccQ[Pan^.AccPos] := DQ;
|
||||
Inc(Pan^.AccPos);
|
||||
|
||||
if Pan^.AccPos >= Pan^.BufSize then
|
||||
begin
|
||||
Pan^.AccPos := 0;
|
||||
if Assigned(FOnSliceAudio) then
|
||||
try
|
||||
ProcessSlicesFor(PanId, Pan^.AccI, Pan^.AccQ, Pan^.BufSize);
|
||||
except
|
||||
// Защита от падения libwdsp в потоке — пропускаем блок (как ProcessRXBlock)
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.FlushRX;
|
||||
@@ -1699,9 +1984,16 @@ end;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TWDSPEngine.AllocSliceBuffers;
|
||||
// FSliceIn/Out должны вмещать наибольший входной блок среди главного тракта
|
||||
// и активных панов (пан на 768к при главном 192к имеет блок больше FBufSize).
|
||||
var
|
||||
Need, p: Integer;
|
||||
begin
|
||||
SetLength(FSliceIn, FBufSize * 2);
|
||||
SetLength(FSliceOut, FBufSize * 2); // с запасом (>= FAudioBufSize*2)
|
||||
Need := FBufSize;
|
||||
for p := 1 to MAX_PANS - 1 do
|
||||
if FPans[p].Active and (FPans[p].BufSize > Need) then Need := FPans[p].BufSize;
|
||||
SetLength(FSliceIn, Need * 2);
|
||||
SetLength(FSliceOut, Need * 2); // с запасом (>= FAudioBufSize*2)
|
||||
SetLength(FSliceOutL, FAudioBufSize);
|
||||
SetLength(FSliceOutR, FAudioBufSize);
|
||||
end;
|
||||
@@ -1714,6 +2006,29 @@ begin
|
||||
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
|
||||
end;
|
||||
|
||||
function TWDSPEngine.SliceInRate(Idx: Integer): Integer;
|
||||
// Входной rate слайса = rate его пана (пан 0 = главный захват).
|
||||
var p: Integer;
|
||||
begin
|
||||
p := FSlices[Idx].PanId;
|
||||
if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then
|
||||
Result := FPans[p].Rate
|
||||
else
|
||||
Result := FSampleRate;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.SliceInSize(Idx: Integer): Integer;
|
||||
// Входной блок слайса = блок его пана. Выходной блок при этом всегда
|
||||
// FAudioBufSize (in_size/in_rate = const по построению BufSize панов).
|
||||
var p: Integer;
|
||||
begin
|
||||
p := FSlices[Idx].PanId;
|
||||
if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then
|
||||
Result := FPans[p].BufSize
|
||||
else
|
||||
Result := FBufSize;
|
||||
end;
|
||||
|
||||
// Применяет AGC к произвольному каналу — вынесено из SetAGC чтобы делить с слайсами.
|
||||
procedure TWDSPEngine.ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode;
|
||||
FixedGain: Double);
|
||||
@@ -1746,20 +2061,23 @@ end;
|
||||
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
|
||||
// под FSliceLock и только при FInitialized.
|
||||
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
|
||||
var Chan, DSPRate, DSPSize: Integer;
|
||||
var Chan, DSPRate, DSPSize, InRate, InSize: Integer;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
if FSlices[Idx].Opened then Exit;
|
||||
Chan := FSlices[Idx].Chan;
|
||||
// Вход канала = rate/блок ПАНА слайса (пан 0 = главный захват).
|
||||
InRate := SliceInRate(Idx);
|
||||
InSize := SliceInSize(Idx);
|
||||
// WFM-слайс, как и главный канал, живёт на WFMDSPRate: rsmpin децимирует
|
||||
// вход, rsmpout сводит к FAudioRate, так что out_size остаётся FAudioBufSize.
|
||||
DSPRate := SliceDSPRate(FSlices[Idx].Mode);
|
||||
DSPRate := SliceDSPRateFor(FSlices[Idx].Mode, InRate);
|
||||
DSPSize := FAudioBufSize * (DSPRate div FAudioRate);
|
||||
OpenChannel(Chan, FBufSize, DSPSize, FSampleRate, DSPRate, FAudioRate,
|
||||
OpenChannel(Chan, InSize, DSPSize, InRate, DSPRate, FAudioRate,
|
||||
0, 0, 0.010, 0.010, 0.010, 0.010, 0);
|
||||
// NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open.
|
||||
create_anbEXT(Chan, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||||
create_nobEXT(Chan, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||||
create_anbEXT(Chan, 1, InSize, InRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||||
create_nobEXT(Chan, 1, 0, InSize, InRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||||
SetRXABandpassWindow(Chan, 1);
|
||||
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
|
||||
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
|
||||
@@ -1791,51 +2109,123 @@ begin
|
||||
FSlices[Idx].Opened := False;
|
||||
end;
|
||||
|
||||
// Fan-out: прогоняет тот же входной IQ-блок через каждый активный слайс-канал.
|
||||
// Вызывается из ProcessRXBlock (DSP-поток) после основного канала.
|
||||
// Fan-out слайсов главного пана: вызывается из ProcessRXBlock (DSP-поток)
|
||||
// после основного канала.
|
||||
procedure TWDSPEngine.ProcessSlices;
|
||||
var
|
||||
s, i, k, Err: Integer;
|
||||
V: Double;
|
||||
begin
|
||||
if not Assigned(FOnSliceAudio) then Exit;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for s := 0 to MAX_SLICES - 1 do
|
||||
begin
|
||||
if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
|
||||
if FSlices[s].Muted then Continue;
|
||||
// Свежая копия входного IQ — fexchange0 портит вход in-place.
|
||||
for i := 0 to FBufSize - 1 do
|
||||
begin
|
||||
FSliceIn[i * 2] := FRXAccI[i];
|
||||
FSliceIn[i * 2 + 1] := FRXAccQ[i];
|
||||
end;
|
||||
Err := 0;
|
||||
fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err);
|
||||
// S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор.
|
||||
FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV);
|
||||
V := FSlices[s].Volume;
|
||||
for k := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
FSliceOutL[k] := FSliceOut[k * 2] * V;
|
||||
FSliceOutR[k] := FSliceOut[k * 2 + 1] * V;
|
||||
end;
|
||||
FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize);
|
||||
end;
|
||||
ProcessSlicesFor(0, FRXAccI, FRXAccQ, FBufSize);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
// Прогоняет входной IQ-блок пана PanId через каждый его активный слайс-канал.
|
||||
// Вызывать ПОД FSliceLock (общая дисциплина слайсов/панов vs DSP).
|
||||
procedure TWDSPEngine.ProcessSlicesFor(PanId: Integer;
|
||||
const AccI, AccQ: array of Double; N: Integer);
|
||||
var
|
||||
s, i, k, Err: Integer;
|
||||
V: Double;
|
||||
begin
|
||||
for s := 0 to MAX_SLICES - 1 do
|
||||
begin
|
||||
if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
|
||||
if FSlices[s].PanId <> PanId then Continue;
|
||||
if FSlices[s].Muted then Continue;
|
||||
// Свежая копия входного IQ — fexchange0 портит вход in-place.
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
FSliceIn[i * 2] := AccI[i];
|
||||
FSliceIn[i * 2 + 1] := AccQ[i];
|
||||
end;
|
||||
Err := 0;
|
||||
fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err);
|
||||
// S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор.
|
||||
FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV);
|
||||
V := FSlices[s].Volume;
|
||||
for k := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
FSliceOutL[k] := FSliceOut[k * 2] * V;
|
||||
FSliceOutR[k] := FSliceOut[k * 2 + 1] * V;
|
||||
end;
|
||||
FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize);
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Панадаптеры на аппаратных DDC (этап 3.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TWDSPEngine.AddPan(PanId, RateHz: Integer): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
// Кратность rate: и BufSize (блок = FAudioBufSize*Rate/FAudioRate), и WDSP
|
||||
// pre_main_build (целочисленное in_rate/dsp_rate) требуют точного отношения.
|
||||
if (RateHz < FAudioRate) or (RateHz mod FAudioRate <> 0) then Exit;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
if FPans[PanId].Active then Exit;
|
||||
FPans[PanId].Rate := RateHz;
|
||||
FPans[PanId].BufSize := FAudioBufSize * (RateHz div FAudioRate);
|
||||
SetLength(FPans[PanId].AccI, FPans[PanId].BufSize);
|
||||
SetLength(FPans[PanId].AccQ, FPans[PanId].BufSize);
|
||||
SetLength(FPans[PanId].DispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
FPans[PanId].AccPos := 0;
|
||||
FPans[PanId].DispPos := 0;
|
||||
FPans[PanId].DispOpen := False;
|
||||
FPans[PanId].Active := True;
|
||||
AllocSliceBuffers; // FSliceIn/Out под новый макс. блок
|
||||
if FAnalyzerOpen then CreatePanAnalyzer(PanId);
|
||||
Result := True;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.RemovePan(PanId: Integer);
|
||||
var s: Integer;
|
||||
begin
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
if not FPans[PanId].Active then Exit;
|
||||
// Слайсы пана: обычно контроллер уже убрал их (вместе со звуковухами);
|
||||
// здесь — страховка, чтобы канал не остался кормиться из мёртвого пана.
|
||||
for s := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[s].Active and (FSlices[s].PanId = PanId) then
|
||||
begin
|
||||
CloseSliceChannel(s);
|
||||
FSlices[s].Active := False;
|
||||
end;
|
||||
DestroyPanAnalyzer(PanId);
|
||||
FPans[PanId].Active := False;
|
||||
FPans[PanId].AccI := nil;
|
||||
FPans[PanId].AccQ := nil;
|
||||
FPans[PanId].DispBuf := nil;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.PanActive(PanId: Integer): Boolean;
|
||||
begin
|
||||
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
|
||||
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean;
|
||||
ShiftHz, Volume: Double; AGC: TWDSPAGCMode; APanId: Integer): Boolean;
|
||||
var i, slot: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
if FindSliceIdx(Id) >= 0 then Exit; // уже существует
|
||||
if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
|
||||
if (APanId > 0) and not FPans[APanId].Active then Exit; // пан не создан
|
||||
slot := -1;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if not FSlices[i].Active then begin slot := i; Break; end;
|
||||
@@ -1843,6 +2233,7 @@ begin
|
||||
FSlices[slot].Active := True;
|
||||
FSlices[slot].Id := Id;
|
||||
FSlices[slot].Chan := SLICE_CHAN_BASE + slot;
|
||||
FSlices[slot].PanId := APanId;
|
||||
FSlices[slot].Mode := Mode;
|
||||
FSlices[slot].FilterLo := FilterLo;
|
||||
FSlices[slot].FilterHi := FilterHi;
|
||||
@@ -1899,7 +2290,8 @@ begin
|
||||
// Уход в/из WFM меняет dsp_rate канала — проще пересоздать его целиком,
|
||||
// чем гонять SetDSPSamplerate: слайс всё равно перестраивает все фильтры.
|
||||
Rerate := FSlices[idx].Opened and
|
||||
(SliceDSPRate(Mode) <> SliceDSPRate(FSlices[idx].Mode));
|
||||
(SliceDSPRateFor(Mode, SliceInRate(idx)) <>
|
||||
SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx)));
|
||||
FSlices[idx].Mode := Mode;
|
||||
if Rerate then
|
||||
begin
|
||||
@@ -1926,7 +2318,7 @@ begin
|
||||
FSlices[idx].FilterHi := High;
|
||||
if FSlices[idx].Opened then
|
||||
begin
|
||||
Rate := SliceDSPRate(FSlices[idx].Mode);
|
||||
Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx));
|
||||
RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate),
|
||||
ClampToNyquist(High, Rate));
|
||||
end;
|
||||
@@ -2843,10 +3235,12 @@ procedure TWDSPEngine.UpdateSpectrum;
|
||||
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
|
||||
// буфера, а не ждёт следующего полного блока.
|
||||
var
|
||||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
Flag: Integer;
|
||||
i, N: Integer;
|
||||
DispID: Integer;
|
||||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
WfBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
Flag: Integer;
|
||||
i, N, p: Integer;
|
||||
DispID: Integer;
|
||||
GotSpec, GotWf: Boolean;
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||||
@@ -2871,6 +3265,34 @@ begin
|
||||
if Assigned(FOnWaterfall) then
|
||||
FOnWaterfall(FWaterfallPixels, N);
|
||||
end;
|
||||
|
||||
// Доп. паны: GetPixels под FSliceLock (гонка с DestroyPanAnalyzer из
|
||||
// RemovePan в UI-потоке), колбэки — уже без лока (UI в них может звать
|
||||
// slice/pan API). Пиксели пана не кэшируем (аналог FSpectrumPixels не нужен).
|
||||
if Assigned(FOnPanSpectrum) or Assigned(FOnPanWaterfall) then
|
||||
for p := 1 to MAX_PANS - 1 do
|
||||
begin
|
||||
GotSpec := False;
|
||||
GotWf := False;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
if FPans[p].Active and FPans[p].DispOpen then
|
||||
begin
|
||||
Flag := 0;
|
||||
GetPixels(PAN_DISP_BASE + p, 0, @PixBuf[0], @Flag);
|
||||
GotSpec := Flag <> 0;
|
||||
Flag := 0;
|
||||
GetPixels(PAN_DISP_BASE + p, 1, @WfBuf[0], @Flag);
|
||||
GotWf := Flag <> 0;
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
if GotSpec and Assigned(FOnPanSpectrum) then
|
||||
FOnPanSpectrum(p, PixBuf, N);
|
||||
if GotWf and Assigned(FOnPanWaterfall) then
|
||||
FOnPanWaterfall(p, WfBuf, N);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
||||
@@ -3015,6 +3437,7 @@ begin
|
||||
SetDisplayNumAverage(RX_DISP_ID, 0, AvgCount);
|
||||
SetDisplayAvBackmult(RX_DISP_ID, 0, Backmult);
|
||||
SetDisplayNormOneHz(RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||
ReapplyPanDisplaySettings; // детектор/усреднение общие с панами
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
|
||||
@@ -3039,6 +3462,7 @@ begin
|
||||
SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount);
|
||||
SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult);
|
||||
SetDisplayNormOneHz(RX_DISP_ID, 1, 0);
|
||||
ReapplyPanDisplaySettings; // детектор/усреднение общие с панами
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
Reference in New Issue
Block a user