mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(filter): произвольные кромки фильтра — таблица по режиму, редактор, drag, TX-полоска
Фильтр главного приёмника перестал быть одним числом. Теперь это таблица (Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки (у LSB своя строка, у USB своя), паритет с Thetis FilterForm. Слайсы независимые кромки умели и раньше — главный приёмник настраивался только пресетами. Модель (Settings/RadioController): - TFilterSlot/TFilterSet/TFilterTable + заводские дефолты из прежних FILT_*_BW и FILT_*_NAMES (переехали в Settings — из них строится таблица). - FFilterLo/FFilterHi = истина, FFilterBW производная (FilterBWFromEdges), поэтому все прежние читатели ширины (web, зум, band-cache, CAT) не тронуты. - ApplyFilterSlot — единственная точка «слот → состояние тракта». - SetFilterEdges: произвольные кромки уходят в VAR1 с автопереключением, пресеты нельзя испортить случайным движением мыши или энкодера. - Персист: секция "filters" под MAC, пишутся только режимы, отличные от заводских; секция режима сносится целиком, иначе возврат слота к заводскому оставлял бы старый ключ. - Диапазон помнит только выбранный слот: кромки живут в таблице, второго источника истины у VAR нет. UI: - 12 кнопок фильтра (2 ряда по 6), подписи из живой таблицы. - FilterPopup — поповер редактора по ПКМ на кнопке фильтра, по образцу PureSignalPopup (дочерний контрол формы, не top-level: Wayland). Слева слоты, справа Name/Low/High/Width, внизу форма полосы на знаковой оси. - Перетаскивание края полосы на спектре: ±5px, снап 10 Гц, Alt = симметрично (не Ctrl — он создаёт слайс). Отказ на передаче и когда полоса на экране уже 12px, иначе захват съедал бы клик-тюн. Отрисовка: - Полоса главного VFO рисуется по РЕАЛЬНЫМ кромкам приёмника, слайса — по его кромкам; вывод из режима и ширины остался фолбэком. Пара→ширина→пара было преобразованием с потерями: несимметричный фильтр из слайс-CAT (ZZFL/ZZFH) рисовался не там, где звучал. - На передаче полоса считается по TX-фильтру (TXSignedEdges), а не по приёмной ширине — TX-полоса от RX не зависит (модель Thetis). - CalcFilterBandXFor больше не держит свою копию таблицы знака боковой: единственная таблица теперь FilterEdgesFromBW. Управление: - CAT ZZFL/ZZFH заработали на основном порту (были заглушками). - Андромеда: энкодеры #4/#5 — независимые Filter High/Low, как на железе G2 (было: #4 ширина, #5 не реализован). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+100
-24
@@ -21,7 +21,8 @@ uses
|
||||
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
|
||||
AppTheme,
|
||||
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
|
||||
WaterfallView, SMeterView, RulerView, RadioModes;
|
||||
WaterfallView, SMeterView, RulerView, RadioModes,
|
||||
Settings; // FilterEdgesFromBW — единственная таблица знака боковой
|
||||
|
||||
const
|
||||
SV_CLR_BG = TColor($00101010);
|
||||
@@ -92,6 +93,12 @@ type
|
||||
FTXSpanHz: Double;
|
||||
FTXVfoIndex: Integer;
|
||||
FTXOverlay: Boolean;
|
||||
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
|
||||
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
|
||||
// Thetis-модели НЕ следует за RX, поэтому при передаче полоса рисуется по
|
||||
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
|
||||
FRXEdgeLo, FRXEdgeHi: Integer;
|
||||
FTXEdgeLo, FTXEdgeHi: Integer;
|
||||
// ── ADC overload overlay ─────────────────────────────────────────────────
|
||||
FADCOverloadVisible: Boolean;
|
||||
FADCAlertImg: TAlertImage; // кэш образа плашки (BuildAlertImage, 1 раз)
|
||||
@@ -179,6 +186,12 @@ type
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcTXBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcBandXEdges(VfoFreq, LoHz, HiHz: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcSliceBandX(O: TVfoOverlay; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
function GetWaterfallDirty: Boolean;
|
||||
procedure SetWaterfallDirty(V: Boolean);
|
||||
// Waterfall sub-view delegating accessors
|
||||
@@ -254,6 +267,10 @@ type
|
||||
property TXSpanHz: Double read FTXSpanHz write SetTXSpanHz;
|
||||
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
|
||||
property TXOverlay: Boolean read FTXOverlay write FTXOverlay;
|
||||
// Кромки фильтров (Гц от несущей, знаковые). Ставит MainForm: RX — из
|
||||
// контроллера, TX — из TXSignedEdges движка.
|
||||
procedure SetRXEdges(LoHz, HiHz: Integer);
|
||||
procedure SetTXEdges(LoHz, HiHz: Integer);
|
||||
|
||||
property ADCOverloadVisible: Boolean read FADCOverloadVisible write FADCOverloadVisible;
|
||||
|
||||
@@ -1172,7 +1189,7 @@ begin
|
||||
// Передающий слайс — красная полоса (как у главного при TX), иначе бейдж-цвет.
|
||||
if O.TxActive then Clr := CLR_TX_BAND
|
||||
else Clr := SliceColor(O.SliceLetter); // $00BBGGRR
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
CalcSliceBandX(O, W, SX1, SX2, SVfoX);
|
||||
if SX2 > SX1 then
|
||||
BlendBand(SX1, SX2, H, Clr and $FF, (Clr shr 8) and $FF, (Clr shr 16) and $FF,
|
||||
IfThen(O.TxActive, 100, 60));
|
||||
@@ -1191,7 +1208,7 @@ begin
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
if O.TxActive then Clr := CLR_TX_BAND // передающий слайс
|
||||
else Clr := SliceColor(O.SliceLetter); // оттенок бейджа слайса
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
CalcSliceBandX(O, W, SX1, SX2, SVfoX);
|
||||
RawVLine(SX1, 0, H - 1, Clr);
|
||||
RawVLine(SX2, 0, H - 1, Clr);
|
||||
RawVLine(SVfoX, 0, H - 13, Clr, 2); // несущая (жирнее)
|
||||
@@ -1255,29 +1272,81 @@ begin
|
||||
W - ALERT_PAD - FADCAlertImg.W, ALERT_PAD);
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure TSpectrumView.SetRXEdges(LoHz, HiHz: Integer);
|
||||
begin
|
||||
CalcFilterBandXFor(VfoFreq, FMode, FFilterBW, W, X1, X2, VfoX);
|
||||
if (LoHz = FRXEdgeLo) and (HiHz = FRXEdgeHi) then Exit;
|
||||
FRXEdgeLo := LoHz;
|
||||
FRXEdgeHi := HiHz;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
// То же, но для произвольного режима/полосы (маркеры слайсов).
|
||||
procedure TSpectrumView.SetTXEdges(LoHz, HiHz: Integer);
|
||||
begin
|
||||
if (LoHz = FTXEdgeLo) and (HiHz = FTXEdgeHi) then Exit;
|
||||
FTXEdgeLo := LoHz;
|
||||
FTXEdgeHi := HiHz;
|
||||
if FTXOverlay then FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
// Главный VFO: кромки приходят из контроллера как есть (фильтр может быть
|
||||
// произвольным — VAR/перетаскивание), выводить их из режима и ширины больше
|
||||
// нельзя. Фолбэк по FMode/FFilterBW остаётся на случай, если кромки ещё не
|
||||
// проставлены (первый кадр до первого rfFilter).
|
||||
begin
|
||||
if FRXEdgeHi > FRXEdgeLo then
|
||||
CalcBandXEdges(VfoFreq, FRXEdgeLo, FRXEdgeHi, W, X1, X2, VfoX)
|
||||
else
|
||||
CalcFilterBandXFor(VfoFreq, FMode, FFilterBW, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
// Полоса TX-фильтра вокруг заданной несущей. Кромки уже знаковые и уже под
|
||||
// режим передачи — их считает WDSPEngine.TXSignedEdges, единственный в проекте
|
||||
// конвертер боковой для передачи.
|
||||
procedure TSpectrumView.CalcTXBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
begin
|
||||
if FTXEdgeHi > FTXEdgeLo then
|
||||
CalcBandXEdges(VfoFreq, FTXEdgeLo, FTXEdgeHi, W, X1, X2, VfoX)
|
||||
else
|
||||
CalcFilterBandX(VfoFreq, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
// Готовые знаковые кромки (Гц от несущей) → пиксели.
|
||||
procedure TSpectrumView.CalcBandXEdges(VfoFreq, LoHz, HiHz: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
begin
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
X1 := Max(0, Min(W - 1, VfoX + Round(LoHz / FSpanHz * W)));
|
||||
X2 := Max(0, Min(W - 1, VfoX + Round(HiHz / FSpanHz * W)));
|
||||
end;
|
||||
|
||||
// Полоса слайса: пока он передаёт — по TX-фильтру (TX-полоса общая, не на
|
||||
// слайс), иначе по своему режиму и ширине.
|
||||
procedure TSpectrumView.CalcSliceBandX(O: TVfoOverlay; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
begin
|
||||
if O.TxActive and (FTXEdgeHi > FTXEdgeLo) then
|
||||
CalcBandXEdges(O.DisplayVfoHz, FTXEdgeLo, FTXEdgeHi, W, X1, X2, VfoX)
|
||||
else if O.DisplayHi > O.DisplayLo then
|
||||
// Настоящие кромки слайса — как и у главного VFO. Вывод из режима и
|
||||
// ширины врал бы на любом фильтре, не подчиняющемся конвенции режима.
|
||||
CalcBandXEdges(O.DisplayVfoHz, O.DisplayLo, O.DisplayHi, W, X1, X2, VfoX)
|
||||
else
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
// То же, но для произвольного режима/полосы (маркеры слайсов). Таблица знака
|
||||
// не дублируется — берём общую (Settings.FilterEdgesFromBW), ту же, из которой
|
||||
// строятся заводские фильтры и которой пользуется контроллер.
|
||||
procedure TSpectrumView.CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
var
|
||||
Lo_Hz, Hi_Hz, Half: Double;
|
||||
Lo, Hi: Integer;
|
||||
begin
|
||||
Half := BW / 2;
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
case Mode of
|
||||
MODE_LSB: begin Lo_Hz := -BW; Hi_Hz := -100; end;
|
||||
MODE_USB: begin Lo_Hz := 100; Hi_Hz := BW; end;
|
||||
MODE_DIGL: begin Lo_Hz := -BW; Hi_Hz := 0; end; // полоса от нуля
|
||||
MODE_DIGU: begin Lo_Hz := 0; Hi_Hz := BW; 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)));
|
||||
FilterEdgesFromBW(Mode, BW, Lo, Hi);
|
||||
CalcBandXEdges(VfoFreq, Lo, Hi, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
@@ -1584,14 +1653,21 @@ begin
|
||||
// фильтра в пару тысяч пикселей стоила 4× дороже полного memcpy кадра),
|
||||
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
||||
|
||||
// Полоса фильтра (X-координаты главного и TX-VFO)
|
||||
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
|
||||
// считается по TX-фильтру: его ширина независима от приёмной (модель Thetis),
|
||||
// и раньше в эфир шло одно, а на экране рисовалось другое.
|
||||
if FTXOverlay and (FTXVfoIndex = FActiveVfo) then
|
||||
begin
|
||||
if FActiveVfo = 0 then CalcTXBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcTXBandX(FVfoB, W, X1, X2, VfoX);
|
||||
end
|
||||
else if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||
TXX1 := 0; TXX2 := 0; TXVfoX := 0;
|
||||
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
|
||||
begin
|
||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
if FTXVfoIndex = 0 then CalcTXBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcTXBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
end;
|
||||
|
||||
// Точки кривой спектра
|
||||
|
||||
Reference in New Issue
Block a user