feat(waterfall): палитры Inferno/Turbo + gamma, разрешение до 4096, HiDPI

- SPECTRUM_PIXELS 1024→4096 + настройка display_pixels (лимит точек
  анализатора, SetDisplayResLimit); web-зеркало остаётся 1024 —
  max-децимация в SnapshotPixels
- палитры водопада Classic/Inferno/Turbo + регулятор gamma (в LUT),
  настройки wf_palette/wf_gamma, страница Waterfall → группа Rendering
- GL-водопад переведён на общий LUT/AGC-уровни базового класса
  (убраны дубли ColorForDB/UpdateLevels)
- явные glViewport в GL-вьюхах умножаются на PixelScale (HiDPI),
  текстура водопада — в физической ширине

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 16:11:58 +03:00
co-authored by Claude Fable 5
parent 4733ddab58
commit 092a202f5f
10 changed files with 397 additions and 263 deletions
+24 -142
View File
@@ -38,9 +38,7 @@ type
procedure DrawTextureWrapped(W, H: Integer);
procedure DrawMarker(W, H: Integer);
procedure UploadMarkerText(const S: string);
procedure UpdateLevels;
function FormatFreqGL(Hz: Double): string;
function ColorForDB(ValueDB, LowDB, HighDB: Double): LongWord;
public
constructor Create;
destructor Destroy; override;
@@ -102,125 +100,6 @@ begin
Tex := 0;
end;
function TWaterfallViewOpenGL.ColorForDB(ValueDB, LowDB, HighDB: Double): LongWord;
const
NSTOPS = 8;
SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255);
SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20);
SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20);
var
Overall, Local, T: Double;
Seg, R, G, B: Integer;
begin
if FLightTheme then
begin
if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end;
if ValueDB >= HighDB then
begin
Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or
(SG[NSTOPS-1] shl 8) or SB[NSTOPS-1];
Exit;
end;
T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1);
Seg := Min(NSTOPS - 2, Trunc(T));
T := T - Seg;
R := EnsureRange(Round(SR[Seg] * (1.0 - T) + SR[Seg+1] * T), 0, 255);
G := EnsureRange(Round(SG[Seg] * (1.0 - T) + SG[Seg+1] * T), 0, 255);
B := EnsureRange(Round(SB[Seg] * (1.0 - T) + SB[Seg+1] * T), 0, 255);
Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
Exit;
end;
if ValueDB <= LowDB then begin Result := $FF000000; Exit; end;
if ValueDB >= HighDB then
begin
Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192;
Exit;
end;
Overall := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB);
if Overall < (2.0 / 9.0) then
begin Local := Overall / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end
else if Overall < (3.0 / 9.0) then
begin Local := (Overall - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end
else if Overall < (4.0 / 9.0) then
begin Local := (Overall - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end
else if Overall < (5.0 / 9.0) then
begin Local := (Overall - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end
else if Overall < (7.0 / 9.0) then
begin Local := (Overall - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end
else if Overall < (8.0 / 9.0) then
begin Local := (Overall - 7.0/9.0) / (1.0/9.0); R := 255; G := 0; B := Round(Local*255.0); end
else
begin
Local := (Overall - 8.0/9.0) / (1.0/9.0);
R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0);
G := Round(Local * 255.0 * 0.5);
B := 255;
end;
Result := ($FF shl 24) or (EnsureRange(R, 0, 255) shl 16) or
(EnsureRange(G, 0, 255) shl 8) or EnsureRange(B, 0, 255);
end;
procedure TWaterfallViewOpenGL.UpdateLevels;
var
X, SrcCount, LowTargetCount, HighTargetCount, CumCount, HistIdx: Integer;
Hist: array[0..191] of Integer;
HistMinDB, HistMaxDB, HistStepDB, NoiseFloorDB, SignalTopDB: Double;
TargetLow, TargetHigh: Double;
const
ALPHA_HIGH = 0.10;
ALPHA_LOW = 0.08;
WF_AUTO_OFFSET = -4.0;
WF_MIN_RANGE = 48.0;
WF_MAX_RANGE = 62.0;
begin
SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
FillChar(Hist, SizeOf(Hist), 0);
HistMinDB := -170.0;
HistMaxDB := 22.0;
HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist);
for X := 0 to SrcCount - 1 do
begin
HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist));
Inc(Hist[HistIdx]);
end;
LowTargetCount := Round(SrcCount * 0.30);
HighTargetCount := Round(SrcCount * 0.98);
CumCount := 0;
NoiseFloorDB := FWfLow;
SignalTopDB := FWfHigh;
for HistIdx := 0 to High(Hist) do
begin
Inc(CumCount, Hist[HistIdx]);
if CumCount >= LowTargetCount then
begin
NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
Break;
end;
end;
CumCount := 0;
for HistIdx := 0 to High(Hist) do
begin
Inc(CumCount, Hist[HistIdx]);
if CumCount >= HighTargetCount then
begin
SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB;
Break;
end;
end;
if FWfAGCEnabled then
begin
TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset;
if FWfNFEnabled then TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0)
else TargetHigh := TargetLow + 52.0;
if TargetHigh > TargetLow + WF_MAX_RANGE then TargetHigh := TargetLow + WF_MAX_RANGE;
FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow);
FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh);
end;
end;
procedure TWaterfallViewOpenGL.EnsureTexture(W, H: Integer);
var
Bg: LongWord;
@@ -235,7 +114,7 @@ begin
FLatestRow := 0;
SetLength(FRowPixels, W * 4);
SetLength(Pixels, W * H * 4);
Bg := ColorForDB(-130.0, FWfManualLow, FWfManualHigh);
Bg := PaletteColor(0.0); // «холодный» конец палитры = фон
for I := 0 to W * H - 1 do
begin
Pixels[I * 4] := Byte(Bg shr 16);
@@ -256,24 +135,18 @@ end;
procedure TWaterfallViewOpenGL.UploadCurrentRow(W: Integer);
var
X, SrcCount, S0, S1, Off: Integer;
SrcF, Step, Frac, DBV, WfHigh, WfLow: Double;
X, SrcCount, S0, S1, Off, Idx: Integer;
SrcF, Step, Frac, DBV, WfHigh, WfLow, LutScale: Double;
Pal: LongWord;
begin
if (FTexture = 0) or (W <= 0) then Exit;
UpdateLevels;
WfHigh := FWfHigh;
WfLow := FWfLow;
if not FWfAGCEnabled then
begin
WfHigh := FWfManualHigh;
WfLow := FWfManualLow;
end;
if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0;
if WfHigh > 0.0 then WfHigh := 0.0;
if WfLow < -160 then WfLow := -160;
UpdateAutoLevels;
GetEffectiveLevels(WfLow, WfHigh);
LutScale := High(FWfLut) / (WfHigh - WfLow);
if LutNeedsRebuild(WfLow, WfHigh) then
BuildWfLut(WfLow, WfHigh);
SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024);
SrcCount := EnsureRange(FWaterfallBufCount, 2, WF_MAX_PIXELS);
Step := (SrcCount - 1.0) / Max(1, W - 1);
SrcF := 0.0;
for X := 0 to W - 1 do
@@ -299,7 +172,9 @@ begin
Frac := SrcF - S0;
DBV := FWaterfallBuf[S0] * (1.0 - Frac) + FWaterfallBuf[S1] * Frac;
end;
Pal := ColorForDB(DBV, WfLow, WfHigh);
Idx := Round((DBV - WfLow) * LutScale);
if Idx < 0 then Idx := 0 else if Idx > High(FWfLut) then Idx := High(FWfLut);
Pal := FWfLutARGB[Idx];
Off := X * 4;
FRowPixels[Off] := Byte(Pal shr 16);
FRowPixels[Off + 1] := Byte(Pal shr 8);
@@ -447,8 +322,10 @@ end;
procedure TWaterfallViewOpenGL.SetWaterfallBitmapSize(W, H: Integer);
begin
if (W <= 0) or (H <= 0) then Exit;
// Ширина текстуры — в физических пикселях (HiDPI): строка водопада должна
// ложиться 1:1 на пиксели фреймбуфера, иначе GL размажет её при увеличении.
if Assigned(FGLControl) and FGLControl.MakeCurrent then
EnsureTexture(W, H);
EnsureTexture(Round(W * FGLControl.PixelScale), H);
FWaterfallDirty := True;
end;
@@ -459,7 +336,8 @@ end;
procedure TWaterfallViewOpenGL.PaintWaterfall(Sender: TObject);
var
W, H: Integer;
W, H, TexW: Integer;
Scale: Double;
R, G, B: GLFloat;
begin
if Sender is TOpenGLControl then FGLControl := TOpenGLControl(Sender);
@@ -467,8 +345,12 @@ begin
W := FGLControl.Width;
H := FGLControl.Height;
if (W <= 0) or (H <= 0) then Exit;
// HiDPI: viewport покрывает весь FBO (физические пиксели), координаты
// остаются логическими; строка водопада строится в физической ширине.
Scale := FGLControl.PixelScale;
TexW := Round(W * Scale);
glViewport(0, 0, W, H);
glViewport(0, 0, TexW, Round(H * Scale));
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
glOrtho(0, W, H, 0, -1, 1);
@@ -483,10 +365,10 @@ begin
glClearColor(R, G, B, 1);
glClear(GL_COLOR_BUFFER_BIT);
EnsureTexture(W, H);
EnsureTexture(TexW, H);
if FWaterfallDirty then
begin
UploadCurrentRow(W);
UploadCurrentRow(TexW);
FWaterfallDirty := False;
end;
DrawTextureWrapped(W, H);