mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(beacon): лупа наведения на маяк QO-100 в окне Beacon
На спектрограмме 576к центральный маяк — пара пикселей, ткнуть в него для
наведения декодера почти нельзя. В окне Beacon появилась карточка BEACON
TUNING: узкий кусок спектра вокруг маяка в высоком разрешении + мини-водопад,
клик по нему = BeaconSeedAtHz, ровно как клик по главному спектру. Наведение
с главной спектрограммы не тронуто.
Увеличение делает сам WDSP, а не растяжка бинов главного дисплея: второй
analyzer BCN_DISP_ID=4 на том же IQ главного тракта, со своей FFT и своим
span-clip. Клипы fscLin/fscHin считаются прямо из желаемых границ окна в
герцах, а не из зум-слайдера — анализатор умеет ассиметричный клип, поэтому
окно ставится в любое место полосы захвата, а не только вокруг центра DDC.
- WDSPEngine: SetBeaconZoom + GetBeaconZoom{Spectrum,Waterfall}. Analyzer живёт
только пока окно маяка открыто; весь доступ к disp'у под FBcnLock (SetAnalyzer
из UI, Spectrum0 из DSP-потока, GetPixels из display-потока). Кадр (1024
точки) фиксирован по размеру — ресайз окна не переармирует analyzer и не
морозит спектр на наполнение FFT-окна.
- Span-clip НЕ удешевляет FFT — она по всей полосе захвата. Отсюда потолок
262144 и своя пониженная частота кадров BCN_ZOOM_FPS=15 через ovrlp: на 60 fps
это было бы ядро под нагрузкой ради картинки, в которой ничего не меняется.
Переарм по смене sample rate, детектор/усреднение — без переарма.
- RadioController: SetBeaconZoomWindow / GetBeaconZoom* / GetCaptureWindow /
BeaconZoomBinHz. Наружу — абсолютные display-Гц, те же, в которых живут
BeaconSeedAtHz и маркеры главного спектра.
- BeaconScopeForm: клик = наведение, колесо = зум вокруг курсора, drag = пан,
двойной клик = центрировать, кнопки -/+/RESET/FOLLOW (FOLLOW подтягивает окно
только когда маркер ушёл за край, а не возит картинку постоянно). Маркеры —
теми же цветами и с той же семантикой, что DrawBeaconMarkersRaw/GL: опорная
пунктиром, центроид, три линии полосы приёма ±450 Гц, без полупрозрачной
заливки. Водопад — TWaterfallView с палитрой/гаммой из контроллера.
- Рендер по конвенции проекта: кадр лупы собирается в офскрин только по
FZoomDirty (новый кадр анализатора, сдвиг маркеров/окна, ресайз, тема), Paint
= блит + курсор. Констелляция с метриками переведена туда же: статичная
обвязка (карточки, оси, рамки и подписи плиток) кэшируется в FScopeChrome и
пересобирается лишь на ресайз/смену темы — раньше десяток RoundRect и TextOut
рисовались заново 25 раз в секунду прямо в обработчике Paint.
Лупа рисуется на CPU: GL-путь в проекте есть только у главного панафолла и
только под ключом, все прочие панели — обычные TPaintBox.
Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1093
-140
File diff suppressed because it is too large
Load Diff
@@ -988,6 +988,20 @@ type
|
||||
function BeaconDecodeInvert: Boolean;
|
||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||
// --- Лупа маяка (окно наведения в BeaconScopeForm) -------------------
|
||||
// Отдельный WDSP-анализатор на IQ главного тракта, вырезающий узкое окно
|
||||
// вокруг маяка: на общей спектрограмме 576к маяк — пара пикселей, точно
|
||||
// ткнуть в него нельзя. Все частоты — АБСОЛЮТНЫЕ display-Гц, как у
|
||||
// BeaconSeedAtHz. Зовётся с таймера окна маяка; окно закрыто → Enabled=False
|
||||
// и анализатор уничтожается.
|
||||
procedure SetBeaconZoomWindow(AEnabled: Boolean; ACenterHz, ASpanHz: Double);
|
||||
function GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
function GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
// Полоса захвата (display-Гц): за её пределы лупу не увести.
|
||||
procedure GetCaptureWindow(out ALowHz, AHighHz: Double);
|
||||
function BeaconZoomBinHz: Double; // разрешение лупы, Гц/бин (для подписи)
|
||||
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
|
||||
|
||||
@@ -3486,6 +3500,64 @@ begin
|
||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Лупа маяка: окно наведения высокого разрешения (BeaconScopeForm)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Движок работает в Гц относительно центра DDC, наружу отдаём абсолютные
|
||||
// display-Гц — в них живут BeaconSeedAtHz, маркеры и клики пользователя.
|
||||
|
||||
procedure TRadioController.GetCaptureWindow(out ALowHz, AHighHz: Double);
|
||||
var Half: Double;
|
||||
begin
|
||||
Half := FSpanHz / 2.0;
|
||||
if Half <= 0 then Half := FSampleRate / 2.0;
|
||||
ALowHz := FCenterFreq - Half;
|
||||
AHighHz := FCenterFreq + Half;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconZoomWindow(AEnabled: Boolean;
|
||||
ACenterHz, ASpanHz: Double);
|
||||
begin
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
if AEnabled and (ACenterHz > 0) then
|
||||
FDSPEngine.SetBeaconZoom(True, ACenterHz - FCenterFreq, ASpanHz)
|
||||
else
|
||||
FDSPEngine.SetBeaconZoom(False, 0, 0);
|
||||
end;
|
||||
|
||||
function TRadioController.GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
begin
|
||||
Count := 0;
|
||||
LowHz := 0;
|
||||
HighHz := 0;
|
||||
Result := False;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
Result := FDSPEngine.GetBeaconZoomSpectrum(Pixels, Count, LowHz, HighHz);
|
||||
if Count > 0 then
|
||||
begin
|
||||
LowHz := LowHz + FCenterFreq;
|
||||
HighHz := HighHz + FCenterFreq;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
begin
|
||||
Count := 0;
|
||||
Result := False;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
Result := FDSPEngine.GetBeaconZoomWaterfall(Pixels, Count);
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconZoomBinHz: Double;
|
||||
begin
|
||||
Result := 0;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
if FDSPEngine.BeaconZoomFFT > 0 then
|
||||
Result := FDSPEngine.SampleRate / FDSPEngine.BeaconZoomFFT;
|
||||
end;
|
||||
|
||||
function TRadioController.GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||
begin
|
||||
Result := Assigned(FDMRDec);
|
||||
|
||||
+356
-2
@@ -142,17 +142,29 @@ const
|
||||
|
||||
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||
// Отдельный analyzer лупы QO-100-маяка (окно BeaconScopeForm). Кормится тем
|
||||
// же IQ главного тракта, но со своим FFT и своим span-clip — узкое окно
|
||||
// вокруг маяка в высоком разрешении. Живёт только пока окно маяка открыто.
|
||||
BCN_DISP_ID = 4;
|
||||
|
||||
// --- Панадаптеры на аппаратных 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.
|
||||
// Analyzer id пана N = PAN_DISP_BASE + N (9..11). ID 4 занят лупой маяка
|
||||
// (BCN_DISP_ID), 5 держим свободным про запас.
|
||||
PAN_DISP_BASE = 8;
|
||||
|
||||
DSP_BUFSIZE = 1024;
|
||||
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
|
||||
// Точек в кадре лупы маяка. Фиксировано (а не по ширине виджета), чтобы
|
||||
// изменение размера окна маяка не переармировало analyzer: переарм сбрасывает
|
||||
// FFT-историю, а на крупных FFT она наполняется почти секунду. Рендер сам
|
||||
// ужимает кадр под свою ширину.
|
||||
BCN_ZOOM_PIXELS = 1024;
|
||||
// Частота кадров лупы. Своя, ниже главного дисплея — см.
|
||||
// ApplyBeaconAnalyzerSettings.
|
||||
BCN_ZOOM_FPS = 15.0;
|
||||
DISPLAY_BLOCK_SIZE = 64;
|
||||
|
||||
// Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны).
|
||||
@@ -336,6 +348,27 @@ type
|
||||
FViewLowHz: Double;
|
||||
FViewHighHz: Double;
|
||||
|
||||
// --- Лупа маяка (BCN_DISP_ID) ---------------------------------------
|
||||
// Второй analyzer на том же IQ главного тракта: своя FFT (крупнее) и свой
|
||||
// span-clip, вырезающий узкое окно вокруг маяка. Создаётся/убивается по
|
||||
// запросу UI (окно маяка открыто/закрыто), поэтому весь доступ к disp'у
|
||||
// сериализуется FBcnLock: SetAnalyzer/Destroy зовёт UI-поток, Spectrum0 —
|
||||
// DSP-поток, GetPixels — display-поток.
|
||||
FBcnLock: TCriticalSection;
|
||||
FBcnOpen: Boolean; // XCreateAnalyzer(BCN_DISP_ID) жив
|
||||
FBcnDispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар)
|
||||
FBcnDispPos: Integer;
|
||||
FBcnReqCenter: Double; // запрошенный центр окна, Гц отн. DDC-центра
|
||||
FBcnReqSpan: Double; // запрошенная ширина окна, Гц
|
||||
FBcnLowHz: Double; // фактическое окно (снаппится по бинам FFT)
|
||||
FBcnHighHz: Double;
|
||||
FBcnFFT: Integer; // размер FFT лупы (свой, не FFFTSize)
|
||||
FBcnPixCount: Integer; // точек в кадре лупы
|
||||
FBcnSpecPix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
FBcnWfPix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
FBcnSpecFresh: Boolean; // в кэше есть непрочитанный кадр спектра
|
||||
FBcnWfFresh: Boolean;
|
||||
|
||||
FOnAudio: TOnAudioReady;
|
||||
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
|
||||
FOnSpectrum: TOnSpectrumReady;
|
||||
@@ -572,6 +605,12 @@ type
|
||||
procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc
|
||||
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
||||
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
|
||||
// --- Лупа маяка ---
|
||||
procedure ApplyBeaconAnalyzerSettings; // span-clip окна маяка; под FBcnLock
|
||||
function BeaconFFTFor(SpanHz: Double): Integer; // FFT под запрошенную ширину
|
||||
procedure CloseBeaconAnalyzer; // Destroy + сброс (сам берёт FBcnLock)
|
||||
procedure RearmBeaconAnalyzer; // полный переарм (смена rate)
|
||||
procedure ReapplyBeaconDisplayModes; // детектор/усреднение без переарма
|
||||
|
||||
public
|
||||
constructor Create(SampleRate: Integer = 192000;
|
||||
@@ -771,6 +810,23 @@ type
|
||||
property ViewLowHz: Double read FViewLowHz;
|
||||
property ViewHighHz: Double read FViewHighHz;
|
||||
|
||||
// --- Лупа маяка (BCN_DISP_ID) ---------------------------------------
|
||||
// Включает/выключает второй analyzer на IQ главного тракта и задаёт его
|
||||
// окно: ACenterHz/ASpanHz — В ГЕРЦАХ ОТНОСИТЕЛЬНО ЦЕНТРА DDC (как
|
||||
// ViewLowHz/ViewHighHz). Окно клампится в полосу захвата. Вызывать из
|
||||
// UI-потока. Enabled=False — analyzer уничтожается (нулевая цена, когда
|
||||
// окно маяка закрыто).
|
||||
procedure SetBeaconZoom(AEnabled: Boolean; ACenterHz, ASpanHz: Double);
|
||||
// Забрать последний кадр лупы. Fresh=False, если нового кадра не было —
|
||||
// вызывающий рисует предыдущий. Low/High — фактическое окно (Гц отн.
|
||||
// центра DDC), уже снаппенное по бинам FFT.
|
||||
function GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
function GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
property BeaconZoomOpen: Boolean read FBcnOpen;
|
||||
property BeaconZoomFFT: Integer read FBcnFFT;
|
||||
|
||||
property Initialized: Boolean read FInitialized;
|
||||
property SampleRate: Integer read FSampleRate;
|
||||
property TXSampleRate: Integer read FTXSampleRate;
|
||||
@@ -1427,6 +1483,16 @@ begin
|
||||
FMainGated := False;
|
||||
FMainLock := TCriticalSection.Create;
|
||||
|
||||
// Лупа маяка — analyzer создаётся по требованию UI (SetBeaconZoom)
|
||||
FBcnLock := TCriticalSection.Create;
|
||||
FBcnOpen := False;
|
||||
FBcnDispPos := 0;
|
||||
FBcnReqCenter := 0.0;
|
||||
FBcnReqSpan := 40000.0;
|
||||
FBcnFFT := 0;
|
||||
FBcnPixCount := 0;
|
||||
SetLength(FBcnDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
|
||||
// Мультислайсы
|
||||
FSliceLock := TCriticalSection.Create;
|
||||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||
@@ -1526,6 +1592,7 @@ begin
|
||||
RTLEventDestroy(FTXMicSem);
|
||||
FSliceLock.Free;
|
||||
FMainLock.Free;
|
||||
FBcnLock.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
@@ -1757,12 +1824,241 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Лупа маяка (BCN_DISP_ID)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TWDSPEngine.BeaconFFTFor(SpanHz: Double): Integer;
|
||||
// FFT лупы выбираем ОТ ШИРИНЫ ОКНА, а не от настройки дисплея: цель — держать
|
||||
// ~BINS_PER_PIXEL бинов на точку кадра, чтобы узкое окно оставалось детальным
|
||||
// (в этом весь смысл лупы). Span-clip НЕ удешевляет FFT — она всё равно по всей
|
||||
// полосе захвата, поэтому потолок жёсткий: 262144 (= max_size в
|
||||
// XCreateAnalyzer), а частота кадров лупы понижена (BCN_ZOOM_FPS).
|
||||
const
|
||||
BINS_PER_PIXEL = 2.0;
|
||||
var
|
||||
Want: Double;
|
||||
begin
|
||||
if SpanHz <= 0 then Exit(65536);
|
||||
Want := BINS_PER_PIXEL * BCN_ZOOM_PIXELS * FSampleRate / SpanHz;
|
||||
Result := 16384;
|
||||
while (Result < Want) and (Result < 262144) do
|
||||
Result := Result shl 1;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyBeaconAnalyzerSettings;
|
||||
// Переармирует analyzer лупы под FBcnReqCenter/FBcnReqSpan. Вызывать ПОД
|
||||
// FBcnLock при FBcnOpen=True.
|
||||
//
|
||||
// Окно вырезается тем же span-clip'ом, что и зум главного спектра, но здесь
|
||||
// клипы считаются НЕ из зум-слайдера, а прямо из желаемых границ в Гц —
|
||||
// анализатор умеет ассиметричный клип, поэтому окно можно поставить в любое
|
||||
// место полосы захвата, а не только вокруг центра DDC.
|
||||
var
|
||||
AvgCount, WfAvgCount, MaxW, Overlap: Integer;
|
||||
Backmult, WfBackmult: Double;
|
||||
Bins, ClipL, ClipH: Integer;
|
||||
BinWidth, BW, WantLo, WantHi: Double;
|
||||
begin
|
||||
FBcnFFT := BeaconFFTFor(FBcnReqSpan);
|
||||
FBcnDispPos := 0;
|
||||
BinWidth := FSampleRate / FBcnFFT;
|
||||
Bins := FBcnFFT;
|
||||
BW := Bins * BinWidth;
|
||||
|
||||
// Желаемые границы, зажатые в полосу захвата с запасом в один бин.
|
||||
WantLo := FBcnReqCenter - FBcnReqSpan / 2.0;
|
||||
WantHi := FBcnReqCenter + FBcnReqSpan / 2.0;
|
||||
if WantLo < -0.5 * BW + BinWidth then WantLo := -0.5 * BW + BinWidth;
|
||||
if WantHi > 0.5 * BW - BinWidth then WantHi := 0.5 * BW - BinWidth;
|
||||
if WantHi - WantLo < 4 * BinWidth then WantHi := WantLo + 4 * BinWidth;
|
||||
|
||||
ClipL := Floor((WantLo + 0.5 * BW + BinWidth / 2.0) / BinWidth);
|
||||
ClipH := Floor((0.5 * BW - BinWidth / 2.0 - WantHi) / BinWidth);
|
||||
ClipL := EnsureRange(ClipL, 0, Bins - 4);
|
||||
ClipH := EnsureRange(ClipH, 0, Bins - 4 - ClipL);
|
||||
// Фактическое окно — то же выражение, что в ApplyRXAnalyzerSettings.
|
||||
FBcnLowHz := -(0.5 * BW - ClipL * BinWidth + BinWidth / 2.0);
|
||||
FBcnHighHz := (0.5 * BW - ClipH * BinWidth - BinWidth / 2.0);
|
||||
|
||||
// Кадры лупы считаем реже главного дисплея: FFT здесь крупная (до 262144) и
|
||||
// идёт по ВСЕЙ полосе захвата, а для наведения на маяк 15 кадров/с с запасом.
|
||||
// На 60 fps это была бы полноценная загрузка ядра ради картинки, в которой
|
||||
// ничего не меняется.
|
||||
Overlap := Max(0, Ceil(FBcnFFT - FSampleRate / BCN_ZOOM_FPS));
|
||||
MaxW := FBcnFFT + Round(Min(0.10 * FSampleRate, 0.10 * FBcnFFT * BCN_ZOOM_FPS));
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||
FBcnPixCount := BCN_ZOOM_PIXELS;
|
||||
|
||||
SetAnalyzer(BCN_DISP_ID, 2, 1, 1, @FFlp[0], FBcnFFT, DISPLAY_BLOCK_SIZE,
|
||||
FWindowType, 14.0, Overlap, 0, ClipL, ClipH, BCN_ZOOM_PIXELS,
|
||||
1, 0, 0.0, 0.0, MaxW);
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 0, FSpecDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 0, FSpecAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 0, AvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 0, Backmult);
|
||||
SetDisplayNormOneHz (BCN_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 1, WfAvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 1, WfBackmult);
|
||||
SetDisplayNormOneHz (BCN_DISP_ID, 1, 0);
|
||||
SetDisplaySampleRate (BCN_DISP_ID, FSampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.CloseBeaconAnalyzer;
|
||||
begin
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnOpen := False; // DSP/display-потоки перестают трогать disp
|
||||
DestroyAnalyzer(BCN_DISP_ID);
|
||||
FBcnSpecFresh := False;
|
||||
FBcnWfFresh := False;
|
||||
FBcnPixCount := 0;
|
||||
FBcnDispPos := 0;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.RearmBeaconAnalyzer;
|
||||
// Полный переарм лупы — после смены sample rate: и окно, и выбор FFT считаются
|
||||
// от FSampleRate.
|
||||
begin
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if FBcnOpen then ApplyBeaconAnalyzerSettings;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ReapplyBeaconDisplayModes;
|
||||
// Детектор/усреднение с главного дисплея на лупу БЕЗ SetAnalyzer: переарм
|
||||
// сбросил бы FFT-историю (на 256к бинах это ~0.5с замершего кадра), а сменой
|
||||
// детектора/усреднения окно лупы не меняется.
|
||||
var
|
||||
AvgCount, WfAvgCount: Integer;
|
||||
Backmult, WfBackmult: Double;
|
||||
begin
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 0, FSpecDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 0, FSpecAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 0, AvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 0, Backmult);
|
||||
SetDisplayNormOneHz (BCN_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 1, WfAvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 1, WfBackmult);
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetBeaconZoom(AEnabled: Boolean; ACenterHz, ASpanHz: Double);
|
||||
var
|
||||
Success: Integer;
|
||||
SameWindow: Boolean;
|
||||
begin
|
||||
if (not AEnabled) or (not FInitialized) or (not FAnalyzerOpen) then
|
||||
begin
|
||||
CloseBeaconAnalyzer;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
ASpanHz := EnsureRange(ASpanHz, 200.0, FSampleRate);
|
||||
ACenterHz := EnsureRange(ACenterHz, -FSampleRate / 2.0, FSampleRate / 2.0);
|
||||
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
// Переарм сбрасывает историю FFT (кадр «замирает» на время наполнения) —
|
||||
// на неизменившемся окне не дёргаем, лупа зовётся из таймера UI.
|
||||
SameWindow := FBcnOpen and
|
||||
(Abs(ACenterHz - FBcnReqCenter) < 0.5) and
|
||||
(Abs(ASpanHz - FBcnReqSpan) < 0.5);
|
||||
FBcnReqCenter := ACenterHz;
|
||||
FBcnReqSpan := ASpanHz;
|
||||
if SameWindow then Exit;
|
||||
|
||||
if not FBcnOpen then
|
||||
begin
|
||||
Success := -1;
|
||||
XCreateAnalyzer(BCN_DISP_ID, @Success, 262144, 1, 1, nil);
|
||||
if Success <> 0 then
|
||||
begin
|
||||
FLastError := Format('XCreateAnalyzer(BCN) failed: %d', [Success]);
|
||||
Exit; // лупа не поднялась — окно маяка просто останется пустым
|
||||
end;
|
||||
FBcnOpen := True;
|
||||
end;
|
||||
ApplyBeaconAnalyzerSettings;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
var N: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
Count := 0;
|
||||
LowHz := 0;
|
||||
HighHz := 0;
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
LowHz := FBcnLowHz;
|
||||
HighHz := FBcnHighHz;
|
||||
N := Min(FBcnPixCount, Length(Pixels));
|
||||
if N <= 0 then Exit;
|
||||
Move(FBcnSpecPix[0], Pixels[0], N * SizeOf(Single));
|
||||
Count := N;
|
||||
Result := FBcnSpecFresh;
|
||||
FBcnSpecFresh := False;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
var N: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
Count := 0;
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
N := Min(FBcnPixCount, Length(Pixels));
|
||||
if N <= 0 then Exit;
|
||||
Move(FBcnWfPix[0], Pixels[0], N * SizeOf(Single));
|
||||
Count := N;
|
||||
Result := FBcnWfFresh;
|
||||
FBcnWfFresh := False;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам.
|
||||
begin
|
||||
ApplyRXAnalyzerSettings;
|
||||
ApplyTXAnalyzerSettings;
|
||||
ReapplyPanDisplaySettings;
|
||||
ReapplyBeaconDisplayModes;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
|
||||
@@ -1796,6 +2092,9 @@ begin
|
||||
end;
|
||||
DestroyAnalyzer(RX_DISP_ID);
|
||||
DestroyAnalyzer(TX_DISP_ID);
|
||||
// Лупа маяка: не пересоздаётся в OpenAnalyzer — окно маяка запросит её сама
|
||||
// (SetBeaconZoom зовётся с таймера формы).
|
||||
CloseBeaconAnalyzer;
|
||||
// Анализаторы доп. панов (записи FPans сохраняются — пересоздание в OpenAnalyzer).
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
@@ -2085,6 +2384,8 @@ begin
|
||||
// главного дисплея сбрасывается (при смене rate это неизбежно), паны
|
||||
// не затронуты.
|
||||
if FAnalyzerOpen then ApplyRXAnalyzerSettings;
|
||||
// Лупа маяка тоже считает окно и FFT от rate — переармируем.
|
||||
RearmBeaconAnalyzer;
|
||||
|
||||
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
|
||||
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
|
||||
@@ -2310,6 +2611,26 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// Лупа маяка — второй analyzer на том же потоке сэмплов. Живёт только пока
|
||||
// окно маяка открыто, поэтому в обычном режиме это одна проверка Boolean.
|
||||
if FBcnOpen then
|
||||
begin
|
||||
FBcnDispBuf[FBcnDispPos * 2] := DI;
|
||||
FBcnDispBuf[FBcnDispPos * 2 + 1] := DQ;
|
||||
Inc(FBcnDispPos);
|
||||
if FBcnDispPos >= DISPLAY_BLOCK_SIZE then
|
||||
begin
|
||||
FBcnDispPos := 0;
|
||||
FBcnLock.Enter; // UI-поток может в этот момент рвать analyzer
|
||||
try
|
||||
if FBcnOpen then
|
||||
Spectrum0(1, BCN_DISP_ID, 0, 0, @FBcnDispBuf[0]);
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Bec <> nil then Bec.Feed(DI, DQ);
|
||||
|
||||
FRXAccI[FRXAccPos] := DI;
|
||||
@@ -4058,6 +4379,7 @@ var
|
||||
i, N, p: Integer;
|
||||
DispID: Integer;
|
||||
Cal: Single;
|
||||
MainCal: Single; // калибровка главного тракта (Cal ниже перетирается панами)
|
||||
GotSpec, GotWf: Boolean;
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
@@ -4068,6 +4390,7 @@ begin
|
||||
// На TX-дисплее коррекция входного тракта не при чём.
|
||||
if Assigned(FOnDispCal) and (DispID = RX_DISP_ID) then Cal := FOnDispCal(0)
|
||||
else Cal := 0;
|
||||
MainCal := Cal;
|
||||
|
||||
Flag := 0;
|
||||
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||||
@@ -4128,6 +4451,37 @@ begin
|
||||
if GotWf and Assigned(FOnPanWaterfall) then
|
||||
FOnPanWaterfall(p, WfBuf, N);
|
||||
end;
|
||||
|
||||
// Лупа маяка: кадр кладём в кэш, а не в колбэк — окно маяка тянет его своим
|
||||
// таймером (форма живёт отдельно от главного рендера и может быть закрыта).
|
||||
if FBcnOpen then
|
||||
begin
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if FBcnOpen then
|
||||
begin
|
||||
N := EnsureRange(FBcnPixCount, 0, SPECTRUM_PIXELS);
|
||||
Flag := 0;
|
||||
GetPixels(BCN_DISP_ID, 0, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
FBcnSpecPix[i] := PixBuf[i] + MainCal;
|
||||
FBcnSpecFresh := True;
|
||||
end;
|
||||
Flag := 0;
|
||||
GetPixels(BCN_DISP_ID, 1, @WfBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
FBcnWfPix[i] := WfBuf[i] + MainCal;
|
||||
FBcnWfFresh := True;
|
||||
end;
|
||||
end;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
||||
|
||||
Reference in New Issue
Block a user