feat(tx): TX-профиль на слайс — пилюля в шапке флага

Передатчик один, профиль до сих пор был один на устройство: активный профиль
звучал независимо от того, какой слайс взял передачу. С панадаптером на FT8 и
вторым на FM это значит, что компрессия и EQ голосового профиля уходили в
эфир на FT8 (FMRAW прикрыт сам — ApplyTXModeSettings глушит там всю обработку,
а DIGU/DIGL нет). У Flex это решается дисциплиной и внешним API; делаем в радио.

Привязка «источник передачи → профиль» (-1 = не переключать, звучит активный):
TCtrlSlice.TXProfile для B..G, TTXProfileList.MainBind для главного (слайс A).
Персист: ключ tx_profile в слайсе пана и main_bind в секции tx_profiles.
SetTxSlice зовёт ApplyBoundTXProfile ДО пуша режима и цепи, иначе
ApplyTXSettingsToDSP лёг бы на старый режим. Привязали источник, который
передаёт прямо сейчас, — применяем сразу.

Привязки — индексы, поэтому RemapTXProfileBindings: удаление профиля из
середины снимает привязки на него и сдвигает те, что правее; Factory-сброс
снимает все (список заменён целиком).

UI: пилюля с именем профиля в первой строке флага сразу за шириной фильтра
(ЛКМ — fly-out список, первая строка «As active»). Правая граница считается
до SPLIT у главного и до RXM у слайса; не рисуем при пустом списке, в DMR
(передачи нет) и в FMRAW (профиль там ни на что не влияет). Прокрутка списка
общая с AUD — блок стрелок вынесен в DrawScrollArrows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 17:06:08 +03:00
co-authored by Claude Opus 5
parent 8563c4e4cc
commit 013d0406ac
5 changed files with 397 additions and 41 deletions
+12
View File
@@ -177,6 +177,7 @@ type
FMSQLevel: Integer;
DevName: string; // имя PortAudio-устройства вывода ('' = default)
InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход)
TXProfile: Integer; // TX-профиль слайса: -1 = какой активен, тот и звучит
end;
TPanCfg = record
@@ -349,6 +350,9 @@ type
TTXProfileList = record
Count: Integer;
ActiveIdx: Integer;
// Привязка профиля к ГЛАВНОМУ приёмнику (слайс A). У слайсов B..G то же
// самое лежит в TSliceCfg.TXProfile; -1 = ничего не переключать.
MainBind: Integer;
Items: array[0..TXPROF_MAX-1] of TTXProfile;
end;
@@ -1938,6 +1942,7 @@ begin
FillChar(L, SizeOf(L), 0);
L.Count := 0;
L.ActiveIdx := 0;
L.MainBind := -1; // главный приёмник профиль не переключает
// «Default» — текущее состояние устройства (микрофонный вход, полоса,
// динамика, мощность), но с заводским EQ: после миграции старого конфига
// звук в эфире не меняется, а сброс к заводским возвращает ровный тембр.
@@ -1970,6 +1975,7 @@ begin
FillChar(L, SizeOf(L), 0);
L.Count := 0;
L.ActiveIdx := 0;
L.MainBind := -1;
Result := False;
MacStr := MacToStr(MAC);
if FRoot.Find(MacStr) = nil then Exit;
@@ -2030,6 +2036,8 @@ begin
if L.Count = 0 then Exit; // пустой список = как будто секции нет
L.ActiveIdx := JI(Root, 'active', 0);
if (L.ActiveIdx < 0) or (L.ActiveIdx >= L.Count) then L.ActiveIdx := 0;
L.MainBind := JI(Root, 'main_bind', -1);
if (L.MainBind < -1) or (L.MainBind >= L.Count) then L.MainBind := -1;
Result := True;
end;
@@ -2042,6 +2050,7 @@ var
begin
Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx_profiles');
JW(Root, 'active', EnsureRange(L.ActiveIdx, 0, Max(0, L.Count - 1)));
JW(Root, 'main_bind', EnsureRange(L.MainBind, -1, Max(-1, L.Count - 1)));
// Список пересоздаётся целиком: удаление профиля не должно оставлять хвост.
Idx := Root.IndexOfName('list');
if Idx >= 0 then Root.Delete(Idx);
@@ -2732,6 +2741,7 @@ begin
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName);
SliceObj.Add('tx_profile', P.Pans[i].Slices[j].TXProfile);
end;
end;
end;
@@ -2812,6 +2822,8 @@ begin
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
P.Pans[id].Slices[n].InDevName := JS(SliceObj, 'in_dev_name', '');
P.Pans[id].Slices[n].TXProfile := EnsureRange(
JI(SliceObj, 'tx_profile', -1), -1, TXPROF_MAX - 1);
end;
end;
end;