feat(fm): add universal FM RAW decoder output

This commit is contained in:
2026-07-16 18:58:28 +03:00
parent 3acd569da5
commit cd816c54d6
16 changed files with 656 additions and 148 deletions
+3 -2
View File
@@ -24,12 +24,13 @@ unit AndromedaPanel;
interface interface
uses uses
SysUtils, RadioController, AndromedaMap; SysUtils, RadioController, AndromedaMap, RadioModes;
const const
AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT) AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT)
AND_BW_STEP_HZ = 50; // Гц на шаг энкодера ширины фильтра AND_BW_STEP_HZ = 50; // Гц на шаг энкодера ширины фильтра
MODE_COUNT_CYCLE = 8; // MODE_LSB..MODE_SAM (0..7) для перебора режимов // Аппаратная панель намеренно перебирает только LSB..SAM.
MODE_COUNT_CYCLE = RadioModes.MODE_SAM - RadioModes.MODE_LSB + 1;
type type
TAndromedaSend = procedure(const S: string) of object; TAndromedaSend = procedure(const S: string) of object;
+1 -1
View File
@@ -307,7 +307,7 @@ begin FSyncInt := V; FController.Invoke(@SyncMode); end;
procedure TCATAdapter.SyncMode; procedure TCATAdapter.SyncMode;
begin begin
if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit; if (FSyncInt < MODE_MIN) or (FSyncInt > MODE_MAX) then Exit;
FController.SetMode(FSyncInt); FController.SetMode(FSyncInt);
end; end;
+14 -14
View File
@@ -24,19 +24,14 @@ unit CATEngine;
interface interface
uses uses
Classes, SysUtils, Math, SyncObjs; Classes, SysUtils, Math, SyncObjs, RadioModes;
const const
CAT_ERROR = '?;'; CAT_ERROR = '?;';
CAT_EXEC_ERROR = 'E;'; CAT_EXEC_ERROR = 'E;';
CAT_ID_NUM = '019'; // TS-2000 compatible CAT_ID_NUM = '019'; // TS-2000 compatible
// Mode mapping ewsdr → CAT (зеркало MODE_* из WDSPEngine) CAT_MODE_COUNT = RadioModes.MODE_COUNT;
MODE_LSB = 0; MODE_USB = 1; MODE_DSB = 2;
MODE_CWL = 3; MODE_CWU = 4; MODE_FM = 5;
MODE_AM = 6; MODE_SAM = 7;
MODE_DIGU = 8; MODE_DIGL = 9; MODE_WFM = 10; MODE_DMR = 11;
CAT_MODE_COUNT = 12;
// FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны // FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны
// совпадать: контроллер хранит FFMRptDir в этих же кодах). // совпадать: контроллер хранит FFMRptDir в этих же кодах).
@@ -392,6 +387,7 @@ begin
MODE_FM: Result := 4; MODE_FM: Result := 4;
MODE_WFM: Result := 4; // treated as FM MODE_WFM: Result := 4; // treated as FM
MODE_DMR: Result := 4; // DMR RF transport is narrow FM MODE_DMR: Result := 4; // DMR RF transport is narrow FM
MODE_FMRAW: Result := 4; // raw discriminator is still FM transport
MODE_AM: Result := 5; MODE_AM: Result := 5;
MODE_SAM: Result := 5; // treated as AM MODE_SAM: Result := 5; // treated as AM
MODE_DIGU: Result := 6; // Kenwood FSK MODE_DIGU: Result := 6; // Kenwood FSK
@@ -1793,20 +1789,24 @@ end;
function TCATEngine.ZZML: string; function TCATEngine.ZZML: string;
// ZZML: mode list (returns all mode names) // ZZML: mode list (returns all mode names)
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR '; end; begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end;
function TCATEngine.ZZMN(const s: string): string; function TCATEngine.ZZMN(const s: string): string;
// ZZMN: mode name — returns mode name for given mode number. // ZZMN: mode name — returns mode name for given mode number.
// Индекс режима теперь двузначный (0..10), но односимвольный запрос // Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
// продолжаем принимать — старые клиенты шлют одну цифру. // продолжаем принимать — старые клиенты шлют одну цифру.
var k: Integer; var k: Integer;
names: array[0..CAT_MODE_COUNT-1] of string; names: array[0..CAT_MODE_COUNT-1] of string;
begin begin
names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL'; names[MODE_LSB]:='LSB'; names[MODE_USB]:='USB';
names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM'; names[MODE_DSB]:='DSB'; names[MODE_CWL]:='CWL';
names[8]:='DIGU'; names[9]:='DIGL'; names[10]:='WFM'; names[11]:='DMR'; names[MODE_CWU]:='CWU'; names[MODE_FM]:='FM';
names[MODE_AM]:='AM'; names[MODE_SAM]:='SAM';
names[MODE_DIGU]:='DIGU'; names[MODE_DIGL]:='DIGL';
names[MODE_WFM]:='WFM'; names[MODE_DMR]:='DMR';
names[MODE_FMRAW]:='FM RAW';
if (Length(s) = 1) or (Length(s) = 2) then begin if (Length(s) = 1) or (Length(s) = 2) then begin
k := EnsureRange(StrToIntDef(s,0),0,CAT_MODE_COUNT-1); k := EnsureRange(StrToIntDef(s, MODE_MIN), MODE_MIN, MODE_MAX);
Result := names[k]; Result := names[k];
end else Result := CAT_ERROR; end else Result := CAT_ERROR;
end; end;
@@ -1819,7 +1819,7 @@ begin
end; end;
function TCATEngine.ZZMV: string; function TCATEngine.ZZMV: string;
// ZZMV: mode value — returns current mode index (ewsdr 0-7) // ZZMV: mode value — returns current ewsdr mode index (MODE_MIN..MODE_MAX)
begin Result := Pad(SafeGetMode, 2); end; begin Result := Pad(SafeGetMode, 2); end;
function TCATEngine.ZZMY: string; function TCATEngine.ZZMY: string;
+8 -5
View File
@@ -8,11 +8,13 @@ unit ChannelStore;
interface interface
uses uses
SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils; SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils,
RadioModes;
const const
CHANNELS_FILE = 'channels.json'; CHANNELS_FILE = 'channels.json';
CHANNEL_MODE_MAX = 11; // LSB..DMR CHANNEL_MODE_MIN = RadioModes.MODE_MIN;
CHANNEL_MODE_MAX = RadioModes.MODE_MAX;
type type
TChannel = record TChannel = record
@@ -20,7 +22,7 @@ type
Name: string; Name: string;
RXFreq: Double; // Hz RXFreq: Double; // Hz
TXFreq: Double; // Hz TXFreq: Double; // Hz
Mode: Integer; // 0=LSB ... 10=WFM 11=DMR Mode: Integer; // RadioModes.MODE_* persisted index
RptDir: Integer; // 0=none 1=minus 2=plus RptDir: Integer; // 0=none 1=minus 2=plus
RptOffsetHz: Double; // Hz RptOffsetHz: Double; // Hz
CTCSSOn: Boolean; CTCSSOn: Boolean;
@@ -59,7 +61,7 @@ begin
Ch.Name := 'New Channel'; Ch.Name := 'New Channel';
Ch.RXFreq := 145500000.0; Ch.RXFreq := 145500000.0;
Ch.TXFreq := 145500000.0; Ch.TXFreq := 145500000.0;
Ch.Mode := 5; // FM Ch.Mode := MODE_FM;
Ch.RptDir := 0; Ch.RptDir := 0;
Ch.RptOffsetHz := 600000.0; Ch.RptOffsetHz := 600000.0;
Ch.CTCSSOn := False; Ch.CTCSSOn := False;
@@ -207,7 +209,8 @@ begin
Ch.Name := JS(O, 'name', 'Channel'); Ch.Name := JS(O, 'name', 'Channel');
Ch.RXFreq := JD(O, 'rx_freq', 145500000.0); Ch.RXFreq := JD(O, 'rx_freq', 145500000.0);
Ch.TXFreq := JD(O, 'tx_freq', 145500000.0); Ch.TXFreq := JD(O, 'tx_freq', 145500000.0);
Ch.Mode := EnsureRange(JI(O, 'mode', 5), 0, CHANNEL_MODE_MAX); Ch.Mode := EnsureRange(JI(O, 'mode', MODE_FM),
CHANNEL_MODE_MIN, CHANNEL_MODE_MAX);
Ch.RptDir := EnsureRange(JI(O, 'rpt_dir', 0), 0, 2); Ch.RptDir := EnsureRange(JI(O, 'rpt_dir', 0), 0, 2);
Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0); Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0);
Ch.CTCSSOn := JB(O, 'ctcss_on', False); Ch.CTCSSOn := JB(O, 'ctcss_on', False);
+5 -5
View File
@@ -14,13 +14,13 @@ uses
Forms, Controls, Graphics, StdCtrls, ExtCtrls, Forms, Controls, Graphics, StdCtrls, ExtCtrls,
FlatButton, FlatCheckBox, FlatComboBox, FlatEdit, FlatButton, FlatCheckBox, FlatComboBox, FlatEdit,
FlatSpinEdit, FlatFloatSpinEdit, FlatListBox, FlatSpinEdit, FlatFloatSpinEdit, FlatListBox,
AppTheme, ChannelStore; AppTheme, ChannelStore, RadioModes;
const const
// Копии из MainForm: нужны для выпадающих списков // Копии из MainForm: нужны для выпадающих списков
CH_MODE_COUNT = 12; CH_MODE_COUNT = RadioModes.MODE_COUNT;
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string = CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string =
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR'); ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FM RAW');
CH_CTCSS_COUNT = 38; CH_CTCSS_COUNT = 38;
CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = ( CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = (
@@ -459,7 +459,7 @@ begin
FEdName.Text := Ch.Name; FEdName.Text := Ch.Name;
FSpinRXFreq.Value := Ch.RXFreq / 1000000.0; FSpinRXFreq.Value := Ch.RXFreq / 1000000.0;
FSpinTXFreq.Value := Ch.TXFreq / 1000000.0; FSpinTXFreq.Value := Ch.TXFreq / 1000000.0;
FCmbMode.ItemIndex := EnsureRange(Ch.Mode, 0, CH_MODE_COUNT - 1); FCmbMode.ItemIndex := EnsureRange(Ch.Mode, MODE_MIN, MODE_MAX);
StyleBtn(FBtnRptNone, Ch.RptDir = 0); StyleBtn(FBtnRptNone, Ch.RptDir = 0);
StyleBtn(FBtnRptMinus, Ch.RptDir = 1); StyleBtn(FBtnRptMinus, Ch.RptDir = 1);
StyleBtn(FBtnRptPlus, Ch.RptDir = 2); StyleBtn(FBtnRptPlus, Ch.RptDir = 2);
@@ -487,7 +487,7 @@ begin
Ch.Name := FEdName.Text; Ch.Name := FEdName.Text;
Ch.RXFreq := FSpinRXFreq.Value * 1000000.0; Ch.RXFreq := FSpinRXFreq.Value * 1000000.0;
Ch.TXFreq := FSpinTXFreq.Value * 1000000.0; Ch.TXFreq := FSpinTXFreq.Value * 1000000.0;
Ch.Mode := EnsureRange(FCmbMode.ItemIndex, 0, CH_MODE_COUNT - 1); Ch.Mode := EnsureRange(FCmbMode.ItemIndex, MODE_MIN, MODE_MAX);
// RptDir берётся из кнопок (Active) // RptDir берётся из кнопок (Active)
if FBtnRptMinus.Active then Ch.RptDir := 1 if FBtnRptMinus.Active then Ch.RptDir := 1
else if FBtnRptPlus.Active then Ch.RptDir := 2 else if FBtnRptPlus.Active then Ch.RptDir := 2
+128 -16
View File
@@ -37,6 +37,7 @@ uses
WidebandView, WidebandView,
StatusBar, StatusBar,
PlatformUtils, PlatformUtils,
RadioModes,
WinFirewall, WinFirewall,
BoardUtils, WisdomBuilder, UISync, BoardUtils, WisdomBuilder, UISync,
FlatEdit, FMRepeater, FlatEdit, FMRepeater,
@@ -67,7 +68,6 @@ const
WDSP_WISDOM_FILE = 'wdspWisdom00'; WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11) BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
MODE_COUNT = 12;
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*) // Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только // перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
// UI-подписи кнопок (NAMES). // UI-подписи кнопок (NAMES).
@@ -83,6 +83,8 @@ const
('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300'); ('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_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k'); FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k');
FILT_RAW_NAMES: array[0..FILT_RAW_COUNT-1] of string =
('6.25k', '10k', '12.5k', '20k', '25k');
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button) // CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController). // CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
@@ -105,8 +107,8 @@ const
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m',''); '160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','');
MODE_NAMES: array[0..MODE_COUNT-1] of string = ( MODE_NAMES: array[0..MODE_COUNT-1] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR'); 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FMRAW');
MODE_ROWS = 3; // 4 колонки; последний ряд неполный MODE_ROWS = 4; // 4 колонки; последний ряд неполный
SMETER_WIDTH_RATIO = 0.32; SMETER_WIDTH_RATIO = 0.32;
SMETER_MARGIN = 3; SMETER_MARGIN = 3;
@@ -299,6 +301,9 @@ type
BtnFMSQ: TFlatButton; BtnFMSQ: TFlatButton;
TrkFMSQ: TFlatSlider; TrkFMSQ: TFlatSlider;
LblFMSQ: TLabel; LblFMSQ: TLabel;
PanelRawDev: TPanel;
TrkRawDev: TFlatSlider;
LblRawDev: TLabel;
PanelFMCTCSS: TPanel; PanelFMCTCSS: TPanel;
BtnFMCTCSS: TFlatButton; BtnFMCTCSS: TFlatButton;
BtnFMCTCSSTone: TFlatButton; BtnFMCTCSSTone: TFlatButton;
@@ -470,9 +475,11 @@ type
procedure Btn2TonClick(Sender: TObject); procedure Btn2TonClick(Sender: TObject);
procedure OnPSPopupChanged(Sender: TObject); // персист PS-параметров procedure OnPSPopupChanged(Sender: TObject); // персист PS-параметров
procedure UpdatePSButtons; // стиль/доступность PS/2TON procedure UpdatePSButtons; // стиль/доступность PS/2TON
procedure UpdateIMDMarkers; // 2TON: маркеры пиков/IMD3 на спектре
procedure UpdateFilterButtons; procedure UpdateFilterButtons;
procedure BtnFMSQClick(Sender: TObject); procedure BtnFMSQClick(Sender: TObject);
procedure TrkFMSQChange(Sender: TObject); procedure TrkFMSQChange(Sender: TObject);
procedure TrkRawDevChange(Sender: TObject);
procedure ApplyFMSquelch; procedure ApplyFMSquelch;
procedure BtnFMCTCSSClick(Sender: TObject); procedure BtnFMCTCSSClick(Sender: TObject);
procedure BtnFMCTCSSToneClick(Sender: TObject); procedure BtnFMCTCSSToneClick(Sender: TObject);
@@ -930,7 +937,7 @@ begin
FController.FVfoA := 14200000; FController.FVfoA := 14200000;
FController.FVfoB := 7100000; FController.FVfoB := 7100000;
FController.FActiveVfo := 0; FController.FActiveVfo := 0;
FController.FMode := 1; FController.FMode := MODE_USB;
FController.FFilter := 5; FController.FFilter := 5;
FController.FAGCMode := 1; // MEDIUM FController.FAGCMode := 1; // MEDIUM
FController.FAGCTop := 90; // 90 dBm FController.FAGCTop := 90; // 90 dBm
@@ -1376,6 +1383,7 @@ begin
ApplySpecViewGridFromState; // сетка спектра под активный тракт ApplySpecViewGridFromState; // сетка спектра под активный тракт
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт) RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
UpdateTXControlAvailability; UpdateTXControlAvailability;
UpdateIMDMarkers; // 2TON: маркеры IMD на TX-спектре
end; // (DUC-очередь сбрасывает SetMOX) end; // (DUC-очередь сбрасывает SetMOX)
rfTuning: rfTuning:
StyleButton(BtnTUN, FController.FTuning); StyleButton(BtnTUN, FController.FTuning);
@@ -1484,6 +1492,12 @@ begin
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter); for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
SyncSpecViewFreq; SyncSpecViewFreq;
end; end;
rfFMDeviation:
if (TrkRawDev <> nil) and (LblRawDev <> nil) then
begin
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]);
end;
rfAGCMode: rfAGCMode:
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
rfVolume: rfVolume:
@@ -1524,6 +1538,7 @@ begin
// подсвечивал → визуал расходился с состоянием. // подсвечивал → визуал расходился с состоянием.
if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex); if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex);
UpdateRxMuteButton; // активность RX MUTE зависит от DUP UpdateRxMuteButton; // активность RX MUTE зависит от DUP
UpdateIMDMarkers; // IMD-маркеры 2TON живут только в DUP
// На передаче переключаем источник пикселей и сбрасываем буферы, чтобы // На передаче переключаем источник пикселей и сбрасываем буферы, чтобы
// старые TX/RX bin'ы не оставались на следующем кадре. // старые TX/RX bin'ы не оставались на следующем кадре.
if FController.FTransmitting then if FController.FTransmitting then
@@ -1550,7 +1565,7 @@ begin
begin begin
if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx); if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx);
if FWebServer <> nil then FWebServer.FMStepIdx := FController.FFMStepIdx; if FWebServer <> nil then FWebServer.FMStepIdx := FController.FFMStepIdx;
if (FController.FMode in [MODE_FM, MODE_DMR]) and if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
(FSpecView <> nil) then (FSpecView <> nil) then
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]; FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx];
end; end;
@@ -2230,6 +2245,35 @@ begin
Inc(Y, 76); Inc(Y, 76);
// FM RAW discriminator deviation. Slider units are 50 Hz: fine enough for
// decoder calibration, while the full 0.5..10 kHz range remains usable.
PanelRawDev := TPanel.Create(Self);
PanelRawDev.Parent := PanelLeft;
PanelRawDev.SetBounds(0, Y, LEFT_W, 50);
PanelRawDev.BevelOuter := bvNone;
PanelRawDev.Visible := False;
MakeLbl(PanelRawDev, 'DEVIATION', 4, 2);
TrkRawDev := TFlatSlider.Create(Self);
TrkRawDev.Parent := PanelRawDev;
TrkRawDev.Left := 5;
TrkRawDev.Top := 22;
TrkRawDev.Width := LEFT_W - 68;
TrkRawDev.Height := 14;
TrkRawDev.Min := RAW_FM_DEV_MIN div 50;
TrkRawDev.Max := RAW_FM_DEV_MAX div 50;
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
TrkRawDev.OnChange := TrkRawDevChange;
LblRawDev := TLabel.Create(Self);
LblRawDev.Parent := PanelRawDev;
LblRawDev.Left := LEFT_W - 61;
LblRawDev.Top := 20;
LblRawDev.Width := 57;
LblRawDev.Alignment := taRightJustify;
LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]);
LblRawDev.Font.Color := CLR_TEXT;
LblRawDev.Font.Name := 'Courier New';
LblRawDev.Font.Size := 8;
// FM Squelch panel (visible only in FM mode, above CTCSS) // FM Squelch panel (visible only in FM mode, above CTCSS)
PanelFMSQ := TPanel.Create(Self); PanelFMSQ := TPanel.Create(Self);
PanelFMSQ.Parent := PanelLeft; PanelFMSQ.Parent := PanelLeft;
@@ -2887,6 +2931,7 @@ begin
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons); DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
DP(PanelBands); DP(PanelMode); DP(PanelFilter); DP(PanelBands); DP(PanelMode); DP(PanelFilter);
if PanelFMSQ <> nil then DP(PanelFMSQ); if PanelFMSQ <> nil then DP(PanelFMSQ);
if PanelRawDev <> nil then DP(PanelRawDev);
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS); if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMStep <> nil then DP(PanelFMStep); if PanelFMStep <> nil then DP(PanelFMStep);
if PanelFMRpt <> nil then DP(PanelFMRpt); if PanelFMRpt <> nil then DP(PanelFMRpt);
@@ -3262,7 +3307,7 @@ begin
FSpecView.SpanHz := ViewSpan; FSpecView.SpanHz := ViewSpan;
FSpecView.Mode := FController.FMode; FSpecView.Mode := FController.FMode;
FSpecView.FilterBW := FController.FFilterBW; FSpecView.FilterBW := FController.FFilterBW;
if FController.FMode in [MODE_FM, MODE_DMR] then if FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx] FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]
else else
FSpecView.FMGridStepHz := 0; FSpecView.FMGridStepHz := 0;
@@ -3554,7 +3599,7 @@ var Allowed: Boolean;
begin begin
if (BtnMOX = nil) or (BtnTUN = nil) or (FController = nil) then Exit; if (BtnMOX = nil) or (BtnTUN = nil) or (FController = nil) then Exit;
Allowed := FController.FRunning and Allowed := FController.FRunning and
(FController.ActiveTXMode <> MODE_DMR); not (FController.ActiveTXMode in [MODE_DMR, MODE_FMRAW]);
BtnMOX.Enabled := Allowed; BtnMOX.Enabled := Allowed;
BtnTUN.Enabled := Allowed; BtnTUN.Enabled := Allowed;
UpdatePSButtons; UpdatePSButtons;
@@ -3568,7 +3613,7 @@ var
begin begin
if (BtnPS = nil) or (Btn2Ton = nil) or (FController = nil) then Exit; if (BtnPS = nil) or (Btn2Ton = nil) or (FController = nil) then Exit;
TXAllowed := FController.FRunning and TXAllowed := FController.FRunning and
(FController.ActiveTXMode <> MODE_DMR); not (FController.ActiveTXMode in [MODE_DMR, MODE_FMRAW]);
PSAllowed := TXAllowed and FController.PSAvailable; PSAllowed := TXAllowed and FController.PSAvailable;
BtnPS.Enabled := PSAllowed; BtnPS.Enabled := PSAllowed;
Btn2Ton.Enabled := TXAllowed; Btn2Ton.Enabled := TXAllowed;
@@ -3576,6 +3621,30 @@ begin
StyleButton(Btn2Ton, FController.FTwoTone); StyleButton(Btn2Ton, FController.FTwoTone);
if (FPSPopup <> nil) and TPureSignalPopup(FPSPopup).Visible then if (FPSPopup <> nil) and TPureSignalPopup(FPSPopup).Visible then
TPureSignalPopup(FPSPopup).RefreshStatus; TPureSignalPopup(FPSPopup).RefreshStatus;
UpdateIMDMarkers;
end;
procedure TMainForm.UpdateIMDMarkers;
// 2TON: маркеры пиков тонов и продуктов IMD3. Только в DUP — дисплей остаётся
// на RX и показывает СВОЙ сигнал после PA (настоящие плечи IMD); цифровой
// TX-спектр продуктов PA не содержит, там замер бессмыслен. Абсолютные
// частоты = TX ± аудио-оффсеты тонов (знак как у генератора: для нижней
// боковой тона лежат НИЖЕ несущей). Замер/отрисовка — в SpectrumView.
var
Sgn: Double;
begin
if (FSpecView = nil) or (FController = nil) then Exit;
if (FController.ActiveTXMode = MODE_LSB) or
(FController.ActiveTXMode = MODE_CWL) or
(FController.ActiveTXMode = MODE_DIGL) then
Sgn := -1.0
else
Sgn := 1.0;
FSpecView.SetIMDMarkers(
FController.FTwoTone and FController.FTransmitting
and FController.FDisplayDuplex,
ActiveTXFreqHz + Sgn * FController.FTXSettings.PSTTFreq1,
ActiveTXFreqHz + Sgn * FController.FTXSettings.PSTTFreq2);
end; end;
procedure TMainForm.UpdateStatusField3; procedure TMainForm.UpdateStatusField3;
@@ -4268,6 +4337,7 @@ begin
EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz); EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz);
PanelFMRpt.Visible := True; PanelFMRpt.Visible := True;
end; end;
if PanelRawDev <> nil then PanelRawDev.Visible := False;
RelayoutBelowBands; RelayoutBelowBands;
Exit; Exit;
end; end;
@@ -4279,7 +4349,7 @@ begin
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False; if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then if PanelFMStep <> nil then
begin begin
if FController.FMode = MODE_DMR then if FController.FMode in [MODE_DMR, MODE_FMRAW] then
begin begin
StyleButton(BtnFMStep, FController.FFMStepOn); StyleButton(BtnFMStep, FController.FFMStepOn);
if FStepDropDown <> nil then if FStepDropDown <> nil then
@@ -4293,6 +4363,7 @@ begin
end; end;
end; end;
if PanelFMRpt <> nil then PanelFMRpt.Visible := False; if PanelFMRpt <> nil then PanelFMRpt.Visible := False;
if PanelRawDev <> nil then PanelRawDev.Visible := False;
if FController.FMode = MODE_WFM then if FController.FMode = MODE_WFM then
begin begin
@@ -4315,6 +4386,33 @@ begin
Exit; Exit;
end; end;
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;
if PanelRawDev <> nil then
begin
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]);
PanelRawDev.Visible := True;
end;
RelayoutBelowBands;
Exit;
end;
if FController.FMode = MODE_DMR then if FController.FMode = MODE_DMR then
begin begin
CloseCTCSSPopup; CloseCTCSSPopup;
@@ -4560,13 +4658,15 @@ end;
procedure TMainForm.OnPSPopupChanged(Sender: TObject); procedure TMainForm.OnPSPopupChanged(Sender: TObject);
// Правка PS-параметров в поповере: применение к WDSP делает сам поповер // Правка PS-параметров в поповере: применение к WDSP делает сам поповер
// (ApplyPSSettingsToDSP), здесь — только персист per-device. // (ApplyPSSettingsToDSP), здесь — персист per-device + live-обновление
// частот IMD-маркеров (если правили тона при активном 2TON).
begin begin
if FController.FDevConnected then if FController.FDevConnected then
begin begin
FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings);
FController.FSettings.Save; FController.FSettings.Save;
end; end;
UpdateIMDMarkers;
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -4587,7 +4687,7 @@ var
ViewCenter, ViewSpan: Double; ViewCenter, ViewSpan: Double;
begin begin
if PanelWidth <= 0 then Exit; if PanelWidth <= 0 then Exit;
if (FController.FMode in [MODE_FM, MODE_DMR]) and if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
FController.FFMStepOn then FController.FFMStepOn then
StepHz := FM_STEP_HZ[FController.FFMStepIdx] StepHz := FM_STEP_HZ[FController.FFMStepIdx]
else else
@@ -4612,7 +4712,7 @@ begin
if (FWidebandView = nil) or if (FWidebandView = nil) or
(not FWidebandView.TryPixelToFrequency(PixelX, PanelWidth, FreqHz)) then Exit; (not FWidebandView.TryPixelToFrequency(PixelX, PanelWidth, FreqHz)) then Exit;
if (FController.FMode in [MODE_FM, MODE_DMR]) and if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
FController.FFMStepOn then FController.FFMStepOn then
StepHz := FM_STEP_HZ[FController.FFMStepIdx] StepHz := FM_STEP_HZ[FController.FFMStepIdx]
else else
@@ -5094,6 +5194,14 @@ begin
SaveCurrentBand; SaveCurrentBand;
end; end;
procedure TMainForm.TrkRawDevChange(Sender: TObject);
var Hz: Integer;
begin
if FSyncingFromController then Exit;
Hz := TrkRawDev.Position * 50;
FController.SetRawFMDeviation(Hz);
end;
procedure TMainForm.CloseCTCSSPopup; procedure TMainForm.CloseCTCSSPopup;
begin begin
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup; if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
@@ -6183,8 +6291,8 @@ begin
Id := P.FirstSliceId; Id := P.FirstSliceId;
StepHz := 100; StepHz := 100;
if ((Id <> 0) and FController.GetSlice(Id, S) and if ((Id <> 0) and FController.GetSlice(Id, S) and
(S.Mode = MODE_DMR)) or (S.Mode in [MODE_DMR, MODE_FMRAW])) or
((Id = 0) and (FController.FMode = MODE_DMR)) then ((Id = 0) and (FController.FMode in [MODE_DMR, MODE_FMRAW])) then
StepHz := 12500; StepHz := 12500;
Hz := Round(VC + (X / W - 0.5) * VS); Hz := Round(VC + (X / W - 0.5) * VS);
Hz := ((Hz + StepHz div 2) div StepHz) * StepHz; Hz := ((Hz + StepHz div 2) div StepHz) * StepHz;
@@ -7443,6 +7551,10 @@ begin
PanelFilter.Top := Y; PanelFilter.Top := Y;
Y := Y + PanelFilter.Height + 2; Y := Y + PanelFilter.Height + 2;
end; end;
if (PanelRawDev <> nil) and PanelRawDev.Visible then
begin
PanelRawDev.Top := Y; Y := Y + PanelRawDev.Height + 2;
end;
if (PanelFMStep <> nil) and PanelFMStep.Visible then if (PanelFMStep <> nil) and PanelFMStep.Visible then
begin begin
PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2; PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2;
@@ -7719,7 +7831,7 @@ begin
end; end;
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
begin begin
if (Sl.Mode = MODE_DMR) and if (Sl.Mode in [MODE_DMR, MODE_FMRAW]) and
not ((ssCtrl in Shift) or (ssShift in Shift)) then Step := 12500 not ((ssCtrl in Shift) or (ssShift in Shift)) then Step := 12500
else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000 else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
else if ssCtrl in Shift then Step := 1000 else if ssCtrl in Shift then Step := 1000
@@ -7743,7 +7855,7 @@ begin
Handled := True; Handled := True;
Exit; Exit;
end; end;
if (FController.FMode in [MODE_FM, MODE_DMR]) and if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
FController.FFMStepOn and FController.FFMStepOn and
not ((ssCtrl in Shift) or (ssShift in Shift)) then not ((ssCtrl in Shift) or (ssShift in Shift)) then
Step := FM_STEP_HZ[FController.FFMStepIdx] Step := FM_STEP_HZ[FController.FFMStepIdx]
+71 -21
View File
@@ -354,6 +354,7 @@ type
// ---- FM ---- // ---- FM ----
FFMDeviation: Double; FFMDeviation: Double;
FRawFMDeviation: Double;
FFMCTCSSOn: Boolean; FFMCTCSSOn: Boolean;
FFMCTCSSToneIdx: Integer; FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean; FFMSQOn: Boolean;
@@ -563,6 +564,7 @@ type
procedure SetMode(M: Integer); procedure SetMode(M: Integer);
procedure SetFilterIdx(Idx: Integer); procedure SetFilterIdx(Idx: Integer);
procedure SetFilterBW(BWHz: Integer); procedure SetFilterBW(BWHz: Integer);
procedure SetRawFMDeviation(Hz: Integer);
procedure SetAGCMode(M: Integer); procedure SetAGCMode(M: Integer);
procedure SetAGCTop(DB: Integer); procedure SetAGCTop(DB: Integer);
procedure AGCTopBy(Delta: Integer); procedure AGCTopBy(Delta: Integer);
@@ -916,7 +918,7 @@ begin
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web) FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны). // Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0; FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := 1; FFilter := 5; FFilterBW := 2700; FMode := MODE_USB; FFilter := 5; FFilterBW := 2700;
FAGCMode := 1; FAGCTop := 90; FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX) FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
@@ -934,6 +936,7 @@ begin
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096; FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
FWfSpeedDiv := 1; FSpecMSAA := 4; FWfSpeedDiv := 1; FSpecMSAA := 4;
FFMStepOn := True; FFMStepOn := True;
FRawFMDeviation := RAW_FM_DEV_DEFAULT;
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
@@ -1316,7 +1319,7 @@ procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
begin begin
// Decoded DMR PCM will use this route later. Until then suppress raw 4FSK; // Decoded DMR PCM will use this route later. Until then suppress raw 4FSK;
// the independent pre-volume tap is handled by OnDemodAudioReady. // the independent pre-volume tap is handled by OnDemodAudioReady.
if FMode = MODE_DMR then Exit; if FMode in [MODE_DMR, MODE_FMRAW] then Exit;
if FSendAudioToRadio then if FSendAudioToRadio then
FNetwork.SendSpeakerAudio(Left, Right, Count); FNetwork.SendSpeakerAudio(Left, Right, Count);
@@ -1332,13 +1335,19 @@ procedure TRadioController.OnDemodAudioReady(const Left, Right: array of Single;
Count: Integer); Count: Integer);
// DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring. // DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring.
begin begin
if not Assigned(FDMRDec) then Exit;
if FMode = MODE_DMR then if FMode = MODE_DMR then
begin begin
if not Assigned(FDMRDec) then Exit;
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True); if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
FDMRDec.FeedAudio(Left, Right, Count); FDMRDec.FeedAudio(Left, Right, Count);
end end
else if FDMRDec.Enabled then else if FMode = MODE_FMRAW then
begin
// FM RAW decoder audio deliberately bypasses speaker volume and mute.
if FLocalAudio and Assigned(FAudioOut) then
FAudioOut.Write(Left, Right, Count);
end
else if Assigned(FDMRDec) and FDMRDec.Enabled then
FDMRDec.SetEnabled(False); FDMRDec.SetEnabled(False);
end; end;
@@ -1396,7 +1405,7 @@ begin
if not FLocalAudio then Exit; if not FLocalAudio then Exit;
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if idx < 0 then Exit; if idx < 0 then Exit;
if FSlices[idx].Mode = MODE_DMR then Exit; if FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW] then Exit;
if SliceMutedByTx(idx) then Exit; if SliceMutedByTx(idx) then Exit;
if FSlices[idx].Audio <> nil then if FSlices[idx].Audio <> nil then
FSlices[idx].Audio.Write(Left, Right, Count); FSlices[idx].Audio.Write(Left, Right, Count);
@@ -1417,9 +1426,12 @@ procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
var idx: Integer; var idx: Integer;
begin begin
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) then Exit; if idx < 0 then Exit;
if Assigned(FSlices[idx].DMR) then if (FSlices[idx].Mode = MODE_DMR) and Assigned(FSlices[idx].DMR) then
FSlices[idx].DMR.FeedAudio(Left, Right, Count); FSlices[idx].DMR.FeedAudio(Left, Right, Count);
if (FSlices[idx].Mode = MODE_FMRAW) and FLocalAudio and
Assigned(FSlices[idx].Audio) then
FSlices[idx].Audio.Write(Left, Right, Count);
end; end;
// DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output. // DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output.
@@ -1821,6 +1833,7 @@ var
AO: TAudioOutput; AO: TAudioOutput;
begin begin
Result := -1; Result := -1;
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
if not Assigned(FDSPEngine) then Exit; if not Assigned(FDSPEngine) then Exit;
if (APanId < 0) or (APanId >= MAX_PANS) then Exit; if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан
@@ -1901,6 +1914,7 @@ end;
procedure TRadioController.SetSliceMode(Id, Mode: Integer); procedure TRadioController.SetSliceMode(Id, Mode: Integer);
var idx, OldMode: Integer; var idx, OldMode: Integer;
begin begin
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
idx := FindSliceIndex(Id); idx := FindSliceIndex(Id);
if idx < 0 then Exit; if idx < 0 then Exit;
OldMode := FSlices[idx].Mode; OldMode := FSlices[idx].Mode;
@@ -1910,7 +1924,7 @@ begin
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode); if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode);
// DMR is receive-only. If this slice used to be the TX source, hand TX back // DMR is receive-only. If this slice used to be the TX source, hand TX back
// to the main receiver as soon as it enters DMR. // to the main receiver as soon as it enters DMR.
if (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0); if (Mode in [MODE_DMR, MODE_FMRAW]) and (FTxSliceId = Id) then SetTxSlice(0);
if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then
FreeAndNil(FSlices[idx].DMR); FreeAndNil(FSlices[idx].DMR);
end; end;
@@ -2221,12 +2235,12 @@ begin
if not GetSlice(SliceId, S) then Exit; if not GetSlice(SliceId, S) then Exit;
// EWSDR has no DMR transmitter/AMBE encoder. Do not accidentally use the // EWSDR has no DMR transmitter/AMBE encoder. Do not accidentally use the
// analog WFM transmitter for a receive-only DMR slice. // analog WFM transmitter for a receive-only DMR slice.
if S.Mode = MODE_DMR then Exit; if S.Mode in [MODE_DMR, MODE_FMRAW] then Exit;
end; end;
if FTxSliceId = SliceId then Exit; if FTxSliceId = SliceId then Exit;
// Selecting main A while it is in DMR is allowed as an RX selection, but it // Selecting main A while it is in DMR is allowed as an RX selection, but it
// cannot inherit an active carrier from a previously selected TX slice. // cannot inherit an active carrier from a previously selected TX slice.
if (SliceId = 0) and (FMode = MODE_DMR) then if (SliceId = 0) and (FMode in [MODE_DMR, MODE_FMRAW]) then
begin begin
if FTuning then SetTune(False) if FTuning then SetTune(False)
else if FTransmitting then SetMOX(False); else if FTransmitting then SetMOX(False);
@@ -2529,9 +2543,13 @@ begin
begin begin
FDSPEngine.SetFilter(Lo, Hi); FDSPEngine.SetFilter(Lo, Hi);
FDSPEngine.SetTXFilter(Lo, Hi); FDSPEngine.SetTXFilter(Lo, Hi);
if (FMode = MODE_FM) or (FMode = MODE_DMR) then if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then
begin begin
if FMode = MODE_FMRAW then
FDSPEngine.SetRawFMDeviation(FRawFMDeviation)
else
FDSPEngine.SetRXFMDeviation(FFMDeviation); FDSPEngine.SetRXFMDeviation(FFMDeviation);
if FMode <> MODE_FMRAW then
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut, FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition); FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
// A squelch would remove sync preambles; DMR always receives open. // A squelch would remove sync preambles; DMR always receives open.
@@ -2565,6 +2583,11 @@ begin
FFilterBW := FILT_DMR_BW; FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV; FFMDeviation := DMR_FM_DEV;
end; 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: MODE_LSB, MODE_USB:
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end; begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
MODE_DIGU, MODE_DIGL: MODE_DIGU, MODE_DIGL:
@@ -2585,6 +2608,7 @@ begin
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)]; 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_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
MODE_DMR: Result := FILT_DMR_BW; MODE_DMR: Result := FILT_DMR_BW;
MODE_FMRAW: Result := FILT_RAW_BW[EnsureRange(Idx, 0, FILT_RAW_COUNT - 1)];
MODE_LSB, MODE_USB: MODE_LSB, MODE_USB:
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DIGU, MODE_DIGL: MODE_DIGU, MODE_DIGL:
@@ -2663,6 +2687,11 @@ begin
FFilterBW := FILT_DMR_BW; FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV; FFMDeviation := DMR_FM_DEV;
end; end;
if FMode = MODE_FMRAW then
begin
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
FFilterBW := FILT_RAW_BW[FFilter];
end;
end; end;
function TRadioController.MakeBandSettings: TBandSettings; function TRadioController.MakeBandSettings: TBandSettings;
@@ -2821,6 +2850,7 @@ begin
Result.AudioOutDevice := FAudioOutDevName; Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName; Result.AudioInDevice := FAudioInDevName;
Result.SendAudioToRadio := FSendAudioToRadio; Result.SendAudioToRadio := FSendAudioToRadio;
Result.RawFMDeviation := Round(FRawFMDeviation);
end; end;
procedure TRadioController.SaveGlobalSettings; procedure TRadioController.SaveGlobalSettings;
@@ -2832,7 +2862,7 @@ procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает. // DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
begin begin
// --- Режим --- // --- Режим ---
FMode := B.Mode; FMode := EnsureRange(B.Mode, MODE_MIN, MODE_MAX);
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR); if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode); if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
@@ -2867,7 +2897,7 @@ begin
end; end;
FFMStepOn := B.FMStepOn; FFMStepOn := B.FMStepOn;
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3); FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
if FMode = MODE_DMR then if FMode in [MODE_DMR, MODE_FMRAW] then
begin begin
FFMStepOn := True; FFMStepOn := True;
FFMStepIdx := 1; // DMR default raster: 12.5 kHz FFMStepIdx := 1; // DMR default raster: 12.5 kHz
@@ -2974,8 +3004,8 @@ begin
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset); if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
HalfSpan := FSpanHz / 2; HalfSpan := FSpanHz / 2;
case FMode of case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end; MODE_LSB: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end; MODE_USB: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end; else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end; end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
@@ -3228,19 +3258,20 @@ end;
procedure TRadioController.SetMode(M: Integer); procedure TRadioController.SetMode(M: Integer);
begin begin
M := EnsureRange(M, MODE_MIN, MODE_MAX);
FMode := M; FMode := M;
// DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP // DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP
// controls enabled so the operator can select another raster afterwards. // controls enabled so the operator can select another raster afterwards.
if FMode = MODE_DMR then if FMode in [MODE_DMR, MODE_FMRAW] then
begin begin
FFMStepOn := True; FFMStepOn := True;
FFMStepIdx := 1; // 12.5 kHz if FMode = MODE_DMR then FFMStepIdx := 1; // 12.5 kHz
Changed(rfFMStep); Changed(rfFMStep);
Changed(rfFMStepIdx); Changed(rfFMStepIdx);
end; end;
// Entering receive-only DMR on the selected main TX source immediately // Entering receive-only DMR on the selected main TX source immediately
// releases an existing carrier. SetTune(False) also releases MOX. // releases an existing carrier. SetTune(False) also releases MOX.
if ActiveTXMode = MODE_DMR then if ActiveTXMode in [MODE_DMR, MODE_FMRAW] then
begin begin
if FTuning then SetTune(False) if FTuning then SetTune(False)
else if FTransmitting then SetMOX(False); else if FTransmitting then SetMOX(False);
@@ -3266,6 +3297,7 @@ begin
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1); MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1); MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
MODE_DMR: Idx := 0; MODE_DMR: Idx := 0;
MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1);
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1); else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
end; end;
FFilter := Idx; FFilter := Idx;
@@ -3276,6 +3308,7 @@ begin
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx]; MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx]; MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
MODE_DMR: FFilterBW := FILT_DMR_BW; MODE_DMR: FFilterBW := FILT_DMR_BW;
MODE_FMRAW: FFilterBW := FILT_RAW_BW[Idx];
MODE_FM: MODE_FM:
begin begin
FFilterBW := FILT_FM_BW[Idx]; FFilterBW := FILT_FM_BW[Idx];
@@ -3309,6 +3342,7 @@ begin
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_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_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_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]; else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end; end;
best := 0; dist := MaxInt; best := 0; dist := MaxInt;
@@ -3325,6 +3359,15 @@ begin
Changed(rfFilter); Changed(rfFilter);
end; end;
procedure TRadioController.SetRawFMDeviation(Hz: Integer);
begin
FRawFMDeviation := EnsureRange(Hz, RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
SaveGlobalSettings;
Changed(rfFMDeviation);
end;
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех // UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех
// фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum). // фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode; class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode;
@@ -4132,7 +4175,7 @@ var
begin begin
// DMR support is receive-only. This guard also covers CAT, web and hardware // DMR support is receive-only. This guard also covers CAT, web and hardware
// PTT. A selected non-DMR TX slice remains valid while main A receives DMR. // PTT. A selected non-DMR TX slice remains valid while main A receives DMR.
if On_ and (ActiveTXMode = MODE_DMR) then Exit; if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
// Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере. // Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере.
if On_ and FDevConnected and (FCurrentXvtr < 0) if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then
@@ -4250,7 +4293,7 @@ end;
procedure TRadioController.SetTune(On_: Boolean); procedure TRadioController.SetTune(On_: Boolean);
begin begin
if On_ = FTuning then Exit; if On_ = FTuning then Exit;
if On_ and (ActiveTXMode = MODE_DMR) then Exit; if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
// Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон, // Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR. // он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
if On_ and FDevConnected and (FCurrentXvtr < 0) if On_ and FDevConnected and (FCurrentXvtr < 0)
@@ -4364,6 +4407,9 @@ begin
FTXSettings.PSPin, FTXSettings.PSMap, FTXSettings.PSPin, FTXSettings.PSMap,
FTXSettings.PSStabilize, Ints, Spi, FTXSettings.PSStabilize, Ints, Spi,
FTXSettings.PSHWPeak); FTXSettings.PSHWPeak);
// Правка частот двухтональника при активном 2TON — применяем живьём.
if FTwoTone then
FDSPEngine.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2);
end; end;
function TRadioController.PSFeedbackLevel: Integer; function TRadioController.PSFeedbackLevel: Integer;
@@ -4541,7 +4587,7 @@ procedure TRadioController.SetTwoTone(On_: Boolean);
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию). // Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
begin begin
if On_ = FTwoTone then Exit; if On_ = FTwoTone then Exit;
if On_ and (ActiveTXMode = MODE_DMR) then Exit; if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
if On_ and FTuning then Exit; // не поверх TUN-тона if On_ and FTuning then Exit; // не поверх TUN-тона
// Safety: те же блокировки, что SetMOX/SetTune. // Safety: те же блокировки, что SetMOX/SetTune.
if On_ and FDevConnected and (FCurrentXvtr < 0) if On_ and FDevConnected and (FCurrentXvtr < 0)
@@ -4952,6 +4998,10 @@ begin
FDitherEnabled := FLoadedGlobal.DitherEnabled; FDitherEnabled := FLoadedGlobal.DitherEnabled;
FRandomEnabled := FLoadedGlobal.RandomEnabled; FRandomEnabled := FLoadedGlobal.RandomEnabled;
FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio; FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio;
FRawFMDeviation := EnsureRange(FLoadedGlobal.RawFMDeviation,
RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
FWfManualHigh := FLoadedGlobal.WfManualHigh; FWfManualHigh := FLoadedGlobal.WfManualHigh;
FWfManualLow := FLoadedGlobal.WfManualLow; FWfManualLow := FLoadedGlobal.WfManualLow;
FWfAGCOffset := FLoadedGlobal.WfAGCOffset; FWfAGCOffset := FLoadedGlobal.WfAGCOffset;
+30
View File
@@ -0,0 +1,30 @@
unit RadioModes;
{$mode objfpc}{$H+}
// Canonical persisted mode indices. Append only: these values are stored in
// settings.json/channels.json and are exposed through CAT and the web API.
interface
const
MODE_LSB = 0;
MODE_USB = 1;
MODE_DSB = 2;
MODE_CWL = 3;
MODE_CWU = 4;
MODE_FM = 5;
MODE_AM = 6;
MODE_SAM = 7;
MODE_DIGU = 8;
MODE_DIGL = 9;
MODE_WFM = 10;
MODE_DMR = 11;
MODE_FMRAW = 12;
MODE_MIN = MODE_LSB;
MODE_MAX = MODE_FMRAW;
MODE_COUNT = MODE_MAX - MODE_MIN + 1;
implementation
end.
+26 -7
View File
@@ -6,13 +6,15 @@ unit Settings;
} }
{$mode objfpc}{$H+} {$mode objfpc}{$H+}
interface interface
uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils; uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils,
RadioModes;
const const
SETTINGS_FILE = 'hpsdr_settings.json'; SETTINGS_FILE = 'hpsdr_settings.json';
CFG_BAND_COUNT = 12; CFG_BAND_COUNT = 12;
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
CFG_MODE_MAX = 11; // LSB..DMR; keep persisted-mode validation current CFG_MODE_MIN = RadioModes.MODE_MIN;
CFG_MODE_MAX = RadioModes.MODE_MAX;
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на // (split-LO транспондер; настраивается на
@@ -61,6 +63,16 @@ const
FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000); FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
FILT_WFM_DEF = 1; // 160k — влезает и в 192k FILT_WFM_DEF = 1; // 160k — влезает и в 192k
// FM RAW: narrow, flat discriminator output for external digital decoders.
// These are RF channel widths; baseband stays flat at 20..7000 Hz.
FILT_RAW_COUNT = 5;
FILT_RAW_BW: array[0..FILT_RAW_COUNT-1] of Integer =
(6250, 10000, 12500, 20000, 25000);
FILT_RAW_DEF = 2; // 12.5 kHz
RAW_FM_DEV_DEFAULT = 2500;
RAW_FM_DEV_MIN = 500;
RAW_FM_DEV_MAX = 10000;
// DMR Tier II: 12.5 kHz RF channel, 4FSK nominal deviation +/-1.944 kHz. // DMR Tier II: 12.5 kHz RF channel, 4FSK nominal deviation +/-1.944 kHz.
FILT_DMR_BW = 12500; FILT_DMR_BW = 12500;
DMR_FM_DEV = 2500; DMR_FM_DEV = 2500;
@@ -359,6 +371,7 @@ type
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000) AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
AudioInDevice: string; // PortAudio input device name for TX mic ('' = none) AudioInDevice: string; // PortAudio input device name for TX mic ('' = none)
SendAudioToRadio: Boolean; // True = send RX audio stream to radio (built-in speaker) SendAudioToRadio: Boolean; // True = send RX audio stream to radio (built-in speaker)
RawFMDeviation: Integer; // Hz, flat RAW FM discriminator scale
// --- Visibility settings --- // --- Visibility settings ---
ShowSpectrum: Boolean; // True = draw spectrum on main form ShowSpectrum: Boolean; // True = draw spectrum on main form
ShowWaterfall: Boolean; // True = draw waterfall on main form ShowWaterfall: Boolean; // True = draw waterfall on main form
@@ -566,6 +579,7 @@ begin
G.AudioSampleRate := 48000; G.AudioSampleRate := 48000;
G.AudioInDevice := ''; G.AudioInDevice := '';
G.SendAudioToRadio := False; G.SendAudioToRadio := False;
G.RawFMDeviation := RAW_FM_DEV_DEFAULT;
G.ShowSpectrum := True; G.ShowSpectrum := True;
G.ShowWaterfall := True; G.ShowWaterfall := True;
G.ShowWideband := False; G.ShowWideband := False;
@@ -796,6 +810,9 @@ begin
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000); G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
G.AudioInDevice := JS(GObj,'audio_in_device',''); G.AudioInDevice := JS(GObj,'audio_in_device','');
G.SendAudioToRadio := JB(GObj,'send_audio_to_radio',False); G.SendAudioToRadio := JB(GObj,'send_audio_to_radio',False);
G.RawFMDeviation := EnsureRange(
JI(GObj,'raw_fm_deviation',RAW_FM_DEV_DEFAULT),
RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
// Visibility settings // Visibility settings
G.ShowSpectrum := JB(GObj,'show_spectrum',True); G.ShowSpectrum := JB(GObj,'show_spectrum',True);
G.ShowWaterfall := JB(GObj,'show_waterfall',True); G.ShowWaterfall := JB(GObj,'show_waterfall',True);
@@ -840,7 +857,7 @@ begin
BObj := GetBandObj(DevObj, i); BObj := GetBandObj(DevObj, i);
Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA); Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA);
Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB); Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB);
Bands[i].Mode := JI(BObj,'mode', Bands[i].Mode); Bands[i].Mode := EnsureRange(JI(BObj,'mode', Bands[i].Mode), CFG_MODE_MIN, CFG_MODE_MAX);
Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx); Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx);
Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW); Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW);
Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode); Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode);
@@ -908,6 +925,7 @@ begin
JW(O,'audio_sample_rate',G.AudioSampleRate); JW(O,'audio_sample_rate',G.AudioSampleRate);
JWS(O,'audio_in_device',G.AudioInDevice); JWS(O,'audio_in_device',G.AudioInDevice);
JW(O,'send_audio_to_radio',G.SendAudioToRadio); JW(O,'send_audio_to_radio',G.SendAudioToRadio);
JW(O,'raw_fm_deviation',G.RawFMDeviation);
// Visibility settings // Visibility settings
JW(O,'show_spectrum',G.ShowSpectrum); JW(O,'show_spectrum',G.ShowSpectrum);
JW(O,'show_waterfall',G.ShowWaterfall); JW(O,'show_waterfall',G.ShowWaterfall);
@@ -973,7 +991,7 @@ begin
O := GetBandObj(GetDevObj(MacStr), BandIdx); O := GetBandObj(GetDevObj(MacStr), BandIdx);
B.VfoA := JD(O,'vfo_a', B.VfoA); B.VfoA := JD(O,'vfo_a', B.VfoA);
B.VfoB := JD(O,'vfo_b', B.VfoB); B.VfoB := JD(O,'vfo_b', B.VfoB);
B.Mode := JI(O,'mode', B.Mode); B.Mode := EnsureRange(JI(O,'mode', B.Mode), CFG_MODE_MIN, CFG_MODE_MAX);
B.FilterIdx := JI(O,'filter_idx', B.FilterIdx); B.FilterIdx := JI(O,'filter_idx', B.FilterIdx);
B.FilterBW := JI(O,'filter_bw', B.FilterBW); B.FilterBW := JI(O,'filter_bw', B.FilterBW);
B.AGCMode := JI(O,'agc_mode', B.AGCMode); B.AGCMode := JI(O,'agc_mode', B.AGCMode);
@@ -1491,7 +1509,7 @@ begin
X.Entries[i].RXAntenna := 0; X.Entries[i].RXAntenna := 0;
X.Entries[i].LastFreq := 0.0; X.Entries[i].LastFreq := 0.0;
X.Entries[i].LastFreqB := 0.0; X.Entries[i].LastFreqB := 0.0;
X.Entries[i].LastMode := 1; // USB — VHF SSB calling default X.Entries[i].LastMode := MODE_USB; // VHF SSB calling default
X.Entries[i].LastFilterIdx := 0; // NFM X.Entries[i].LastFilterIdx := 0; // NFM
X.Entries[i].LastFMSQOn := False; X.Entries[i].LastFMSQOn := False;
X.Entries[i].LastFMSQLevel := 30; X.Entries[i].LastFMSQLevel := 30;
@@ -1572,7 +1590,7 @@ begin
X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3); X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3);
X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq); 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].LastFreqB := JD(EObj, 'last_freq_b', X.Entries[i].LastFreqB);
X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), 0, CFG_MODE_MAX); 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); X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9);
X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn); 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].LastFMSQLevel := EnsureRange(JI(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel), 0, 100);
@@ -1908,7 +1926,8 @@ begin
n := Length(P.Pans[id].Slices); n := Length(P.Pans[id].Slices);
SetLength(P.Pans[id].Slices, n+1); SetLength(P.Pans[id].Slices, n+1);
P.Pans[id].Slices[n].TargetHz := JD(SliceObj, 'target_hz', 0.0); P.Pans[id].Slices[n].TargetHz := JD(SliceObj, 'target_hz', 0.0);
P.Pans[id].Slices[n].Mode := JI(SliceObj, 'mode', 0); P.Pans[id].Slices[n].Mode := EnsureRange(
JI(SliceObj, 'mode', MODE_LSB), CFG_MODE_MIN, CFG_MODE_MAX);
P.Pans[id].Slices[n].FilterLo := JI(SliceObj, 'filter_lo', -1500); P.Pans[id].Slices[n].FilterLo := JI(SliceObj, 'filter_lo', -1500);
P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500); P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500);
P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1); P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1);
+165 -5
View File
@@ -21,7 +21,7 @@ uses
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math, Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
AppTheme, AppTheme,
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
WaterfallView, SMeterView, RulerView; WaterfallView, SMeterView, RulerView, RadioModes;
const const
SV_CLR_BG = TColor($00101010); SV_CLR_BG = TColor($00101010);
@@ -36,6 +36,16 @@ type
LastUse: Int64; LastUse: Int64;
end; end;
// Маркер 2TON/IMD-измерения на спектре: кружок на пике продукта + подпись.
// Тона подписываются уровнем шкалы (dB), IMD-продукты — в dBc от среднего
// уровня тонов.
TIMDMarker = record
FreqHz: Double; // абсолютная частота продукта
LevelDB: Double; // измеренный пик (шкала дисплея)
Caption: string;
IsTone: Boolean; // True = основной тон, False = IMD-продукт
end;
TSpectrumView = class TSpectrumView = class
protected protected
// ── Off-screen bitmaps ──────────────────────────────────────────────────── // ── Off-screen bitmaps ────────────────────────────────────────────────────
@@ -99,6 +109,11 @@ type
FBeaconDecActive: Boolean; FBeaconDecActive: Boolean;
FBeaconDecFreq: Double; FBeaconDecFreq: Double;
FBeaconDecHalf: Double; // полуширина фильтра, Гц FBeaconDecHalf: Double; // полуширина фильтра, Гц
// ── 2TON/IMD-маркеры (пики тонов и продуктов IMD3 при двухтональнике) ────
FIMDActive: Boolean;
FIMDTone1Hz: Double; // абс. частоты тонов (TX freq + аудио-оффсет)
FIMDTone2Hz: Double;
FIMDSummary: string; // сводка «IMD3 −NN dBc» (обновляет ComputeIMDMarkers)
// ── Spectrum buffer ─────────────────────────────────────────────────────── // ── Spectrum buffer ───────────────────────────────────────────────────────
FSpectrumBuf: array[0..WF_MAX_PIXELS-1] of Single; FSpectrumBuf: array[0..WF_MAX_PIXELS-1] of Single;
FSpectrumBufCount: Integer; FSpectrumBufCount: Integer;
@@ -149,6 +164,12 @@ type
procedure DrawSliceFilterLinesRaw(W, H: Integer); procedure DrawSliceFilterLinesRaw(W, H: Integer);
procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor); procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor);
procedure DrawBeaconMarkersRaw(W, H: Integer); procedure DrawBeaconMarkersRaw(W, H: Integer);
// 2TON/IMD: замер пиков по TX-буферу (общий для CPU/GL рендеров).
// Возвращает число валидных маркеров в M (0 если измерять нечего):
// [0..1] тона, [2..3] IMD3 (2f1f2, 2f2f1). Обновляет FIMDSummary.
function ComputeIMDMarkers(var M: array of TIMDMarker): Integer;
procedure DrawIMDMarkersRaw(W, H: Integer; DBmax, InvRange: Double);
procedure RawCircle(CX, CY, R: Integer; Color: TColor);
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte); procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
procedure FillBandRaw(X1, X2, H: Integer; Color: TColor); procedure FillBandRaw(X1, X2, H: Integer; Color: TColor);
procedure CopyGridToSpectrum(W, H: Integer); procedure CopyGridToSpectrum(W, H: Integer);
@@ -244,6 +265,12 @@ type
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double); procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double); procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double);
// 2TON/IMD-маркеры: кружки с уровнями на пиках тонов и продуктов IMD3
// (замер линейности/PureSignal). Включает MainForm при 2TON+TX+DUP:
// дисплей на RX показывает СВОЙ сигнал после PA — настоящие IMD-плечи
// (на цифровом TX-спектре продуктов PA нет, там замер бессмыслен).
procedure SetIMDMarkers(Active: Boolean; Tone1Hz, Tone2Hz: Double);
// ── S-метр ──────────────────────────────────────────────────────────────── // ── S-метр ────────────────────────────────────────────────────────────────
property LastSMeter: Double read GetLastSMeter write SetLastSMeter; property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak; property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak;
@@ -653,6 +680,136 @@ begin
end; end;
end; end;
procedure TSpectrumView.SetIMDMarkers(Active: Boolean; Tone1Hz, Tone2Hz: Double);
begin
FIMDActive := Active;
FIMDTone1Hz := Tone1Hz;
FIMDTone2Hz := Tone2Hz;
if not Active then FIMDSummary := '';
end;
function TSpectrumView.ComputeIMDMarkers(var M: array of TIMDMarker): Integer;
// Пики меряются по FSpectrumBuf. Основной сценарий — DUP: дисплей на RX
// (TXMode=False), буфер покрывает видимое окно FCenterFreq ± FSpanHz/2, и мы
// видим СВОЙ сигнал после PA (реальные IMD-плечи). При TXMode буфер покрывает
// FTXFreq ± FTXSpanHz/2 (цифровой TX-тракт — оставлено для общности).
// Окно поиска ±250 Гц вокруг ожидаемой частоты — ловим пик независимо от
// джиттера FFT.
var
SrcCount, i: Integer;
ToneAvg, MapLo, MapSpan: Double;
function PeakDB(FreqHz: Double): Double;
var C, Win, k, i1, i2: Integer;
begin
Result := -999.0;
C := Round((FreqHz - MapLo) / MapSpan * (SrcCount - 1));
// Ожидаемая частота вне окна буфера (зум/узкий span) — маркер невалиден,
// иначе намеряли бы уровень краевого бина.
if (C < 0) or (C > SrcCount - 1) then Exit;
Win := Max(2, Round(250.0 * (SrcCount - 1) / MapSpan));
i1 := Max(0, C - Win);
i2 := Min(SrcCount - 1, C + Win);
for k := i1 to i2 do
if FSpectrumBuf[k] > Result then Result := FSpectrumBuf[k];
end;
begin
Result := 0;
FIMDSummary := '';
if not FIMDActive then Exit;
if (FSpectrumBufCount < 2) or (Length(M) < 4) then Exit;
if FTXMode then
begin
if FTXSpanHz <= 0 then Exit;
MapLo := FTXFreq - FTXSpanHz * 0.5;
MapSpan := FTXSpanHz;
end
else
begin
if FSpanHz <= 0 then Exit;
MapLo := FCenterFreq - FSpanHz * 0.5;
MapSpan := FSpanHz;
end;
SrcCount := EnsureRange(FSpectrumBufCount, 2, WF_MAX_PIXELS);
M[0].FreqHz := FIMDTone1Hz; M[0].IsTone := True;
M[1].FreqHz := FIMDTone2Hz; M[1].IsTone := True;
M[2].FreqHz := 2 * FIMDTone1Hz - FIMDTone2Hz; M[2].IsTone := False;
M[3].FreqHz := 2 * FIMDTone2Hz - FIMDTone1Hz; M[3].IsTone := False;
for i := 0 to 3 do
M[i].LevelDB := PeakDB(M[i].FreqHz);
ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5;
M[0].Caption := Format('%.0f', [M[0].LevelDB]);
M[1].Caption := Format('%.0f', [M[1].LevelDB]);
// IMD-продукты — в dBc относительно среднего уровня тонов
M[2].Caption := Format('%.0fc', [M[2].LevelDB - ToneAvg]);
M[3].Caption := Format('%.0fc', [M[3].LevelDB - ToneAvg]);
// Сводка только когда оба тона и хотя бы один продукт в окне буфера
if (M[0].LevelDB > -500) and (M[1].LevelDB > -500) and
(Max(M[2].LevelDB, M[3].LevelDB) > -500) then
FIMDSummary := Format('IMD3 %.0f dBc',
[Max(M[2].LevelDB, M[3].LevelDB) - ToneAvg]);
Result := 4;
end;
procedure TSpectrumView.RawCircle(CX, CY, R: Integer; Color: TColor);
// Залитый кружок радиуса R: по строкам, ширина из уравнения окружности.
var
DY, HalfW, Y, X1, X2, K: Integer;
Px: LongWord;
P: PLongWord;
begin
if FRawBase = nil then Exit;
Px := RawPack(Color);
for DY := -R to R do
begin
Y := CY + DY;
if (Y < 0) or (Y >= FRawH) then Continue;
HalfW := Round(Sqrt(R * R - DY * DY));
X1 := Max(0, CX - HalfW);
X2 := Min(FRawW - 1, CX + HalfW);
if X2 < X1 then Continue;
P := PLongWord(FRawBase + Y * FRawBPL + X1 * 4);
for K := X1 to X2 do begin P^ := Px; Inc(P); end;
end;
end;
procedure TSpectrumView.DrawIMDMarkersRaw(W, H: Integer; DBmax, InvRange: Double);
const
CLR_TONE = TColor($0050FF50); // зелёный — основные тона
CLR_IMD = TColor($000A8CFF); // оранжевый — продукты IMD3
var
M: array[0..3] of TIMDMarker;
N, i, X, Y, TY: Integer;
Col: TColor;
begin
if FSpanHz <= 0 then Exit;
N := ComputeIMDMarkers(M);
if N = 0 then Exit;
for i := 0 to N - 1 do
begin
if M[i].LevelDB < -500 then Continue; // частота вне окна буфера
X := Round((M[i].FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
if (X < 0) or (X >= W) then Continue;
Y := Max(0, Min(H - 1, Round((DBmax - M[i].LevelDB) * InvRange * H)));
if M[i].IsTone then Col := CLR_TONE else Col := CLR_IMD;
RawCircle(X, Y, 4, Col);
// Подпись правее-выше кружка; у верхней кромки — вниз, у правой — влево.
TY := Y - 14;
if TY < 2 then TY := Y + 6;
if X + 6 + RawTextWidth(M[i].Caption, 7, True) < W then
RawText(X + 6, TY, M[i].Caption, Col, 7, True, clNone)
else
RawText(X - 6 - RawTextWidth(M[i].Caption, 7, True), TY,
M[i].Caption, Col, 7, True, clNone);
end;
// Сводка в левом верхнем углу
if FIMDSummary <> '' then
RawText(6, 6, FIMDSummary, CLR_IMD, 9, True, clNone);
end;
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
// Full-raw примитивы кадра (валидны только между RawBegin/RawEnd) // Full-raw примитивы кадра (валидны только между RawBegin/RawEnd)
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
@@ -1098,10 +1255,10 @@ begin
Half := BW / 2; Half := BW / 2;
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W); VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
case Mode of case Mode of
0: begin Lo_Hz := -BW; Hi_Hz := -100; end; // LSB MODE_LSB: begin Lo_Hz := -BW; Hi_Hz := -100; end;
1: begin Lo_Hz := 100; Hi_Hz := BW; end; // USB MODE_USB: begin Lo_Hz := 100; Hi_Hz := BW; end;
9: begin Lo_Hz := -BW; Hi_Hz := 0; end; // DIGL — полоса от нуля MODE_DIGL: begin Lo_Hz := -BW; Hi_Hz := 0; end; // полоса от нуля
8: begin Lo_Hz := 0; Hi_Hz := BW; end; // DIGU MODE_DIGU: begin Lo_Hz := 0; Hi_Hz := BW; end;
else begin Lo_Hz := -Half; Hi_Hz := Half; end; else begin Lo_Hz := -Half; Hi_Hz := Half; end;
end; end;
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W))); X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
@@ -1546,6 +1703,9 @@ begin
if FMarkerActive then DrawMarkerLineRaw(W, H); if FMarkerActive then DrawMarkerLineRaw(W, H);
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(W, H); if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(W, H);
// 2TON: кружки с уровнями на пиках тонов/IMD3 (активирует MainForm при
// 2TON+TX+DUP — реальный сигнал после PA виден на RX-спектре)
if FIMDActive then DrawIMDMarkersRaw(W, H, DBmax, InvRange);
// Композиты оверлеев — внутри внешнего лока: их собственные BeginUpdate/ // Композиты оверлеев — внутри внешнего лока: их собственные BeginUpdate/
+79
View File
@@ -57,6 +57,9 @@ type
FLastAGCHangY: Integer; FLastAGCHangY: Integer;
FLastMarkerText: string; FLastMarkerText: string;
FLastMarkerX: Integer; FLastMarkerX: Integer;
// 2TON/IMD: текстуры подписей маркеров (0..3 = кружки, 4 = сводка IMD3)
FIMDLabelTex: array[0..4] of TGLTextureCache;
FIMDLastText: array[0..4] of string;
FLastADCVisible: Boolean; FLastADCVisible: Boolean;
FLastSampleX: Integer; FLastSampleX: Integer;
FLastSampleY: Integer; FLastSampleY: Integer;
@@ -90,6 +93,8 @@ type
procedure DrawSpectrumCurve(W, H: Integer; DBmax, InvRange: Double); procedure DrawSpectrumCurve(W, H: Integer; DBmax, InvRange: Double);
procedure DrawMarker(W, H: Integer); procedure DrawMarker(W, H: Integer);
procedure DrawBeaconMarkersGL(W, H: Integer); procedure DrawBeaconMarkersGL(W, H: Integer);
procedure DrawCircleGL(CX, CY: Integer; R: Single; C: TColor);
procedure DrawIMDMarkersGL(W, H: Integer; DBmax, InvRange: Double);
procedure DrawADCOverlay(W, H: Integer); procedure DrawADCOverlay(W, H: Integer);
procedure DrawCachedOverlays(W, H: Integer); procedure DrawCachedOverlays(W, H: Integer);
procedure DrawSliceOverlays(W, H: Integer); procedure DrawSliceOverlays(W, H: Integer);
@@ -170,6 +175,7 @@ begin
DeleteTexture(FBandOverlayTex); DeleteTexture(FBandOverlayTex);
for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex); for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex);
for i := 0 to High(FLetterTex) do DeleteTexture(FLetterTex[i]); for i := 0 to High(FLetterTex) do DeleteTexture(FLetterTex[i]);
for i := 0 to High(FIMDLabelTex) do DeleteTexture(FIMDLabelTex[i]);
end; end;
SetLength(FSliceTex, 0); SetLength(FSliceTex, 0);
inherited Destroy; inherited Destroy;
@@ -451,6 +457,11 @@ begin
Zap(FBandOverlayTex); Zap(FBandOverlayTex);
for i := 0 to High(FSliceTex) do Zap(FSliceTex[i].Tex); for i := 0 to High(FSliceTex) do Zap(FSliceTex[i].Tex);
for i := 0 to High(FLetterTex) do Zap(FLetterTex[i]); for i := 0 to High(FLetterTex) do Zap(FLetterTex[i]);
for i := 0 to High(FIMDLabelTex) do
begin
Zap(FIMDLabelTex[i]);
FIMDLastText[i] := '';
end;
FLastAGCText := ''; FLastAGCText := '';
FLastMarkerText := ''; FLastMarkerText := '';
FGridDirty := True; FGridDirty := True;
@@ -803,6 +814,71 @@ begin
end; end;
end; end;
procedure TSpectrumViewOpenGL.DrawCircleGL(CX, CY: Integer; R: Single; C: TColor);
// Залитый кружок (triangle fan, 20 сегментов) — маркер пика 2TON/IMD.
var
CR, CG, CB: GLFloat;
a: Integer;
Ang: Single;
begin
ColorToRGB(C, CR, CG, CB);
glColor4f(CR, CG, CB, 1);
glBegin(GL_TRIANGLE_FAN);
glVertex2f(CX + 0.5, CY + 0.5);
for a := 0 to 20 do
begin
Ang := a * 2 * Pi / 20;
glVertex2f(CX + 0.5 + R * Cos(Ang), CY + 0.5 + R * Sin(Ang));
end;
glEnd;
end;
procedure TSpectrumViewOpenGL.DrawIMDMarkersGL(W, H: Integer;
DBmax, InvRange: Double);
// GL-вариант DrawIMDMarkersRaw: замер общий (ComputeIMDMarkers), подписи —
// текстуры с кэшем по строке (перезаливка только при смене значения).
const
CLR_TONE = TColor($0050FF50); // зелёный — основные тона
CLR_IMD = TColor($000A8CFF); // оранжевый — продукты IMD3
var
M: array[0..3] of TIMDMarker;
N, i, X, Y, TX, TY: Integer;
Col: TColor;
begin
if FSpanHz <= 0 then Exit;
N := ComputeIMDMarkers(M);
if N = 0 then Exit;
for i := 0 to N - 1 do
begin
if M[i].LevelDB < -500 then Continue; // частота вне окна буфера
X := Round((M[i].FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
if (X < 0) or (X >= W) then Continue;
Y := Max(0, Min(H - 1, Round((DBmax - M[i].LevelDB) * InvRange * H)));
if M[i].IsTone then Col := CLR_TONE else Col := CLR_IMD;
DrawCircleGL(X, Y, 4.0, Col);
if (M[i].Caption <> FIMDLastText[i]) or FIMDLabelTex[i].Dirty then
begin
UploadText(FIMDLabelTex[i], M[i].Caption, Col, 7, True);
FIMDLastText[i] := M[i].Caption;
end;
TY := Y - 14;
if TY < 2 then TY := Y + 6;
TX := X + 6;
if TX + FIMDLabelTex[i].W >= W then TX := X - 6 - FIMDLabelTex[i].W;
DrawTexture(FIMDLabelTex[i], TX, TY);
end;
// Сводка в левом верхнем углу
if FIMDSummary <> '' then
begin
if (FIMDSummary <> FIMDLastText[4]) or FIMDLabelTex[4].Dirty then
begin
UploadText(FIMDLabelTex[4], FIMDSummary, CLR_IMD, 9, True);
FIMDLastText[4] := FIMDSummary;
end;
DrawTexture(FIMDLabelTex[4], 6, 6);
end;
end;
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer); procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer);
var var
B: TBitmap; B: TBitmap;
@@ -1016,6 +1092,9 @@ begin
DrawSpectrumCurve(W, H, DBmax, InvRange); DrawSpectrumCurve(W, H, DBmax, InvRange);
DrawMarker(W, H); DrawMarker(W, H);
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersGL(W, H); if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersGL(W, H);
// 2TON: кружки с уровнями на пиках тонов/IMD3 (активирует MainForm при
// 2TON+TX+DUP — реальный сигнал после PA виден на RX-спектре)
if FIMDActive then DrawIMDMarkersGL(W, H, DBmax, InvRange);
DrawADCOverlay(W, H); DrawADCOverlay(W, H);
DrawCachedOverlays(W, H); DrawCachedOverlays(W, H);
+39 -29
View File
@@ -17,18 +17,22 @@ unit VfoOverlay;
interface interface
uses uses
Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme; Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme,
RadioModes;
const const
VFO_OVERLAY_ALPHA = 210; VFO_OVERLAY_ALPHA = 210;
OVL_W = 280; // ширина карточки OVL_W = 280; // ширина карточки
// Индексы совпадают с MODE_* контроллера (WDSPEngine.pas). // Индексы совпадают с MODE_* контроллера (WDSPEngine.pas).
OVL_MODE_COUNT = 12; OVL_MODE_COUNT = RadioModes.MODE_COUNT;
OVL_MODE_FM = RadioModes.MODE_FM;
OVL_MODE_DMR = RadioModes.MODE_DMR;
OVL_MODE_FMRAW = RadioModes.MODE_FMRAW;
OVL_MODE_COLS = 4; OVL_MODE_COLS = 4;
OVL_MODE_ROWS = (OVL_MODE_COUNT + OVL_MODE_COLS - 1) div OVL_MODE_COLS; OVL_MODE_ROWS = (OVL_MODE_COUNT + OVL_MODE_COLS - 1) div OVL_MODE_COLS;
OVL_MODE_NAMES: array[0..OVL_MODE_COUNT-1] of string = ( OVL_MODE_NAMES: array[0..OVL_MODE_COUNT-1] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR'); 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FMRAW');
// Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для // Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре. // бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
@@ -307,6 +311,8 @@ const
WFM_LBL: array[0..2] of string = ('200K', '160K', '120K'); WFM_LBL: array[0..2] of string = ('200K', '160K', '120K');
DIGI_BW: array[0..5] of Integer = (300, 600, 1200, 1800, 2400, 3000); DIGI_BW: array[0..5] of Integer = (300, 600, 1200, 1800, 2400, 3000);
DIGI_LBL: array[0..5] of string = ('300','600','1.2K','1.8K','2.4K','3.0K'); DIGI_LBL: array[0..5] of string = ('300','600','1.2K','1.8K','2.4K','3.0K');
RAW_BW: array[0..4] of Integer = (6250,10000,12500,20000,25000);
RAW_LBL: array[0..4] of string = ('6.25K','10K','12.5K','20K','25K');
DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73); DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73);
@@ -400,13 +406,14 @@ end;
function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer; function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
begin begin
case ModeIdx of case ModeIdx of
0,1: Result := SSB_BW[FilterIdx]; MODE_LSB, MODE_USB: Result := SSB_BW[FilterIdx];
2: Result := DSB_BW[FilterIdx]; MODE_DSB: Result := DSB_BW[FilterIdx];
3,4: Result := CW_BW[FilterIdx]; MODE_CWL, MODE_CWU: Result := CW_BW[FilterIdx];
5: Result := FM_BW[FilterIdx]; OVL_MODE_FM: Result := FM_BW[FilterIdx];
8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля MODE_DIGU, MODE_DIGL: Result := DIGI_BW[FilterIdx]; // полоса от нуля
10: Result := WFM_BW[FilterIdx]; MODE_WFM: Result := WFM_BW[FilterIdx];
11: Result := 12500; OVL_MODE_DMR: Result := 12500;
OVL_MODE_FMRAW: Result := RAW_BW[FilterIdx];
else Result := AM_BW[FilterIdx]; else Result := AM_BW[FilterIdx];
end; end;
end; end;
@@ -414,13 +421,14 @@ end;
function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string; function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
begin begin
case ModeIdx of case ModeIdx of
0,1: Result := SSB_LBL[FilterIdx]; MODE_LSB, MODE_USB: Result := SSB_LBL[FilterIdx];
2: Result := DSB_LBL[FilterIdx]; MODE_DSB: Result := DSB_LBL[FilterIdx];
3,4: Result := CW_LBL[FilterIdx]; MODE_CWL, MODE_CWU: Result := CW_LBL[FilterIdx];
5: Result := FM_LBL[FilterIdx]; OVL_MODE_FM: Result := FM_LBL[FilterIdx];
8,9: Result := DIGI_LBL[FilterIdx]; MODE_DIGU, MODE_DIGL: Result := DIGI_LBL[FilterIdx];
10: Result := WFM_LBL[FilterIdx]; MODE_WFM: Result := WFM_LBL[FilterIdx];
11: Result := '12.5k'; OVL_MODE_DMR: Result := '12.5k';
OVL_MODE_FMRAW: Result := RAW_LBL[FilterIdx];
else Result := AM_LBL[FilterIdx]; else Result := AM_LBL[FilterIdx];
end; end;
end; end;
@@ -428,9 +436,10 @@ end;
function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer; function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
begin begin
case ModeIdx of case ModeIdx of
5: Result := 2; // FM: NFM/FM OVL_MODE_FM: Result := 2; // FM: NFM/FM
10: Result := 3; // WFM: 200K/160K/120K MODE_WFM: Result := 3; // WFM: 200K/160K/120K
11: Result := 0; // DMR: fixed 12.5 kHz, no redundant selector OVL_MODE_DMR: Result := 0; // DMR: fixed 12.5 kHz, no redundant selector
OVL_MODE_FMRAW: Result := 5; // FM RAW decoder channel widths
else Result := 6; else Result := 6;
end; end;
end; end;
@@ -438,7 +447,7 @@ end;
function TVfoOverlay.TxSelectionAllowed: Boolean; function TVfoOverlay.TxSelectionAllowed: Boolean;
begin begin
// Main A remains the normal TX source. Additional DMR slices are RX-only. // Main A remains the normal TX source. Additional DMR slices are RX-only.
Result := FIsMain or (FMode <> 11); Result := FIsMain or not (FMode in [OVL_MODE_DMR, OVL_MODE_FMRAW]);
end; end;
{ ---- Lifecycle ---- } { ---- Lifecycle ---- }
@@ -447,7 +456,7 @@ constructor TVfoOverlay.Create(AOwner: TComponent);
begin begin
inherited Create(AOwner); inherited Create(AOwner);
ControlStyle := ControlStyle + [csOpaque]; ControlStyle := ControlStyle + [csOpaque];
FMode := 1; FMode := MODE_USB;
FFilterBW := 2700; FFilterBW := 2700;
FVfoHz := 14200000; FVfoHz := 14200000;
FSMeterDB := -121; FSMeterDB := -121;
@@ -457,6 +466,7 @@ begin
FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000; FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000;
FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[8] := 3000; FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[8] := 3000;
FModeFilter[9] := 3000; FModeFilter[10] := 160000; FModeFilter[9] := 3000; FModeFilter[10] := 160000;
FModeFilter[11] := 12500; FModeFilter[12] := 12500;
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0; FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False; FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False; FVolume := 70; FMuted := False; FSplit := False; FTx := False;
@@ -594,7 +604,7 @@ end;
function TVfoOverlay.IsFM: Boolean; function TVfoOverlay.IsFM: Boolean;
begin begin
Result := FMode = 5; // индекс FM в OVL_MODE_NAMES (= MODE_FM контроллера) Result := FMode = OVL_MODE_FM;
end; end;
function TVfoOverlay.PanelContentH: Integer; function TVfoOverlay.PanelContentH: Integer;
@@ -1039,7 +1049,7 @@ begin
// Краткий статус дополнительного DMR-слайса. Он обновляется только при // Краткий статус дополнительного DMR-слайса. Он обновляется только при
// устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру // устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру
// перезаливаться на каждом burst. // перезаливаться на каждом burst.
if (not FIsMain) and (FMode = 11) and (FDMRText <> '') then if (not FIsMain) and (FMode = OVL_MODE_DMR) and (FDMRText <> '') then
begin begin
DMRStr := FDMRText; DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8; DMRX := X + 22 + C.TextWidth(BWStr) + 8;
@@ -1276,7 +1286,7 @@ begin
begin begin
FModeFilter[FMode] := FFilterBW; FModeFilter[FMode] := FFilterBW;
FMode := NewMode; FMode := NewMode;
if FMode = 11 then FFilterBW := 12500 if FMode = OVL_MODE_DMR then FFilterBW := 12500
else FFilterBW := FModeFilter[FMode]; else FFilterBW := FModeFilter[FMode];
Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3) Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3)
RequestInvalidate; RequestInvalidate;
@@ -1449,19 +1459,19 @@ var
OldMode: Integer; OldMode: Integer;
begin begin
OldMode := FMode; OldMode := FMode;
FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode)); FMode := EnsureRange(AMode, MODE_MIN, MODE_MAX);
FFilterBW := ABW; FFilterBW := ABW;
FModeFilter[FMode] := ABW; FModeFilter[FMode] := ABW;
FVfoHz := AVfoHz; FVfoHz := AVfoHz;
FSMeterDB := ASMeterDB; FSMeterDB := ASMeterDB;
if FMode <> 11 then if FMode <> OVL_MODE_DMR then
begin begin
FDMRText := ''; FDMRText := '';
FDMRSynced := False; FDMRSynced := False;
FDMRCandidateText := ''; FDMRCandidateText := '';
FDMRCandidateSince := 0; FDMRCandidateSince := 0;
end end
else if OldMode <> 11 then else if OldMode <> OVL_MODE_DMR then
begin begin
FDMRText := 'DMR...'; FDMRText := 'DMR...';
FDMRSynced := False; FDMRSynced := False;
@@ -1555,7 +1565,7 @@ procedure TVfoOverlay.UpdateDMRStatus(const AText: string; Synced: Boolean);
var var
NowTick, HoldMs: QWord; NowTick, HoldMs: QWord;
begin begin
if FMode <> 11 then Exit; if FMode <> OVL_MODE_DMR then Exit;
if (FDMRText = AText) and (FDMRSynced = Synced) then if (FDMRText = AText) and (FDMRSynced = Synced) then
begin begin
FDMRCandidateText := ''; FDMRCandidateText := '';
+63 -22
View File
@@ -25,7 +25,7 @@ interface
uses uses
Classes, SysUtils, Math, SyncObjs, Classes, SysUtils, Math, SyncObjs,
BeaconDecoder, BeaconDecoder, RadioModes,
WDSP; WDSP;
const const
@@ -49,18 +49,22 @@ const
// Наши индексы режимов (совпадают с кнопками MainForm). // Наши индексы режимов (совпадают с кнопками MainForm).
// Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json, // Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json,
// band-cache, CAT (ZZMD/ZZMV) и web-API. // band-cache, CAT (ZZMD/ZZMV) и web-API.
MODE_LSB = 0; MODE_LSB = RadioModes.MODE_LSB;
MODE_USB = 1; MODE_USB = RadioModes.MODE_USB;
MODE_DSB = 2; MODE_DSB = RadioModes.MODE_DSB;
MODE_CWL = 3; MODE_CWL = RadioModes.MODE_CWL;
MODE_CWU = 4; MODE_CWU = RadioModes.MODE_CWU;
MODE_FM = 5; MODE_FM = RadioModes.MODE_FM;
MODE_AM = 6; MODE_AM = RadioModes.MODE_AM;
MODE_SAM = 7; MODE_SAM = RadioModes.MODE_SAM;
MODE_DIGU = 8; MODE_DIGU = RadioModes.MODE_DIGU;
MODE_DIGL = 9; MODE_DIGL = RadioModes.MODE_DIGL;
MODE_WFM = 10; MODE_WFM = RadioModes.MODE_WFM;
MODE_DMR = 11; // DMR receive: flat WDSP discriminator -> DMRDecoder MODE_DMR = RadioModes.MODE_DMR;
MODE_FMRAW = RadioModes.MODE_FMRAW;
MODE_MIN = RadioModes.MODE_MIN;
MODE_MAX = RadioModes.MODE_MAX;
MODE_COUNT = RadioModes.MODE_COUNT;
// Минимальная длина FIR у WDSP: create_* поднимают fircore с // Минимальная длина FIR у WDSP: create_* поднимают fircore с
// nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1, // nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1,
@@ -443,6 +447,7 @@ type
FTXWfAvgTimeMS: Double; FTXWfAvgTimeMS: Double;
FMode: Integer; FMode: Integer;
FRawFMDeviation: Double;
FSMeter: Double; FSMeter: Double;
FFilterLow: Integer; FFilterLow: Integer;
FFilterHigh: Integer; FFilterHigh: Integer;
@@ -479,6 +484,7 @@ type
procedure ApplyDSPRate(Mode: Integer); procedure ApplyDSPRate(Mode: Integer);
procedure ApplyRXWFMToChan(Chan: Integer); procedure ApplyRXWFMToChan(Chan: Integer);
procedure ApplyRXDMRToChan(Chan: Integer); procedure ApplyRXDMRToChan(Chan: Integer);
procedure ApplyRXRawToChan(Chan: Integer);
procedure ApplyWFMSettings; procedure ApplyWFMSettings;
// --- Мультислайсы --- // --- Мультислайсы ---
function FindSliceIdx(Id: Integer): Integer; // -1 если нет function FindSliceIdx(Id: Integer): Integer; // -1 если нет
@@ -615,6 +621,7 @@ type
procedure SetSNB(Enable: Boolean); procedure SetSNB(Enable: Boolean);
procedure SetANF(Enable: Boolean); procedure SetANF(Enable: Boolean);
procedure SetRXFMDeviation(Deviation: Double); procedure SetRXFMDeviation(Deviation: Double);
procedure SetRawFMDeviation(Deviation: Double);
procedure SetFMSquelch(Enable: Boolean; Level: Integer); procedure SetFMSquelch(Enable: Boolean; Level: Integer);
procedure SetVolume(Vol: Double); procedure SetVolume(Vol: Double);
procedure SetMute(Mute: Boolean); procedure SetMute(Mute: Boolean);
@@ -991,6 +998,7 @@ begin
// de-emphasis. Those filters collapse the four DMR discriminator levels. // de-emphasis. Those filters collapse the four DMR discriminator levels.
// WFMD is a plain quadrature discriminator and permits flat data audio. // WFMD is a plain quadrature discriminator and permits flat data audio.
MODE_DMR: Result := WDSP_RX_WFM; MODE_DMR: Result := WDSP_RX_WFM;
MODE_FMRAW: Result := WDSP_RX_WFM;
else Result := WDSP_USB; else Result := WDSP_USB;
end; end;
end; end;
@@ -999,7 +1007,7 @@ function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer;
// txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM. // txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM.
begin begin
if Mode = MODE_WFM then Result := WDSP_TX_WFM if Mode = MODE_WFM then Result := WDSP_TX_WFM
else if Mode = MODE_DMR then Result := WDSP_FM else if Mode in [MODE_DMR, MODE_FMRAW] then Result := WDSP_FM
else Result := ModeToWDSP(Mode); else Result := ModeToWDSP(Mode);
end; end;
@@ -1122,6 +1130,15 @@ begin
SetRXAWFMDeemphRun (Chan, 0); SetRXAWFMDeemphRun (Chan, 0);
end; end;
procedure TWDSPEngine.ApplyRXRawToChan(Chan: Integer);
begin
// Decoder-grade discriminator: no broadcast de-emphasis or limiter.
SetRXAWFMDeviation (Chan, FRawFMDeviation);
SetRXAWFMAFFilter (Chan, 20.0, 7000.0);
SetRXAWFMDeemphRun (Chan, 0);
SetRXAWFMLimRun (Chan, 0);
end;
procedure TWDSPEngine.ApplyWFMSettings; procedure TWDSPEngine.ApplyWFMSettings;
begin begin
ApplyRXWFMToChan(RXA_CHAN); ApplyRXWFMToChan(RXA_CHAN);
@@ -1153,6 +1170,7 @@ begin
MODE_DIGL: SetFilter(-3000, 0); MODE_DIGL: SetFilter(-3000, 0);
MODE_WFM: SetFilter(-80000, 80000); MODE_WFM: SetFilter(-80000, 80000);
MODE_DMR: SetFilter(-6250, 6250); MODE_DMR: SetFilter(-6250, 6250);
MODE_FMRAW: SetFilter(-6250, 6250);
end; end;
end; end;
@@ -1306,6 +1324,7 @@ begin
FTXEQFreqs[10] := 3500; FTXEQFreqs[10] := 3500;
FTXAMCarrier := 0.5; FTXAMCarrier := 0.5;
FTXFMDeviation := 5000.0; FTXFMDeviation := 5000.0;
FRawFMDeviation := 2500.0;
FTXFMLowCut := 300; FTXFMLowCut := 300;
FTXFMHighCut := 3000; FTXFMHighCut := 3000;
FTXFMEmphPos := 1; FTXFMEmphPos := 1;
@@ -2312,6 +2331,7 @@ begin
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan); if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
if FSlices[Idx].Mode = MODE_DMR then ApplyRXDMRToChan(Chan); if FSlices[Idx].Mode = MODE_DMR then ApplyRXDMRToChan(Chan);
if FSlices[Idx].Mode = MODE_FMRAW then ApplyRXRawToChan(Chan);
SetRXAShiftRun(Chan, 1); SetRXAShiftRun(Chan, 1);
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz);
RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate), RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate),
@@ -2324,7 +2344,7 @@ begin
ApplyANFToChan(Chan, FSlices[Idx].ANFOn); ApplyANFToChan(Chan, FSlices[Idx].ANFOn);
ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel); ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel);
// Decoder input must not disappear or change scale with speaker volume. // Decoder input must not disappear or change scale with speaker volume.
if FSlices[Idx].Mode = MODE_DMR then if FSlices[Idx].Mode in [MODE_DMR, MODE_FMRAW] then
SetRXAPanelGain1(Chan, 1.0) SetRXAPanelGain1(Chan, 1.0)
else else
SetRXAPanelGain1(Chan, FSlices[Idx].Volume); SetRXAPanelGain1(Chan, FSlices[Idx].Volume);
@@ -2374,7 +2394,7 @@ begin
if not (FSlices[s].Active and FSlices[s].Opened) then Continue; if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
if FSlices[s].PanId <> PanId then Continue; if FSlices[s].PanId <> PanId then Continue;
// Muting a DMR speaker must not make its decoder lose synchronization. // Muting a DMR speaker must not make its decoder lose synchronization.
if FSlices[s].Muted and (FSlices[s].Mode <> MODE_DMR) then Continue; if FSlices[s].Muted and not (FSlices[s].Mode in [MODE_DMR, MODE_FMRAW]) then Continue;
// Свежая копия входного IQ — fexchange0 портит вход in-place. // Свежая копия входного IQ — fexchange0 портит вход in-place.
for i := 0 to N - 1 do for i := 0 to N - 1 do
begin begin
@@ -2390,12 +2410,12 @@ begin
FSliceOutL[k] := FSliceOut[k * 2]; FSliceOutL[k] := FSliceOut[k * 2];
FSliceOutR[k] := FSliceOut[k * 2 + 1]; FSliceOutR[k] := FSliceOut[k * 2 + 1];
end; end;
if FSlices[s].Mode = MODE_DMR then if FSlices[s].Mode in [MODE_DMR, MODE_FMRAW] then
begin begin
if Assigned(FOnSliceDemodAudio) then if Assigned(FOnSliceDemodAudio) then
FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR,
FAudioBufSize); FAudioBufSize);
Continue; // never route the raw 4FSK discriminator to the speaker Continue; // decoder-grade signal uses the independent pre-volume route
end; end;
if Assigned(FOnSliceAudio) then if Assigned(FOnSliceAudio) then
begin begin
@@ -2616,7 +2636,8 @@ begin
SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode)); SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode));
if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan); if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan);
if Mode = MODE_DMR then ApplyRXDMRToChan(FSlices[idx].Chan); if Mode = MODE_DMR then ApplyRXDMRToChan(FSlices[idx].Chan);
if Mode = MODE_DMR then if Mode = MODE_FMRAW then ApplyRXRawToChan(FSlices[idx].Chan);
if Mode in [MODE_DMR, MODE_FMRAW] then
SetRXAPanelGain1(FSlices[idx].Chan, 1.0) SetRXAPanelGain1(FSlices[idx].Chan, 1.0)
else else
SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume); SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume);
@@ -2665,7 +2686,7 @@ begin
try try
idx := FindSliceIdx(Id); if idx < 0 then Exit; idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].Volume := Vol; FSlices[idx].Volume := Vol;
if FSlices[idx].Opened and (FSlices[idx].Mode <> MODE_DMR) then if FSlices[idx].Opened and not (FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW]) then
SetRXAPanelGain1(FSlices[idx].Chan, Vol); SetRXAPanelGain1(FSlices[idx].Chan, Vol);
finally finally
FSliceLock.Leave; FSliceLock.Leave;
@@ -2810,7 +2831,8 @@ begin
SetRXAWFMDeviation (RXA_CHAN, DMR_DEVIATION); SetRXAWFMDeviation (RXA_CHAN, DMR_DEVIATION);
SetRXAWFMAFFilter (RXA_CHAN, 20.0, 7000.0); SetRXAWFMAFFilter (RXA_CHAN, 20.0, 7000.0);
SetRXAWFMDeemphRun (RXA_CHAN, 0); SetRXAWFMDeemphRun (RXA_CHAN, 0);
end; end
else if Mode = MODE_FMRAW then ApplyRXRawToChan(RXA_CHAN);
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
end; end;
@@ -3046,12 +3068,31 @@ end;
procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double); procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double);
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
if FMode = MODE_DMR then if FMode = MODE_FMRAW then SetRawFMDeviation(Deviation)
else if FMode = MODE_DMR then
SetRXAWFMDeviation(RXA_CHAN, Deviation) SetRXAWFMDeviation(RXA_CHAN, Deviation)
else else
SetRXAFMDeviation(RXA_CHAN, Deviation); SetRXAFMDeviation(RXA_CHAN, Deviation);
end; end;
procedure TWDSPEngine.SetRawFMDeviation(Deviation: Double);
var i: Integer;
begin
FRawFMDeviation := EnsureRange(Deviation, 500.0, 10000.0);
if not FInitialized then Exit;
if FMode = MODE_FMRAW then SetRXAWFMDeviation(RXA_CHAN, FRawFMDeviation);
// One calibration value applies to every active FM RAW decoder output.
FSliceLock.Enter;
try
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and FSlices[i].Opened and
(FSlices[i].Mode = MODE_FMRAW) then
SetRXAWFMDeviation(FSlices[i].Chan, FRawFMDeviation);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer); procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer);
var Threshold: Double; var Threshold: Double;
begin begin
+1 -1
View File
@@ -145,7 +145,7 @@ begin FSyncInt := Mode; FController.Invoke(@SyncMode); end;
procedure TWebAdapter.SyncMode; procedure TWebAdapter.SyncMode;
begin begin
if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit; if (FSyncInt < MODE_MIN) or (FSyncInt > MODE_MAX) then Exit;
FController.SetMode(FSyncInt); // дефолт фильтра + DSP + cache; рендер через rfMode FController.SetMode(FSyncInt); // дефолт фильтра + DSP + cache; рендер через rfMode
end; end;
+15 -12
View File
@@ -193,7 +193,7 @@ begin
'<select id="modGrpSel">' + '<select id="modGrpSel">' +
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' + '<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
'<option value="fm">FM</option><option value="am">AM</option>' + '<option value="fm">FM</option><option value="am">AM</option>' +
'<option value="digi">DIGI</option><option value="dmr">DMR</option>' + '<option value="digi">DIGI</option><option value="dmr">DMR</option><option value="raw">FM RAW</option>' +
'</select>' + '</select>' +
'<select id="stepSel">' + '<select id="stepSel">' +
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' + '<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
@@ -299,14 +299,15 @@ begin
'function updRfAgc(){const b=q("rfagcBtn");if(!b)return;b.style.display=cur.rf_agc_visible?"":"none";b.textContent="RF AGC "+rfagcModeName(cur.rx_gain_mode);if(rfagcEls)rfagcRefresh();}' + 'function updRfAgc(){const b=q("rfagcBtn");if(!b)return;b.style.display=cur.rf_agc_visible?"":"none";b.textContent="RF AGC "+rfagcModeName(cur.rx_gain_mode);if(rfagcEls)rfagcRefresh();}' +
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM","DMR"];' + 'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM","DMR","FM RAW"];' +
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,10],am:[6,7],digi:[8,9],dmr:[11,11]};' + 'const M_LSB=MODE_N.indexOf("LSB"),M_USB=MODE_N.indexOf("USB"),M_DSB=MODE_N.indexOf("DSB"),M_CWL=MODE_N.indexOf("CWL"),M_CWU=MODE_N.indexOf("CWU"),M_FM=MODE_N.indexOf("FM"),M_AM=MODE_N.indexOf("AM"),M_SAM=MODE_N.indexOf("SAM"),M_DIGU=MODE_N.indexOf("DIGU"),M_DIGL=MODE_N.indexOf("DIGL"),M_WFM=MODE_N.indexOf("WFM"),M_DMR=MODE_N.indexOf("DMR"),M_FMRAW=MODE_N.indexOf("FM RAW");' +
'const MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am","digi","digi","fm","dmr"];' + 'const GRP_MAP={ssb:[M_LSB,M_USB],cw:[M_CWL,M_CWU],fm:[M_FM,M_WFM],am:[M_AM,M_SAM],digi:[M_DIGU,M_DIGL],dmr:[M_DMR,M_DMR],raw:[M_FMRAW,M_FMRAW]};' +
'const MODE_TO_GRP=[];Object.entries(GRP_MAP).forEach(([g,ms])=>ms.forEach(m=>MODE_TO_GRP[m]=g));MODE_TO_GRP[M_DSB]="ssb";' +
'function modGrpFromMode(m){return MODE_TO_GRP[m]||"ssb";}' + 'function modGrpFromMode(m){return MODE_TO_GRP[m]||"ssb";}' +
'function modGrp(){return q("modGrpSel").value||"ssb";}' + 'function modGrp(){return q("modGrpSel").value||"ssb";}' +
'function modTglTarget(){const grp=GRP_MAP[modGrp()];const m=cur.mode||0;return(m===grp[0])?grp[1]:grp[0];}' + 'function modTglTarget(){const grp=GRP_MAP[modGrp()];const m=cur.mode||0;return(m===grp[0])?grp[1]:grp[0];}' +
'function updModTglLabel(){const b=q("modTglBtn");if(!b)return;const grp=GRP_MAP[modGrp()];const m=cur.mode||0;const show=(m===grp[0]||m===grp[1])?m:grp[0];b.textContent=MODE_N[show];}' + 'function updModTglLabel(){const b=q("modTglBtn");if(!b)return;const grp=GRP_MAP[modGrp()];const m=cur.mode||0;const show=(m===grp[0]||m===grp[1])?m:grp[0];b.textContent=MODE_N[show];}' +
'function syncModGrp(m){const g=MODE_TO_GRP[m]||"ssb";const s=q("modGrpSel");if(s&&s.value!==g)s.value=g;const f=q("fnpBtn");if(f)f.style.display=(m===11)?"none":"";updModTglLabel();}' + 'function syncModGrp(m){const g=MODE_TO_GRP[m]||"ssb";const s=q("modGrpSel");if(s&&s.value!==g)s.value=g;const f=q("fnpBtn");if(f)f.style.display=(m===M_DMR)?"none":"";updModTglLabel();}' +
'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;syncModGrp(defaultMode);pend("mode");ws2({cmd:"mode",mode:defaultMode});};' + 'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;syncModGrp(defaultMode);pend("mode");ws2({cmd:"mode",mode:defaultMode});};' +
'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;syncModGrp(next);pend("mode");ws2({cmd:"mode",mode:next});};' + 'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;syncModGrp(next);pend("mode");ws2({cmd:"mode",mode:next});};' +
'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' + 'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' +
@@ -320,6 +321,7 @@ begin
'fm:["NFM","FM"],' + 'fm:["NFM","FM"],' +
'wfm:["200k","160k","120k"],' + 'wfm:["200k","160k","120k"],' +
'dmr:["12.5k"],' + 'dmr:["12.5k"],' +
'raw:["6.25k","10k","12.5k","20k","25k"],' +
'digi:["3.0k","2.7k","2.4k","2.1k","1.8k","1.5k","1.2k","900","600","300"]};' + 'digi:["3.0k","2.7k","2.4k","2.1k","1.8k","1.5k","1.2k","900","600","300"]};' +
'var FNP_BW={ssb:[5000,4400,3800,3300,2900,2700,2400,2100,1800,1000],' + 'var FNP_BW={ssb:[5000,4400,3800,3300,2900,2700,2400,2100,1800,1000],' +
'dsb:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' + 'dsb:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' +
@@ -328,13 +330,14 @@ begin
'fm:[11000,16000],' + 'fm:[11000,16000],' +
'wfm:[200000,160000,120000],' + 'wfm:[200000,160000,120000],' +
'dmr:[12500],' + 'dmr:[12500],' +
'raw:[6250,10000,12500,20000,25000],' +
'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' + 'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' +
'var fnpOv=null;' + 'var fnpOv=null;' +
'function fnpGrp(){var m=cur.mode|0;return(m===3||m===4)?"cw":(m===5)?"fm":(m===10)?"wfm":(m===11)?"dmr":(m===8||m===9)?"digi":(m===2)?"dsb":(m===6||m===7)?"am":"ssb";}' + 'function fnpGrp(){var m=cur.mode|0;return(m===M_CWL||m===M_CWU)?"cw":(m===M_FM)?"fm":(m===M_WFM)?"wfm":(m===M_DMR)?"dmr":(m===M_FMRAW)?"raw":(m===M_DIGU||m===M_DIGL)?"digi":(m===M_DSB)?"dsb":(m===M_AM||m===M_SAM)?"am":"ssb";}' +
'function closeFnp(){if(fnpOv){fnpOv.remove();fnpOv=null;}document.removeEventListener("click",fnpOutside);}' + 'function closeFnp(){if(fnpOv){fnpOv.remove();fnpOv=null;}document.removeEventListener("click",fnpOutside);}' +
'function fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' + 'function fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' +
'function showFnp(){' + 'function showFnp(){' +
'if((cur.mode|0)===11)return;' + 'if((cur.mode|0)===M_DMR)return;' +
'if(fnpOv){closeFnp();return;}' + 'if(fnpOv){closeFnp();return;}' +
'var g=fnpGrp(),ns=FNP_N[g],bws=FNP_BW[g],cb=cur.filter_bw||2700;' + 'var g=fnpGrp(),ns=FNP_N[g],bws=FNP_BW[g],cb=cur.filter_bw||2700;' +
'fnpOv=document.createElement("div");' + 'fnpOv=document.createElement("div");' +
@@ -791,7 +794,7 @@ begin
'const nowTx=!!cur.transmitting;if(nowTx!==prevTx){prevTx=nowTx;if(nowTx)startTXMic();else stopTXMic();}}' + 'const nowTx=!!cur.transmitting;if(nowTx!==prevTx){prevTx=nowTx;if(nowTx)startTXMic();else stopTXMic();}}' +
'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' + 'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' +
'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===0)return[-bw,-100];if(m===1)return[100,bw];return[-bw/2,bw/2];}' + 'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===M_LSB)return[-bw,-100];if(m===M_USB)return[100,bw];return[-bw/2,bw/2];}' +
'function xToHz(x,w){return localCenter+((x/Math.max(1,w))-.5)*localSpan;}' + 'function xToHz(x,w){return localCenter+((x/Math.max(1,w))-.5)*localSpan;}' +
'function hzToX(f,w){return((f-localCenter)/localSpan+.5)*w;}' + 'function hzToX(f,w){return((f-localCenter)/localSpan+.5)*w;}' +
@@ -817,7 +820,7 @@ begin
'function drawRuler(){const w=rulerCv.clientWidth,h=rulerCv.clientHeight;rctx.fillStyle="#0c1210";rctx.fillRect(0,0,w,h);' + 'function drawRuler(){const w=rulerCv.clientWidth,h=rulerCv.clientHeight;rctx.fillStyle="#0c1210";rctx.fillRect(0,0,w,h);' +
'const st=localCenter-localSpan/2;' + 'const st=localCenter-localSpan/2;' +
'const isFM=(cur.mode===5||cur.mode===11),fmStep=FM_STEP_HZ[fmStepIdx];' + 'const isFM=(cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW),fmStep=FM_STEP_HZ[fmStepIdx];' +
'if(isFM&&fmStep>0){' + 'if(isFM&&fmStep>0){' +
'const pxPerStep=w*fmStep/localSpan;' + 'const pxPerStep=w*fmStep/localSpan;' +
'const labelMult=Math.max(1,Math.ceil((rctx.measureText("000.000").width+6)/pxPerStep));' + 'const labelMult=Math.max(1,Math.ceil((rctx.measureText("000.000").width+6)/pxPerStep));' +
@@ -832,7 +835,7 @@ begin
'function drawSpec(bins){const w=specCv.clientWidth,h=specCv.clientHeight;sctx.fillStyle="#050a05";sctx.fillRect(0,0,w,h);' + 'function drawSpec(bins){const w=specCv.clientWidth,h=specCv.clientHeight;sctx.fillStyle="#050a05";sctx.fillRect(0,0,w,h);' +
'for(let i=1;i<5;i++){const y=i*h/5;sctx.strokeStyle="#121e12";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(0,y);sctx.lineTo(w,y);sctx.stroke();}' + 'for(let i=1;i<5;i++){const y=i*h/5;sctx.strokeStyle="#121e12";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(0,y);sctx.lineTo(w,y);sctx.stroke();}' +
'if((cur.mode===5||cur.mode===11)&&FM_STEP_HZ[fmStepIdx]>0){' + 'if((cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW)&&FM_STEP_HZ[fmStepIdx]>0){' +
'const fmSt=FM_STEP_HZ[fmStepIdx],st2=localCenter-localSpan/2;' + 'const fmSt=FM_STEP_HZ[fmStepIdx],st2=localCenter-localSpan/2;' +
'const pxSt=w*fmSt/localSpan,gm=Math.max(1,Math.ceil(4/pxSt));' + 'const pxSt=w*fmSt/localSpan,gm=Math.max(1,Math.ceil(4/pxSt));' +
'const fn=Math.ceil(st2/fmSt);' + 'const fn=Math.ceil(st2/fmSt);' +
@@ -875,7 +878,7 @@ begin
'if(smCv.width!==(sw*dpr|0)||smCv.height!==(sh*dpr|0)){smCv.width=sw*dpr|0;smCv.height=sh*dpr|0;mctx.setTransform(dpr,0,0,dpr,0,0);}' + 'if(smCv.width!==(sw*dpr|0)||smCv.height!==(sh*dpr|0)){smCv.width=sw*dpr|0;smCv.height=sh*dpr|0;mctx.setTransform(dpr,0,0,dpr,0,0);}' +
'if(wfBuf.width!==wfCv.clientWidth||wfBuf.height!==wfCv.clientHeight){wfBuf.width=wfCv.clientWidth;wfBuf.height=wfCv.clientHeight;wfBCtx.fillStyle="#000";wfBCtx.fillRect(0,0,wfBuf.width,wfBuf.height);}}' + 'if(wfBuf.width!==wfCv.clientWidth||wfBuf.height!==wfCv.clientHeight){wfBuf.width=wfCv.clientWidth;wfBuf.height=wfCv.clientHeight;wfBCtx.fillStyle="#000";wfBCtx.fillRect(0,0,wfBuf.width,wfBuf.height);}}' +
'function syncStepSel(){const isFM=(cur.mode===5||cur.mode===11);const sel=q("stepSel");if(!sel)return;' + 'function syncStepSel(){const isFM=(cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW);const sel=q("stepSel");if(!sel)return;' +
'if(isFM&&!inFMStep){lastHFStep=+sel.value||100;sel.innerHTML="";' + 'if(isFM&&!inFMStep){lastHFStep=+sel.value||100;sel.innerHTML="";' +
'FM_STEP_LBL.forEach((l,i)=>{const o=document.createElement("option");o.value=FM_STEP_HZ[i];o.textContent=l;sel.appendChild(o);});' + 'FM_STEP_LBL.forEach((l,i)=>{const o=document.createElement("option");o.value=FM_STEP_HZ[i];o.textContent=l;sel.appendChild(o);});' +
'sel.value=FM_STEP_HZ[fmStepIdx];' + 'sel.value=FM_STEP_HZ[fmStepIdx];' +
@@ -937,7 +940,7 @@ begin
'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' + 'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' +
// клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули. // клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули.
// Спектр остаётся на месте, сервер пришлёт center_hz через ~200ms и всё перецентрируется. // Спектр остаётся на месте, сервер пришлёт center_hz через ~200ms и всё перецентрируется.
'const snap=((cur.mode===5||cur.mode===11)&&inFMStep)?FM_STEP_HZ[fmStepIdx]:100;' + 'const snap=((cur.mode===M_FM||cur.mode===M_DMR||cur.mode===M_FMRAW)&&inFMStep)?FM_STEP_HZ[fmStepIdx]:100;' +
'const nf=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/snap)*snap,vfoMin(),vfoMax());' + 'const nf=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/snap)*snap,vfoMin(),vfoMax());' +
'setActiveVfoHz(nf);' + 'setActiveVfoHz(nf);' +
'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' + 'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' +
+5 -5
View File
@@ -69,7 +69,7 @@ interface
uses uses
Classes, SysUtils, Math, Classes, SysUtils, Math,
WebUtils, WsClient, WebPageHtml WebUtils, WsClient, WebPageHtml, RadioModes
{$IFDEF WINDOWS}, Windows, WinSock2{$ELSE}, BaseUnix, Sockets{$ENDIF}, {$IFDEF WINDOWS}, Windows, WinSock2{$ELSE}, BaseUnix, Sockets{$ENDIF},
SyncObjs; // ← после платформенных юнитов: исключает конфликт идентификатора Create SyncObjs; // ← после платформенных юнитов: исключает конфликт идентификатора Create
@@ -560,7 +560,7 @@ begin
FStateLock := TCriticalSection.Create; FStateLock := TCriticalSection.Create;
// Начальные значения состояния // Начальные значения состояния
FFreq := 14200000; FFreq := 14200000;
FMode := 1; FMode := MODE_USB;
FFMStepIdx := 3; // 25 kHz default FFMStepIdx := 3; // 25 kHz default
FFilterBW:= 2700; FFilterBW:= 2700;
FAGCMode := 1; FAGCMode := 1;
@@ -1717,8 +1717,8 @@ end;
function TWebServer.BuildStateJson: string; function TWebServer.BuildStateJson: string;
const const
MODE_N: array[0..11] of string = MODE_N: array[RadioModes.MODE_MIN..RadioModes.MODE_MAX] of string =
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR'); ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FM RAW');
var var
FS: TFormatSettings; FS: TFormatSettings;
XvtrJson, DevJson, RateJson, BandJson: string; XvtrJson, DevJson, RateJson, BandJson: string;
@@ -1790,7 +1790,7 @@ begin
'"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' + '"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' +
'"rx_mute_visible":%s,"rx_mute_on":%s}', '"rx_mute_visible":%s,"rx_mute_on":%s}',
[FFreq, FVfoB, FActiveVfo, [FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod Length(MODE_N)], FMode, MODE_N[EnsureRange(FMode, MODE_MIN, MODE_MAX)],
FFilterIdx, FFilterBW, FFilterIdx, FFilterBW,
FAGCMode, FAGCTop, FAGCMode, FAGCTop,
FSpanHz, FCenterHz, FVolume, FSpanHz, FCenterHz, FVolume,