mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
Merge feature/filter-edges: произвольные кромки фильтра, редактор, drag, TX-полоска
Фильтр главного приёмника стал таблицей (Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки. Поповер-редактор по ПКМ на кнопке фильтра, перетаскивание края полосы на спектре (Alt = симметрично), полоса при передаче рисуется по TX-фильтру, CAT ZZFL/ZZFH на главном порту, энкодеры #4/#5 Андромеды крутят High/Low раздельно. Попутно устранён дубль таблицы знака боковой (RX-конвенция жила и в контроллере, и в отрисовке) и потеря информации при рисовании полосы слайса (пара кромок схлопывалась в одну ширину). Проверено на железе:25521a2+ фиксce43116(слот VAR терялся при рестарте в режиме трансвертера).
This commit is contained in:
+6
-5
@@ -34,7 +34,7 @@ type
|
||||
// Параметр, управляемый поворотным энкодером панели.
|
||||
TAndEncoder = (
|
||||
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
|
||||
aeFilterWidth, aeMulti);
|
||||
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
|
||||
|
||||
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis.
|
||||
TAndIndicator = (
|
||||
@@ -104,15 +104,16 @@ const
|
||||
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
|
||||
// Нумерация = «Encoder Number» Thetis − 1 (дефолт MakeNewAndromedaDataset).
|
||||
// Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel.
|
||||
// #4/#5 на панели — Filter High/Low (независимые края), но в ewsdr фильтр
|
||||
// только симметричный (SetFilterBW): #4 крутит ширину, #5 пока не задействован.
|
||||
// #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2).
|
||||
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один
|
||||
// энкодер «ширина» — в дефолтной раскладке не используется.
|
||||
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
|
||||
aeAFGain, // 0 RX1 AF Gain
|
||||
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF»)
|
||||
aeNone, // 2 RX2 AF — нет второго приёмника
|
||||
aeNone, // 3 RX2 AGC
|
||||
aeFilterWidth, // 4 Filter High → ширина фильтра (краёв нет)
|
||||
aeNone, // 5 Filter Low — не реализован
|
||||
aeFilterHigh, // 4 Filter High — верхний край
|
||||
aeFilterLow, // 5 Filter Low — нижний край
|
||||
aeNone, // 6 Diversity gain
|
||||
aeNone, // 7 Diversity phase
|
||||
aeNone, // 8 RIT
|
||||
|
||||
@@ -126,6 +126,13 @@ begin
|
||||
aeDrive: FController.SetDrive(FController.FDrivePercent + FPendingStep);
|
||||
aeSquelch: FController.SetFMSquelchLevel(FController.FFMSQLevel + FPendingStep);
|
||||
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 — мультифункц. энкодер: пока не назначен
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -49,6 +49,10 @@ type
|
||||
function GetDriveLevel: Integer;
|
||||
function GetFilterIdx: Integer;
|
||||
function GetFilterBW: Integer;
|
||||
function GetFilterLow: Integer;
|
||||
function GetFilterHigh: Integer;
|
||||
procedure SetFilterLow(V: Integer);
|
||||
procedure SetFilterHigh(V: Integer);
|
||||
function GetNRMode: Integer;
|
||||
function GetNBMode: Integer;
|
||||
function GetSNB: Boolean;
|
||||
@@ -161,6 +165,10 @@ begin
|
||||
Ctx.GetDriveLevel := @GetDriveLevel;
|
||||
Ctx.GetFilterIdx := @GetFilterIdx;
|
||||
Ctx.GetFilterBW := @GetFilterBW;
|
||||
// Кромки фильтра (ZZFL/ZZFH). Раньше у главного приёмника были nil (фильтр
|
||||
// был индексным) — теперь он хранит те же знаковые кромки, что и слайс.
|
||||
Ctx.GetFilterLow := @GetFilterLow;
|
||||
Ctx.GetFilterHigh := @GetFilterHigh;
|
||||
Ctx.GetNRMode := @GetNRMode;
|
||||
Ctx.GetNBMode := @GetNBMode;
|
||||
Ctx.GetSNBEnabled := @GetSNB;
|
||||
@@ -177,6 +185,8 @@ begin
|
||||
Ctx.SetVolume := @SetVolume;
|
||||
Ctx.SetDriveLevel := @SetDriveLevel;
|
||||
Ctx.SetFilterIdx := @SetFilterIdx;
|
||||
Ctx.SetFilterLow := @SetFilterLow;
|
||||
Ctx.SetFilterHigh := @SetFilterHigh;
|
||||
Ctx.SetNRMode := @SetNRMode;
|
||||
Ctx.SetNBMode := @SetNBMode;
|
||||
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.GetFilterIdx: Integer; begin Result := FController.FFilter; 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.GetNBMode: Integer; begin Result := FController.FNBMode; 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
|
||||
|
||||
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,
|
||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
||||
OpenGLContextEx,
|
||||
@@ -68,23 +69,12 @@ const
|
||||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||||
|
||||
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
|
||||
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
|
||||
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
|
||||
// UI-подписи кнопок (NAMES).
|
||||
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');
|
||||
// Полосы/дефолты/подписи фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF,
|
||||
// FILT_*_NAMES) живут в Settings.pas: из них строится заводская таблица
|
||||
// фильтров, а подписи кнопок теперь берутся из живой таблицы контроллера.
|
||||
FILT_GRID_COLS = 6; // сетка кнопок фильтра: 2 ряда по 6 (12 = 10 + VAR1/2)
|
||||
EDGE_GRAB_PX = 5; // зона захвата края полосы фильтра мышью, px
|
||||
EDGE_SNAP_HZ = 10; // снап края при перетаскивании
|
||||
|
||||
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
|
||||
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
|
||||
@@ -132,11 +122,17 @@ type
|
||||
// ---- Network ----
|
||||
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
||||
FDeviceDialog: TDeviceDialog;
|
||||
FFilterPop: TFilterPopup; // поповер редактора фильтров (ПКМ по кнопке FILTER)
|
||||
FPendingIP: string; // IP устройства для подключения
|
||||
// Флаг главного (слайс A): создаётся здесь (проводка событий), живёт в
|
||||
// раскладке/диспетчере FPan (AttachMainFlag). Флаги B+ — целиком в FPan.
|
||||
FVfoOverlay: TVfoOverlay;
|
||||
FActiveSliceId: Integer; // выбранный активный слайс (0 = главный)
|
||||
// Перетаскивание края полосы фильтра: -1 левый, +1 правый, 0 не тянем.
|
||||
// Кромки на момент нажатия — чтобы неподвижный край не «плыл» от снапа.
|
||||
FEdgeDrag: Integer;
|
||||
FEdgeDragLo: Integer;
|
||||
FEdgeDragHi: Integer;
|
||||
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
||||
FSliceDragId: Integer; // какой слайс тащим
|
||||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||||
@@ -298,7 +294,7 @@ type
|
||||
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
|
||||
|
||||
PanelFilter: TPanel;
|
||||
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
||||
BtnFilter: array[0..FILT_SLOTS-1] of TFlatButton; // 10 пресетов + VAR1/VAR2
|
||||
BtnCTun: TFlatButton;
|
||||
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере)
|
||||
BtnDUP: TFlatButton;
|
||||
@@ -467,6 +463,16 @@ type
|
||||
procedure BtnBandClick(Sender: TObject);
|
||||
procedure BtnModeClick(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 BtnBeaconClick(Sender: TObject);
|
||||
procedure BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
@@ -980,7 +986,9 @@ begin
|
||||
FController.FAGCTop := 90; // −90 dBm
|
||||
// default: зависит от режима, будет сброшен в UpdateFilterButtons
|
||||
FController.FCTun := False;
|
||||
FController.FFilterBW := 2700;
|
||||
// Фильтр (индекс + кромки + производная ширина) — одной точкой, чтобы
|
||||
// состояние не расползалось: ширину отдельно больше не выставляем.
|
||||
FController.SelectFilterSlotState(FController.FFilter);
|
||||
FSpecDrag := False;
|
||||
FController.FDriveLevel := 0;
|
||||
FController.FPAMaxPower := 100.0;
|
||||
@@ -1426,6 +1434,7 @@ begin
|
||||
// TX со слайса краснеет полоса ТОГО слайса (DrawSliceFilterMarkers).
|
||||
FSpecView.TXOverlay := FController.FTransmitting and
|
||||
(FController.FTxSliceId = 0);
|
||||
PushFilterEdgesToView; // на передаче полоса рисуется по TX-фильтру
|
||||
ApplySpecViewGridFromState; // сетка спектра под активный тракт
|
||||
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
|
||||
UpdateTXControlAvailability;
|
||||
@@ -1526,17 +1535,29 @@ begin
|
||||
SavedFilter := FController.FFilter;
|
||||
SavedFilterBW := FController.FFilterBW;
|
||||
UpdateFilterButtons; // подписи + FM-панели под режим
|
||||
FController.FFilter := SavedFilter;
|
||||
FController.FFilterBW := SavedFilterBW;
|
||||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
// Возвращаем СЛОТ целиком: кромки — истина, одной ширины мало.
|
||||
FController.SelectFilterSlotState(SavedFilter);
|
||||
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; // иначе полоса фильтра рисуется под старый режим
|
||||
UpdateTXControlAvailability;
|
||||
UpdateDMRStatus;
|
||||
end;
|
||||
rfFilter:
|
||||
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;
|
||||
if (FFilterPop <> nil) and FFilterPop.Visible then
|
||||
FFilterPop.SyncSelection(FController.FMode, FController.FFilter);
|
||||
end;
|
||||
rfFMDeviation:
|
||||
if (TrkRawDev <> nil) and (LblRawDev <> nil) then
|
||||
@@ -1775,6 +1796,7 @@ begin
|
||||
rfFMSQ, rfFMSQLevel]) then
|
||||
begin
|
||||
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.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
|
||||
end;
|
||||
@@ -2315,20 +2337,22 @@ begin
|
||||
|
||||
Inc(Y, MODE_PANEL_H + 2);
|
||||
|
||||
// Filter selector (10 кнопок в 2 ряда по 5)
|
||||
// Filter selector (12 кнопок в 2 ряда по 6: 10 пресетов + VAR1/VAR2).
|
||||
// ПКМ по любой из них открывает редактор фильтров.
|
||||
PanelFilter := TPanel.Create(Self);
|
||||
PanelFilter.Parent := PanelLeft;
|
||||
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
|
||||
PanelFilter.BevelOuter := bvNone;
|
||||
MakeLbl(PanelFilter, 'FILTER', 4, 2);
|
||||
for i := 0 to FILT_COUNT - 1 do
|
||||
for i := 0 to FILT_SLOTS - 1 do
|
||||
begin
|
||||
B := MakeBtn(PanelFilter, '---',
|
||||
LeftPanelButtonLeft(LEFT_W, 5, i mod 5),
|
||||
16 + (i div 5) * 27,
|
||||
LeftPanelButtonWidth(LEFT_W, 5, i mod 5),
|
||||
LeftPanelButtonLeft(LEFT_W, FILT_GRID_COLS, i mod FILT_GRID_COLS),
|
||||
16 + (i div FILT_GRID_COLS) * 27,
|
||||
LeftPanelButtonWidth(LEFT_W, FILT_GRID_COLS, i mod FILT_GRID_COLS),
|
||||
BTN_SM, BtnFilterClick);
|
||||
B.Tag := i;
|
||||
B.OnMouseDown := BtnFilterMouseDown;
|
||||
BtnFilter[i] := B;
|
||||
end;
|
||||
|
||||
@@ -3121,7 +3145,7 @@ begin
|
||||
// Обычные кнопки — все массивы
|
||||
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 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);
|
||||
StyleButton(BtnCTun, BtnCTun.Active);
|
||||
StyleButton(BtnDUP, BtnDUP.Active);
|
||||
@@ -4454,10 +4478,38 @@ end;
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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;
|
||||
var
|
||||
i: Integer;
|
||||
Names: array[0..FILT_COUNT-1] of string;
|
||||
DefIdx: Integer;
|
||||
begin
|
||||
// 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;
|
||||
if FController.FMode = MODE_FM then
|
||||
begin
|
||||
// FM: show only NFM and WFM buttons, resize them to fill the row
|
||||
// FM: NFM/FM + VAR1/VAR2 в один ряд (панель остаётся низкой, SQL/CTCSS
|
||||
// садятся сразу под ней).
|
||||
CloseCTCSSPopup;
|
||||
if FController.FFilter > 1 then FController.FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
|
||||
FController.FFilterBW := FILT_FM_BW[FController.FFilter];
|
||||
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;
|
||||
FController.SanitizeFilterState;
|
||||
LayoutFilterButtons(FILT_FM_COUNT);
|
||||
if PanelFMSQ <> nil then
|
||||
begin
|
||||
PanelFMSQ.Visible := True;
|
||||
@@ -4536,21 +4573,10 @@ begin
|
||||
|
||||
if FController.FMode = MODE_WFM then
|
||||
begin
|
||||
// WFM: полосы вещательного канала — три кнопки в один ряд.
|
||||
// WFM: полосы вещательного канала — три кнопки + VAR1/VAR2 в один ряд.
|
||||
CloseCTCSSPopup;
|
||||
if FController.FFilter > FILT_WFM_COUNT - 1 then FController.FFilter := FILT_WFM_DEF;
|
||||
FController.FFilterBW := FILT_WFM_BW[FController.FFilter];
|
||||
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;
|
||||
FController.SanitizeFilterState;
|
||||
LayoutFilterButtons(FILT_WFM_COUNT);
|
||||
RelayoutBelowBands;
|
||||
Exit;
|
||||
end;
|
||||
@@ -4558,20 +4584,8 @@ begin
|
||||
if FController.FMode = MODE_FMRAW then
|
||||
begin
|
||||
CloseCTCSSPopup;
|
||||
if FController.FFilter > FILT_RAW_COUNT - 1 then
|
||||
FController.FFilter := FILT_RAW_DEF;
|
||||
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;
|
||||
FController.SanitizeFilterState;
|
||||
LayoutFilterButtons(FILT_RAW_COUNT);
|
||||
if PanelRawDev <> nil then
|
||||
begin
|
||||
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
|
||||
@@ -4585,67 +4599,18 @@ begin
|
||||
if FController.FMode = MODE_DMR then
|
||||
begin
|
||||
CloseCTCSSPopup;
|
||||
FController.FFilter := 0;
|
||||
FController.FFilterBW := FILT_DMR_BW;
|
||||
FController.FFMDeviation := DMR_FM_DEV;
|
||||
for i := 0 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
||||
FController.SanitizeFilterState;
|
||||
for i := 0 to FILT_SLOTS - 1 do BtnFilter[i].Visible := False;
|
||||
RelayoutBelowBands;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
for i := 0 to FILT_COUNT - 1 do
|
||||
begin
|
||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
|
||||
BtnFilter[i].Top := 16 + (i div 5) * 27;
|
||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, 5, i mod 5);
|
||||
BtnFilter[i].Visible := True;
|
||||
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;
|
||||
// Прочие режимы: смена режима возвращает заводской индекс фильтра (как было
|
||||
// до таблицы), кромки и подписи приезжают из таблицы контроллера.
|
||||
PanelFilter.Height := 74;
|
||||
DefIdx := FilterDefIdx(FController.FMode);
|
||||
FController.SelectFilterSlotState(DefIdx);
|
||||
LayoutFilterButtons(FILT_COUNT);
|
||||
RelayoutBelowBands;
|
||||
end;
|
||||
|
||||
@@ -5105,6 +5070,17 @@ begin
|
||||
UpdateBeaconButton;
|
||||
Exit;
|
||||
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
|
||||
begin
|
||||
@@ -5164,6 +5140,13 @@ procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
var
|
||||
VC, VS, NewHz, LoHz, HiHz: Double;
|
||||
begin
|
||||
// Перетаскивание края полосы фильтра.
|
||||
if FEdgeDrag <> 0 then
|
||||
begin
|
||||
if ssLeft in Shift then DoFilterEdgeDrag(X, Shift);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Перетаскивание маркера несущей слайса — перетюн в пределах захваченной полосы.
|
||||
if FSliceDrag then
|
||||
begin
|
||||
@@ -5192,6 +5175,8 @@ begin
|
||||
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
||||
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
||||
PbSpectrum.Cursor := crHandPoint
|
||||
else if FilterEdgeAtPixel(X) <> 0 then
|
||||
PbSpectrum.Cursor := crSizeWE // над краем фильтра — край можно тянуть
|
||||
else if FPan.SliceAtPixel(X) <> 0 then
|
||||
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
||||
else
|
||||
@@ -5211,6 +5196,12 @@ end;
|
||||
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if FEdgeDrag <> 0 then
|
||||
begin
|
||||
FEdgeDrag := 0;
|
||||
PbSpectrum.Cursor := crDefault;
|
||||
Exit;
|
||||
end;
|
||||
if FSliceDrag then
|
||||
begin
|
||||
FSliceDrag := False;
|
||||
@@ -5335,11 +5326,130 @@ end;
|
||||
|
||||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||||
begin
|
||||
// Логика (FFilter/FFilterBW по таблицам режима, ApplyModeFilter, band cache)
|
||||
// — в контроллере; рендер — в OnControllerState(rfFilter).
|
||||
// Логика (слот → кромки, ApplyModeFilter, band cache) — в контроллере;
|
||||
// рендер — в OnControllerState(rfFilter).
|
||||
FController.SetFilterIdx((Sender as TFlatButton).Tag);
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1339,6 +1339,9 @@ begin
|
||||
else BW := S.FilterHi - S.FilterLo;
|
||||
end;
|
||||
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,
|
||||
TRadioController.AGCModeToUI(S.AGC));
|
||||
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
|
||||
|
||||
+176
-136
@@ -215,6 +215,11 @@ type
|
||||
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||||
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 AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
|
||||
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
||||
@@ -275,8 +280,14 @@ type
|
||||
FVfoA, FVfoB: Double;
|
||||
FActiveVfo: 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;
|
||||
FFilterTable: TFilterTable; // редактируемые фильтры по режиму
|
||||
FAGCMode: Integer;
|
||||
FAGCTop: Integer;
|
||||
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 SetSliceModeBW(Id, Mode, BW: 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 AGCModeToUI(A: TWDSPAGCMode): Integer; // и обратно
|
||||
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||||
@@ -1037,7 +1065,9 @@ begin
|
||||
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
|
||||
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
|
||||
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;
|
||||
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
|
||||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||||
@@ -3161,18 +3191,10 @@ end;
|
||||
|
||||
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||||
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||
var Half: Integer;
|
||||
// Обёртка над единственной таблицей знака (Settings.FilterEdgesFromBW) —
|
||||
// оставлена ради прежних вызывающих (MainForm, SetSliceModeBW).
|
||||
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;
|
||||
FilterEdgesFromBW(Mode, BW, Lo, Hi);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceModeBW(Id, Mode, BW: Integer);
|
||||
@@ -3189,7 +3211,10 @@ procedure TRadioController.ApplyModeFilter;
|
||||
var
|
||||
Lo, Hi: Integer;
|
||||
begin
|
||||
FilterEdgesFor(FMode, FFilterBW, Lo, Hi);
|
||||
// Кромки берём как есть: они уже знаковые и уже под текущий режим (таблица
|
||||
// фильтров ведётся ПО режиму), конвертировать боковую больше нечем и негде.
|
||||
Lo := FFilterLo;
|
||||
Hi := FFilterHi;
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
begin
|
||||
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
|
||||
@@ -3216,61 +3241,53 @@ end;
|
||||
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
|
||||
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
|
||||
// корректной и без 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;
|
||||
begin
|
||||
// FM/WFM/DMR/FMRAW сохраняют выбранный пресет (NFM↔WFM и т.п.), остальные
|
||||
// режимы возвращаются к заводскому индексу — как было до таблицы фильтров.
|
||||
case FMode of
|
||||
MODE_FM:
|
||||
begin
|
||||
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;
|
||||
MODE_FM, MODE_WFM, MODE_DMR, MODE_FMRAW: ApplyFilterSlot(FFilter);
|
||||
else ApplyFilterSlot(FilterDefIdx(FMode));
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
|
||||
// Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons,
|
||||
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
|
||||
var S: TFilterSlot;
|
||||
begin
|
||||
case M of
|
||||
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)];
|
||||
MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
|
||||
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;
|
||||
M := EnsureRange(M, 0, CFG_MODE_MAX);
|
||||
S := FFilterTable[M][ClampFilterIdx(M, Idx)];
|
||||
Result := FilterBWFromEdges(M, S.Lo, S.Hi);
|
||||
end;
|
||||
|
||||
function TRadioController.FilterIdxFor(M, BW: Integer): Integer;
|
||||
@@ -3346,26 +3363,10 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.SanitizeFilterForMode;
|
||||
// Для FM индексы фильтров только 0/1 (NFM/WFM); приводим FFilter и
|
||||
// синхронизируем FFilterBW/FFMDeviation. Чистая (state) часть UpdateFilterButtons.
|
||||
// У FM/DMR/FMRAW пресетов меньше, чем у SSB: приводим индекс к допустимому и
|
||||
// синхронизируем кромки/BW/девиацию. Чистая (state) часть UpdateFilterButtons.
|
||||
begin
|
||||
if FMode = MODE_FM then
|
||||
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;
|
||||
if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then ApplyFilterSlot(FFilter);
|
||||
end;
|
||||
|
||||
function TRadioController.MakeBandSettings: TBandSettings;
|
||||
@@ -3544,9 +3545,10 @@ begin
|
||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||||
|
||||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
||||
FFilter := B.FilterIdx;
|
||||
FFilterBW := B.FilterBW;
|
||||
SanitizeFilterForMode;
|
||||
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
|
||||
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины —
|
||||
// ровно та болезнь, от которой уходили.
|
||||
ApplyFilterSlot(B.FilterIdx);
|
||||
ApplyModeFilter;
|
||||
|
||||
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
|
||||
@@ -3985,72 +3987,107 @@ end;
|
||||
|
||||
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
||||
begin
|
||||
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
|
||||
// Idx может прийти из CAT/web без проверки; FM/WFM-таблицы короче остальных.
|
||||
case FMode of
|
||||
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
|
||||
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
|
||||
MODE_DMR: Idx := 0;
|
||||
MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1);
|
||||
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
|
||||
// Публичная команда: Idx может прийти из CAT/web без проверки. Клампится
|
||||
// внутри ApplyFilterSlot (у FM/WFM/FMRAW пресетов меньше; VAR всегда есть).
|
||||
ApplyFilterSlot(Idx);
|
||||
CacheFilterToBand;
|
||||
ApplyModeFilter;
|
||||
Changed(rfFilter);
|
||||
end;
|
||||
|
||||
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;
|
||||
FFilter := Idx;
|
||||
case FMode of
|
||||
MODE_LSB, MODE_USB: FFilterBW := FILT_SSB_BW[Idx];
|
||||
MODE_DIGU, MODE_DIGL: FFilterBW := FILT_DIGI_BW[Idx];
|
||||
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
|
||||
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
|
||||
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
|
||||
MODE_DMR: FFilterBW := FILT_DMR_BW;
|
||||
MODE_FMRAW: FFilterBW := FILT_RAW_BW[Idx];
|
||||
MODE_FM:
|
||||
begin
|
||||
FFilterBW := FILT_FM_BW[Idx];
|
||||
FFMDeviation := FILT_FM_DEV[Idx];
|
||||
end;
|
||||
else FFilterBW := FILT_AM_BW[Idx];
|
||||
end;
|
||||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||||
end;
|
||||
|
||||
procedure TRadioController.ResetFilterSlot(Mode, Idx: Integer);
|
||||
var Def: TFilterSet;
|
||||
begin
|
||||
Mode := EnsureRange(Mode, 0, CFG_MODE_MAX);
|
||||
Def := DefaultFilterSet(Mode);
|
||||
SetFilterSlot(Mode, Idx, Def[EnsureRange(Idx, 0, FILT_SLOTS - 1)]);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ResetFilterSet(Mode: Integer);
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to FILT_SLOTS - 1 do ResetFilterSlot(Mode, i);
|
||||
end;
|
||||
|
||||
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;
|
||||
Changed(rfFilter);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetFilterBW(BWHz: Integer);
|
||||
// Непрерывная полоса (web-ползунок): применяем запрошенный BW как есть, а
|
||||
// FFilter подбираем ближайшим пресетом — только для подсветки кнопки.
|
||||
// Непрерывная полоса (web-ползунок). Точное совпадение с пресетом выбирает
|
||||
// пресет, произвольная ширина уходит в VAR — как и перетаскивание края мышью.
|
||||
var
|
||||
BW_arr: array[0..FILT_COUNT-1] of Integer;
|
||||
best, dist, i: Integer;
|
||||
Lo, Hi, i, n: Integer;
|
||||
begin
|
||||
if FMode = MODE_DMR then BWHz := FILT_DMR_BW;
|
||||
FFilterBW := BWHz;
|
||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
|
||||
case FMode of
|
||||
MODE_LSB, MODE_USB:
|
||||
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
|
||||
FilterEdgesFromBW(FMode, BWHz, Lo, Hi);
|
||||
n := FilterPresetCount(FMode);
|
||||
for i := 0 to n - 1 do
|
||||
if (FFilterTable[FMode][i].Lo = Lo) and (FFilterTable[FMode][i].Hi = Hi) then
|
||||
begin
|
||||
dist := Abs(BW_arr[i] - BWHz);
|
||||
best := i;
|
||||
SetFilterIdx(i);
|
||||
Exit;
|
||||
end;
|
||||
FFilter := best;
|
||||
if FMode = MODE_FM then FFMDeviation := FILT_FM_DEV[best];
|
||||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||||
ApplyModeFilter;
|
||||
Changed(rfFilter);
|
||||
SetFilterEdges(Lo, Hi);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetRawFMDeviation(Hz: Integer);
|
||||
@@ -5788,6 +5825,8 @@ begin
|
||||
FNetwork.SetAlexConfig(FAlexSettings);
|
||||
FSettings.LoadOC(FDevMAC, FOCSettings);
|
||||
FNetwork.SetOCConfig(FOCSettings); // no-op на Pluto (base-класс)
|
||||
FSettings.LoadFilters(FDevMAC, FFilterTable);
|
||||
ApplyFilterSlot(FFilter); // кромки под таблицу этого устройства
|
||||
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
|
||||
FCurrentXvtr := -1;
|
||||
ApplyXvtrToNetwork;
|
||||
@@ -5959,6 +5998,7 @@ begin
|
||||
FSettings.SaveAlex(FDevMAC, FAlexSettings);
|
||||
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
|
||||
FSettings.SaveOC(FDevMAC, FOCSettings);
|
||||
FSettings.SaveFilters(FDevMAC, FFilterTable);
|
||||
// XVTR LastFreq → актуальная видимая частота в момент остановки.
|
||||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||||
|
||||
+223
-2
@@ -89,12 +89,52 @@ const
|
||||
FILT_DMR_BW = 12500;
|
||||
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
|
||||
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
|
||||
VfoA: Double;
|
||||
VfoB: Double;
|
||||
Mode: Integer;
|
||||
FilterIdx: Integer;
|
||||
// FilterIdx — слот в таблице фильтров режима (см. TFilterSet); сами
|
||||
// кромки живут в таблице, а не здесь, поэтому диапазон помнит только
|
||||
// выбор. FilterBW остался легаси-полем для чтения старых конфигов.
|
||||
FilterBW: Integer;
|
||||
AGCMode: Integer;
|
||||
AGCTop: Integer;
|
||||
@@ -300,7 +340,7 @@ type
|
||||
LastFreq: Double; // Hz — последняя tuned visible частота VFO A (BandStack)
|
||||
LastFreqB: Double; // Hz — последняя tuned visible частота VFO B
|
||||
LastMode: Integer; // Mode (LSB=0,USB=1,…FM=5)
|
||||
LastFilterIdx: Integer; // FM: 0=NFM, 1=WFM; иначе 0..FILT_COUNT-1
|
||||
LastFilterIdx: Integer; // слот фильтра 0..FILT_SLOTS-1 (10/11 = VAR1/VAR2)
|
||||
LastFMSQOn: Boolean;
|
||||
LastFMSQLevel: Integer;
|
||||
LastCTCSSOn: Boolean;
|
||||
@@ -581,10 +621,120 @@ type
|
||||
// Тема — глобальная настройка, не привязана к устройству
|
||||
procedure SaveTheme(ALightTheme: 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;
|
||||
|
||||
// Сколько пресетов реально есть у режима (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
|
||||
|
||||
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;
|
||||
Backmult, DefaultMS: Double): Double;
|
||||
var
|
||||
@@ -1680,6 +1830,74 @@ begin
|
||||
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);
|
||||
var i: Integer;
|
||||
begin
|
||||
@@ -1867,7 +2085,10 @@ begin
|
||||
X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq);
|
||||
X.Entries[i].LastFreqB := JD(EObj, 'last_freq_b', X.Entries[i].LastFreqB);
|
||||
X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), CFG_MODE_MIN, CFG_MODE_MAX);
|
||||
X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9);
|
||||
// Верхняя граница = FILT_SLOTS-1, а не 9: слоты VAR1/VAR2 (10/11) иначе
|
||||
// схлопывались в 9, и при восстановлении трансвертера ClampFilterIdx
|
||||
// видел индекс вне таблицы режима и откатывал на заводской фильтр.
|
||||
X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, FILT_SLOTS - 1);
|
||||
X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);
|
||||
X.Entries[i].LastFMSQLevel := EnsureRange(JI(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel), 0, 100);
|
||||
X.Entries[i].LastCTCSSOn := JB(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn);
|
||||
|
||||
+100
-24
@@ -21,7 +21,8 @@ uses
|
||||
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
|
||||
AppTheme,
|
||||
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
|
||||
WaterfallView, SMeterView, RulerView, RadioModes;
|
||||
WaterfallView, SMeterView, RulerView, RadioModes,
|
||||
Settings; // FilterEdgesFromBW — единственная таблица знака боковой
|
||||
|
||||
const
|
||||
SV_CLR_BG = TColor($00101010);
|
||||
@@ -92,6 +93,12 @@ type
|
||||
FTXSpanHz: Double;
|
||||
FTXVfoIndex: Integer;
|
||||
FTXOverlay: Boolean;
|
||||
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
|
||||
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
|
||||
// Thetis-модели НЕ следует за RX, поэтому при передаче полоса рисуется по
|
||||
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
|
||||
FRXEdgeLo, FRXEdgeHi: Integer;
|
||||
FTXEdgeLo, FTXEdgeHi: Integer;
|
||||
// ── ADC overload overlay ─────────────────────────────────────────────────
|
||||
FADCOverloadVisible: Boolean;
|
||||
FADCAlertImg: TAlertImage; // кэш образа плашки (BuildAlertImage, 1 раз)
|
||||
@@ -179,6 +186,12 @@ type
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcTXBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcBandXEdges(VfoFreq, LoHz, HiHz: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcSliceBandX(O: TVfoOverlay; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
function GetWaterfallDirty: Boolean;
|
||||
procedure SetWaterfallDirty(V: Boolean);
|
||||
// Waterfall sub-view delegating accessors
|
||||
@@ -254,6 +267,10 @@ type
|
||||
property TXSpanHz: Double read FTXSpanHz write SetTXSpanHz;
|
||||
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
|
||||
property TXOverlay: Boolean read FTXOverlay write FTXOverlay;
|
||||
// Кромки фильтров (Гц от несущей, знаковые). Ставит MainForm: RX — из
|
||||
// контроллера, TX — из TXSignedEdges движка.
|
||||
procedure SetRXEdges(LoHz, HiHz: Integer);
|
||||
procedure SetTXEdges(LoHz, HiHz: Integer);
|
||||
|
||||
property ADCOverloadVisible: Boolean read FADCOverloadVisible write FADCOverloadVisible;
|
||||
|
||||
@@ -1172,7 +1189,7 @@ begin
|
||||
// Передающий слайс — красная полоса (как у главного при TX), иначе бейдж-цвет.
|
||||
if O.TxActive then Clr := CLR_TX_BAND
|
||||
else Clr := SliceColor(O.SliceLetter); // $00BBGGRR
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
CalcSliceBandX(O, W, SX1, SX2, SVfoX);
|
||||
if SX2 > SX1 then
|
||||
BlendBand(SX1, SX2, H, Clr and $FF, (Clr shr 8) and $FF, (Clr shr 16) and $FF,
|
||||
IfThen(O.TxActive, 100, 60));
|
||||
@@ -1191,7 +1208,7 @@ begin
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
if O.TxActive then Clr := CLR_TX_BAND // передающий слайс
|
||||
else Clr := SliceColor(O.SliceLetter); // оттенок бейджа слайса
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
CalcSliceBandX(O, W, SX1, SX2, SVfoX);
|
||||
RawVLine(SX1, 0, H - 1, Clr);
|
||||
RawVLine(SX2, 0, H - 1, Clr);
|
||||
RawVLine(SVfoX, 0, H - 13, Clr, 2); // несущая (жирнее)
|
||||
@@ -1255,29 +1272,81 @@ begin
|
||||
W - ALERT_PAD - FADCAlertImg.W, ALERT_PAD);
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure TSpectrumView.SetRXEdges(LoHz, HiHz: Integer);
|
||||
begin
|
||||
CalcFilterBandXFor(VfoFreq, FMode, FFilterBW, W, X1, X2, VfoX);
|
||||
if (LoHz = FRXEdgeLo) and (HiHz = FRXEdgeHi) then Exit;
|
||||
FRXEdgeLo := LoHz;
|
||||
FRXEdgeHi := HiHz;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
// То же, но для произвольного режима/полосы (маркеры слайсов).
|
||||
procedure TSpectrumView.SetTXEdges(LoHz, HiHz: Integer);
|
||||
begin
|
||||
if (LoHz = FTXEdgeLo) and (HiHz = FTXEdgeHi) then Exit;
|
||||
FTXEdgeLo := LoHz;
|
||||
FTXEdgeHi := HiHz;
|
||||
if FTXOverlay then FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
// Главный VFO: кромки приходят из контроллера как есть (фильтр может быть
|
||||
// произвольным — VAR/перетаскивание), выводить их из режима и ширины больше
|
||||
// нельзя. Фолбэк по FMode/FFilterBW остаётся на случай, если кромки ещё не
|
||||
// проставлены (первый кадр до первого rfFilter).
|
||||
begin
|
||||
if FRXEdgeHi > FRXEdgeLo then
|
||||
CalcBandXEdges(VfoFreq, FRXEdgeLo, FRXEdgeHi, W, X1, X2, VfoX)
|
||||
else
|
||||
CalcFilterBandXFor(VfoFreq, FMode, FFilterBW, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
// Полоса TX-фильтра вокруг заданной несущей. Кромки уже знаковые и уже под
|
||||
// режим передачи — их считает WDSPEngine.TXSignedEdges, единственный в проекте
|
||||
// конвертер боковой для передачи.
|
||||
procedure TSpectrumView.CalcTXBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
begin
|
||||
if FTXEdgeHi > FTXEdgeLo then
|
||||
CalcBandXEdges(VfoFreq, FTXEdgeLo, FTXEdgeHi, W, X1, X2, VfoX)
|
||||
else
|
||||
CalcFilterBandX(VfoFreq, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
// Готовые знаковые кромки (Гц от несущей) → пиксели.
|
||||
procedure TSpectrumView.CalcBandXEdges(VfoFreq, LoHz, HiHz: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
begin
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
X1 := Max(0, Min(W - 1, VfoX + Round(LoHz / FSpanHz * W)));
|
||||
X2 := Max(0, Min(W - 1, VfoX + Round(HiHz / FSpanHz * W)));
|
||||
end;
|
||||
|
||||
// Полоса слайса: пока он передаёт — по TX-фильтру (TX-полоса общая, не на
|
||||
// слайс), иначе по своему режиму и ширине.
|
||||
procedure TSpectrumView.CalcSliceBandX(O: TVfoOverlay; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
begin
|
||||
if O.TxActive and (FTXEdgeHi > FTXEdgeLo) then
|
||||
CalcBandXEdges(O.DisplayVfoHz, FTXEdgeLo, FTXEdgeHi, W, X1, X2, VfoX)
|
||||
else if O.DisplayHi > O.DisplayLo then
|
||||
// Настоящие кромки слайса — как и у главного VFO. Вывод из режима и
|
||||
// ширины врал бы на любом фильтре, не подчиняющемся конвенции режима.
|
||||
CalcBandXEdges(O.DisplayVfoHz, O.DisplayLo, O.DisplayHi, W, X1, X2, VfoX)
|
||||
else
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
// То же, но для произвольного режима/полосы (маркеры слайсов). Таблица знака
|
||||
// не дублируется — берём общую (Settings.FilterEdgesFromBW), ту же, из которой
|
||||
// строятся заводские фильтры и которой пользуется контроллер.
|
||||
procedure TSpectrumView.CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
var
|
||||
Lo_Hz, Hi_Hz, Half: Double;
|
||||
Lo, Hi: Integer;
|
||||
begin
|
||||
Half := BW / 2;
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
case Mode of
|
||||
MODE_LSB: begin Lo_Hz := -BW; Hi_Hz := -100; end;
|
||||
MODE_USB: begin Lo_Hz := 100; Hi_Hz := BW; end;
|
||||
MODE_DIGL: begin Lo_Hz := -BW; Hi_Hz := 0; end; // полоса от нуля
|
||||
MODE_DIGU: begin Lo_Hz := 0; Hi_Hz := BW; end;
|
||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||
end;
|
||||
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||
X2 := Max(0, Min(W - 1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
||||
FilterEdgesFromBW(Mode, BW, Lo, Hi);
|
||||
CalcBandXEdges(VfoFreq, Lo, Hi, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
@@ -1584,14 +1653,21 @@ begin
|
||||
// фильтра в пару тысяч пикселей стоила 4× дороже полного memcpy кадра),
|
||||
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
||||
|
||||
// Полоса фильтра (X-координаты главного и TX-VFO)
|
||||
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
|
||||
// считается по TX-фильтру: его ширина независима от приёмной (модель Thetis),
|
||||
// и раньше в эфир шло одно, а на экране рисовалось другое.
|
||||
if FTXOverlay and (FTXVfoIndex = FActiveVfo) then
|
||||
begin
|
||||
if FActiveVfo = 0 then CalcTXBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcTXBandX(FVfoB, W, X1, X2, VfoX);
|
||||
end
|
||||
else if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||
TXX1 := 0; TXX2 := 0; TXVfoX := 0;
|
||||
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
|
||||
begin
|
||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
if FTXVfoIndex = 0 then CalcTXBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcTXBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
end;
|
||||
|
||||
// Точки кривой спектра
|
||||
|
||||
+10
-4
@@ -586,7 +586,13 @@ begin
|
||||
TXX1 := 0;
|
||||
TXX2 := 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);
|
||||
|
||||
if FTXOverlay then
|
||||
@@ -594,8 +600,8 @@ begin
|
||||
if FTXVfoIndex <> FActiveVfo then
|
||||
begin
|
||||
if X2 > X1 then DrawRect(X1, 0, X2, H, TColor($0030C030), 100 / 255);
|
||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
if FTXVfoIndex = 0 then CalcTXBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcTXBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
if TXX2 > TXX1 then DrawRect(TXX1, 0, TXX2, H, CLR_TX_BAND, 100 / 255);
|
||||
end
|
||||
else if X2 > X1 then DrawRect(X1, 0, X2, H, CLR_TX_BAND, 100 / 255);
|
||||
@@ -638,7 +644,7 @@ begin
|
||||
// Передающий слайс — красная полоса (как у главного при TX), иначе оттенок бейджа.
|
||||
if O.TxActive then Clr := CLR_TX_BAND
|
||||
else Clr := SliceColor(O.SliceLetter);
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
CalcSliceBandX(O, W, SX1, SX2, SVfoX);
|
||||
if SX2 > SX1 then
|
||||
DrawRect(SX1, 0, SX2, H, Clr, IfThen(O.TxActive, 100, 60) / 255);
|
||||
DrawLine(SX1, 0, SX1, H, Clr, 1, False);
|
||||
|
||||
@@ -63,6 +63,8 @@ type
|
||||
private
|
||||
FMode: Integer;
|
||||
FFilterBW: Integer;
|
||||
FFilterLo: Integer; // знаковые кромки (0/0 = не заданы)
|
||||
FFilterHi: Integer;
|
||||
FVfoHz: Double;
|
||||
FSMeterDB: Double;
|
||||
FModeFilter: array[0..OVL_MODE_COUNT-1] of Integer;
|
||||
@@ -227,6 +229,13 @@ type
|
||||
property DisplayVfoHz: Double read FVfoHz;
|
||||
property DisplayMode: Integer read FMode;
|
||||
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;
|
||||
// Битмап-кэш требует перерисовки (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);
|
||||
var
|
||||
OldMode: Integer;
|
||||
|
||||
@@ -690,6 +690,9 @@ type
|
||||
|
||||
// Расширенное TX управление (применяются мгновенно если канал открыт)
|
||||
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 SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||||
procedure SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||||
@@ -3719,6 +3722,14 @@ begin
|
||||
GetPSMaxTX(TXA_CHAN, @Result);
|
||||
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);
|
||||
var BpLo, BpHi: Double;
|
||||
begin
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="293"/>
|
||||
<BuildNr Value="294"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="126">
|
||||
<MacroValues Count="127">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
@@ -146,6 +146,7 @@
|
||||
<Macro124 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro125 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro126 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro127 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
|
||||
Reference in New Issue
Block a user