mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +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:
+68
-20
@@ -93,10 +93,13 @@ const
|
||||
MIC_MAX_PLC = 5;
|
||||
MAX_WS_CLIENTS = 4;
|
||||
WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
// Потолок точек в кадре спектра/водопада (настройка web.spec_pixels режет
|
||||
// ниже). Совпадает с WDSPEngine.SPECTRUM_PIXELS — больше анализатор не даёт.
|
||||
WEB_SPEC_MAX = 4096;
|
||||
|
||||
// Типы бинарных фреймов (первый байт = тип)
|
||||
WS_MSG_SPECTRUM = Byte(Ord('S')); // S + 1024×Float32
|
||||
WS_MSG_WATERFALL = Byte(Ord('W')); // W + N×Float32
|
||||
WS_MSG_SPECTRUM = Byte(Ord('S')); // S + N×Float32 (N = длина фрейма, <= WEB_SPEC_MAX)
|
||||
WS_MSG_WATERFALL = Byte(Ord('W')); // W + N×Float32 (N = длина фрейма, <= WEB_SPEC_MAX)
|
||||
WS_MSG_AUDIO = Byte(Ord('A')); // A + Opus bytes
|
||||
WS_MSG_AUDIO_PCM = Byte(Ord('P')); // P + N×Float32 (mono 48k)
|
||||
WS_MSG_STATE = Byte(Ord('J')); // J + JSON text
|
||||
@@ -240,9 +243,11 @@ type
|
||||
FBindIP: string;
|
||||
|
||||
// ── Состояние (обновляется из MainForm) ──
|
||||
FSpectrumBuf: array[0..1023] of Single;
|
||||
FWfBuf: array[0..1023] of Single;
|
||||
FWfCount: Integer;
|
||||
FSpectrumBuf: array[0..WEB_SPEC_MAX-1] of Single;
|
||||
FSpecCount: Integer; // точек в кадре спектра (0 = кадра ещё не было)
|
||||
FWfBuf: array[0..WEB_SPEC_MAX-1] of Single;
|
||||
FWfCount: Integer; // точек в строке водопада
|
||||
FWfFresh: Boolean; // строка накапливается (max-hold) и ещё не отдана
|
||||
FSMeter: Double;
|
||||
FFreq: Double;
|
||||
FMode: Integer;
|
||||
@@ -395,7 +400,7 @@ type
|
||||
// Вызывается из таймера спектра (UI thread)
|
||||
procedure PushSpectrum(
|
||||
const Buf: array of Single; Count: Integer;
|
||||
const WfBuf_: array of Single;
|
||||
const WfBuf_: array of Single; WfCount: Integer; WfNew: Boolean;
|
||||
SMeter: Double;
|
||||
Freq: Double; Mode, FilterBW, AGCMode, AGCTop: Integer;
|
||||
SpanHz: Double; Volume: Integer;
|
||||
@@ -1625,9 +1630,9 @@ end;
|
||||
procedure TWebServer.PushLoop;
|
||||
var
|
||||
Tick, StateLastTick: QWord;
|
||||
SpecBuf: array[0..4096] of Byte;
|
||||
WfBuf: array[0..4096] of Byte;
|
||||
i: Integer;
|
||||
SpecBuf: array[0..1 + WEB_SPEC_MAX*4] of Byte;
|
||||
WfBuf: array[0..1 + WEB_SPEC_MAX*4] of Byte;
|
||||
i, SN, WN: Integer;
|
||||
HasClients: Boolean;
|
||||
begin
|
||||
StateLastTick := GetTickCount64;
|
||||
@@ -1640,23 +1645,49 @@ begin
|
||||
HasClients := FClientCount > 0;
|
||||
FClientLock.Leave;
|
||||
|
||||
if not HasClients then Continue;
|
||||
if not HasClients then
|
||||
begin
|
||||
// Без клиентов строку никто не забирает — иначе накопитель max-hold рос
|
||||
// бы всё время простоя и первая строка после подключения была бы
|
||||
// максимумом за минуты.
|
||||
FStateLock.Enter;
|
||||
FWfFresh := False;
|
||||
FStateLock.Leave;
|
||||
Continue;
|
||||
end;
|
||||
|
||||
FStateLock.Enter;
|
||||
try
|
||||
// Длина фрейма = число реально заполненных точек: клиент считает N из
|
||||
// byteLength и растягивает ровно их на всю ширину канваса.
|
||||
SN := FSpecCount;
|
||||
SpecBuf[0] := WS_MSG_SPECTRUM;
|
||||
for i := 0 to 1023 do
|
||||
for i := 0 to SN - 1 do
|
||||
PSingle(Pointer(PByte(@SpecBuf[1]) + i*4))^ := FSpectrumBuf[i];
|
||||
|
||||
WfBuf[0] := WS_MSG_WATERFALL;
|
||||
for i := 0 to 1023 do
|
||||
PSingle(Pointer(PByte(@WfBuf[1]) + i*4))^ := FWfBuf[i];
|
||||
// Водопад — ТОЛЬКО если с прошлой отправки пришла новая строка. Иначе в
|
||||
// демоне (PushState 20 Гц и этот цикл 20 Гц идут вразнобой) на дрейфе фаз
|
||||
// одна и та же строка уезжала клиенту дважды — водопад «двоил» и полз
|
||||
// рывками, а на остановленном RX прокручивал застывшую строку вечно.
|
||||
// Спектр так гейтить нельзя: это живая кривая, её надо перерисовывать.
|
||||
if FWfFresh then
|
||||
begin
|
||||
WN := FWfCount;
|
||||
WfBuf[0] := WS_MSG_WATERFALL;
|
||||
for i := 0 to WN - 1 do
|
||||
PSingle(Pointer(PByte(@WfBuf[1]) + i*4))^ := FWfBuf[i];
|
||||
FWfFresh := False; // следующий кадр DSP начнёт новое накопление
|
||||
end
|
||||
else
|
||||
WN := 0;
|
||||
finally
|
||||
FStateLock.Leave;
|
||||
end;
|
||||
|
||||
BroadcastBinary(SpecBuf[0], 1 + 1024*4);
|
||||
BroadcastBinary(WfBuf[0], 1 + 1024*4);
|
||||
// Кадр спектра шлём и пустым (SN=0, до первого кадра DSP): клиент
|
||||
// перерисовывает по нему линейку и оверлеи, а пустой спектр не рисует.
|
||||
BroadcastBinary(SpecBuf[0], 1 + SN*4);
|
||||
if WN > 0 then BroadcastBinary(WfBuf[0], 1 + WN*4);
|
||||
|
||||
if Tick - StateLastTick >= 200 then
|
||||
begin
|
||||
@@ -1878,7 +1909,7 @@ end;
|
||||
|
||||
procedure TWebServer.PushSpectrum(
|
||||
const Buf: array of Single; Count: Integer;
|
||||
const WfBuf_: array of Single;
|
||||
const WfBuf_: array of Single; WfCount: Integer; WfNew: Boolean;
|
||||
SMeter: Double;
|
||||
Freq: Double; Mode, FilterBW, AGCMode, AGCTop: Integer;
|
||||
SpanHz: Double; Volume: Integer;
|
||||
@@ -1893,14 +1924,31 @@ procedure TWebServer.PushSpectrum(
|
||||
FwdW, SWRV, PAMaxPower: Double;
|
||||
const StatusText, BoardText, IPText, SupplyText, PLLText,
|
||||
RXText, TXText, SeqText: string);
|
||||
var N, i: Integer;
|
||||
var N, W, i: Integer;
|
||||
begin
|
||||
if not FRunning then Exit;
|
||||
N := Min(Count, 1024);
|
||||
N := EnsureRange(Min(Count, Length(Buf)), 0, WEB_SPEC_MAX);
|
||||
W := EnsureRange(Min(WfCount, Length(WfBuf_)), 0, WEB_SPEC_MAX);
|
||||
FStateLock.Enter;
|
||||
try
|
||||
for i := 0 to N-1 do FSpectrumBuf[i] := Buf[i];
|
||||
for i := 0 to Min(High(WfBuf_), 1023) do FWfBuf[i] := WfBuf_[i];
|
||||
FSpecCount := N;
|
||||
// Водопад: зовут нас чаще, чем PushLoop отдаёт строку (в GUI 60 против 20
|
||||
// раз/с) — между отправками копим max-hold'ом, иначе кадры терялись бы.
|
||||
// WfNew=False — новой строки от DSP не было: не трогаем ни накопитель, ни
|
||||
// флаг, иначе PushLoop отправил бы прежнюю строку повторно.
|
||||
if WfNew then
|
||||
begin
|
||||
if (not FWfFresh) or (W <> FWfCount) then
|
||||
begin
|
||||
for i := 0 to W-1 do FWfBuf[i] := WfBuf_[i];
|
||||
FWfCount := W;
|
||||
end
|
||||
else
|
||||
for i := 0 to W-1 do
|
||||
if WfBuf_[i] > FWfBuf[i] then FWfBuf[i] := WfBuf_[i];
|
||||
FWfFresh := True;
|
||||
end;
|
||||
FSMeter := SMeter;
|
||||
FFreq := Freq;
|
||||
FMode := Mode;
|
||||
|
||||
Reference in New Issue
Block a user