feature(calibration): офсет спектра/водопада на диапазон и на трансвертер

Вторая калибровочная таблица рядом с S-метром (паритет с Display Cal Offset
в Thetis): у display-тракта своё окно/детектор, и его уровень расходится с
показаниями стрелки на пару дБ, поэтому таблицы раздельные.

- Settings: DispBandDB[12] / DispXvtrDB[8] (JSON cal_disp_band_i /
  cal_disp_xvtr_i, дефолт 0 — картинка не меняется до первой правки).
- WDSPEngine: колбэк OnDispCal(PanId) — движок раз в кадр спрашивает офсет в
  UpdateSpectrum и прибавляет к пикселям (главный спектр/водопад в копирующем
  цикле, паны in-place при ненулевом офсете). На TX-дисплее не применяется.
  Так обошлись без хуков на каждое присваивание FCenterFreq.
- RadioController: общий CalLookup(Hz, BandTbl, XvtrTbl) — на нём SMeterCalFor
  и DispCalFor(PanId): главный дисплей по FCenterFreq, пан по FPanFreqHz[p],
  т.е. кросс-банд паны получают свой офсет. Колбэк вешается в CreateEngines,
  поэтому работает и в GUI, и в ewsdrd; web-зеркало получает готовые кадры.
- SettingsForm: группа «Spectrum / waterfall» с той же разбивкой (диапазоны +
  включённые трансвертеры активного железа) и кнопка «= S-meter» — копия
  таблицы стрелки как старт калибровки; общая раскладка сеток вынесена в
  LayoutCalOffsets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 11:16:34 +03:00
co-authored by Claude Opus 4.8
parent 797bfc2e7c
commit 366d4c9821
4 changed files with 244 additions and 61 deletions
+28 -2
View File
@@ -191,6 +191,11 @@ type
// SliceId — логический id (из RadioController), не WDSP-канал.
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
Count: Integer) of object;
// Калибровочный офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный,
// 1..MAX_PANS-1 = доп. панадаптер. Спрашивается раз в кадр из display-потока:
// величина зависит от диапазона/трансвертера, о которых знает только
// TRadioController (аналог SMeterCalFor для стрелки).
TOnDispCalQuery = function(PanId: Integer): Double of object;
// Пиксели спектра/водопада доп. пана — из display-потока (как FOnSpectrum/
// FOnWaterfall главного). Внутри колбэка НЕ звать slice/pan API движка.
TOnPanPixels = procedure(PanId: Integer; const Pixels: array of Single;
@@ -331,6 +336,7 @@ type
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
FOnSpectrum: TOnSpectrumReady;
FOnWaterfall: TOnWaterfallReady;
FOnDispCal: TOnDispCalQuery;
FOnTXIQ: TOnTXIQReady;
// --- Мультислайсы ---
@@ -756,6 +762,8 @@ type
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
property OnSliceDemodAudio: TOnSliceAudio
read FOnSliceDemodAudio write FOnSliceDemodAudio;
// Офсет дисплея (дБ) на кадр — см. TOnDispCalQuery. nil = без коррекции.
property OnDispCal: TOnDispCalQuery read FOnDispCal write FOnDispCal;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
// Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3).
@@ -3810,18 +3818,24 @@ var
Flag: Integer;
i, N, p: Integer;
DispID: Integer;
Cal: Single;
GotSpec, GotWf: Boolean;
begin
if not FInitialized or not FAnalyzerOpen then Exit;
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
// Калибровка дисплея (дБ) — сдвиг всей трассы, спрашиваем раз в кадр.
// На TX-дисплее коррекция входного тракта не при чём.
if Assigned(FOnDispCal) and (DispID = RX_DISP_ID) then Cal := FOnDispCal(0)
else Cal := 0;
Flag := 0;
GetPixels(DispID, 0, @PixBuf[0], @Flag);
if Flag <> 0 then
begin
for i := 0 to N - 1 do
FSpectrumPixels[i] := PixBuf[i];
FSpectrumPixels[i] := PixBuf[i] + Cal;
if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, N);
end;
@@ -3831,7 +3845,7 @@ begin
if Flag <> 0 then
begin
for i := 0 to N - 1 do
FWaterfallPixels[i] := PixBuf[i];
FWaterfallPixels[i] := PixBuf[i] + Cal;
if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, N);
end;
@@ -3858,6 +3872,18 @@ begin
finally
FSliceLock.Leave;
end;
// Пан может стоять на своём диапазоне — офсет спрашиваем по его PanId.
if GotSpec or GotWf then
begin
if Assigned(FOnDispCal) then Cal := FOnDispCal(p) else Cal := 0;
if Cal <> 0 then
begin
if GotSpec then
for i := 0 to N - 1 do PixBuf[i] := PixBuf[i] + Cal;
if GotWf then
for i := 0 to N - 1 do WfBuf[i] := WfBuf[i] + Cal;
end;
end;
if GotSpec and Assigned(FOnPanSpectrum) then
FOnPanSpectrum(p, PixBuf, N);
if GotWf and Assigned(FOnPanWaterfall) then