mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
TX: фиксированный sample rate, per-device grid, CTUN-aware overlay, alpha-blend filter bands
Декаплинг TX от RX sample rate (как в Thetis):
- WDSPEngine.pas: константа TX_SAMPLE_RATE=192000, поля FTXSampleRate/FTXOutBufSize.
TXA OpenChannel теперь использует FTXSampleRate как out_rate, TX-анализатор
через SetDisplaySampleRate(TX_DISP_ID, FTXSampleRate). ProcessTXBlock работает
с FTXOutBufSize вместо FBufSize. Public TXSampleRate для MainForm.
- MainForm.SendDUCSpecificFromSettings: DUC0Rate теперь берётся из
FDSPEngine.TXSampleRate, а не из RX-настроек — TX всегда 192k независимо от
выбранного RX-rate (48/96/192/384k).
DUC Specific cache + keepalive resend (HPSDRNetwork.pas):
- FCachedDUCSpec/FCachedDUCValid параллельно DDC-кэшу.
- KeepAlive thread в первые 5s после Run=1 переотправляет и DDC, и DUC
Specific — устраняет гонку, когда DUC настройки терялись железом.
- Disconnect сбрасывает оба valid-флага.
Per-device TX grid settings (Thetis-style):
- Settings.pas: TXSpecRefLevel/TXSpecRange/TXSpecGridStep в TTXSettings
(defaults 0.0/80.0/10.0). LoadTX/SaveTX — ключи tx_spec_ref_level/range/grid_step.
- SettingsForm.pas: новая группа \"TX Grid\" во вкладке Transmit
(RefLevel edit, Range edit, GridStep combo). CollectTXFromUI читает значения,
LoadTXSettings заполняет UI.
- MainForm.pas: поля FTXSpecRefLevel/Range/GridStep, ApplySpecViewGridFromState
пушит в FSpecView либо TX, либо RX параметры в зависимости от FTransmitting.
ApplyGridParams (RX-путь) уважает FTransmitting и не перебивает TX-grid.
OnTXSettingsChange при активном TX моментально применяет новый grid.
TX-aware spectrum/waterfall с CTUN-выравниванием (SpectrumView.pas):
- Поля FTXMode/FTXFreq/FTXSpanHz/FTXVfoIndex. TXSpanHz=192000 при создании.
- DrawSpectrum/DrawWaterfall в FTXMode мапят данные TX-анализатора (192k вокруг
FTXFreq) в RX-координаты FCenterFreq/FSpanHz. Формула:
freq = FCenterFreq - FSpanHz/2 + x*FSpanHz/W
idx = (freq-FTXFreq + TXSpanHz/2)/TXSpanHz * (SrcCount-1)
Вне диапазона — спектр уходит в -200 dB; водопад копирует пиксель из
предыдущей строки (FWfPixels[X+W]) — вместо чёрной полосы продолжает
скроллиться старая RX-картинка.
- Эффект: при CTUN ON TX-сигнал рисуется на полосе фильтра (на VFO), а не в
центре. При RX rate ≠ 192k частотная сетка совпадает между RX и TX
автоматически (ruler всегда в RX-координатах).
- MainForm.SyncSpecViewFreq/ApplyMOX обновляют TXFreq/TXVfoIndex/TXMode при
каждом PTT и смене VFO; учитывается split (FSplitTxB → TXVfoIndex=1).
Alpha-blended filter bands (визуальный feedback TX/RX):
- SpectrumView: PixelFormat:=pf32bit, процедура BlendBand через ScanLine[Y]
внутри BeginUpdate/EndUpdate. Альфа-смешение по формуле
(a*src + (255-a)*dst) / 255 для каждого канала BGR.
- В FTXMode полоса фильтра становится полупрозрачно красной. Если split
(FTXVfoIndex<>FActiveVfo): на RX-частоте полоса зелёная, на TX-частоте —
красная (CalcFilterBandX вычисляет границы для произвольной VFO-freq).
- При FTXMode=False — старое поведение (сплошная заливка SpecFilter).
Build: lazbuild --ws=qt6 ewsdr.lpi — clean, 0 errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+188
-28
@@ -80,6 +80,16 @@ type
|
||||
FSpecRange: Double;
|
||||
FSpecGridStep: Double;
|
||||
|
||||
// ── TX overlay (Thetis-style: при TX данные в FSpectrumBuf приходят
|
||||
// от TX-анализатора @ FTXSpanHz Гц вокруг FTXFreq, и должны быть
|
||||
// отрисованы в RX-координатах (FCenterFreq/FSpanHz), чтобы линейка
|
||||
// частот совпадала с RX и сигнал стоял на правильной частоте даже
|
||||
// при включённом CTUN. Если RX rate ≠ TX rate, видна только пересечение.
|
||||
FTXMode: Boolean;
|
||||
FTXFreq: Double; // центр TX-сигнала (Hz)
|
||||
FTXSpanHz: Double; // ширина TX-окна (192000 для Hermes/Saturn)
|
||||
FTXVfoIndex: Integer; // 0=A, 1=B — какой VFO привязан к TX (для split)
|
||||
|
||||
// ── Marker ────────────────────────────────────────────────────────────────
|
||||
FMarkerActive: Boolean;
|
||||
FMarkerX: Integer;
|
||||
@@ -114,6 +124,14 @@ type
|
||||
function ScaleX(X, Total, Width: Integer): Integer;
|
||||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||||
// Альфа-blending вертикальной полосы поверх FSpectrumBitmap (pf32bit, BGRA).
|
||||
// Используется для прозрачной заливки полосы фильтра разными цветами
|
||||
// (RX-зелёный / TX-красный) поверх градиента сетки.
|
||||
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
// Считает X1/X2 для полосы фильтра вокруг указанной частоты VFO, c учётом
|
||||
// текущего режима (LSB/USB/DSB и пр.) и FFilterBW.
|
||||
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
|
||||
public
|
||||
constructor Create;
|
||||
@@ -144,6 +162,12 @@ type
|
||||
property SpecRange: Double read FSpecRange write FSpecRange;
|
||||
property SpecGridStep: Double read FSpecGridStep write FSpecGridStep;
|
||||
|
||||
// ── TX overlay ────────────────────────────────────────────────────────────
|
||||
property TXMode: Boolean read FTXMode write FTXMode;
|
||||
property TXFreq: Double read FTXFreq write FTXFreq;
|
||||
property TXSpanHz: Double read FTXSpanHz write FTXSpanHz;
|
||||
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
|
||||
|
||||
// ── Маркер ────────────────────────────────────────────────────────────────
|
||||
property MarkerActive: Boolean read FMarkerActive write FMarkerActive;
|
||||
property MarkerX: Integer read FMarkerX write FMarkerX;
|
||||
@@ -358,6 +382,8 @@ constructor TSpectrumView.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
FSpectrumBitmap := TBitmap.Create;
|
||||
// pf32bit нужен для попиксельного alpha-blend в BlendBand (TX/RX-полосы фильтра).
|
||||
FSpectrumBitmap.PixelFormat := pf32bit;
|
||||
FWaterfallBitmap := TBitmap.Create;
|
||||
FWfBitmap := TBitmap.Create;
|
||||
FGridBitmap := TBitmap.Create;
|
||||
@@ -383,6 +409,11 @@ begin
|
||||
FSpecRange := 110.0;
|
||||
FSpecGridStep := 10.0;
|
||||
FSpanHz := 192000;
|
||||
// TX overlay defaults — сетка совпадает с RX по умолчанию
|
||||
FTXMode := False;
|
||||
FTXFreq := 0.0;
|
||||
FTXSpanHz := 192000.0;
|
||||
FTXVfoIndex := 0;
|
||||
FSpectrumBufCount := 1024;
|
||||
FWaterfallBufCount := 1024;
|
||||
FWfFrameInterval := 2;
|
||||
@@ -442,6 +473,61 @@ begin
|
||||
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
// Накладывает полупрозрачную вертикальную полосу [X1..X2) высотой H на
|
||||
// FSpectrumBitmap. Канал A — сила оверлея 0..255 (255 = полностью непрозрачно).
|
||||
// Формула: dst = src*(1-a) + color*a, BGRA-порядок (pf32bit Lazarus default).
|
||||
// BeginUpdate(False) — нужен для прямого доступа к raw-пикселям через ScanLine.
|
||||
var
|
||||
Y, X, InvA: Integer;
|
||||
Row: PByte;
|
||||
W: Integer;
|
||||
begin
|
||||
if (FSpectrumBitmap = nil) or (Alpha = 0) then Exit;
|
||||
W := FSpectrumBitmap.Width;
|
||||
if X1 < 0 then X1 := 0;
|
||||
if X2 > W then X2 := W;
|
||||
if X2 <= X1 then Exit;
|
||||
InvA := 255 - Alpha;
|
||||
FSpectrumBitmap.BeginUpdate(False);
|
||||
try
|
||||
for Y := 0 to H - 1 do
|
||||
begin
|
||||
Row := PByte(FSpectrumBitmap.ScanLine[Y]);
|
||||
if Row = nil then Continue;
|
||||
Inc(Row, X1 * 4);
|
||||
for X := X1 to X2 - 1 do
|
||||
begin
|
||||
Row[0] := Byte((Alpha * B + InvA * Row[0]) div 255);
|
||||
Row[1] := Byte((Alpha * G + InvA * Row[1]) div 255);
|
||||
Row[2] := Byte((Alpha * R + InvA * Row[2]) div 255);
|
||||
// Row[3] оставляем — на BitBlt в TPaintBox не влияет.
|
||||
Inc(Row, 4);
|
||||
end;
|
||||
end;
|
||||
finally
|
||||
FSpectrumBitmap.EndUpdate(False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
// Возвращает X-координаты левого/правого края полосы фильтра вокруг VfoFreq
|
||||
// и саму позицию VFO. Учитывает FMode (LSB/USB/DSB) и FFilterBW.
|
||||
var
|
||||
Lo_Hz, Hi_Hz, Half: Double;
|
||||
begin
|
||||
Half := FFilterBW / 2;
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
case FMode of
|
||||
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
||||
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||
end;
|
||||
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||
X2 := Max(0, Min(W - 1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
var
|
||||
Row, Col: Integer; YMin, Alpha: Integer;
|
||||
@@ -630,7 +716,7 @@ var
|
||||
i, Yp, W, H, GX: Integer;
|
||||
DBmin, DBmax, dB: Double;
|
||||
VfoX, X1, X2: Integer;
|
||||
Lo_Hz, Hi_Hz, Half: Double;
|
||||
TXVfoX, TXX1, TXX2: Integer;
|
||||
AGCy, AGCHangY: Integer;
|
||||
SrcF, Frac, dBv: Double;
|
||||
S0, S1: Integer;
|
||||
@@ -679,21 +765,34 @@ begin
|
||||
LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020);
|
||||
|
||||
// ── 2. Полоса фильтра ─────────────────────────────────────────────────────
|
||||
Half := FFilterBW / 2;
|
||||
if FActiveVfo = 0 then
|
||||
VfoX := Round((FVfoA - FCenterFreq + FSpanHz/2) / FSpanHz * W)
|
||||
else
|
||||
VfoX := Round((FVfoB - FCenterFreq + FSpanHz/2) / FSpanHz * W);
|
||||
case FMode of
|
||||
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
||||
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||
// Логика:
|
||||
// Не TX → одна полоса на FActiveVfo, штатным цветом FTheme.SpecFilter (solid).
|
||||
// TX, не split → одна полоса на FActiveVfo, alpha-blend красный.
|
||||
// TX, split → две полосы: RX (FActiveVfo) — alpha-blend зелёный,
|
||||
// TX (FTXVfoIndex) — alpha-blend красный.
|
||||
// Edges + VFO-cursor рисуются позже на VfoX/X1/X2 — это RX-VFO-полоса
|
||||
// (для split-режима TX-полоса рисуется без edges чтобы визуально отличалась).
|
||||
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||
if FTXMode then
|
||||
begin
|
||||
if FTXVfoIndex <> FActiveVfo then
|
||||
begin
|
||||
// Split TX: RX-полоса зелёная, TX-полоса (на другом VFO) красная.
|
||||
if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100);
|
||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100);
|
||||
end else
|
||||
// Не split: единственная полоса — TX на активном VFO, красная.
|
||||
if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100);
|
||||
end else
|
||||
begin
|
||||
// Обычный приём — заливка штатным цветом без alpha.
|
||||
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
|
||||
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
|
||||
C.Pen.Style := psSolid;
|
||||
end;
|
||||
X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||
X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
||||
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
|
||||
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
|
||||
C.Pen.Style := psSolid;
|
||||
|
||||
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
||||
if FWDSPReady then
|
||||
@@ -729,15 +828,40 @@ begin
|
||||
if FSpPtsLen <> W + 2 then
|
||||
begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end;
|
||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
||||
for i := 0 to W - 1 do
|
||||
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
|
||||
begin
|
||||
SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
|
||||
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
||||
S1 := Min(S0 + 1, SrcCount - 1);
|
||||
Frac := SrcF - S0;
|
||||
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
||||
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
||||
end;
|
||||
// TX-режим: данные приходят @ FTXSpanHz вокруг FTXFreq (TX-анализатор —
|
||||
// baseband), а рисуем в координатах RX (FCenterFreq/FSpanHz). Это даёт
|
||||
// правильное положение TX-сигнала при CTUN ON и выравнивает сетку, когда
|
||||
// RX rate ≠ TX rate (192k).
|
||||
for i := 0 to W - 1 do
|
||||
begin
|
||||
// freq, соответствующая пикселю i в RX-координатах
|
||||
SrcF := FCenterFreq - FSpanHz * 0.5 + i * FSpanHz / Max(1, W - 1);
|
||||
// смещение от центра TX-окна
|
||||
Frac := SrcF - FTXFreq;
|
||||
if (Frac < -FTXSpanHz * 0.5) or (Frac > FTXSpanHz * 0.5) then
|
||||
dBv := -200.0 // вне диапазона TX-анализатора
|
||||
else
|
||||
begin
|
||||
SrcF := (Frac + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
|
||||
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
||||
S1 := Min(S0 + 1, SrcCount - 1);
|
||||
Frac := SrcF - S0;
|
||||
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
||||
end;
|
||||
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
||||
end;
|
||||
end else
|
||||
for i := 0 to W - 1 do
|
||||
begin
|
||||
SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
|
||||
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
||||
S1 := Min(S0 + 1, SrcCount - 1);
|
||||
Frac := SrcF - S0;
|
||||
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
||||
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
||||
end;
|
||||
FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H);
|
||||
|
||||
// ── 5. Градиент ───────────────────────────────────────────────────────────
|
||||
@@ -756,6 +880,17 @@ begin
|
||||
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
|
||||
C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1;
|
||||
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
|
||||
// Split TX: рисуем края + cursor для TX-VFO красным, чтобы было видно где идёт передача
|
||||
if FTXMode and (FTXVfoIndex <> FActiveVfo) then
|
||||
begin
|
||||
C.Pen.Color := TColor($002030E0); C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||||
C.MoveTo(TXX1, 0); C.LineTo(TXX1, H);
|
||||
C.MoveTo(TXX2, 0); C.LineTo(TXX2, H);
|
||||
C.Pen.Width := 2;
|
||||
C.MoveTo(TXVfoX, 0); C.LineTo(TXVfoX, H - 12);
|
||||
C.Brush.Color := TColor($002030E0); C.Brush.Style := bsSolid; C.Pen.Width := 1;
|
||||
C.Polygon([Point(TXVfoX-5,0), Point(TXVfoX+5,0), Point(TXVfoX,8)]);
|
||||
end;
|
||||
|
||||
if FMarkerActive then DrawMarkerLine(C, W, H);
|
||||
end;
|
||||
@@ -851,11 +986,36 @@ begin
|
||||
WatSrcF := 0.0; Px := @FWfPixels[0];
|
||||
for X := 0 to W - 1 do
|
||||
begin
|
||||
WatS0 := Trunc(WatSrcF);
|
||||
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
||||
WatS1 := WatS0 + 1;
|
||||
frac := WatSrcF - WatS0;
|
||||
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
||||
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
|
||||
begin
|
||||
// частота для пикселя X в RX-координатах → индекс в TX-буфере
|
||||
dB := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq;
|
||||
if (dB < -FTXSpanHz * 0.5) or (dB > FTXSpanHz * 0.5) then
|
||||
begin
|
||||
// Вне TX-окна: копируем пиксель из предыдущей строки (она лежит на
|
||||
// расстоянии W в FWfPixels — Move уже сдвинул её ниже). Так старая
|
||||
// RX-картинка продолжается без чёрной "дыры", пока TX рисует своё
|
||||
// окно в центре.
|
||||
if H > 1 then Px^ := FWfPixels[X + W]
|
||||
else Px^ := 0;
|
||||
Inc(Px);
|
||||
WatSrcF := WatSrcF + Step;
|
||||
Continue;
|
||||
end;
|
||||
frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
|
||||
WatS0 := Trunc(frac);
|
||||
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
||||
WatS1 := WatS0 + 1;
|
||||
frac := frac - WatS0;
|
||||
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
||||
end else
|
||||
begin
|
||||
WatS0 := Trunc(WatSrcF);
|
||||
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
||||
WatS1 := WatS0 + 1;
|
||||
frac := WatSrcF - WatS0;
|
||||
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
||||
end;
|
||||
V := Trunc((dB - WfLow) * InvRange);
|
||||
if V < 0 then V := 0; if V > 255 then V := 255;
|
||||
if FLightTheme then
|
||||
|
||||
Reference in New Issue
Block a user