mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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:
+6
-5
@@ -34,7 +34,7 @@ type
|
|||||||
// Параметр, управляемый поворотным энкодером панели.
|
// Параметр, управляемый поворотным энкодером панели.
|
||||||
TAndEncoder = (
|
TAndEncoder = (
|
||||||
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
|
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
|
||||||
aeFilterWidth, aeMulti);
|
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
|
||||||
|
|
||||||
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis.
|
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis.
|
||||||
TAndIndicator = (
|
TAndIndicator = (
|
||||||
@@ -104,15 +104,16 @@ const
|
|||||||
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
|
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
|
||||||
// Нумерация = «Encoder Number» Thetis − 1 (дефолт MakeNewAndromedaDataset).
|
// Нумерация = «Encoder Number» Thetis − 1 (дефолт MakeNewAndromedaDataset).
|
||||||
// Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel.
|
// Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel.
|
||||||
// #4/#5 на панели — Filter High/Low (независимые края), но в ewsdr фильтр
|
// #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2).
|
||||||
// только симметричный (SetFilterBW): #4 крутит ширину, #5 пока не задействован.
|
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один
|
||||||
|
// энкодер «ширина» — в дефолтной раскладке не используется.
|
||||||
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
|
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
|
||||||
aeAFGain, // 0 RX1 AF Gain
|
aeAFGain, // 0 RX1 AF Gain
|
||||||
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF»)
|
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF»)
|
||||||
aeNone, // 2 RX2 AF — нет второго приёмника
|
aeNone, // 2 RX2 AF — нет второго приёмника
|
||||||
aeNone, // 3 RX2 AGC
|
aeNone, // 3 RX2 AGC
|
||||||
aeFilterWidth, // 4 Filter High → ширина фильтра (краёв нет)
|
aeFilterHigh, // 4 Filter High — верхний край
|
||||||
aeNone, // 5 Filter Low — не реализован
|
aeFilterLow, // 5 Filter Low — нижний край
|
||||||
aeNone, // 6 Diversity gain
|
aeNone, // 6 Diversity gain
|
||||||
aeNone, // 7 Diversity phase
|
aeNone, // 7 Diversity phase
|
||||||
aeNone, // 8 RIT
|
aeNone, // 8 RIT
|
||||||
|
|||||||
@@ -126,6 +126,13 @@ begin
|
|||||||
aeDrive: FController.SetDrive(FController.FDrivePercent + FPendingStep);
|
aeDrive: FController.SetDrive(FController.FDrivePercent + FPendingStep);
|
||||||
aeSquelch: FController.SetFMSquelchLevel(FController.FFMSQLevel + FPendingStep);
|
aeSquelch: FController.SetFMSquelchLevel(FController.FFMSQLevel + FPendingStep);
|
||||||
aeFilterWidth: FController.SetFilterBW(FController.FFilterBW + FPendingStep * AND_BW_STEP_HZ);
|
aeFilterWidth: FController.SetFilterBW(FController.FFilterBW + FPendingStep * AND_BW_STEP_HZ);
|
||||||
|
// Края независимы: крутим один, второй стоит. Контроллер уводит правку в
|
||||||
|
// VAR, поэтому пресеты энкодером не портятся.
|
||||||
|
aeFilterHigh: FController.SetFilterEdges(FController.FilterLo,
|
||||||
|
FController.FilterHi + FPendingStep * AND_BW_STEP_HZ);
|
||||||
|
aeFilterLow: FController.SetFilterEdges(
|
||||||
|
FController.FilterLo + FPendingStep * AND_BW_STEP_HZ,
|
||||||
|
FController.FilterHi);
|
||||||
// aeMulti — мультифункц. энкодер: пока не назначен
|
// aeMulti — мультифункц. энкодер: пока не назначен
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ type
|
|||||||
function GetDriveLevel: Integer;
|
function GetDriveLevel: Integer;
|
||||||
function GetFilterIdx: Integer;
|
function GetFilterIdx: Integer;
|
||||||
function GetFilterBW: Integer;
|
function GetFilterBW: Integer;
|
||||||
|
function GetFilterLow: Integer;
|
||||||
|
function GetFilterHigh: Integer;
|
||||||
|
procedure SetFilterLow(V: Integer);
|
||||||
|
procedure SetFilterHigh(V: Integer);
|
||||||
function GetNRMode: Integer;
|
function GetNRMode: Integer;
|
||||||
function GetNBMode: Integer;
|
function GetNBMode: Integer;
|
||||||
function GetSNB: Boolean;
|
function GetSNB: Boolean;
|
||||||
@@ -161,6 +165,10 @@ begin
|
|||||||
Ctx.GetDriveLevel := @GetDriveLevel;
|
Ctx.GetDriveLevel := @GetDriveLevel;
|
||||||
Ctx.GetFilterIdx := @GetFilterIdx;
|
Ctx.GetFilterIdx := @GetFilterIdx;
|
||||||
Ctx.GetFilterBW := @GetFilterBW;
|
Ctx.GetFilterBW := @GetFilterBW;
|
||||||
|
// Кромки фильтра (ZZFL/ZZFH). Раньше у главного приёмника были nil (фильтр
|
||||||
|
// был индексным) — теперь он хранит те же знаковые кромки, что и слайс.
|
||||||
|
Ctx.GetFilterLow := @GetFilterLow;
|
||||||
|
Ctx.GetFilterHigh := @GetFilterHigh;
|
||||||
Ctx.GetNRMode := @GetNRMode;
|
Ctx.GetNRMode := @GetNRMode;
|
||||||
Ctx.GetNBMode := @GetNBMode;
|
Ctx.GetNBMode := @GetNBMode;
|
||||||
Ctx.GetSNBEnabled := @GetSNB;
|
Ctx.GetSNBEnabled := @GetSNB;
|
||||||
@@ -177,6 +185,8 @@ begin
|
|||||||
Ctx.SetVolume := @SetVolume;
|
Ctx.SetVolume := @SetVolume;
|
||||||
Ctx.SetDriveLevel := @SetDriveLevel;
|
Ctx.SetDriveLevel := @SetDriveLevel;
|
||||||
Ctx.SetFilterIdx := @SetFilterIdx;
|
Ctx.SetFilterIdx := @SetFilterIdx;
|
||||||
|
Ctx.SetFilterLow := @SetFilterLow;
|
||||||
|
Ctx.SetFilterHigh := @SetFilterHigh;
|
||||||
Ctx.SetNRMode := @SetNRMode;
|
Ctx.SetNRMode := @SetNRMode;
|
||||||
Ctx.SetNBMode := @SetNBMode;
|
Ctx.SetNBMode := @SetNBMode;
|
||||||
Ctx.SetSNBEnabled := @SetSNB;
|
Ctx.SetSNBEnabled := @SetSNB;
|
||||||
@@ -297,6 +307,14 @@ function TCATAdapter.GetVolume: Integer; begin Result := FController.FVolume
|
|||||||
function TCATAdapter.GetDriveLevel: Integer; begin Result := FController.FDrivePercent; end;
|
function TCATAdapter.GetDriveLevel: Integer; begin Result := FController.FDrivePercent; end;
|
||||||
function TCATAdapter.GetFilterIdx: Integer; begin Result := FController.FFilter; end;
|
function TCATAdapter.GetFilterIdx: Integer; begin Result := FController.FFilter; end;
|
||||||
function TCATAdapter.GetFilterBW: Integer; begin Result := FController.FFilterBW; end;
|
function TCATAdapter.GetFilterBW: Integer; begin Result := FController.FFilterBW; end;
|
||||||
|
function TCATAdapter.GetFilterLow: Integer; begin Result := FController.FilterLo; end;
|
||||||
|
function TCATAdapter.GetFilterHigh: Integer; begin Result := FController.FilterHi; end;
|
||||||
|
// Кромку меняем по одной — вторую держим текущей (контроллер уводит правку в
|
||||||
|
// VAR, поэтому пресеты внешняя программа не испортит).
|
||||||
|
procedure TCATAdapter.SetFilterLow(V: Integer);
|
||||||
|
begin FController.SetFilterEdges(V, FController.FilterHi); end;
|
||||||
|
procedure TCATAdapter.SetFilterHigh(V: Integer);
|
||||||
|
begin FController.SetFilterEdges(FController.FilterLo, V); end;
|
||||||
function TCATAdapter.GetNRMode: Integer; begin Result := FController.FNRMode; end;
|
function TCATAdapter.GetNRMode: Integer; begin Result := FController.FNRMode; end;
|
||||||
function TCATAdapter.GetNBMode: Integer; begin Result := FController.FNBMode; end;
|
function TCATAdapter.GetNBMode: Integer; begin Result := FController.FNBMode; end;
|
||||||
function TCATAdapter.GetSNB: Boolean; begin Result := FController.FSNB; end;
|
function TCATAdapter.GetSNB: Boolean; begin Result := FController.FSNB; end;
|
||||||
|
|||||||
+514
@@ -0,0 +1,514 @@
|
|||||||
|
unit FilterPopup;
|
||||||
|
|
||||||
|
{
|
||||||
|
TFilterPopup — поповер редактора фильтров приёмника, открывается ПКМ по
|
||||||
|
любой кнопке фильтра на левой панели. Слева список слотов режима (10
|
||||||
|
пресетов + VAR1/VAR2), справа Name/Low/High/Width, внизу форма полосы в
|
||||||
|
масштабе на ЗНАКОВОЙ оси частот.
|
||||||
|
|
||||||
|
Знак и есть главный смысл нижней картинки: у LSB полоса лежит в минусе, и
|
||||||
|
это видно глазами — ровно то место, где в проекте дважды заводились баги с
|
||||||
|
боковой. Скат нарисован схематично: реальная крутизна у WDSP определяется
|
||||||
|
длиной FIR (fircore) и никаким пользовательским параметром не управляется.
|
||||||
|
|
||||||
|
Эталон содержимого — Thetis FilterForm.cs (12 слотов + Name/Low/High/Width);
|
||||||
|
графика полосы — наша добавка, у них её нет.
|
||||||
|
|
||||||
|
Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно) —
|
||||||
|
как TPureSignalPopup/TPanDisplayPopup: Wayland не даёт клиенту
|
||||||
|
позиционировать top-level окна.
|
||||||
|
}
|
||||||
|
|
||||||
|
{$IFDEF FPC}
|
||||||
|
{$MODE Delphi}
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils, Math, Types, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
|
||||||
|
RadioController, RadioModes, Settings,
|
||||||
|
FlatButton, FlatEdit, FlatSpinEdit, FlatListBox;
|
||||||
|
|
||||||
|
type
|
||||||
|
TFilterSlotEvent = procedure(Mode, Idx: Integer; const S: TFilterSlot) of object;
|
||||||
|
TFilterResetEvent = procedure(Mode, Idx: Integer) of object;
|
||||||
|
|
||||||
|
TFilterPopup = class(TCustomControl)
|
||||||
|
private
|
||||||
|
FController: TRadioController; // не владеем
|
||||||
|
FMode: Integer;
|
||||||
|
FSlotIdx: Integer;
|
||||||
|
FSet: TFilterSet;
|
||||||
|
FPresets: Integer; // пресетов у режима (VAR идут сверх)
|
||||||
|
FLoading: Boolean; // подавляет Apply при программной заливке
|
||||||
|
FBg: TPanel;
|
||||||
|
FTitle: TLabel;
|
||||||
|
FBtnClose: TFlatButton;
|
||||||
|
FList: TFlatListBox;
|
||||||
|
FEdName: TFlatEdit;
|
||||||
|
FEdLow: TFlatSpinEdit;
|
||||||
|
FEdHigh: TFlatSpinEdit;
|
||||||
|
FEdWidth: TFlatSpinEdit;
|
||||||
|
FBtnReset: TFlatButton;
|
||||||
|
FBtnResetAll: TFlatButton;
|
||||||
|
FShape: TPaintBox;
|
||||||
|
FOnSlotEdited: TFilterSlotEvent;
|
||||||
|
FOnSlotReset: TFilterResetEvent;
|
||||||
|
function MakeLbl(const Cap: string; ALeft, ATop, AW: Integer): TLabel;
|
||||||
|
procedure FillList;
|
||||||
|
procedure ShowSlot;
|
||||||
|
procedure Commit;
|
||||||
|
procedure ListClick(Sender: TObject);
|
||||||
|
procedure EdgeChange(Sender: TObject);
|
||||||
|
procedure WidthChange(Sender: TObject);
|
||||||
|
procedure NameChange(Sender: TObject);
|
||||||
|
procedure ResetClick(Sender: TObject);
|
||||||
|
procedure ResetAllClick(Sender: TObject);
|
||||||
|
procedure CloseClick(Sender: TObject);
|
||||||
|
procedure ShapePaint(Sender: TObject);
|
||||||
|
function SlotLabel(Idx: Integer): string;
|
||||||
|
protected
|
||||||
|
procedure Paint; override;
|
||||||
|
public
|
||||||
|
constructor Create(AOwner: TComponent); override;
|
||||||
|
// Перечитывает таблицу контроллера и показывает слот SlotIdx режима Mode.
|
||||||
|
procedure LoadFrom(C: TRadioController; Mode, SlotIdx: Integer);
|
||||||
|
// Подсветить выбор, когда фильтр переключили снаружи (кнопка/CAT/drag).
|
||||||
|
procedure SyncSelection(Mode, Idx: Integer);
|
||||||
|
// Показать под контролом-якорем (кнопкой фильтра) как дочерний контрол формы.
|
||||||
|
procedure PopupBelow(C: TRadioController; Anchor: TControl;
|
||||||
|
Mode, SlotIdx: Integer);
|
||||||
|
property SlotIdx: Integer read FSlotIdx;
|
||||||
|
property OnSlotEdited: TFilterSlotEvent read FOnSlotEdited write FOnSlotEdited;
|
||||||
|
property OnSlotReset: TFilterResetEvent read FOnSlotReset write FOnSlotReset;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
{$IFDEF WINDOWS}
|
||||||
|
UI_FONT = 'Segoe UI';
|
||||||
|
{$ELSE}
|
||||||
|
UI_FONT = 'Sans';
|
||||||
|
{$ENDIF}
|
||||||
|
CLR_BG = TColor($00303030); // рамка (1px кайма поповера)
|
||||||
|
CLR_PANEL = TColor($001A1A1A);
|
||||||
|
CLR_TEXT = TColor($00E0E0E0);
|
||||||
|
CLR_TEXTDIM = TColor($00888888);
|
||||||
|
CLR_ACCENT = TColor($0040FF80);
|
||||||
|
CLR_INPUT = TColor($00FFFFFF);
|
||||||
|
CLR_INPUT_TEXT = TColor($00202020);
|
||||||
|
CLR_GRID = TColor($00303030);
|
||||||
|
CLR_BAND = TColor($0040FF80);
|
||||||
|
|
||||||
|
POP_W = 300;
|
||||||
|
PAD = 10;
|
||||||
|
LISTW = 78;
|
||||||
|
LBLW = 40;
|
||||||
|
ROWH = 24;
|
||||||
|
ITEMH = 22; // = BASE_ITEM_H в FlatListBox
|
||||||
|
SHAPE_H = 96;
|
||||||
|
RULER_H = 13;
|
||||||
|
EDGE_LIM = 20000;
|
||||||
|
|
||||||
|
FE_MODE_NAMES: array[0..MODE_MAX] of string = (
|
||||||
|
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FMRAW');
|
||||||
|
|
||||||
|
constructor TFilterPopup.Create(AOwner: TComponent);
|
||||||
|
var
|
||||||
|
Y, CtlX, CtlW, BW: Integer;
|
||||||
|
|
||||||
|
function MakeSpin(ATop: Integer; H: TNotifyEvent): TFlatSpinEdit;
|
||||||
|
begin
|
||||||
|
Result := TFlatSpinEdit.Create(Self);
|
||||||
|
Result.Parent := FBg;
|
||||||
|
Result.SetBounds(CtlX, ATop, CtlW, ROWH - 2);
|
||||||
|
Result.MinValue := -EDGE_LIM;
|
||||||
|
Result.MaxValue := EDGE_LIM;
|
||||||
|
Result.Increment := 10;
|
||||||
|
Result.Color := CLR_INPUT;
|
||||||
|
Result.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
Result.Font.Name := UI_FONT;
|
||||||
|
Result.Font.Size := 9;
|
||||||
|
Result.OnChange := H;
|
||||||
|
end;
|
||||||
|
|
||||||
|
begin
|
||||||
|
inherited Create(AOwner);
|
||||||
|
ControlStyle := ControlStyle + [csOpaque];
|
||||||
|
Visible := False;
|
||||||
|
Color := CLR_BG;
|
||||||
|
Font.Name := UI_FONT;
|
||||||
|
Font.Size := 9;
|
||||||
|
Font.Color := CLR_TEXT;
|
||||||
|
FMode := MODE_USB;
|
||||||
|
FSlotIdx := 0;
|
||||||
|
FPresets := FILT_COUNT;
|
||||||
|
// ★Гард на ВСЁ время постройки: сеттеры Flat*-контролов (напр.
|
||||||
|
// TFlatSpinEdit.SetMinValue → SetValue → DoChange) стреляют OnChange прямо
|
||||||
|
// из конструктора, а обработчики трогают контролы, созданные ниже.
|
||||||
|
FLoading := True;
|
||||||
|
|
||||||
|
FBg := TPanel.Create(Self);
|
||||||
|
FBg.Parent := Self;
|
||||||
|
FBg.BevelOuter := bvNone;
|
||||||
|
FBg.Color := CLR_PANEL;
|
||||||
|
FBg.SetBounds(1, 1, POP_W - 2, 10);
|
||||||
|
|
||||||
|
Y := PAD;
|
||||||
|
FTitle := MakeLbl('Filters', PAD, Y + 2, POP_W - 2 * PAD - 18);
|
||||||
|
FTitle.Font.Color := CLR_ACCENT;
|
||||||
|
FTitle.Font.Style := [fsBold];
|
||||||
|
FBtnClose := MakeFlatBtn(FBg, '×', POP_W - PAD - 16, Y, 16, 16, CloseClick);
|
||||||
|
FBtnClose.ClrText := CLR_TEXTDIM;
|
||||||
|
FBtnClose.ClrTextAct := CLR_TEXT;
|
||||||
|
Inc(Y, ROWH);
|
||||||
|
|
||||||
|
// ---- Слева список слотов, справа поля ----
|
||||||
|
FList := TFlatListBox.Create(Self);
|
||||||
|
FList.Parent := FBg;
|
||||||
|
FList.SetBounds(PAD, Y, LISTW, FILT_SLOTS * ITEMH + 4);
|
||||||
|
FList.Font.Name := UI_FONT;
|
||||||
|
FList.Font.Size := 9;
|
||||||
|
FList.OnClick := ListClick; // выбор строки = клик (у TFlatListBox нет OnSelect)
|
||||||
|
|
||||||
|
CtlX := PAD + LISTW + 10 + LBLW + 6;
|
||||||
|
CtlW := POP_W - PAD - CtlX;
|
||||||
|
|
||||||
|
MakeLbl('Name', PAD + LISTW + 10, Y + 3, LBLW);
|
||||||
|
FEdName := TFlatEdit.Create(Self);
|
||||||
|
FEdName.Parent := FBg;
|
||||||
|
FEdName.SetBounds(CtlX, Y, CtlW, ROWH - 2);
|
||||||
|
FEdName.MaxLength := 6;
|
||||||
|
FEdName.Color := CLR_INPUT;
|
||||||
|
FEdName.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
FEdName.Font.Name := UI_FONT;
|
||||||
|
FEdName.Font.Size := 9;
|
||||||
|
FEdName.OnChange := NameChange;
|
||||||
|
Inc(Y, ROWH + 4);
|
||||||
|
|
||||||
|
MakeLbl('Low', PAD + LISTW + 10, Y + 3, LBLW);
|
||||||
|
FEdLow := MakeSpin(Y, EdgeChange);
|
||||||
|
Inc(Y, ROWH + 4);
|
||||||
|
|
||||||
|
MakeLbl('High', PAD + LISTW + 10, Y + 3, LBLW);
|
||||||
|
FEdHigh := MakeSpin(Y, EdgeChange);
|
||||||
|
Inc(Y, ROWH + 4);
|
||||||
|
|
||||||
|
MakeLbl('Width', PAD + LISTW + 10, Y + 3, LBLW);
|
||||||
|
FEdWidth := MakeSpin(Y, WidthChange);
|
||||||
|
FEdWidth.MinValue := FILT_MIN_WIDTH;
|
||||||
|
Inc(Y, ROWH + 8);
|
||||||
|
|
||||||
|
BW := (POP_W - PAD - (PAD + LISTW + 10) - 6) div 2;
|
||||||
|
FBtnReset := MakeFlatBtn(FBg, 'Reset', PAD + LISTW + 10, Y, BW, ROWH - 2,
|
||||||
|
ResetClick);
|
||||||
|
FBtnResetAll := MakeFlatBtn(FBg, 'Reset all', PAD + LISTW + 10 + BW + 6, Y,
|
||||||
|
BW, ROWH - 2, ResetAllClick);
|
||||||
|
|
||||||
|
// Список выше полей — берём максимум по высоте.
|
||||||
|
Y := Max(Y + ROWH - 2, FList.Top + FList.Height) + 8;
|
||||||
|
|
||||||
|
MakeLbl('— Passband —', PAD, Y, POP_W - 2 * PAD).Font.Color := CLR_TEXTDIM;
|
||||||
|
Inc(Y, 18);
|
||||||
|
FShape := TPaintBox.Create(Self);
|
||||||
|
FShape.Parent := FBg;
|
||||||
|
FShape.SetBounds(PAD, Y, POP_W - 2 * PAD, SHAPE_H);
|
||||||
|
FShape.OnPaint := ShapePaint;
|
||||||
|
Inc(Y, SHAPE_H);
|
||||||
|
|
||||||
|
FBg.Height := Y - 1 + PAD;
|
||||||
|
Width := POP_W;
|
||||||
|
Height := Y + 1 + PAD;
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.Paint;
|
||||||
|
begin
|
||||||
|
Canvas.Brush.Style := bsSolid;
|
||||||
|
Canvas.Brush.Color := CLR_BG;
|
||||||
|
Canvas.Pen.Style := psClear;
|
||||||
|
Canvas.FillRect(0, 0, Width, Height);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TFilterPopup.MakeLbl(const Cap: string; ALeft, ATop, AW: Integer): TLabel;
|
||||||
|
begin
|
||||||
|
Result := TLabel.Create(Self);
|
||||||
|
Result.Parent := FBg;
|
||||||
|
Result.Caption := Cap;
|
||||||
|
Result.Left := ALeft;
|
||||||
|
Result.Top := ATop;
|
||||||
|
if AW > 0 then Result.Width := AW;
|
||||||
|
Result.Font.Color := CLR_TEXT;
|
||||||
|
Result.Font.Name := UI_FONT;
|
||||||
|
Result.Font.Size := 9;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TFilterPopup.SlotLabel(Idx: Integer): string;
|
||||||
|
begin
|
||||||
|
Result := FSet[Idx].Name;
|
||||||
|
if Result = '' then Result := '—';
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.FillList;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
FList.Items.BeginUpdate;
|
||||||
|
try
|
||||||
|
FList.Items.Clear;
|
||||||
|
for i := 0 to FPresets - 1 do FList.Items.Add(SlotLabel(i));
|
||||||
|
FList.Items.Add(SlotLabel(FILT_VAR1));
|
||||||
|
FList.Items.Add(SlotLabel(FILT_VAR2));
|
||||||
|
finally
|
||||||
|
FList.Items.EndUpdate;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.LoadFrom(C: TRadioController; Mode, SlotIdx: Integer);
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
if C = nil then Exit;
|
||||||
|
FController := C;
|
||||||
|
FMode := EnsureRange(Mode, 0, CFG_MODE_MAX);
|
||||||
|
FPresets := FilterPresetCount(FMode);
|
||||||
|
for i := 0 to FILT_SLOTS - 1 do FSet[i] := C.FilterSlot(FMode, i);
|
||||||
|
FSlotIdx := EnsureRange(SlotIdx, 0, FILT_SLOTS - 1);
|
||||||
|
// Пресет вне таблицы режима (FM/WFM короче) — показываем VAR1.
|
||||||
|
if (FSlotIdx >= FPresets) and (FSlotIdx < FILT_VAR1) then FSlotIdx := FILT_VAR1;
|
||||||
|
FTitle.Caption := 'Filters — ' + FE_MODE_NAMES[EnsureRange(FMode, 0, MODE_MAX)];
|
||||||
|
FLoading := True;
|
||||||
|
try
|
||||||
|
FillList;
|
||||||
|
ShowSlot;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.SyncSelection(Mode, Idx: Integer);
|
||||||
|
begin
|
||||||
|
if (Mode <> FMode) or (Idx = FSlotIdx) then Exit;
|
||||||
|
FSlotIdx := EnsureRange(Idx, 0, FILT_SLOTS - 1);
|
||||||
|
FLoading := True;
|
||||||
|
try ShowSlot; finally FLoading := False; end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.ShowSlot;
|
||||||
|
var Row: Integer;
|
||||||
|
begin
|
||||||
|
if FSlotIdx >= FILT_VAR1 then Row := FPresets + (FSlotIdx - FILT_VAR1)
|
||||||
|
else Row := FSlotIdx;
|
||||||
|
FList.ItemIndex := Row;
|
||||||
|
FEdName.Text := FSet[FSlotIdx].Name;
|
||||||
|
FEdLow.Value := FSet[FSlotIdx].Lo;
|
||||||
|
FEdHigh.Value := FSet[FSlotIdx].Hi;
|
||||||
|
FEdWidth.Value := FSet[FSlotIdx].Hi - FSet[FSlotIdx].Lo;
|
||||||
|
FShape.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.Commit;
|
||||||
|
begin
|
||||||
|
if FLoading or (FShape = nil) then Exit;
|
||||||
|
FShape.Invalidate;
|
||||||
|
if Assigned(FOnSlotEdited) then FOnSlotEdited(FMode, FSlotIdx, FSet[FSlotIdx]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.ListClick(Sender: TObject);
|
||||||
|
var Row: Integer;
|
||||||
|
begin
|
||||||
|
if FLoading then Exit;
|
||||||
|
Row := FList.ItemIndex;
|
||||||
|
if Row < 0 then Exit;
|
||||||
|
if Row >= FPresets then FSlotIdx := FILT_VAR1 + (Row - FPresets)
|
||||||
|
else FSlotIdx := Row;
|
||||||
|
FLoading := True;
|
||||||
|
try ShowSlot; finally FLoading := False; end;
|
||||||
|
// Клик по строке = выбрать этот фильтр в приёмнике (как кнопка на панели).
|
||||||
|
if Assigned(FController) then FController.SetFilterIdx(FSlotIdx);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.EdgeChange(Sender: TObject);
|
||||||
|
var Lo, Hi: Integer;
|
||||||
|
begin
|
||||||
|
if FLoading then Exit;
|
||||||
|
Lo := FEdLow.Value;
|
||||||
|
Hi := FEdHigh.Value;
|
||||||
|
if Hi - Lo < FILT_MIN_WIDTH then
|
||||||
|
begin
|
||||||
|
// Края не пускаем друг через друга: двигаем тот, который НЕ трогали.
|
||||||
|
if Sender = FEdLow then Hi := Lo + FILT_MIN_WIDTH
|
||||||
|
else Lo := Hi - FILT_MIN_WIDTH;
|
||||||
|
FLoading := True;
|
||||||
|
try
|
||||||
|
FEdLow.Value := Lo;
|
||||||
|
FEdHigh.Value := Hi;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
FSet[FSlotIdx].Lo := Lo;
|
||||||
|
FSet[FSlotIdx].Hi := Hi;
|
||||||
|
FLoading := True;
|
||||||
|
try FEdWidth.Value := Hi - Lo; finally FLoading := False; end;
|
||||||
|
Commit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.WidthChange(Sender: TObject);
|
||||||
|
// Width производная: держим центр полосы, разъезжаются оба края (как Thetis).
|
||||||
|
var W, C, Lo, Hi: Integer;
|
||||||
|
begin
|
||||||
|
if FLoading then Exit;
|
||||||
|
W := Max(FILT_MIN_WIDTH, FEdWidth.Value);
|
||||||
|
C := (FSet[FSlotIdx].Lo + FSet[FSlotIdx].Hi) div 2;
|
||||||
|
Lo := C - W div 2;
|
||||||
|
Hi := Lo + W;
|
||||||
|
FSet[FSlotIdx].Lo := Lo;
|
||||||
|
FSet[FSlotIdx].Hi := Hi;
|
||||||
|
FLoading := True;
|
||||||
|
try
|
||||||
|
FEdLow.Value := Lo;
|
||||||
|
FEdHigh.Value := Hi;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
Commit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.NameChange(Sender: TObject);
|
||||||
|
var Row: Integer;
|
||||||
|
begin
|
||||||
|
if FLoading then Exit;
|
||||||
|
FSet[FSlotIdx].Name := FEdName.Text;
|
||||||
|
Row := FList.ItemIndex;
|
||||||
|
FLoading := True;
|
||||||
|
try
|
||||||
|
FillList;
|
||||||
|
FList.ItemIndex := Row;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
Commit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.ResetClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(FOnSlotReset) then FOnSlotReset(FMode, FSlotIdx);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.ResetAllClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(FOnSlotReset) then FOnSlotReset(FMode, -1);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.CloseClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
Hide;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.ShapePaint(Sender: TObject);
|
||||||
|
// Трапеция полосы в масштабе на знаковой оси. Окно оси симметрично нулю и
|
||||||
|
// шире полосы, поэтому сразу видно, по какую сторону от несущей она лежит.
|
||||||
|
var
|
||||||
|
W, H, GH, ZeroX, i, X, Skirt, Lo, Hi, X1, X2: Integer;
|
||||||
|
Span, Step: Double;
|
||||||
|
S: string;
|
||||||
|
|
||||||
|
function FreqToX(F: Double): Integer;
|
||||||
|
begin
|
||||||
|
Result := Round((F + Span) / (2 * Span) * W);
|
||||||
|
end;
|
||||||
|
|
||||||
|
begin
|
||||||
|
W := FShape.Width;
|
||||||
|
H := FShape.Height;
|
||||||
|
if (W <= 0) or (H <= 0) then Exit;
|
||||||
|
GH := H - RULER_H;
|
||||||
|
Lo := FSet[FSlotIdx].Lo;
|
||||||
|
Hi := FSet[FSlotIdx].Hi;
|
||||||
|
|
||||||
|
with FShape.Canvas do
|
||||||
|
begin
|
||||||
|
Brush.Style := bsSolid;
|
||||||
|
Brush.Color := CLR_PANEL;
|
||||||
|
FillRect(0, 0, W, H);
|
||||||
|
|
||||||
|
Span := Max(Abs(Lo), Abs(Hi)) * 1.6;
|
||||||
|
if Span < 500 then Span := 500;
|
||||||
|
|
||||||
|
// Сетка: шаг кратен 1 кГц, подстраивается под окно.
|
||||||
|
Step := 1000;
|
||||||
|
while Span / Step > 12 do Step := Step * 2;
|
||||||
|
Pen.Style := psSolid;
|
||||||
|
Pen.Color := CLR_GRID;
|
||||||
|
i := -Trunc(Span / Step);
|
||||||
|
while i <= Trunc(Span / Step) do
|
||||||
|
begin
|
||||||
|
X := FreqToX(i * Step);
|
||||||
|
Line(X, 0, X, GH);
|
||||||
|
Inc(i);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Ноль = несущая, ярче сетки.
|
||||||
|
ZeroX := FreqToX(0);
|
||||||
|
Pen.Color := CLR_TEXTDIM;
|
||||||
|
Line(ZeroX, 0, ZeroX, GH);
|
||||||
|
|
||||||
|
// Полоса: плоская вершина + схематичные скаты.
|
||||||
|
X1 := FreqToX(Lo);
|
||||||
|
X2 := FreqToX(Hi);
|
||||||
|
Skirt := Max(2, Round(0.06 * (X2 - X1)));
|
||||||
|
Brush.Color := CLR_BAND;
|
||||||
|
Pen.Color := CLR_BAND;
|
||||||
|
Polygon([Point(X1, GH - 2),
|
||||||
|
Point(X1 + Skirt, Round(GH * 0.2)),
|
||||||
|
Point(X2 - Skirt, Round(GH * 0.2)),
|
||||||
|
Point(X2, GH - 2)]);
|
||||||
|
|
||||||
|
// Линейка: края полосы + ноль.
|
||||||
|
Brush.Style := bsClear;
|
||||||
|
Font.Name := UI_FONT;
|
||||||
|
Font.Size := 7;
|
||||||
|
Font.Color := CLR_TEXTDIM;
|
||||||
|
S := '0';
|
||||||
|
TextOut(ZeroX - TextWidth(S) div 2, GH, S);
|
||||||
|
Font.Color := CLR_BAND;
|
||||||
|
S := IntToStr(Lo);
|
||||||
|
TextOut(Max(0, Min(W - TextWidth(S), X1 - TextWidth(S) div 2)), GH, S);
|
||||||
|
S := IntToStr(Hi);
|
||||||
|
TextOut(Max(0, Min(W - TextWidth(S), X2 - TextWidth(S) div 2)), GH, S);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFilterPopup.PopupBelow(C: TRadioController; Anchor: TControl;
|
||||||
|
Mode, SlotIdx: Integer);
|
||||||
|
var
|
||||||
|
Root: TCustomForm;
|
||||||
|
L, T: Integer;
|
||||||
|
Cl: TPoint;
|
||||||
|
begin
|
||||||
|
if Anchor = nil then Exit;
|
||||||
|
Root := GetParentForm(Anchor);
|
||||||
|
if Root = nil then Exit;
|
||||||
|
// ★Родителя назначаем ДО заливки данных. TFlatListBox при заполнении и при
|
||||||
|
// установке ItemIndex считает ItemHeight, а тот трогает Canvas
|
||||||
|
// (Canvas.Font.Assign / TextHeight) — у контрола без родительского окна это
|
||||||
|
// AV. У TPureSignalPopup списка нет, поэтому там порядок был обратный и
|
||||||
|
// проблемы не возникало.
|
||||||
|
Parent := Root;
|
||||||
|
LoadFrom(C, Mode, SlotIdx);
|
||||||
|
// Позиция под якорем в КЛИЕНТСКИХ координатах формы (не top-level окно!).
|
||||||
|
Cl := Root.ScreenToClient(Anchor.ClientToScreen(Point(0, Anchor.Height + 2)));
|
||||||
|
L := Cl.X;
|
||||||
|
T := Cl.Y;
|
||||||
|
if L + Width > Root.ClientWidth then L := Root.ClientWidth - Width;
|
||||||
|
if L < 0 then L := 0;
|
||||||
|
if T + Height > Root.ClientHeight then
|
||||||
|
T := Root.ScreenToClient(Anchor.ClientToScreen(Point(0, 0))).Y - Height;
|
||||||
|
if T < 0 then T := 0;
|
||||||
|
SetBounds(L, T, Width, Height);
|
||||||
|
Visible := True;
|
||||||
|
BringToFront;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
+247
-137
@@ -22,7 +22,8 @@ unit MainForm;
|
|||||||
interface
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, FilterPopup,
|
||||||
|
VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
||||||
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
||||||
OpenGLContextEx,
|
OpenGLContextEx,
|
||||||
@@ -68,23 +69,12 @@ const
|
|||||||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||||||
|
|
||||||
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
|
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
|
||||||
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
|
// Полосы/дефолты/подписи фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF,
|
||||||
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
|
// FILT_*_NAMES) живут в Settings.pas: из них строится заводская таблица
|
||||||
// UI-подписи кнопок (NAMES).
|
// фильтров, а подписи кнопок теперь берутся из живой таблицы контроллера.
|
||||||
FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string =
|
FILT_GRID_COLS = 6; // сетка кнопок фильтра: 2 ряда по 6 (12 = 10 + VAR1/2)
|
||||||
('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k');
|
EDGE_GRAB_PX = 5; // зона захвата края полосы фильтра мышью, px
|
||||||
FILT_CW_NAMES: array[0..FILT_COUNT-1] of string =
|
EDGE_SNAP_HZ = 10; // снап края при перетаскивании
|
||||||
('1.0k','800','600','500','400','250','150','100','50','25');
|
|
||||||
FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string =
|
|
||||||
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
|
|
||||||
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
|
|
||||||
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
|
|
||||||
FILT_DIGI_NAMES: array[0..FILT_COUNT-1] of string =
|
|
||||||
('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300');
|
|
||||||
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
|
|
||||||
FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k');
|
|
||||||
FILT_RAW_NAMES: array[0..FILT_RAW_COUNT-1] of string =
|
|
||||||
('6.25k', '10k', '12.5k', '20k', '25k');
|
|
||||||
|
|
||||||
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
|
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
|
||||||
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
|
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
|
||||||
@@ -132,11 +122,17 @@ type
|
|||||||
// ---- Network ----
|
// ---- Network ----
|
||||||
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
||||||
FDeviceDialog: TDeviceDialog;
|
FDeviceDialog: TDeviceDialog;
|
||||||
|
FFilterPop: TFilterPopup; // поповер редактора фильтров (ПКМ по кнопке FILTER)
|
||||||
FPendingIP: string; // IP устройства для подключения
|
FPendingIP: string; // IP устройства для подключения
|
||||||
// Флаг главного (слайс A): создаётся здесь (проводка событий), живёт в
|
// Флаг главного (слайс A): создаётся здесь (проводка событий), живёт в
|
||||||
// раскладке/диспетчере FPan (AttachMainFlag). Флаги B+ — целиком в FPan.
|
// раскладке/диспетчере FPan (AttachMainFlag). Флаги B+ — целиком в FPan.
|
||||||
FVfoOverlay: TVfoOverlay;
|
FVfoOverlay: TVfoOverlay;
|
||||||
FActiveSliceId: Integer; // выбранный активный слайс (0 = главный)
|
FActiveSliceId: Integer; // выбранный активный слайс (0 = главный)
|
||||||
|
// Перетаскивание края полосы фильтра: -1 левый, +1 правый, 0 не тянем.
|
||||||
|
// Кромки на момент нажатия — чтобы неподвижный край не «плыл» от снапа.
|
||||||
|
FEdgeDrag: Integer;
|
||||||
|
FEdgeDragLo: Integer;
|
||||||
|
FEdgeDragHi: Integer;
|
||||||
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
||||||
FSliceDragId: Integer; // какой слайс тащим
|
FSliceDragId: Integer; // какой слайс тащим
|
||||||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||||||
@@ -298,7 +294,7 @@ type
|
|||||||
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
|
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
|
||||||
|
|
||||||
PanelFilter: TPanel;
|
PanelFilter: TPanel;
|
||||||
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
BtnFilter: array[0..FILT_SLOTS-1] of TFlatButton; // 10 пресетов + VAR1/VAR2
|
||||||
BtnCTun: TFlatButton;
|
BtnCTun: TFlatButton;
|
||||||
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере)
|
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере)
|
||||||
BtnDUP: TFlatButton;
|
BtnDUP: TFlatButton;
|
||||||
@@ -467,6 +463,16 @@ type
|
|||||||
procedure BtnBandClick(Sender: TObject);
|
procedure BtnBandClick(Sender: TObject);
|
||||||
procedure BtnModeClick(Sender: TObject);
|
procedure BtnModeClick(Sender: TObject);
|
||||||
procedure BtnFilterClick(Sender: TObject);
|
procedure BtnFilterClick(Sender: TObject);
|
||||||
|
procedure BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
procedure LayoutFilterButtons(PresetCount: Integer);
|
||||||
|
procedure PushFilterEdgesToView; // кромки RX/TX в SpectrumView
|
||||||
|
procedure RefreshFilterCaptions; // подписи кнопок без сброса выбора
|
||||||
|
function FilterEdgeAtPixel(X: Integer): Integer; // -1 левый край, +1 правый, 0 мимо
|
||||||
|
procedure DoFilterEdgeDrag(X: Integer; Shift: TShiftState);
|
||||||
|
procedure OpenFilterEditor(SlotIdx: Integer);
|
||||||
|
procedure OnFilterSlotEdited(Mode, Idx: Integer; const S: TFilterSlot);
|
||||||
|
procedure OnFilterSlotReset(Mode, Idx: Integer); // Idx < 0 = весь режим
|
||||||
procedure BtnCTunClick(Sender: TObject);
|
procedure BtnCTunClick(Sender: TObject);
|
||||||
procedure BtnBeaconClick(Sender: TObject);
|
procedure BtnBeaconClick(Sender: TObject);
|
||||||
procedure BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
procedure BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
@@ -980,7 +986,9 @@ begin
|
|||||||
FController.FAGCTop := 90; // −90 dBm
|
FController.FAGCTop := 90; // −90 dBm
|
||||||
// default: зависит от режима, будет сброшен в UpdateFilterButtons
|
// default: зависит от режима, будет сброшен в UpdateFilterButtons
|
||||||
FController.FCTun := False;
|
FController.FCTun := False;
|
||||||
FController.FFilterBW := 2700;
|
// Фильтр (индекс + кромки + производная ширина) — одной точкой, чтобы
|
||||||
|
// состояние не расползалось: ширину отдельно больше не выставляем.
|
||||||
|
FController.SelectFilterSlotState(FController.FFilter);
|
||||||
FSpecDrag := False;
|
FSpecDrag := False;
|
||||||
FController.FDriveLevel := 0;
|
FController.FDriveLevel := 0;
|
||||||
FController.FPAMaxPower := 100.0;
|
FController.FPAMaxPower := 100.0;
|
||||||
@@ -1426,6 +1434,7 @@ begin
|
|||||||
// TX со слайса краснеет полоса ТОГО слайса (DrawSliceFilterMarkers).
|
// TX со слайса краснеет полоса ТОГО слайса (DrawSliceFilterMarkers).
|
||||||
FSpecView.TXOverlay := FController.FTransmitting and
|
FSpecView.TXOverlay := FController.FTransmitting and
|
||||||
(FController.FTxSliceId = 0);
|
(FController.FTxSliceId = 0);
|
||||||
|
PushFilterEdgesToView; // на передаче полоса рисуется по TX-фильтру
|
||||||
ApplySpecViewGridFromState; // сетка спектра под активный тракт
|
ApplySpecViewGridFromState; // сетка спектра под активный тракт
|
||||||
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
|
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
|
||||||
UpdateTXControlAvailability;
|
UpdateTXControlAvailability;
|
||||||
@@ -1526,17 +1535,29 @@ begin
|
|||||||
SavedFilter := FController.FFilter;
|
SavedFilter := FController.FFilter;
|
||||||
SavedFilterBW := FController.FFilterBW;
|
SavedFilterBW := FController.FFilterBW;
|
||||||
UpdateFilterButtons; // подписи + FM-панели под режим
|
UpdateFilterButtons; // подписи + FM-панели под режим
|
||||||
FController.FFilter := SavedFilter;
|
// Возвращаем СЛОТ целиком: кромки — истина, одной ширины мало.
|
||||||
FController.FFilterBW := SavedFilterBW;
|
FController.SelectFilterSlotState(SavedFilter);
|
||||||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
for i := 0 to FILT_SLOTS - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||||
|
PushFilterEdgesToView;
|
||||||
|
// Редактор всегда показывает фильтры ТЕКУЩЕГО режима — иначе правка
|
||||||
|
// ушла бы в чужую таблицу.
|
||||||
|
if (FFilterPop <> nil) and FFilterPop.Visible then
|
||||||
|
begin
|
||||||
|
if FController.FMode = MODE_DMR then FFilterPop.Hide
|
||||||
|
else FFilterPop.LoadFrom(FController, FController.FMode,
|
||||||
|
FController.FFilter);
|
||||||
|
end;
|
||||||
SyncSpecViewFreq; // иначе полоса фильтра рисуется под старый режим
|
SyncSpecViewFreq; // иначе полоса фильтра рисуется под старый режим
|
||||||
UpdateTXControlAvailability;
|
UpdateTXControlAvailability;
|
||||||
UpdateDMRStatus;
|
UpdateDMRStatus;
|
||||||
end;
|
end;
|
||||||
rfFilter:
|
rfFilter:
|
||||||
begin
|
begin
|
||||||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
for i := 0 to FILT_SLOTS - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||||
|
PushFilterEdgesToView;
|
||||||
SyncSpecViewFreq;
|
SyncSpecViewFreq;
|
||||||
|
if (FFilterPop <> nil) and FFilterPop.Visible then
|
||||||
|
FFilterPop.SyncSelection(FController.FMode, FController.FFilter);
|
||||||
end;
|
end;
|
||||||
rfFMDeviation:
|
rfFMDeviation:
|
||||||
if (TrkRawDev <> nil) and (LblRawDev <> nil) then
|
if (TrkRawDev <> nil) and (LblRawDev <> nil) then
|
||||||
@@ -1775,6 +1796,7 @@ begin
|
|||||||
rfFMSQ, rfFMSQLevel]) then
|
rfFMSQ, rfFMSQLevel]) then
|
||||||
begin
|
begin
|
||||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||||
|
FVfoOverlay.SetFilterEdges(FController.FilterLo, FController.FilterHi);
|
||||||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||||||
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
|
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
|
||||||
end;
|
end;
|
||||||
@@ -2315,20 +2337,22 @@ begin
|
|||||||
|
|
||||||
Inc(Y, MODE_PANEL_H + 2);
|
Inc(Y, MODE_PANEL_H + 2);
|
||||||
|
|
||||||
// Filter selector (10 кнопок в 2 ряда по 5)
|
// Filter selector (12 кнопок в 2 ряда по 6: 10 пресетов + VAR1/VAR2).
|
||||||
|
// ПКМ по любой из них открывает редактор фильтров.
|
||||||
PanelFilter := TPanel.Create(Self);
|
PanelFilter := TPanel.Create(Self);
|
||||||
PanelFilter.Parent := PanelLeft;
|
PanelFilter.Parent := PanelLeft;
|
||||||
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
|
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
|
||||||
PanelFilter.BevelOuter := bvNone;
|
PanelFilter.BevelOuter := bvNone;
|
||||||
MakeLbl(PanelFilter, 'FILTER', 4, 2);
|
MakeLbl(PanelFilter, 'FILTER', 4, 2);
|
||||||
for i := 0 to FILT_COUNT - 1 do
|
for i := 0 to FILT_SLOTS - 1 do
|
||||||
begin
|
begin
|
||||||
B := MakeBtn(PanelFilter, '---',
|
B := MakeBtn(PanelFilter, '---',
|
||||||
LeftPanelButtonLeft(LEFT_W, 5, i mod 5),
|
LeftPanelButtonLeft(LEFT_W, FILT_GRID_COLS, i mod FILT_GRID_COLS),
|
||||||
16 + (i div 5) * 27,
|
16 + (i div FILT_GRID_COLS) * 27,
|
||||||
LeftPanelButtonWidth(LEFT_W, 5, i mod 5),
|
LeftPanelButtonWidth(LEFT_W, FILT_GRID_COLS, i mod FILT_GRID_COLS),
|
||||||
BTN_SM, BtnFilterClick);
|
BTN_SM, BtnFilterClick);
|
||||||
B.Tag := i;
|
B.Tag := i;
|
||||||
|
B.OnMouseDown := BtnFilterMouseDown;
|
||||||
BtnFilter[i] := B;
|
BtnFilter[i] := B;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -3121,7 +3145,7 @@ begin
|
|||||||
// Обычные кнопки — все массивы
|
// Обычные кнопки — все массивы
|
||||||
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
|
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
|
||||||
for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active);
|
for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active);
|
||||||
for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
|
for i := 0 to FILT_SLOTS-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
|
||||||
for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active);
|
for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active);
|
||||||
StyleButton(BtnCTun, BtnCTun.Active);
|
StyleButton(BtnCTun, BtnCTun.Active);
|
||||||
StyleButton(BtnDUP, BtnDUP.Active);
|
StyleButton(BtnDUP, BtnDUP.Active);
|
||||||
@@ -4454,10 +4478,38 @@ end;
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
|
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
// Раскладывает N кнопок фильтра: пресеты режима + VAR1/VAR2 следом. Ряды по
|
||||||
|
// FILT_GRID_COLS, но узкие режимы (FM=2+2, WFM=3+2) занимают один ряд целиком,
|
||||||
|
// растягивая кнопки — как было до появления VAR.
|
||||||
|
procedure TMainForm.LayoutFilterButtons(PresetCount: Integer);
|
||||||
|
var
|
||||||
|
i, Total, Cols, Rows, RowOf, ColOf, InRow: Integer;
|
||||||
|
begin
|
||||||
|
Total := PresetCount + 2; // + VAR1/VAR2
|
||||||
|
if Total <= FILT_GRID_COLS then Cols := Total // FM/WFM: всё в один ряд
|
||||||
|
else Cols := FILT_GRID_COLS;
|
||||||
|
Rows := (Total + Cols - 1) div Cols;
|
||||||
|
for i := 0 to FILT_SLOTS - 1 do
|
||||||
|
begin
|
||||||
|
if i < PresetCount then InRow := i // пресет на своём месте
|
||||||
|
else if i = FILT_VAR1 then InRow := PresetCount // VAR1 сразу за пресетами
|
||||||
|
else if i = FILT_VAR2 then InRow := PresetCount + 1
|
||||||
|
else begin BtnFilter[i].Visible := False; Continue; end;
|
||||||
|
RowOf := InRow div Cols;
|
||||||
|
ColOf := InRow mod Cols;
|
||||||
|
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, Cols, ColOf);
|
||||||
|
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, Cols, ColOf);
|
||||||
|
BtnFilter[i].Top := 16 + RowOf * 27;
|
||||||
|
BtnFilter[i].Caption := FController.FilterSlot(FController.FMode, i).Name;
|
||||||
|
BtnFilter[i].Visible := True;
|
||||||
|
StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||||
|
end;
|
||||||
|
PanelFilter.Height := 16 + Rows * 27 + 4;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.UpdateFilterButtons;
|
procedure TMainForm.UpdateFilterButtons;
|
||||||
var
|
var
|
||||||
i: Integer;
|
i: Integer;
|
||||||
Names: array[0..FILT_COUNT-1] of string;
|
|
||||||
DefIdx: Integer;
|
DefIdx: Integer;
|
||||||
begin
|
begin
|
||||||
// DMR has a mandatory 12.5 kHz channel. There is nothing for the operator
|
// DMR has a mandatory 12.5 kHz channel. There is nothing for the operator
|
||||||
@@ -4465,26 +4517,11 @@ begin
|
|||||||
PanelFilter.Visible := FController.FMode <> MODE_DMR;
|
PanelFilter.Visible := FController.FMode <> MODE_DMR;
|
||||||
if FController.FMode = MODE_FM then
|
if FController.FMode = MODE_FM then
|
||||||
begin
|
begin
|
||||||
// FM: show only NFM and WFM buttons, resize them to fill the row
|
// FM: NFM/FM + VAR1/VAR2 в один ряд (панель остаётся низкой, SQL/CTCSS
|
||||||
|
// садятся сразу под ней).
|
||||||
CloseCTCSSPopup;
|
CloseCTCSSPopup;
|
||||||
if FController.FFilter > 1 then FController.FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
|
FController.SanitizeFilterState;
|
||||||
FController.FFilterBW := FILT_FM_BW[FController.FFilter];
|
LayoutFilterButtons(FILT_FM_COUNT);
|
||||||
FController.FFMDeviation := FILT_FM_DEV[FController.FFilter];
|
|
||||||
BtnFilter[0].Caption := FILT_FM_NAMES[0];
|
|
||||||
BtnFilter[0].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 0);
|
|
||||||
BtnFilter[0].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 0);
|
|
||||||
BtnFilter[0].Top := 16;
|
|
||||||
BtnFilter[0].Visible := True;
|
|
||||||
StyleButton(BtnFilter[0], FController.FFilter = 0);
|
|
||||||
BtnFilter[1].Caption := FILT_FM_NAMES[1];
|
|
||||||
BtnFilter[1].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 1);
|
|
||||||
BtnFilter[1].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 1);
|
|
||||||
BtnFilter[1].Top := 16;
|
|
||||||
BtnFilter[1].Visible := True;
|
|
||||||
StyleButton(BtnFilter[1], FController.FFilter = 1);
|
|
||||||
for i := 2 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
|
||||||
// Shrink panel to fit label + 1 button row so SQL/CTCSS sit right below
|
|
||||||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
|
||||||
if PanelFMSQ <> nil then
|
if PanelFMSQ <> nil then
|
||||||
begin
|
begin
|
||||||
PanelFMSQ.Visible := True;
|
PanelFMSQ.Visible := True;
|
||||||
@@ -4536,21 +4573,10 @@ begin
|
|||||||
|
|
||||||
if FController.FMode = MODE_WFM then
|
if FController.FMode = MODE_WFM then
|
||||||
begin
|
begin
|
||||||
// WFM: полосы вещательного канала — три кнопки в один ряд.
|
// WFM: полосы вещательного канала — три кнопки + VAR1/VAR2 в один ряд.
|
||||||
CloseCTCSSPopup;
|
CloseCTCSSPopup;
|
||||||
if FController.FFilter > FILT_WFM_COUNT - 1 then FController.FFilter := FILT_WFM_DEF;
|
FController.SanitizeFilterState;
|
||||||
FController.FFilterBW := FILT_WFM_BW[FController.FFilter];
|
LayoutFilterButtons(FILT_WFM_COUNT);
|
||||||
for i := 0 to FILT_WFM_COUNT - 1 do
|
|
||||||
begin
|
|
||||||
BtnFilter[i].Caption := FILT_WFM_NAMES[i];
|
|
||||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_WFM_COUNT, i);
|
|
||||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_WFM_COUNT, i);
|
|
||||||
BtnFilter[i].Top := 16;
|
|
||||||
BtnFilter[i].Visible := True;
|
|
||||||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
|
||||||
end;
|
|
||||||
for i := FILT_WFM_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
|
||||||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
|
||||||
RelayoutBelowBands;
|
RelayoutBelowBands;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
@@ -4558,20 +4584,8 @@ begin
|
|||||||
if FController.FMode = MODE_FMRAW then
|
if FController.FMode = MODE_FMRAW then
|
||||||
begin
|
begin
|
||||||
CloseCTCSSPopup;
|
CloseCTCSSPopup;
|
||||||
if FController.FFilter > FILT_RAW_COUNT - 1 then
|
FController.SanitizeFilterState;
|
||||||
FController.FFilter := FILT_RAW_DEF;
|
LayoutFilterButtons(FILT_RAW_COUNT);
|
||||||
FController.FFilterBW := FILT_RAW_BW[FController.FFilter];
|
|
||||||
for i := 0 to FILT_RAW_COUNT - 1 do
|
|
||||||
begin
|
|
||||||
BtnFilter[i].Caption := FILT_RAW_NAMES[i];
|
|
||||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_RAW_COUNT, i);
|
|
||||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_RAW_COUNT, i);
|
|
||||||
BtnFilter[i].Top := 16;
|
|
||||||
BtnFilter[i].Visible := True;
|
|
||||||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
|
||||||
end;
|
|
||||||
for i := FILT_RAW_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
|
||||||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
|
||||||
if PanelRawDev <> nil then
|
if PanelRawDev <> nil then
|
||||||
begin
|
begin
|
||||||
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
|
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
|
||||||
@@ -4585,67 +4599,18 @@ begin
|
|||||||
if FController.FMode = MODE_DMR then
|
if FController.FMode = MODE_DMR then
|
||||||
begin
|
begin
|
||||||
CloseCTCSSPopup;
|
CloseCTCSSPopup;
|
||||||
FController.FFilter := 0;
|
FController.SanitizeFilterState;
|
||||||
FController.FFilterBW := FILT_DMR_BW;
|
for i := 0 to FILT_SLOTS - 1 do BtnFilter[i].Visible := False;
|
||||||
FController.FFMDeviation := DMR_FM_DEV;
|
|
||||||
for i := 0 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
|
||||||
RelayoutBelowBands;
|
RelayoutBelowBands;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
for i := 0 to FILT_COUNT - 1 do
|
// Прочие режимы: смена режима возвращает заводской индекс фильтра (как было
|
||||||
begin
|
// до таблицы), кромки и подписи приезжают из таблицы контроллера.
|
||||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
|
PanelFilter.Height := 74;
|
||||||
BtnFilter[i].Top := 16 + (i div 5) * 27;
|
DefIdx := FilterDefIdx(FController.FMode);
|
||||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, 5, i mod 5);
|
FController.SelectFilterSlotState(DefIdx);
|
||||||
BtnFilter[i].Visible := True;
|
LayoutFilterButtons(FILT_COUNT);
|
||||||
end;
|
|
||||||
|
|
||||||
case FController.FMode of
|
|
||||||
MODE_LSB, MODE_USB:
|
|
||||||
begin
|
|
||||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
|
|
||||||
DefIdx := FILT_SSB_DEF;
|
|
||||||
end;
|
|
||||||
MODE_DIGU, MODE_DIGL:
|
|
||||||
begin
|
|
||||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DIGI_NAMES[i];
|
|
||||||
DefIdx := FILT_DIGI_DEF;
|
|
||||||
end;
|
|
||||||
MODE_DSB:
|
|
||||||
begin
|
|
||||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i];
|
|
||||||
DefIdx := FILT_DSB_DEF;
|
|
||||||
end;
|
|
||||||
MODE_CWL, MODE_CWU:
|
|
||||||
begin
|
|
||||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
|
|
||||||
DefIdx := FILT_CW_DEF;
|
|
||||||
end;
|
|
||||||
else begin
|
|
||||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i];
|
|
||||||
DefIdx := FILT_AM_DEF;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
FController.FFilter := DefIdx;
|
|
||||||
case FController.FMode of
|
|
||||||
MODE_LSB, MODE_USB:
|
|
||||||
FController.FFilterBW := FILT_SSB_BW[DefIdx];
|
|
||||||
MODE_DIGU, MODE_DIGL:
|
|
||||||
FController.FFilterBW := FILT_DIGI_BW[DefIdx];
|
|
||||||
MODE_DSB:
|
|
||||||
FController.FFilterBW := FILT_DSB_BW[DefIdx];
|
|
||||||
MODE_CWL, MODE_CWU:
|
|
||||||
FController.FFilterBW := FILT_CW_BW[DefIdx];
|
|
||||||
else FController.FFilterBW := FILT_AM_BW[DefIdx];
|
|
||||||
end;
|
|
||||||
|
|
||||||
for i := 0 to FILT_COUNT-1 do
|
|
||||||
begin
|
|
||||||
BtnFilter[i].Caption := Names[i];
|
|
||||||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
|
||||||
end;
|
|
||||||
RelayoutBelowBands;
|
RelayoutBelowBands;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -5105,6 +5070,17 @@ begin
|
|||||||
UpdateBeaconButton;
|
UpdateBeaconButton;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
// ЛКМ по краю полосы фильтра — тянем именно этот край (левый/правый
|
||||||
|
// независимо). Проверяется РАНЬШЕ слайс-маркера и панорамирования, иначе
|
||||||
|
// край у самой несущей (узкий CW) было бы не поймать.
|
||||||
|
if (PbSpectrum.Width > 0) and (FilterEdgeAtPixel(X) <> 0) then
|
||||||
|
begin
|
||||||
|
FEdgeDrag := FilterEdgeAtPixel(X);
|
||||||
|
FEdgeDragLo := FController.FilterLo;
|
||||||
|
FEdgeDragHi := FController.FilterHi;
|
||||||
|
PbSpectrum.Cursor := crSizeWE;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
// ЛКМ по маркеру несущей слайса — начинаем его перетаскивание (перетюн).
|
// ЛКМ по маркеру несущей слайса — начинаем его перетаскивание (перетюн).
|
||||||
if (PbSpectrum.Width > 0) then
|
if (PbSpectrum.Width > 0) then
|
||||||
begin
|
begin
|
||||||
@@ -5164,6 +5140,13 @@ procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
|||||||
var
|
var
|
||||||
VC, VS, NewHz, LoHz, HiHz: Double;
|
VC, VS, NewHz, LoHz, HiHz: Double;
|
||||||
begin
|
begin
|
||||||
|
// Перетаскивание края полосы фильтра.
|
||||||
|
if FEdgeDrag <> 0 then
|
||||||
|
begin
|
||||||
|
if ssLeft in Shift then DoFilterEdgeDrag(X, Shift);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
// Перетаскивание маркера несущей слайса — перетюн в пределах захваченной полосы.
|
// Перетаскивание маркера несущей слайса — перетюн в пределах захваченной полосы.
|
||||||
if FSliceDrag then
|
if FSliceDrag then
|
||||||
begin
|
begin
|
||||||
@@ -5192,6 +5175,8 @@ begin
|
|||||||
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
||||||
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
||||||
PbSpectrum.Cursor := crHandPoint
|
PbSpectrum.Cursor := crHandPoint
|
||||||
|
else if FilterEdgeAtPixel(X) <> 0 then
|
||||||
|
PbSpectrum.Cursor := crSizeWE // над краем фильтра — край можно тянуть
|
||||||
else if FPan.SliceAtPixel(X) <> 0 then
|
else if FPan.SliceAtPixel(X) <> 0 then
|
||||||
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
||||||
else
|
else
|
||||||
@@ -5211,6 +5196,12 @@ end;
|
|||||||
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
begin
|
begin
|
||||||
|
if FEdgeDrag <> 0 then
|
||||||
|
begin
|
||||||
|
FEdgeDrag := 0;
|
||||||
|
PbSpectrum.Cursor := crDefault;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
if FSliceDrag then
|
if FSliceDrag then
|
||||||
begin
|
begin
|
||||||
FSliceDrag := False;
|
FSliceDrag := False;
|
||||||
@@ -5335,11 +5326,130 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
// Логика (FFilter/FFilterBW по таблицам режима, ApplyModeFilter, band cache)
|
// Логика (слот → кромки, ApplyModeFilter, band cache) — в контроллере;
|
||||||
// — в контроллере; рендер — в OnControllerState(rfFilter).
|
// рендер — в OnControllerState(rfFilter).
|
||||||
FController.SetFilterIdx((Sender as TFlatButton).Tag);
|
FController.SetFilterIdx((Sender as TFlatButton).Tag);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TMainForm.FilterEdgeAtPixel(X: Integer): Integer;
|
||||||
|
// -1 = курсор у левого края полосы фильтра, +1 = у правого, 0 = мимо.
|
||||||
|
// Работает по тем же кромкам, что нарисованы (главный VFO, не слайсы).
|
||||||
|
var
|
||||||
|
VC, VS, Lo, Hi: Double;
|
||||||
|
W, X1, X2: Integer;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
W := PbSpectrum.Width;
|
||||||
|
if (W <= 0) or (FController.FMode = MODE_DMR) then Exit; // DMR: полоса фиксирована
|
||||||
|
if FController.FTransmitting then Exit; // на передаче фильтр мышью не крутим
|
||||||
|
FController.GetViewWindow(VC, VS);
|
||||||
|
if VS <= 0 then Exit;
|
||||||
|
Lo := ActiveVfoFreq + FController.FilterLo;
|
||||||
|
Hi := ActiveVfoFreq + FController.FilterHi;
|
||||||
|
X1 := Round((Lo - VC + VS / 2) / VS * W);
|
||||||
|
X2 := Round((Hi - VC + VS / 2) / VS * W);
|
||||||
|
// Слишком узкая на экране полоса (CW на полном спане) — края слиплись с
|
||||||
|
// несущей, и захват съел бы обычный клик-тюн. Тогда фильтр правится только
|
||||||
|
// в редакторе; чтобы потянуть мышью, достаточно зазумиться.
|
||||||
|
if X2 - X1 < 2 * EDGE_GRAB_PX + 2 then Exit;
|
||||||
|
// Приоритет правому краю: при узкой полосе левый иначе «съедает» оба.
|
||||||
|
if Abs(X - X2) <= EDGE_GRAB_PX then Result := 1
|
||||||
|
else if Abs(X - X1) <= EDGE_GRAB_PX then Result := -1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.DoFilterEdgeDrag(X: Integer; Shift: TShiftState);
|
||||||
|
// Тянем ОДИН край; второй стоит. Alt — симметрично (второй край зеркалит).
|
||||||
|
// Именно Alt, а не Ctrl: Ctrl+ЛКМ по спектру уже создаёт слайс. Модификатор
|
||||||
|
// читаем из Shift обработчика, а не GetKeyState — надёжнее на Wayland.
|
||||||
|
var
|
||||||
|
VC, VS, Hz: Double;
|
||||||
|
W, Lo, Hi, D: Integer;
|
||||||
|
begin
|
||||||
|
W := PbSpectrum.Width;
|
||||||
|
if W <= 0 then Exit;
|
||||||
|
FController.GetViewWindow(VC, VS);
|
||||||
|
if VS <= 0 then Exit;
|
||||||
|
Hz := PixelToFreq(X, W, VC, VS) - ActiveVfoFreq;
|
||||||
|
Lo := FEdgeDragLo;
|
||||||
|
Hi := FEdgeDragHi;
|
||||||
|
if FEdgeDrag > 0 then Hi := Round(Hz / EDGE_SNAP_HZ) * EDGE_SNAP_HZ
|
||||||
|
else Lo := Round(Hz / EDGE_SNAP_HZ) * EDGE_SNAP_HZ;
|
||||||
|
if ssAlt in Shift then
|
||||||
|
begin
|
||||||
|
// Симметрия относительно центра исходной полосы.
|
||||||
|
D := (FEdgeDragLo + FEdgeDragHi) div 2;
|
||||||
|
if FEdgeDrag > 0 then Lo := 2 * D - Hi
|
||||||
|
else Hi := 2 * D - Lo;
|
||||||
|
end;
|
||||||
|
if Hi - Lo < FILT_MIN_WIDTH then Exit; // схлопывать полосу не даём
|
||||||
|
FController.SetFilterEdges(Lo, Hi); // уходит в VAR — пресеты целы
|
||||||
|
FSpectrumDirty := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.PushFilterEdgesToView;
|
||||||
|
// Кромки в спектр: RX — то, что реально стоит в приёмнике (фильтр может быть
|
||||||
|
// произвольным), TX — из TXSignedEdges движка. Полоса на экране обязана
|
||||||
|
// совпадать с тем, что слышно и что уходит в эфир.
|
||||||
|
var Lo, Hi: Integer;
|
||||||
|
begin
|
||||||
|
if FSpecView = nil then Exit;
|
||||||
|
FSpecView.SetRXEdges(FController.FilterLo, FController.FilterHi);
|
||||||
|
FController.TXFilterEdges(Lo, Hi);
|
||||||
|
FSpecView.SetTXEdges(Lo, Hi);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
|
||||||
|
// у них это пункт контекстного меню «RX1 Filter Configure»). ЛКМ не трогаем —
|
||||||
|
// иначе окно вылезало бы при каждом обычном переключении фильтра.
|
||||||
|
begin
|
||||||
|
if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OpenFilterEditor(SlotIdx: Integer);
|
||||||
|
var Anchor: TFlatButton;
|
||||||
|
begin
|
||||||
|
if FController.FMode = MODE_DMR then Exit; // у DMR полоса жёстко задана
|
||||||
|
if FFilterPop = nil then
|
||||||
|
begin
|
||||||
|
FFilterPop := TFilterPopup.Create(Self);
|
||||||
|
FFilterPop.OnSlotEdited := OnFilterSlotEdited;
|
||||||
|
FFilterPop.OnSlotReset := OnFilterSlotReset;
|
||||||
|
end;
|
||||||
|
if FFilterPop.Visible then begin FFilterPop.Hide; Exit; end; // повторный ПКМ закрывает
|
||||||
|
Anchor := BtnFilter[EnsureRange(SlotIdx, 0, FILT_SLOTS - 1)];
|
||||||
|
if (Anchor = nil) or not Anchor.Visible then Anchor := BtnFilter[0];
|
||||||
|
FFilterPop.PopupBelow(FController, Anchor, FController.FMode, SlotIdx);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Обновляет ТОЛЬКО подписи/раскладку кнопок фильтра. Полный
|
||||||
|
// UpdateFilterButtons для не-FM режимов сбрасывает выбранный фильтр на
|
||||||
|
// заводской — из редактора это отобрало бы у оператора текущий фильтр на
|
||||||
|
// каждом нажатии клавиши.
|
||||||
|
procedure TMainForm.RefreshFilterCaptions;
|
||||||
|
begin
|
||||||
|
if FController.FMode = MODE_DMR then Exit;
|
||||||
|
LayoutFilterButtons(FilterPresetCount(FController.FMode));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnFilterSlotEdited(Mode, Idx: Integer; const S: TFilterSlot);
|
||||||
|
begin
|
||||||
|
FController.SetFilterSlot(Mode, Idx, S);
|
||||||
|
RefreshFilterCaptions; // подпись слота могла измениться
|
||||||
|
OnControllerState(FController, rfFilter);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnFilterSlotReset(Mode, Idx: Integer);
|
||||||
|
begin
|
||||||
|
if Idx < 0 then FController.ResetFilterSet(Mode)
|
||||||
|
else FController.ResetFilterSlot(Mode, Idx);
|
||||||
|
if FFilterPop <> nil then
|
||||||
|
FFilterPop.LoadFrom(FController, Mode, FFilterPop.SlotIdx);
|
||||||
|
RefreshFilterCaptions;
|
||||||
|
OnControllerState(FController, rfFilter);
|
||||||
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// FM CTCSS helpers
|
// FM CTCSS helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1339,6 +1339,9 @@ begin
|
|||||||
else BW := S.FilterHi - S.FilterLo;
|
else BW := S.FilterHi - S.FilterLo;
|
||||||
end;
|
end;
|
||||||
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
|
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
|
||||||
|
// Ширина выше — для подписи во флаге; полосу на спектре рисуем по настоящим
|
||||||
|
// кромкам, иначе несимметричный фильтр (слайс-CAT ZZFL/ZZFH) уезжал бы.
|
||||||
|
O.SetFilterEdges(S.FilterLo, S.FilterHi);
|
||||||
O.SetDSPState(S.NRMode, S.NBMode, S.SNB, S.ANF,
|
O.SetDSPState(S.NRMode, S.NBMode, S.SNB, S.ANF,
|
||||||
TRadioController.AGCModeToUI(S.AGC));
|
TRadioController.AGCModeToUI(S.AGC));
|
||||||
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
|
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
|
||||||
|
|||||||
+176
-136
@@ -215,6 +215,11 @@ type
|
|||||||
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||||||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||||||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||||||
|
// Единственная точка, где выбранный слот превращается в состояние тракта
|
||||||
|
// (FFilter/кромки/производная BW/FM-девиация). Тракт не трогает — только state.
|
||||||
|
procedure ApplyFilterSlot(Idx: Integer);
|
||||||
|
function ClampFilterIdx(Mode, Idx: Integer): Integer;
|
||||||
|
procedure CacheFilterToBand; // фильтр текущего банда в band-cache
|
||||||
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
||||||
function AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
|
function AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
|
||||||
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
||||||
@@ -275,8 +280,14 @@ type
|
|||||||
FVfoA, FVfoB: Double;
|
FVfoA, FVfoB: Double;
|
||||||
FActiveVfo: Integer;
|
FActiveVfo: Integer;
|
||||||
FMode: Integer;
|
FMode: Integer;
|
||||||
FFilter: Integer;
|
FFilter: Integer; // слот 0..FILT_SLOTS-1 (10/11 = VAR1/VAR2)
|
||||||
|
// Знаковые кромки текущего фильтра — ИСТИНА. FFilterBW производная
|
||||||
|
// (FilterBWFromEdges) и оставлена ради прежних читателей (web, зум,
|
||||||
|
// band-cache, CAT); писать в неё напрямую нельзя.
|
||||||
|
FFilterLo: Integer;
|
||||||
|
FFilterHi: Integer;
|
||||||
FFilterBW: Integer;
|
FFilterBW: Integer;
|
||||||
|
FFilterTable: TFilterTable; // редактируемые фильтры по режиму
|
||||||
FAGCMode: Integer;
|
FAGCMode: Integer;
|
||||||
FAGCTop: Integer;
|
FAGCTop: Integer;
|
||||||
FRxGainMode: Integer; // Pluto hw-gain: 0=manual,1=fast,2=slow,3=hybrid
|
FRxGainMode: Integer; // Pluto hw-gain: 0=manual,1=fast,2=slow,3=hybrid
|
||||||
@@ -670,6 +681,23 @@ type
|
|||||||
procedure SetSliceFilter(Id, Low, High: Integer);
|
procedure SetSliceFilter(Id, Low, High: Integer);
|
||||||
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
|
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
|
||||||
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||||
|
// --- Редактируемая таблица фильтров (пресеты + VAR1/VAR2 на режим) ---
|
||||||
|
function FilterSlot(Mode, Idx: Integer): TFilterSlot;
|
||||||
|
procedure SetFilterSlot(Mode, Idx: Integer; const S: TFilterSlot);
|
||||||
|
procedure ResetFilterSlot(Mode, Idx: Integer);
|
||||||
|
procedure ResetFilterSet(Mode: Integer);
|
||||||
|
// Произвольные кромки (drag на спектре, CAT ZZFL/ZZFH, web): пишутся в
|
||||||
|
// VAR1 и переключают на него, если текущий слот — пресет. Пресеты таким
|
||||||
|
// образом невозможно испортить случайным движением мыши.
|
||||||
|
procedure SetFilterEdges(LoHz, HiHz: Integer);
|
||||||
|
// Только state, без применения к тракту: UpdateFilterButtons — рендер,
|
||||||
|
// DSP в его путях перестраивается отдельно (как было до таблицы).
|
||||||
|
procedure SelectFilterSlotState(Idx: Integer);
|
||||||
|
procedure SanitizeFilterState;
|
||||||
|
// Знаковые края TX-полосы (для отрисовки полосы фильтра при передаче).
|
||||||
|
procedure TXFilterEdges(out Lo, Hi: Integer);
|
||||||
|
property FilterLo: Integer read FFilterLo;
|
||||||
|
property FilterHi: Integer read FFilterHi;
|
||||||
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
|
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
|
||||||
class function AGCModeToUI(A: TWDSPAGCMode): Integer; // и обратно
|
class function AGCModeToUI(A: TWDSPAGCMode): Integer; // и обратно
|
||||||
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||||||
@@ -1037,7 +1065,9 @@ begin
|
|||||||
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
|
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
|
||||||
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
|
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
|
||||||
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
|
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
|
||||||
FMode := MODE_USB; FFilter := 5; FFilterBW := 2700;
|
FMode := MODE_USB; FFilter := 5;
|
||||||
|
DefaultFilterTable(FFilterTable); // до подключения — заводские фильтры
|
||||||
|
ApplyFilterSlot(FFilter);
|
||||||
FAGCMode := 1; FAGCTop := 90;
|
FAGCMode := 1; FAGCTop := 90;
|
||||||
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
|
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
|
||||||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||||||
@@ -3161,18 +3191,10 @@ end;
|
|||||||
|
|
||||||
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||||||
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||||
var Half: Integer;
|
// Обёртка над единственной таблицей знака (Settings.FilterEdgesFromBW) —
|
||||||
|
// оставлена ради прежних вызывающих (MainForm, SetSliceModeBW).
|
||||||
begin
|
begin
|
||||||
Half := BW div 2;
|
FilterEdgesFromBW(Mode, BW, Lo, Hi);
|
||||||
case Mode of
|
|
||||||
MODE_LSB: begin Lo := -BW; Hi := -100; end;
|
|
||||||
MODE_USB: begin Lo := 100; Hi := BW; end;
|
|
||||||
// DIGI: полоса от нуля — поднесущие цифровых видов начинаются низко,
|
|
||||||
// срез на 100 Гц как в SSB съел бы их.
|
|
||||||
MODE_DIGL: begin Lo := -BW; Hi := 0; end;
|
|
||||||
MODE_DIGU: begin Lo := 0; Hi := BW; end;
|
|
||||||
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM/WFM
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetSliceModeBW(Id, Mode, BW: Integer);
|
procedure TRadioController.SetSliceModeBW(Id, Mode, BW: Integer);
|
||||||
@@ -3189,7 +3211,10 @@ procedure TRadioController.ApplyModeFilter;
|
|||||||
var
|
var
|
||||||
Lo, Hi: Integer;
|
Lo, Hi: Integer;
|
||||||
begin
|
begin
|
||||||
FilterEdgesFor(FMode, FFilterBW, Lo, Hi);
|
// Кромки берём как есть: они уже знаковые и уже под текущий режим (таблица
|
||||||
|
// фильтров ведётся ПО режиму), конвертировать боковую больше нечем и негде.
|
||||||
|
Lo := FFilterLo;
|
||||||
|
Hi := FFilterHi;
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
begin
|
begin
|
||||||
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
|
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
|
||||||
@@ -3216,61 +3241,53 @@ end;
|
|||||||
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
|
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
|
||||||
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
|
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
|
||||||
// корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт.
|
// корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт.
|
||||||
|
function TRadioController.ClampFilterIdx(Mode, Idx: Integer): Integer;
|
||||||
|
// VAR1/VAR2 есть у любого режима, поэтому проверяем только пресетную часть.
|
||||||
|
// Выход за таблицу режима возвращает ЗАВОДСКОЙ индекс (а не крайний) — так
|
||||||
|
// вело себя прежнее «if FFilter > FILT_WFM_COUNT-1 then FILT_WFM_DEF».
|
||||||
|
begin
|
||||||
|
Result := Idx;
|
||||||
|
if (Idx = FILT_VAR1) or (Idx = FILT_VAR2) then Exit;
|
||||||
|
if (Idx < 0) or (Idx > FilterPresetCount(Mode) - 1) then
|
||||||
|
Result := FilterDefIdx(Mode);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ApplyFilterSlot(Idx: Integer);
|
||||||
|
var S: TFilterSlot;
|
||||||
|
begin
|
||||||
|
FFilter := ClampFilterIdx(FMode, Idx);
|
||||||
|
S := FFilterTable[FMode][FFilter];
|
||||||
|
FFilterLo := S.Lo;
|
||||||
|
FFilterHi := S.Hi;
|
||||||
|
FFilterBW := FilterBWFromEdges(FMode, S.Lo, S.Hi);
|
||||||
|
// FM-девиация привязана к пресету (NFM/WFM), у VAR берём девиацию дефолта.
|
||||||
|
if FMode = MODE_FM then
|
||||||
|
begin
|
||||||
|
if FFilter <= 1 then FFMDeviation := FILT_FM_DEV[FFilter]
|
||||||
|
else FFMDeviation := FILT_FM_DEV[FILT_FM_DEF];
|
||||||
|
end
|
||||||
|
else if FMode = MODE_DMR then
|
||||||
|
FFMDeviation := DMR_FM_DEV;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TRadioController.ApplyModeDefaults;
|
procedure TRadioController.ApplyModeDefaults;
|
||||||
begin
|
begin
|
||||||
|
// FM/WFM/DMR/FMRAW сохраняют выбранный пресет (NFM↔WFM и т.п.), остальные
|
||||||
|
// режимы возвращаются к заводскому индексу — как было до таблицы фильтров.
|
||||||
case FMode of
|
case FMode of
|
||||||
MODE_FM:
|
MODE_FM, MODE_WFM, MODE_DMR, MODE_FMRAW: ApplyFilterSlot(FFilter);
|
||||||
begin
|
else ApplyFilterSlot(FilterDefIdx(FMode));
|
||||||
if FFilter > 1 then FFilter := FILT_FM_DEF;
|
|
||||||
FFilterBW := FILT_FM_BW[FFilter];
|
|
||||||
FFMDeviation := FILT_FM_DEV[FFilter];
|
|
||||||
end;
|
|
||||||
MODE_WFM:
|
|
||||||
begin
|
|
||||||
if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
|
|
||||||
FFilterBW := FILT_WFM_BW[FFilter];
|
|
||||||
end;
|
|
||||||
MODE_DMR:
|
|
||||||
begin
|
|
||||||
FFilter := 0;
|
|
||||||
FFilterBW := FILT_DMR_BW;
|
|
||||||
FFMDeviation := DMR_FM_DEV;
|
|
||||||
end;
|
|
||||||
MODE_FMRAW:
|
|
||||||
begin
|
|
||||||
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
|
|
||||||
FFilterBW := FILT_RAW_BW[FFilter];
|
|
||||||
end;
|
|
||||||
MODE_LSB, MODE_USB:
|
|
||||||
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
|
|
||||||
MODE_DIGU, MODE_DIGL:
|
|
||||||
begin FFilter := FILT_DIGI_DEF; FFilterBW := FILT_DIGI_BW[FFilter]; end;
|
|
||||||
MODE_DSB:
|
|
||||||
begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
|
|
||||||
MODE_CWL, MODE_CWU:
|
|
||||||
begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end;
|
|
||||||
else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end;
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
|
function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
|
||||||
// Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons,
|
// Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons,
|
||||||
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
|
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
|
||||||
|
var S: TFilterSlot;
|
||||||
begin
|
begin
|
||||||
case M of
|
M := EnsureRange(M, 0, CFG_MODE_MAX);
|
||||||
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)];
|
S := FFilterTable[M][ClampFilterIdx(M, Idx)];
|
||||||
MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
|
Result := FilterBWFromEdges(M, S.Lo, S.Hi);
|
||||||
MODE_DMR: Result := FILT_DMR_BW;
|
|
||||||
MODE_FMRAW: Result := FILT_RAW_BW[EnsureRange(Idx, 0, FILT_RAW_COUNT - 1)];
|
|
||||||
MODE_LSB, MODE_USB:
|
|
||||||
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
|
||||||
MODE_DIGU, MODE_DIGL:
|
|
||||||
Result := FILT_DIGI_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
|
||||||
MODE_DSB: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
|
||||||
MODE_CWL, MODE_CWU:
|
|
||||||
Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
|
||||||
else Result := FILT_AM_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.FilterIdxFor(M, BW: Integer): Integer;
|
function TRadioController.FilterIdxFor(M, BW: Integer): Integer;
|
||||||
@@ -3346,26 +3363,10 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SanitizeFilterForMode;
|
procedure TRadioController.SanitizeFilterForMode;
|
||||||
// Для FM индексы фильтров только 0/1 (NFM/WFM); приводим FFilter и
|
// У FM/DMR/FMRAW пресетов меньше, чем у SSB: приводим индекс к допустимому и
|
||||||
// синхронизируем FFilterBW/FFMDeviation. Чистая (state) часть UpdateFilterButtons.
|
// синхронизируем кромки/BW/девиацию. Чистая (state) часть UpdateFilterButtons.
|
||||||
begin
|
begin
|
||||||
if FMode = MODE_FM then
|
if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then ApplyFilterSlot(FFilter);
|
||||||
begin
|
|
||||||
if FFilter > 1 then FFilter := FILT_FM_DEF;
|
|
||||||
FFilterBW := FILT_FM_BW[FFilter];
|
|
||||||
FFMDeviation := FILT_FM_DEV[FFilter];
|
|
||||||
end;
|
|
||||||
if FMode = MODE_DMR then
|
|
||||||
begin
|
|
||||||
FFilter := 0;
|
|
||||||
FFilterBW := FILT_DMR_BW;
|
|
||||||
FFMDeviation := DMR_FM_DEV;
|
|
||||||
end;
|
|
||||||
if FMode = MODE_FMRAW then
|
|
||||||
begin
|
|
||||||
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
|
|
||||||
FFilterBW := FILT_RAW_BW[FFilter];
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.MakeBandSettings: TBandSettings;
|
function TRadioController.MakeBandSettings: TBandSettings;
|
||||||
@@ -3544,9 +3545,10 @@ begin
|
|||||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||||||
|
|
||||||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
||||||
FFilter := B.FilterIdx;
|
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
|
||||||
FFilterBW := B.FilterBW;
|
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины —
|
||||||
SanitizeFilterForMode;
|
// ровно та болезнь, от которой уходили.
|
||||||
|
ApplyFilterSlot(B.FilterIdx);
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
|
|
||||||
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
|
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
|
||||||
@@ -3985,72 +3987,107 @@ end;
|
|||||||
|
|
||||||
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
||||||
begin
|
begin
|
||||||
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
|
// Публичная команда: Idx может прийти из CAT/web без проверки. Клампится
|
||||||
// Idx может прийти из CAT/web без проверки; FM/WFM-таблицы короче остальных.
|
// внутри ApplyFilterSlot (у FM/WFM/FMRAW пресетов меньше; VAR всегда есть).
|
||||||
case FMode of
|
ApplyFilterSlot(Idx);
|
||||||
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
|
CacheFilterToBand;
|
||||||
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
|
ApplyModeFilter;
|
||||||
MODE_DMR: Idx := 0;
|
Changed(rfFilter);
|
||||||
MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1);
|
end;
|
||||||
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
|
|
||||||
|
procedure TRadioController.CacheFilterToBand;
|
||||||
|
begin
|
||||||
|
FBandCache[FCurrentBand].FilterIdx := FFilter;
|
||||||
|
FBandCache[FCurrentBand].FilterBW := FFilterBW; // легаси-поле, для старых конфигов
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.SelectFilterSlotState(Idx: Integer);
|
||||||
|
begin
|
||||||
|
ApplyFilterSlot(Idx);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.SanitizeFilterState;
|
||||||
|
begin
|
||||||
|
SanitizeFilterForMode;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.TXFilterEdges(out Lo, Hi: Integer);
|
||||||
|
begin
|
||||||
|
Lo := 0; Hi := 0;
|
||||||
|
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.TXFilterEdgesHz(Lo, Hi);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRadioController.FilterSlot(Mode, Idx: Integer): TFilterSlot;
|
||||||
|
begin
|
||||||
|
Result := FFilterTable[EnsureRange(Mode, 0, CFG_MODE_MAX)]
|
||||||
|
[EnsureRange(Idx, 0, FILT_SLOTS - 1)];
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.SetFilterSlot(Mode, Idx: Integer; const S: TFilterSlot);
|
||||||
|
begin
|
||||||
|
Mode := EnsureRange(Mode, 0, CFG_MODE_MAX);
|
||||||
|
Idx := EnsureRange(Idx, 0, FILT_SLOTS - 1);
|
||||||
|
if S.Hi <= S.Lo then Exit; // пустая полоса = глухой приём
|
||||||
|
FFilterTable[Mode][Idx] := S;
|
||||||
|
if Assigned(FSettings) then FSettings.SaveFilters(FDevMAC, FFilterTable);
|
||||||
|
// Правка активного слота слышна сразу — форма настроек в EWSDR мгновенная.
|
||||||
|
if (Mode = FMode) and (Idx = FFilter) then
|
||||||
|
begin
|
||||||
|
ApplyFilterSlot(Idx);
|
||||||
|
CacheFilterToBand;
|
||||||
|
ApplyModeFilter;
|
||||||
|
Changed(rfFilter);
|
||||||
end;
|
end;
|
||||||
FFilter := Idx;
|
end;
|
||||||
case FMode of
|
|
||||||
MODE_LSB, MODE_USB: FFilterBW := FILT_SSB_BW[Idx];
|
procedure TRadioController.ResetFilterSlot(Mode, Idx: Integer);
|
||||||
MODE_DIGU, MODE_DIGL: FFilterBW := FILT_DIGI_BW[Idx];
|
var Def: TFilterSet;
|
||||||
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
|
begin
|
||||||
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
|
Mode := EnsureRange(Mode, 0, CFG_MODE_MAX);
|
||||||
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
|
Def := DefaultFilterSet(Mode);
|
||||||
MODE_DMR: FFilterBW := FILT_DMR_BW;
|
SetFilterSlot(Mode, Idx, Def[EnsureRange(Idx, 0, FILT_SLOTS - 1)]);
|
||||||
MODE_FMRAW: FFilterBW := FILT_RAW_BW[Idx];
|
end;
|
||||||
MODE_FM:
|
|
||||||
begin
|
procedure TRadioController.ResetFilterSet(Mode: Integer);
|
||||||
FFilterBW := FILT_FM_BW[Idx];
|
var i: Integer;
|
||||||
FFMDeviation := FILT_FM_DEV[Idx];
|
begin
|
||||||
end;
|
for i := 0 to FILT_SLOTS - 1 do ResetFilterSlot(Mode, i);
|
||||||
else FFilterBW := FILT_AM_BW[Idx];
|
end;
|
||||||
end;
|
|
||||||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
procedure TRadioController.SetFilterEdges(LoHz, HiHz: Integer);
|
||||||
|
// Drag на спектре / CAT ZZFL-ZZFH / web: пресеты не портим — уходим в VAR1.
|
||||||
|
var S: TFilterSlot; Target: Integer;
|
||||||
|
begin
|
||||||
|
if HiHz - LoHz < FILT_MIN_WIDTH then Exit;
|
||||||
|
if (FFilter = FILT_VAR1) or (FFilter = FILT_VAR2) then Target := FFilter
|
||||||
|
else Target := FILT_VAR1;
|
||||||
|
S := FFilterTable[FMode][Target];
|
||||||
|
S.Lo := LoHz;
|
||||||
|
S.Hi := HiHz;
|
||||||
|
FFilterTable[FMode][Target] := S;
|
||||||
|
if Assigned(FSettings) then FSettings.SaveFilters(FDevMAC, FFilterTable);
|
||||||
|
ApplyFilterSlot(Target);
|
||||||
|
CacheFilterToBand;
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
Changed(rfFilter);
|
Changed(rfFilter);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetFilterBW(BWHz: Integer);
|
procedure TRadioController.SetFilterBW(BWHz: Integer);
|
||||||
// Непрерывная полоса (web-ползунок): применяем запрошенный BW как есть, а
|
// Непрерывная полоса (web-ползунок). Точное совпадение с пресетом выбирает
|
||||||
// FFilter подбираем ближайшим пресетом — только для подсветки кнопки.
|
// пресет, произвольная ширина уходит в VAR — как и перетаскивание края мышью.
|
||||||
var
|
var
|
||||||
BW_arr: array[0..FILT_COUNT-1] of Integer;
|
Lo, Hi, i, n: Integer;
|
||||||
best, dist, i: Integer;
|
|
||||||
begin
|
begin
|
||||||
if FMode = MODE_DMR then BWHz := FILT_DMR_BW;
|
if FMode = MODE_DMR then BWHz := FILT_DMR_BW;
|
||||||
FFilterBW := BWHz;
|
FilterEdgesFromBW(FMode, BWHz, Lo, Hi);
|
||||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
|
n := FilterPresetCount(FMode);
|
||||||
case FMode of
|
for i := 0 to n - 1 do
|
||||||
MODE_LSB, MODE_USB:
|
if (FFilterTable[FMode][i].Lo = Lo) and (FFilterTable[FMode][i].Hi = Hi) then
|
||||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
|
|
||||||
MODE_DIGU, MODE_DIGL:
|
|
||||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DIGI_BW[i];
|
|
||||||
MODE_DSB: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
|
|
||||||
MODE_CWL, MODE_CWU:
|
|
||||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
|
|
||||||
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
|
|
||||||
MODE_WFM: for i := 0 to FILT_WFM_COUNT-1 do BW_arr[i] := FILT_WFM_BW[i];
|
|
||||||
MODE_DMR: BW_arr[0] := FILT_DMR_BW;
|
|
||||||
MODE_FMRAW: for i := 0 to FILT_RAW_COUNT-1 do BW_arr[i] := FILT_RAW_BW[i];
|
|
||||||
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
|
|
||||||
end;
|
|
||||||
best := 0; dist := MaxInt;
|
|
||||||
for i := 0 to FILT_COUNT-1 do
|
|
||||||
if Abs(BW_arr[i] - BWHz) < dist then
|
|
||||||
begin
|
begin
|
||||||
dist := Abs(BW_arr[i] - BWHz);
|
SetFilterIdx(i);
|
||||||
best := i;
|
Exit;
|
||||||
end;
|
end;
|
||||||
FFilter := best;
|
SetFilterEdges(Lo, Hi);
|
||||||
if FMode = MODE_FM then FFMDeviation := FILT_FM_DEV[best];
|
|
||||||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
|
||||||
ApplyModeFilter;
|
|
||||||
Changed(rfFilter);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetRawFMDeviation(Hz: Integer);
|
procedure TRadioController.SetRawFMDeviation(Hz: Integer);
|
||||||
@@ -5788,6 +5825,8 @@ begin
|
|||||||
FNetwork.SetAlexConfig(FAlexSettings);
|
FNetwork.SetAlexConfig(FAlexSettings);
|
||||||
FSettings.LoadOC(FDevMAC, FOCSettings);
|
FSettings.LoadOC(FDevMAC, FOCSettings);
|
||||||
FNetwork.SetOCConfig(FOCSettings); // no-op на Pluto (base-класс)
|
FNetwork.SetOCConfig(FOCSettings); // no-op на Pluto (base-класс)
|
||||||
|
FSettings.LoadFilters(FDevMAC, FFilterTable);
|
||||||
|
ApplyFilterSlot(FFilter); // кромки под таблицу этого устройства
|
||||||
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
|
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
|
||||||
FCurrentXvtr := -1;
|
FCurrentXvtr := -1;
|
||||||
ApplyXvtrToNetwork;
|
ApplyXvtrToNetwork;
|
||||||
@@ -5959,6 +5998,7 @@ begin
|
|||||||
FSettings.SaveAlex(FDevMAC, FAlexSettings);
|
FSettings.SaveAlex(FDevMAC, FAlexSettings);
|
||||||
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
|
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
|
||||||
FSettings.SaveOC(FDevMAC, FOCSettings);
|
FSettings.SaveOC(FDevMAC, FOCSettings);
|
||||||
|
FSettings.SaveFilters(FDevMAC, FFilterTable);
|
||||||
// XVTR LastFreq → актуальная видимая частота в момент остановки.
|
// XVTR LastFreq → актуальная видимая частота в момент остановки.
|
||||||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||||||
|
|||||||
+218
@@ -89,12 +89,52 @@ const
|
|||||||
FILT_DMR_BW = 12500;
|
FILT_DMR_BW = 12500;
|
||||||
DMR_FM_DEV = 2500;
|
DMR_FM_DEV = 2500;
|
||||||
|
|
||||||
|
// UI-подписи пресетов. Раньше жили в MainForm — переехали сюда, потому что
|
||||||
|
// из них строится заводская таблица фильтров (DefaultFilterSet).
|
||||||
|
FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string =
|
||||||
|
('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k');
|
||||||
|
FILT_CW_NAMES: array[0..FILT_COUNT-1] of string =
|
||||||
|
('1.0k','800','600','500','400','250','150','100','50','25');
|
||||||
|
FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string =
|
||||||
|
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
|
||||||
|
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
|
||||||
|
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
|
||||||
|
FILT_DIGI_NAMES: array[0..FILT_COUNT-1] of string =
|
||||||
|
('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300');
|
||||||
|
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
|
||||||
|
FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k');
|
||||||
|
FILT_RAW_NAMES: array[0..FILT_RAW_COUNT-1] of string =
|
||||||
|
('6.25k', '10k', '12.5k', '20k', '25k');
|
||||||
|
|
||||||
|
// --- Редактируемая таблица фильтров -------------------------------------
|
||||||
|
// 10 пресетов + VAR1/VAR2 на режим (паритет с Thetis FilterForm). Кромки
|
||||||
|
// ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB
|
||||||
|
// своя — поэтому при применении фильтра конвертировать боковую больше не
|
||||||
|
// нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW,
|
||||||
|
// ниже) — только для постройки заводских дефолтов.
|
||||||
|
FILT_VAR1 = FILT_COUNT; // 10
|
||||||
|
FILT_VAR2 = FILT_COUNT + 1; // 11
|
||||||
|
FILT_SLOTS = FILT_COUNT + 2; // 12 — сетка кнопок 2 ряда по 6
|
||||||
|
// Минимальная ширина полосы: ниже неё фильтр звенит, а перетаскивание края
|
||||||
|
// мышью легко схлопнуло бы полосу в ноль.
|
||||||
|
FILT_MIN_WIDTH = 50;
|
||||||
|
|
||||||
type
|
type
|
||||||
|
TFilterSlot = record
|
||||||
|
Name: string;
|
||||||
|
Lo, Hi: Integer; // знаковые кромки в Гц относительно несущей
|
||||||
|
end;
|
||||||
|
TFilterSet = array[0..FILT_SLOTS-1] of TFilterSlot; // на режим
|
||||||
|
TFilterTable = array[0..CFG_MODE_MAX] of TFilterSet; // по режиму
|
||||||
|
|
||||||
TBandSettings = record
|
TBandSettings = record
|
||||||
VfoA: Double;
|
VfoA: Double;
|
||||||
VfoB: Double;
|
VfoB: Double;
|
||||||
Mode: Integer;
|
Mode: Integer;
|
||||||
FilterIdx: Integer;
|
FilterIdx: Integer;
|
||||||
|
// FilterIdx — слот в таблице фильтров режима (см. TFilterSet); сами
|
||||||
|
// кромки живут в таблице, а не здесь, поэтому диапазон помнит только
|
||||||
|
// выбор. FilterBW остался легаси-полем для чтения старых конфигов.
|
||||||
FilterBW: Integer;
|
FilterBW: Integer;
|
||||||
AGCMode: Integer;
|
AGCMode: Integer;
|
||||||
AGCTop: Integer;
|
AGCTop: Integer;
|
||||||
@@ -581,10 +621,120 @@ type
|
|||||||
// Тема — глобальная настройка, не привязана к устройству
|
// Тема — глобальная настройка, не привязана к устройству
|
||||||
procedure SaveTheme(ALightTheme: Boolean);
|
procedure SaveTheme(ALightTheme: Boolean);
|
||||||
function LoadTheme: Boolean;
|
function LoadTheme: Boolean;
|
||||||
|
// Таблица фильтров per-device (JSON-секция "filters" под MAC). Пишутся
|
||||||
|
// только режимы, отличающиеся от заводских, — конфиг не пухнет.
|
||||||
|
function LoadFilters(const MAC: array of Byte; out T: TFilterTable): Boolean;
|
||||||
|
procedure SaveFilters(const MAC: array of Byte; const T: TFilterTable);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Сколько пресетов реально есть у режима (VAR1/VAR2 идут сверх этого числа).
|
||||||
|
function FilterPresetCount(Mode: Integer): Integer;
|
||||||
|
// Заводской индекс фильтра режима.
|
||||||
|
function FilterDefIdx(Mode: Integer): Integer;
|
||||||
|
// Единственная в проекте таблица «магнитуда полосы → знаковые кромки».
|
||||||
|
// Нужна только для постройки заводских дефолтов и для чтения старых
|
||||||
|
// конфигов, где хранилась одна ширина.
|
||||||
|
procedure FilterEdgesFromBW(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||||
|
// Обратное преобразование для легаси-читателей FFilterBW. У SSB/DIGI «полоса»
|
||||||
|
// исторически = внешняя кромка (2.7k при кромках 100..2700), у остальных
|
||||||
|
// режимов — полная ширина; здесь это воспроизведено один в один.
|
||||||
|
function FilterBWFromEdges(Mode, Lo, Hi: Integer): Integer;
|
||||||
|
function DefaultFilterSet(Mode: Integer): TFilterSet;
|
||||||
|
procedure DefaultFilterTable(out T: TFilterTable);
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
|
function FilterPresetCount(Mode: Integer): Integer;
|
||||||
|
begin
|
||||||
|
case Mode of
|
||||||
|
MODE_FM: Result := FILT_FM_COUNT;
|
||||||
|
MODE_WFM: Result := FILT_WFM_COUNT;
|
||||||
|
MODE_DMR: Result := 1;
|
||||||
|
MODE_FMRAW: Result := FILT_RAW_COUNT;
|
||||||
|
else Result := FILT_COUNT;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function FilterDefIdx(Mode: Integer): Integer;
|
||||||
|
begin
|
||||||
|
case Mode of
|
||||||
|
MODE_LSB, MODE_USB: Result := FILT_SSB_DEF;
|
||||||
|
MODE_DIGL, MODE_DIGU: Result := FILT_DIGI_DEF;
|
||||||
|
MODE_DSB: Result := FILT_DSB_DEF;
|
||||||
|
MODE_CWL, MODE_CWU: Result := FILT_CW_DEF;
|
||||||
|
MODE_FM: Result := FILT_FM_DEF;
|
||||||
|
MODE_WFM: Result := FILT_WFM_DEF;
|
||||||
|
MODE_DMR: Result := 0;
|
||||||
|
MODE_FMRAW: Result := FILT_RAW_DEF;
|
||||||
|
else Result := FILT_AM_DEF;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure FilterEdgesFromBW(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||||
|
var Half: Integer;
|
||||||
|
begin
|
||||||
|
Half := BW div 2;
|
||||||
|
case Mode of
|
||||||
|
MODE_LSB: begin Lo := -BW; Hi := -100; end;
|
||||||
|
MODE_USB: begin Lo := 100; Hi := BW; end;
|
||||||
|
// DIGI: полоса считается от нуля — поднесущие цифровых видов начинаются
|
||||||
|
// низко, срез на 100 Гц как в SSB съел бы их.
|
||||||
|
MODE_DIGL: begin Lo := -BW; Hi := 0; end;
|
||||||
|
MODE_DIGU: begin Lo := 0; Hi := BW; end;
|
||||||
|
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM/WFM
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function FilterBWFromEdges(Mode, Lo, Hi: Integer): Integer;
|
||||||
|
begin
|
||||||
|
case Mode of
|
||||||
|
MODE_LSB, MODE_USB, MODE_DIGL, MODE_DIGU:
|
||||||
|
Result := Max(Abs(Lo), Abs(Hi));
|
||||||
|
else
|
||||||
|
Result := Hi - Lo;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function DefaultFilterSet(Mode: Integer): TFilterSet;
|
||||||
|
var i, n, bw: Integer;
|
||||||
|
begin
|
||||||
|
n := FilterPresetCount(Mode);
|
||||||
|
for i := 0 to FILT_SLOTS - 1 do
|
||||||
|
begin
|
||||||
|
Result[i].Name := '';
|
||||||
|
Result[i].Lo := 0;
|
||||||
|
Result[i].Hi := 0;
|
||||||
|
end;
|
||||||
|
bw := 0;
|
||||||
|
for i := 0 to n - 1 do
|
||||||
|
begin
|
||||||
|
case Mode of
|
||||||
|
MODE_LSB, MODE_USB: begin bw := FILT_SSB_BW[i]; Result[i].Name := FILT_SSB_NAMES[i]; end;
|
||||||
|
MODE_DIGL, MODE_DIGU: begin bw := FILT_DIGI_BW[i]; Result[i].Name := FILT_DIGI_NAMES[i]; end;
|
||||||
|
MODE_DSB: begin bw := FILT_DSB_BW[i]; Result[i].Name := FILT_DSB_NAMES[i]; end;
|
||||||
|
MODE_CWL, MODE_CWU: begin bw := FILT_CW_BW[i]; Result[i].Name := FILT_CW_NAMES[i]; end;
|
||||||
|
MODE_FM: begin bw := FILT_FM_BW[i]; Result[i].Name := FILT_FM_NAMES[i]; end;
|
||||||
|
MODE_WFM: begin bw := FILT_WFM_BW[i]; Result[i].Name := FILT_WFM_NAMES[i]; end;
|
||||||
|
MODE_DMR: begin bw := FILT_DMR_BW; Result[i].Name := '12.5k'; end;
|
||||||
|
MODE_FMRAW: begin bw := FILT_RAW_BW[i]; Result[i].Name := FILT_RAW_NAMES[i]; end;
|
||||||
|
else begin bw := FILT_AM_BW[i]; Result[i].Name := FILT_AM_NAMES[i]; end;
|
||||||
|
end;
|
||||||
|
FilterEdgesFromBW(Mode, bw, Result[i].Lo, Result[i].Hi);
|
||||||
|
end;
|
||||||
|
// VAR1/VAR2 стартуют копией заводского пресета режима — «чистый лист»,
|
||||||
|
// который не сдвигает звук, пока его не тронули.
|
||||||
|
Result[FILT_VAR1] := Result[FilterDefIdx(Mode)];
|
||||||
|
Result[FILT_VAR1].Name := 'VAR1';
|
||||||
|
Result[FILT_VAR2] := Result[FilterDefIdx(Mode)];
|
||||||
|
Result[FILT_VAR2].Name := 'VAR2';
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure DefaultFilterTable(out T: TFilterTable);
|
||||||
|
var m: Integer;
|
||||||
|
begin
|
||||||
|
for m := 0 to CFG_MODE_MAX do T[m] := DefaultFilterSet(m);
|
||||||
|
end;
|
||||||
|
|
||||||
function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer;
|
function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer;
|
||||||
Backmult, DefaultMS: Double): Double;
|
Backmult, DefaultMS: Double): Double;
|
||||||
var
|
var
|
||||||
@@ -1680,6 +1830,74 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TSettingsManager.LoadFilters(const MAC: array of Byte;
|
||||||
|
out T: TFilterTable): Boolean;
|
||||||
|
var
|
||||||
|
MacStr, K: string;
|
||||||
|
DevObj, Root, Obj: TJSONObject;
|
||||||
|
D: TJSONData;
|
||||||
|
m, s: Integer;
|
||||||
|
begin
|
||||||
|
DefaultFilterTable(T);
|
||||||
|
MacStr := MacToStr(MAC);
|
||||||
|
Result := FRoot.Find(MacStr) <> nil;
|
||||||
|
if not Result then Exit;
|
||||||
|
DevObj := GetDevObj(MacStr);
|
||||||
|
if DevObj.Find('filters') = nil then begin Result := False; Exit; end;
|
||||||
|
Root := EnsureObj(DevObj, 'filters');
|
||||||
|
for m := 0 to CFG_MODE_MAX do
|
||||||
|
begin
|
||||||
|
D := Root.Find('m' + IntToStr(m));
|
||||||
|
if (D = nil) or not (D is TJSONObject) then Continue;
|
||||||
|
Obj := TJSONObject(D);
|
||||||
|
for s := 0 to FILT_SLOTS - 1 do
|
||||||
|
begin
|
||||||
|
K := 's' + IntToStr(s) + '_';
|
||||||
|
T[m][s].Name := JS(Obj, K + 'n', T[m][s].Name);
|
||||||
|
T[m][s].Lo := JI(Obj, K + 'lo', T[m][s].Lo);
|
||||||
|
T[m][s].Hi := JI(Obj, K + 'hi', T[m][s].Hi);
|
||||||
|
if T[m][s].Hi <= T[m][s].Lo then // битый конфиг не должен глушить приём
|
||||||
|
begin
|
||||||
|
T[m][s].Lo := DefaultFilterSet(m)[s].Lo;
|
||||||
|
T[m][s].Hi := DefaultFilterSet(m)[s].Hi;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsManager.SaveFilters(const MAC: array of Byte;
|
||||||
|
const T: TFilterTable);
|
||||||
|
var
|
||||||
|
K: string;
|
||||||
|
Root, Obj: TJSONObject;
|
||||||
|
Def: TFilterSet;
|
||||||
|
m, s, Idx: Integer;
|
||||||
|
Differs: Boolean;
|
||||||
|
begin
|
||||||
|
Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'filters');
|
||||||
|
for m := 0 to CFG_MODE_MAX do
|
||||||
|
begin
|
||||||
|
Def := DefaultFilterSet(m);
|
||||||
|
Differs := False;
|
||||||
|
for s := 0 to FILT_SLOTS - 1 do
|
||||||
|
if (T[m][s].Lo <> Def[s].Lo) or (T[m][s].Hi <> Def[s].Hi) or
|
||||||
|
(T[m][s].Name <> Def[s].Name) then Differs := True;
|
||||||
|
// Секцию режима всегда сносим целиком: иначе возврат слота к заводскому
|
||||||
|
// оставил бы старый ключ и он всплыл бы при следующей загрузке.
|
||||||
|
Idx := Root.IndexOfName('m' + IntToStr(m));
|
||||||
|
if Idx >= 0 then Root.Delete(Idx);
|
||||||
|
if not Differs then Continue;
|
||||||
|
Obj := EnsureObj(Root, 'm' + IntToStr(m));
|
||||||
|
for s := 0 to FILT_SLOTS - 1 do
|
||||||
|
begin
|
||||||
|
K := 's' + IntToStr(s) + '_';
|
||||||
|
JWS(Obj, K + 'n', T[m][s].Name);
|
||||||
|
JW (Obj, K + 'lo', T[m][s].Lo);
|
||||||
|
JW (Obj, K + 'hi', T[m][s].Hi);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
class procedure TSettingsManager.DefaultAnt936x(out A: TAD936xAnt);
|
class procedure TSettingsManager.DefaultAnt936x(out A: TAD936xAnt);
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
|
|||||||
+100
-24
@@ -21,7 +21,8 @@ uses
|
|||||||
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
|
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
|
||||||
AppTheme,
|
AppTheme,
|
||||||
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
|
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
|
||||||
WaterfallView, SMeterView, RulerView, RadioModes;
|
WaterfallView, SMeterView, RulerView, RadioModes,
|
||||||
|
Settings; // FilterEdgesFromBW — единственная таблица знака боковой
|
||||||
|
|
||||||
const
|
const
|
||||||
SV_CLR_BG = TColor($00101010);
|
SV_CLR_BG = TColor($00101010);
|
||||||
@@ -92,6 +93,12 @@ type
|
|||||||
FTXSpanHz: Double;
|
FTXSpanHz: Double;
|
||||||
FTXVfoIndex: Integer;
|
FTXVfoIndex: Integer;
|
||||||
FTXOverlay: Boolean;
|
FTXOverlay: Boolean;
|
||||||
|
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
|
||||||
|
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
|
||||||
|
// Thetis-модели НЕ следует за RX, поэтому при передаче полоса рисуется по
|
||||||
|
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
|
||||||
|
FRXEdgeLo, FRXEdgeHi: Integer;
|
||||||
|
FTXEdgeLo, FTXEdgeHi: Integer;
|
||||||
// ── ADC overload overlay ─────────────────────────────────────────────────
|
// ── ADC overload overlay ─────────────────────────────────────────────────
|
||||||
FADCOverloadVisible: Boolean;
|
FADCOverloadVisible: Boolean;
|
||||||
FADCAlertImg: TAlertImage; // кэш образа плашки (BuildAlertImage, 1 раз)
|
FADCAlertImg: TAlertImage; // кэш образа плашки (BuildAlertImage, 1 раз)
|
||||||
@@ -179,6 +186,12 @@ type
|
|||||||
out X1, X2, VfoX: Integer);
|
out X1, X2, VfoX: Integer);
|
||||||
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||||
out X1, X2, VfoX: 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;
|
function GetWaterfallDirty: Boolean;
|
||||||
procedure SetWaterfallDirty(V: Boolean);
|
procedure SetWaterfallDirty(V: Boolean);
|
||||||
// Waterfall sub-view delegating accessors
|
// Waterfall sub-view delegating accessors
|
||||||
@@ -254,6 +267,10 @@ type
|
|||||||
property TXSpanHz: Double read FTXSpanHz write SetTXSpanHz;
|
property TXSpanHz: Double read FTXSpanHz write SetTXSpanHz;
|
||||||
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
|
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
|
||||||
property TXOverlay: Boolean read FTXOverlay write FTXOverlay;
|
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;
|
property ADCOverloadVisible: Boolean read FADCOverloadVisible write FADCOverloadVisible;
|
||||||
|
|
||||||
@@ -1172,7 +1189,7 @@ begin
|
|||||||
// Передающий слайс — красная полоса (как у главного при TX), иначе бейдж-цвет.
|
// Передающий слайс — красная полоса (как у главного при TX), иначе бейдж-цвет.
|
||||||
if O.TxActive then Clr := CLR_TX_BAND
|
if O.TxActive then Clr := CLR_TX_BAND
|
||||||
else Clr := SliceColor(O.SliceLetter); // $00BBGGRR
|
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
|
if SX2 > SX1 then
|
||||||
BlendBand(SX1, SX2, H, Clr and $FF, (Clr shr 8) and $FF, (Clr shr 16) and $FF,
|
BlendBand(SX1, SX2, H, Clr and $FF, (Clr shr 8) and $FF, (Clr shr 16) and $FF,
|
||||||
IfThen(O.TxActive, 100, 60));
|
IfThen(O.TxActive, 100, 60));
|
||||||
@@ -1191,7 +1208,7 @@ begin
|
|||||||
if (O = nil) or (not O.Visible) then Continue;
|
if (O = nil) or (not O.Visible) then Continue;
|
||||||
if O.TxActive then Clr := CLR_TX_BAND // передающий слайс
|
if O.TxActive then Clr := CLR_TX_BAND // передающий слайс
|
||||||
else Clr := SliceColor(O.SliceLetter); // оттенок бейджа слайса
|
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(SX1, 0, H - 1, Clr);
|
||||||
RawVLine(SX2, 0, H - 1, Clr);
|
RawVLine(SX2, 0, H - 1, Clr);
|
||||||
RawVLine(SVfoX, 0, H - 13, Clr, 2); // несущая (жирнее)
|
RawVLine(SVfoX, 0, H - 13, Clr, 2); // несущая (жирнее)
|
||||||
@@ -1255,29 +1272,81 @@ begin
|
|||||||
W - ALERT_PAD - FADCAlertImg.W, ALERT_PAD);
|
W - ALERT_PAD - FADCAlertImg.W, ALERT_PAD);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
procedure TSpectrumView.SetRXEdges(LoHz, HiHz: Integer);
|
||||||
out X1, X2, VfoX: Integer);
|
|
||||||
begin
|
begin
|
||||||
CalcFilterBandXFor(VfoFreq, FMode, FFilterBW, W, X1, X2, VfoX);
|
if (LoHz = FRXEdgeLo) and (HiHz = FRXEdgeHi) then Exit;
|
||||||
|
FRXEdgeLo := LoHz;
|
||||||
|
FRXEdgeHi := HiHz;
|
||||||
|
FSpectrumDirty := True;
|
||||||
end;
|
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;
|
procedure TSpectrumView.CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||||
out X1, X2, VfoX: Integer);
|
out X1, X2, VfoX: Integer);
|
||||||
var
|
var
|
||||||
Lo_Hz, Hi_Hz, Half: Double;
|
Lo, Hi: Integer;
|
||||||
begin
|
begin
|
||||||
Half := BW / 2;
|
FilterEdgesFromBW(Mode, BW, Lo, Hi);
|
||||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
CalcBandXEdges(VfoFreq, Lo, Hi, W, X1, X2, VfoX);
|
||||||
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)));
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||||
@@ -1584,14 +1653,21 @@ begin
|
|||||||
// фильтра в пару тысяч пикселей стоила 4× дороже полного memcpy кадра),
|
// фильтра в пару тысяч пикселей стоила 4× дороже полного memcpy кадра),
|
||||||
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
||||||
|
|
||||||
// Полоса фильтра (X-координаты главного и TX-VFO)
|
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
|
||||||
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
// считается по TX-фильтру: его ширина независима от приёмной (модель Thetis),
|
||||||
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
// и раньше в эфир шло одно, а на экране рисовалось другое.
|
||||||
|
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;
|
TXX1 := 0; TXX2 := 0; TXVfoX := 0;
|
||||||
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
|
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
|
||||||
begin
|
begin
|
||||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
if FTXVfoIndex = 0 then CalcTXBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
else CalcTXBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Точки кривой спектра
|
// Точки кривой спектра
|
||||||
|
|||||||
+10
-4
@@ -586,7 +586,13 @@ begin
|
|||||||
TXX1 := 0;
|
TXX1 := 0;
|
||||||
TXX2 := 0;
|
TXX2 := 0;
|
||||||
TXVfoX := 0;
|
TXVfoX := 0;
|
||||||
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
// На передаче полоса активного VFO рисуется по TX-фильтру (см. CPU-путь).
|
||||||
|
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);
|
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||||
|
|
||||||
if FTXOverlay then
|
if FTXOverlay then
|
||||||
@@ -594,8 +600,8 @@ begin
|
|||||||
if FTXVfoIndex <> FActiveVfo then
|
if FTXVfoIndex <> FActiveVfo then
|
||||||
begin
|
begin
|
||||||
if X2 > X1 then DrawRect(X1, 0, X2, H, TColor($0030C030), 100 / 255);
|
if X2 > X1 then DrawRect(X1, 0, X2, H, TColor($0030C030), 100 / 255);
|
||||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
if FTXVfoIndex = 0 then CalcTXBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
else CalcTXBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||||
if TXX2 > TXX1 then DrawRect(TXX1, 0, TXX2, H, CLR_TX_BAND, 100 / 255);
|
if TXX2 > TXX1 then DrawRect(TXX1, 0, TXX2, H, CLR_TX_BAND, 100 / 255);
|
||||||
end
|
end
|
||||||
else if X2 > X1 then DrawRect(X1, 0, X2, H, CLR_TX_BAND, 100 / 255);
|
else if X2 > X1 then DrawRect(X1, 0, X2, H, CLR_TX_BAND, 100 / 255);
|
||||||
@@ -638,7 +644,7 @@ begin
|
|||||||
// Передающий слайс — красная полоса (как у главного при TX), иначе оттенок бейджа.
|
// Передающий слайс — красная полоса (как у главного при TX), иначе оттенок бейджа.
|
||||||
if O.TxActive then Clr := CLR_TX_BAND
|
if O.TxActive then Clr := CLR_TX_BAND
|
||||||
else Clr := SliceColor(O.SliceLetter);
|
else Clr := SliceColor(O.SliceLetter);
|
||||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
CalcSliceBandX(O, W, SX1, SX2, SVfoX);
|
||||||
if SX2 > SX1 then
|
if SX2 > SX1 then
|
||||||
DrawRect(SX1, 0, SX2, H, Clr, IfThen(O.TxActive, 100, 60) / 255);
|
DrawRect(SX1, 0, SX2, H, Clr, IfThen(O.TxActive, 100, 60) / 255);
|
||||||
DrawLine(SX1, 0, SX1, H, Clr, 1, False);
|
DrawLine(SX1, 0, SX1, H, Clr, 1, False);
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ type
|
|||||||
private
|
private
|
||||||
FMode: Integer;
|
FMode: Integer;
|
||||||
FFilterBW: Integer;
|
FFilterBW: Integer;
|
||||||
|
FFilterLo: Integer; // знаковые кромки (0/0 = не заданы)
|
||||||
|
FFilterHi: Integer;
|
||||||
FVfoHz: Double;
|
FVfoHz: Double;
|
||||||
FSMeterDB: Double;
|
FSMeterDB: Double;
|
||||||
FModeFilter: array[0..OVL_MODE_COUNT-1] of Integer;
|
FModeFilter: array[0..OVL_MODE_COUNT-1] of Integer;
|
||||||
@@ -227,6 +229,13 @@ type
|
|||||||
property DisplayVfoHz: Double read FVfoHz;
|
property DisplayVfoHz: Double read FVfoHz;
|
||||||
property DisplayMode: Integer read FMode;
|
property DisplayMode: Integer read FMode;
|
||||||
property DisplayBW: Integer read FFilterBW;
|
property DisplayBW: Integer read FFilterBW;
|
||||||
|
// Настоящие кромки фильтра слайса (Гц от несущей, знаковые). Одной ширины
|
||||||
|
// мало: пара→ширина→пара — преобразование с потерями, и полоса на экране
|
||||||
|
// разъезжалась со звуком, когда кромки заданы не по конвенции режима
|
||||||
|
// (слайс-CAT ZZFL/ZZFH). Hi<=Lo = не заданы, рисуем по режиму и ширине.
|
||||||
|
property DisplayLo: Integer read FFilterLo;
|
||||||
|
property DisplayHi: Integer read FFilterHi;
|
||||||
|
procedure SetFilterEdges(ALo, AHi: Integer);
|
||||||
// Слайс сейчас передаёт (полоса фильтра на спектре краснеет, как у главного).
|
// Слайс сейчас передаёт (полоса фильтра на спектре краснеет, как у главного).
|
||||||
property TxActive: Boolean read FTx;
|
property TxActive: Boolean read FTx;
|
||||||
// Битмап-кэш требует перерисовки (GL-вьюха решает, перезаливать ли текстуру).
|
// Битмап-кэш требует перерисовки (GL-вьюха решает, перезаливать ли текстуру).
|
||||||
@@ -1595,6 +1604,13 @@ end;
|
|||||||
|
|
||||||
{ ---- Обновление состояния из контроллера ---- }
|
{ ---- Обновление состояния из контроллера ---- }
|
||||||
|
|
||||||
|
procedure TVfoOverlay.SetFilterEdges(ALo, AHi: Integer);
|
||||||
|
begin
|
||||||
|
if (ALo = FFilterLo) and (AHi = FFilterHi) then Exit;
|
||||||
|
FFilterLo := ALo;
|
||||||
|
FFilterHi := AHi;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
|
procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
|
||||||
var
|
var
|
||||||
OldMode: Integer;
|
OldMode: Integer;
|
||||||
|
|||||||
@@ -690,6 +690,9 @@ type
|
|||||||
|
|
||||||
// Расширенное TX управление (применяются мгновенно если канал открыт)
|
// Расширенное TX управление (применяются мгновенно если канал открыт)
|
||||||
procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||||||
|
// Знаковые края TX-полосы текущего режима передачи — ровно то, что стоит
|
||||||
|
// в bp0. Нужны спектру, чтобы на передаче рисовать TX-ширину, а не RX.
|
||||||
|
procedure TXFilterEdgesHz(out Lo, Hi: Integer);
|
||||||
procedure SetTXCompressor(Enable: Boolean; GainDB: Double);
|
procedure SetTXCompressor(Enable: Boolean; GainDB: Double);
|
||||||
procedure SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
procedure SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||||||
procedure SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
procedure SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||||||
@@ -3719,6 +3722,14 @@ begin
|
|||||||
GetPSMaxTX(TXA_CHAN, @Result);
|
GetPSMaxTX(TXA_CHAN, @Result);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.TXFilterEdgesHz(out Lo, Hi: Integer);
|
||||||
|
var L, H: Double;
|
||||||
|
begin
|
||||||
|
TXSignedEdges(FTXMode, L, H);
|
||||||
|
Lo := Round(L);
|
||||||
|
Hi := Round(H);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||||||
var BpLo, BpHi: Double;
|
var BpLo, BpHi: Double;
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<UseVersionInfo Value="True"/>
|
<UseVersionInfo Value="True"/>
|
||||||
<AutoIncrementBuild Value="True"/>
|
<AutoIncrementBuild Value="True"/>
|
||||||
<MinorVersionNr Value="9"/>
|
<MinorVersionNr Value="9"/>
|
||||||
<BuildNr Value="293"/>
|
<BuildNr Value="294"/>
|
||||||
</VersionInfo>
|
</VersionInfo>
|
||||||
<MacroValues Count="126">
|
<MacroValues Count="127">
|
||||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||||
@@ -146,6 +146,7 @@
|
|||||||
<Macro124 Name="LCLWidgetType" Value="qt6"/>
|
<Macro124 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro125 Name="LCLWidgetType" Value="qt6"/>
|
<Macro125 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro126 Name="LCLWidgetType" Value="qt6"/>
|
<Macro126 Name="LCLWidgetType" Value="qt6"/>
|
||||||
|
<Macro127 Name="LCLWidgetType" Value="qt6"/>
|
||||||
</MacroValues>
|
</MacroValues>
|
||||||
<BuildModes>
|
<BuildModes>
|
||||||
<Item Name="Debug" Default="True"/>
|
<Item Name="Debug" Default="True"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user