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:
2026-08-14 11:42:25 +03:00
co-authored by Claude Opus 5
parent b0a93ddb14
commit 705da75c26
3 changed files with 1523 additions and 144 deletions
+1095 -142
View File
File diff suppressed because it is too large Load Diff
+72
View File
@@ -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
View File
@@ -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;