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
+39 -29
View File
@@ -17,18 +17,22 @@ unit VfoOverlay;
interface
uses
Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme;
Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme,
RadioModes;
const
VFO_OVERLAY_ALPHA = 210;
OVL_W = 280; // ширина карточки
// Индексы совпадают с 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_ROWS = (OVL_MODE_COUNT + OVL_MODE_COLS - 1) div OVL_MODE_COLS;
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): одна на букву — общий цвет для
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
@@ -307,6 +311,8 @@ const
WFM_LBL: array[0..2] of string = ('200K', '160K', '120K');
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');
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);
@@ -400,13 +406,14 @@ end;
function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
begin
case ModeIdx of
0,1: Result := SSB_BW[FilterIdx];
2: Result := DSB_BW[FilterIdx];
3,4: Result := CW_BW[FilterIdx];
5: Result := FM_BW[FilterIdx];
8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля
10: Result := WFM_BW[FilterIdx];
11: Result := 12500;
MODE_LSB, MODE_USB: Result := SSB_BW[FilterIdx];
MODE_DSB: Result := DSB_BW[FilterIdx];
MODE_CWL, MODE_CWU: Result := CW_BW[FilterIdx];
OVL_MODE_FM: Result := FM_BW[FilterIdx];
MODE_DIGU, MODE_DIGL: Result := DIGI_BW[FilterIdx]; // полоса от нуля
MODE_WFM: Result := WFM_BW[FilterIdx];
OVL_MODE_DMR: Result := 12500;
OVL_MODE_FMRAW: Result := RAW_BW[FilterIdx];
else Result := AM_BW[FilterIdx];
end;
end;
@@ -414,13 +421,14 @@ end;
function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
begin
case ModeIdx of
0,1: Result := SSB_LBL[FilterIdx];
2: Result := DSB_LBL[FilterIdx];
3,4: Result := CW_LBL[FilterIdx];
5: Result := FM_LBL[FilterIdx];
8,9: Result := DIGI_LBL[FilterIdx];
10: Result := WFM_LBL[FilterIdx];
11: Result := '12.5k';
MODE_LSB, MODE_USB: Result := SSB_LBL[FilterIdx];
MODE_DSB: Result := DSB_LBL[FilterIdx];
MODE_CWL, MODE_CWU: Result := CW_LBL[FilterIdx];
OVL_MODE_FM: Result := FM_LBL[FilterIdx];
MODE_DIGU, MODE_DIGL: Result := DIGI_LBL[FilterIdx];
MODE_WFM: Result := WFM_LBL[FilterIdx];
OVL_MODE_DMR: Result := '12.5k';
OVL_MODE_FMRAW: Result := RAW_LBL[FilterIdx];
else Result := AM_LBL[FilterIdx];
end;
end;
@@ -428,9 +436,10 @@ end;
function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
begin
case ModeIdx of
5: Result := 2; // FM: NFM/FM
10: Result := 3; // WFM: 200K/160K/120K
11: Result := 0; // DMR: fixed 12.5 kHz, no redundant selector
OVL_MODE_FM: Result := 2; // FM: NFM/FM
MODE_WFM: Result := 3; // WFM: 200K/160K/120K
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;
end;
end;
@@ -438,7 +447,7 @@ end;
function TVfoOverlay.TxSelectionAllowed: Boolean;
begin
// 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;
{ ---- Lifecycle ---- }
@@ -447,7 +456,7 @@ constructor TVfoOverlay.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := ControlStyle + [csOpaque];
FMode := 1;
FMode := MODE_USB;
FFilterBW := 2700;
FVfoHz := 14200000;
FSMeterDB := -121;
@@ -457,6 +466,7 @@ begin
FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000;
FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[8] := 3000;
FModeFilter[9] := 3000; FModeFilter[10] := 160000;
FModeFilter[11] := 12500; FModeFilter[12] := 12500;
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
@@ -594,7 +604,7 @@ end;
function TVfoOverlay.IsFM: Boolean;
begin
Result := FMode = 5; // индекс FM в OVL_MODE_NAMES (= MODE_FM контроллера)
Result := FMode = OVL_MODE_FM;
end;
function TVfoOverlay.PanelContentH: Integer;
@@ -1039,7 +1049,7 @@ begin
// Краткий статус дополнительного DMR-слайса. Он обновляется только при
// устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру
// перезаливаться на каждом burst.
if (not FIsMain) and (FMode = 11) and (FDMRText <> '') then
if (not FIsMain) and (FMode = OVL_MODE_DMR) and (FDMRText <> '') then
begin
DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8;
@@ -1276,7 +1286,7 @@ begin
begin
FModeFilter[FMode] := FFilterBW;
FMode := NewMode;
if FMode = 11 then FFilterBW := 12500
if FMode = OVL_MODE_DMR then FFilterBW := 12500
else FFilterBW := FModeFilter[FMode];
Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3)
RequestInvalidate;
@@ -1449,19 +1459,19 @@ var
OldMode: Integer;
begin
OldMode := FMode;
FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode));
FMode := EnsureRange(AMode, MODE_MIN, MODE_MAX);
FFilterBW := ABW;
FModeFilter[FMode] := ABW;
FVfoHz := AVfoHz;
FSMeterDB := ASMeterDB;
if FMode <> 11 then
if FMode <> OVL_MODE_DMR then
begin
FDMRText := '';
FDMRSynced := False;
FDMRCandidateText := '';
FDMRCandidateSince := 0;
end
else if OldMode <> 11 then
else if OldMode <> OVL_MODE_DMR then
begin
FDMRText := 'DMR...';
FDMRSynced := False;
@@ -1555,7 +1565,7 @@ procedure TVfoOverlay.UpdateDMRStatus(const AText: string; Synced: Boolean);
var
NowTick, HoldMs: QWord;
begin
if FMode <> 11 then Exit;
if FMode <> OVL_MODE_DMR then Exit;
if (FDMRText = AText) and (FDMRSynced = Synced) then
begin
FDMRCandidateText := '';