mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(web): детализация спектра/водопада — честный размер кадра, поток 'W', настройка разрешения
Веб-пульт рисовал заметно грубее десктопа, и часть причин копилась давно. Клиент вообще не разбирал бинарный фрейм 0x57: водопад рисовался из кадра СПЕКТРА, а собственный поток водопада (свой детектор и усреднение wf_avg_time_ms) уходил в никуда, занимая половину трафика. Теперь водопад идёт из своего потока. Адаптер слал константу 1024 вместо реального числа точек. Разрешение анализатора = ширина панадаптера, и при узкой панели (грид из двух колонок, маленькое окно) кадр короче — хвост буфера, нули = 0 dBm, уезжал клиенту белой полосой, а частотная шкала сжималась. В демоне это обходили DAEMON_SPEC_WIDTH. Длина фрейма теперь равна числу заполненных точек; клиент и так считал N из byteLength. Децимация под потолок протокола делила «i*Count div N» — на нецелом отношении соседние группы получались по 1 и 2 точки, а смещение max-детектора зависит от размера группы: по спектру шла гребёнка ±3 дБ, в водопаде — вертикальная рябь. Заменено на целочисленный фактор (группы одинаковой ширины). Водопад терял 2 кадра из 3: анализатор даёт до 60 кадров/с, web шлёт 20 строк/с, а бралась просто «последняя». Введено двухступенчатое накопление max-hold (DSP-поток → PushState → PushLoop) по идиоме TWaterfallView.SetWaterfallData — короткие посылки CW/FT8 больше не проваливаются между строками. Отдельно исправлено в headless: PushState демона (20 Гц) и PushLoop (20 Гц) идут вразнобой, и на дрейфе фаз одна и та же строка уезжала клиенту дважды (водопад двоил и полз рывками), а на остановленном RX прокручивалась застывшая строка. Фрейм 'W' теперь отправляется только при реально пришедшем от DSP кадре (FWfAccumFresh → WfNew → FWfFresh). Спектр так не гейтится — это живая кривая. Рендер в браузере: значение бина для пикселя берётся максимумом по диапазону вместо «ближайшего» (узкие сигналы мерцали и пропадали на узком экране), при бинах реже пикселей — интерполяция; сглаживание спектра вынесено отдельным проходом по всем бинам (в цикле по пикселям часть бинов не обновлялась вовсе); строка водопада рисуется из сырых бинов, IIR ~200 мс остался только для слежения за уровнями AGC — он размазывал водопад по вертикали. Потолок точек кадра вынесен в настройку web.spec_pixels (Settings → Advanced → Web Server → Spectrum: 1024/2048/4096, дефолт 1024). Буферы кадра и WS-фреймы выросли под 4096 (WEB_SPEC_MAX, синхронно с WDSPEngine.SPECTRUM_PIXELS). В демоне эта же настройка задаёт ширину анализатора — рендера там нет, поэтому DAEMON_SPEC_WIDTH убран. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+54
-14
@@ -229,10 +229,17 @@ type
|
||||
// Снимок для исходящего web-зеркала (PushState читает напрямую). Пишется из
|
||||
// DSP-потока в OnSpectrumReady/OnWaterfallReady; читается из GUI-таймера —
|
||||
// та же толерантная гонка, что и раньше (косметика пикселей).
|
||||
FSpectrumBuf: array[0..1023] of Single;
|
||||
FWaterfallBuf: array[0..1023] of Single;
|
||||
FSpectrumBuf: array[0..SPECTRUM_PIXELS-1] of Single;
|
||||
FWaterfallBuf: array[0..SPECTRUM_PIXELS-1] of Single;
|
||||
FSpectrumBufCount: Integer;
|
||||
FWaterfallBufCount: Integer;
|
||||
// Потолок точек кадра для web (настройка web.spec_pixels, 1024/2048/4096).
|
||||
// Кадр анализатора шире — децимируем до него; уже — уходит как есть.
|
||||
FWebSpecPixels: Integer;
|
||||
// Водопад копится max-hold'ом до тех пор, пока PushState не заберёт строку
|
||||
// (анализатор даёт до 60 кадров/с, web шлёт 20 строк/с). Сбрасывает флаг
|
||||
// тот, кто забрал строку, — тогда следующий кадр начинает новую.
|
||||
FWfAccumFresh: Boolean;
|
||||
|
||||
// ---- Движки (владение: см. шапку юнита) ----
|
||||
// FNetwork — абстрактный бэкенд железа (TRadioBackend): конкретный класс
|
||||
@@ -1041,7 +1048,10 @@ begin
|
||||
ResetPanDDCMaps; // паны: все слоты свободны
|
||||
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
||||
FCurrentBand := 5; FCurrentXvtr := -1;
|
||||
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
||||
// Кадра ещё не было — 0 точек. Ненулевой счётчик заставил бы web рисовать
|
||||
// нулевой (= 0 dBm, полная шкала) буфер белой полосой.
|
||||
FSpectrumBufCount := 0; FWaterfallBufCount := 0; FWfAccumFresh := False;
|
||||
FWebSpecPixels := 1024; // дефолт web.spec_pixels; хост переопределит из настроек
|
||||
FPAMaxPower := 100.0;
|
||||
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
|
||||
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
|
||||
@@ -2650,32 +2660,45 @@ begin
|
||||
SetMOX(True);
|
||||
end;
|
||||
|
||||
// Снимок кадра для web-зеркала: протокол фиксирован на 1024 точки, поэтому
|
||||
// при большем разрешении анализатора децимируем max-детектором (не обрезаем).
|
||||
// Снимок кадра для web-зеркала: потолок точек задаёт настройка web.spec_pixels
|
||||
// (Limit), при большем разрешении анализатора децимируем max-детектором
|
||||
// (не обрезаем).
|
||||
// DstCount — сколько точек РЕАЛЬНО заполнено; отправлять больше нельзя, иначе
|
||||
// хвост буфера (нули = 0 dBm) уедет в эфир белой полосой, а частотная шкала
|
||||
// клиента растянется на несуществующие точки.
|
||||
//
|
||||
// Группы — строго одинаковой ширины (целочисленный фактор). Прежнее деление
|
||||
// «i*Count div N» давало на нецелом отношении соседние группы разной ширины
|
||||
// (1 и 2 точки), а смещение max-детектора зависит от числа точек в группе —
|
||||
// по спектру шла гребёнка ±3 дБ, в водопаде видимая как вертикальная рябь.
|
||||
procedure SnapshotPixels(const Pixels: array of Single; Count: Integer;
|
||||
var Dst: array of Single; out DstCount: Integer);
|
||||
var Dst: array of Single; Limit: Integer; out DstCount: Integer);
|
||||
var
|
||||
i, j, S0, S1, N: Integer;
|
||||
i, j, S0, S1, N, K, M: Integer;
|
||||
V: Single;
|
||||
begin
|
||||
N := Length(Dst);
|
||||
N := EnsureRange(Limit, 64, Length(Dst));
|
||||
if Count > Length(Pixels) then Count := Length(Pixels);
|
||||
if Count < 0 then Count := 0;
|
||||
if Count <= N then
|
||||
begin
|
||||
for i := 0 to Count - 1 do Dst[i] := Pixels[i];
|
||||
DstCount := Count;
|
||||
Exit;
|
||||
end;
|
||||
for i := 0 to N - 1 do
|
||||
K := (Count + N - 1) div N; // точек анализатора на точку web
|
||||
M := (Count + K - 1) div K; // столько точек получится (<= N)
|
||||
for i := 0 to M - 1 do
|
||||
begin
|
||||
S0 := Int64(i) * Count div N;
|
||||
S1 := (Int64(i) + 1) * Count div N - 1;
|
||||
S0 := i * K;
|
||||
S1 := S0 + K - 1;
|
||||
if S1 > Count - 1 then S1 := Count - 1; // хвостовая группа короче
|
||||
V := Pixels[S0];
|
||||
for j := S0 + 1 to S1 do
|
||||
if Pixels[j] > V then V := Pixels[j];
|
||||
Dst[i] := V;
|
||||
end;
|
||||
DstCount := N;
|
||||
DstCount := M;
|
||||
end;
|
||||
|
||||
procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
|
||||
@@ -2683,7 +2706,7 @@ procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
|
||||
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
|
||||
// (UI рисует FSpecView). Децимации у спектра нет.
|
||||
begin
|
||||
SnapshotPixels(Pixels, Count, FSpectrumBuf, FSpectrumBufCount);
|
||||
SnapshotPixels(Pixels, Count, FSpectrumBuf, FWebSpecPixels, FSpectrumBufCount);
|
||||
if Assigned(FOnSpectrumData) then FOnSpectrumData(Pixels, Count);
|
||||
end;
|
||||
|
||||
@@ -2691,8 +2714,25 @@ procedure TRadioController.OnWaterfallReady(const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
// DSP-поток: web-зеркало получает КАЖДЫЙ кадр; децимация рендера водопада —
|
||||
// забота фронтенда (OnWaterfallData).
|
||||
//
|
||||
// Web шлёт 20 строк/с, анализатор даёт до 60 кадров/с — просто «последний
|
||||
// кадр» терял бы 2 из 3 (короткая посылка CW/FT8 могла не попасть ни в одну
|
||||
// строку). Копим кадры max-hold'ом до тех пор, пока PushState не заберёт
|
||||
// строку (идиома TWaterfallView.SetWaterfallData при WfFrameInterval>1).
|
||||
var
|
||||
Tmp: array[0..SPECTRUM_PIXELS-1] of Single;
|
||||
N, i: Integer;
|
||||
begin
|
||||
SnapshotPixels(Pixels, Count, FWaterfallBuf, FWaterfallBufCount);
|
||||
SnapshotPixels(Pixels, Count, Tmp, FWebSpecPixels, N);
|
||||
if (not FWfAccumFresh) or (N <> FWaterfallBufCount) then
|
||||
begin
|
||||
for i := 0 to N - 1 do FWaterfallBuf[i] := Tmp[i];
|
||||
FWaterfallBufCount := N;
|
||||
end
|
||||
else
|
||||
for i := 0 to N - 1 do
|
||||
if Tmp[i] > FWaterfallBuf[i] then FWaterfallBuf[i] := Tmp[i];
|
||||
FWfAccumFresh := True;
|
||||
if Assigned(FOnWaterfallData) then FOnWaterfallData(Pixels, Count);
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user