feat(spectrum): zoom + pan для спектра и водопада (WDSP span-clip)

Зум через нативный span-clip WDSP (fscLin/fscHin в SetAnalyzer): clp=0,
видимое окно сужается клипом, при зуме 0 полоса == sample rate (старое
поведение сохранено). Конвенция как в Thetis: ZoomFactor 0=без зума..1=макс.

- WDSPEngine: SetZoomPan + расчёт span-clip, ViewLowHz/ViewHighHz.
- RadioController: GetViewWindow (единая истина видимого окна для оверлеев и
  мыши), ApplyZoom, ZoomIn/Out по лестнице кратностей, ZoomDefault. Якорь
  зума кнопками — на маркере активного VFO. rfZoom.
- Per-band персист зума/пана (TBandSettings + Save/Load/Restore).
- MainForm: маршрутизация оверлеев/кликов через GetViewWindow (покрывает CPU
  и OpenGL), резерв строки PANZOOM_H в layout + сплиттере, rfZoom-рендер.
- WebAdapter: web-спектр/наведение на view-окно (делит те же WDSP-пиксели).
- PanZoomBar (новый): нижняя полоса = линейка полного диапазона (частоты +
  деления), поверх окно-селектор «стекло» (полупрозрачное, объёмная фаска).
  Середина=пан, края=зум, клик мимо=центрировать, двойной клик=сброс.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 21:18:24 +03:00
co-authored by Claude Opus 4.8
parent 0e550dd9f9
commit 371b2858bd
6 changed files with 814 additions and 22 deletions
+55 -2
View File
@@ -204,6 +204,16 @@ type
FWfAvgMode: Integer;
FWfAvgTimeMS: Double;
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
// пересчитываются в ApplyRXAnalyzerSettings.
FZoomFactor: Double;
FPanSlider: Double;
FViewLowHz: Double;
FViewHighHz: Double;
FOnAudio: TOnAudioReady;
FOnSpectrum: TOnSpectrumReady;
FOnWaterfall: TOnWaterfallReady;
@@ -431,6 +441,14 @@ type
// S-meter
function GetSMeterDBm: Double;
// Zoom/pan: задать коэффициент зума и положение пана и переармировать
// RX-анализатор. View* — границы видимого окна (Гц отн. DDC-центра).
procedure SetZoomPan(AZoom, APan: Double);
property ZoomFactor: Double read FZoomFactor;
property PanSlider: Double read FPanSlider;
property ViewLowHz: Double read FViewLowHz;
property ViewHighHz: Double read FViewHighHz;
property Initialized: Boolean read FInitialized;
property SampleRate: Integer read FSampleRate;
property TXSampleRate: Integer read FTXSampleRate;
@@ -768,6 +786,10 @@ begin
FWfDetector := 0; // Peak
FWfAvgMode := 3; // Log Recursive
FWfAvgTimeMS := 120.0;
FZoomFactor := 0.0; // 0.0 = весь span, без зума
FPanSlider := 0.5; // окно по центру
FViewLowHz := -FSampleRate / 2.0;
FViewHighHz := FSampleRate / 2.0;
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
@@ -906,7 +928,11 @@ procedure TWDSPEngine.ApplyRXAnalyzerSettings;
var
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
Backmult, WfBackmult: Double;
const KEEP_TIME_SEC = 0.10;
Bins, SpanClipL, SpanClipH, ZWidth: Integer;
BinWidth, BW, ZoomSlider: Double;
const
KEEP_TIME_SEC = 0.10;
ZOOM_LIMIT = 100.0; // максимальная кратность зума
begin
AnalyzerFFT := CalcDisplayFFTSize;
FDispPos := 0;
@@ -919,8 +945,25 @@ begin
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
// Zoom/pan: считаем span-clip для SetAnalyzer (fscLin/fscHin).
// clp=0 — оставляем полный span FFT, чтобы при зуме 0.0 видимая полоса была
// ровно == FSampleRate (остальной код это предполагает). Зум делает только
// span-clip: FZoomFactor=0.0 → ZWidth=Bins → SpanClipL=SpanClipH=0.
BinWidth := FSampleRate / AnalyzerFFT;
Bins := AnalyzerFFT;
BW := Bins * BinWidth;
ZoomSlider := Log10(9.0 * EnsureRange(FZoomFactor, 0.0, 1.0) + 1.0);
ZWidth := Round(Bins * (1.0 - (1.0 - 1.0 / ZOOM_LIMIT) * ZoomSlider));
ZWidth := EnsureRange(ZWidth, 1, Bins);
SpanClipL := Floor(EnsureRange(FPanSlider, 0.0, 1.0) * (Bins - ZWidth));
SpanClipH := Bins - ZWidth - SpanClipL;
// Видимое окно в Гц относительно DDC-центра (low отрицательный). Поправка
// BinWidth/2 — у комплексного FFT на одну отрицательную ячейку больше.
FViewLowHz := -(0.5 * BW - SpanClipL * BinWidth + BinWidth / 2.0);
FViewHighHz := (0.5 * BW - SpanClipH * BinWidth - BinWidth / 2.0);
SetAnalyzer(RX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
FWindowType, 14.0, Overlap, 0, 0.0, 0.0, DisplayPixels,
FWindowType, 14.0, Overlap, 0, SpanClipL, SpanClipH, DisplayPixels,
1, 0, 0.0, 0.0, MaxW);
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode (RX_DISP_ID, 0, FSpecAvgMode);
@@ -978,6 +1021,16 @@ begin
ApplyTXAnalyzerSettings;
end;
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
// Задаёт зум/пан и переармирует RX-анализатор (SetAnalyzer внутри WDSP
// защищён собственной критической секцией, дисплей-поток не мешает).
begin
FZoomFactor := EnsureRange(AZoom, 0.0, 1.0);
FPanSlider := EnsureRange(APan, 0.0, 1.0);
if FAnalyzerOpen then
ApplyRXAnalyzerSettings;
end;
procedure TWDSPEngine.CloseAnalyzer;
begin
if not FAnalyzerOpen then Exit;