feat(tx): привязка профиля к паре «флаг + модуляция»

Одного значения на флаг мало: на слайсе с FT8 и на нём же голосом нужен разный
звук, а слайс/главный меняют модуляцию на лету. Привязка стала таблицей
TTXProfModeTable (режим → индекс профиля, -1 = не переключать), своя у каждого
флага: MainByMode у главного (одна на устройство, общая для КВ и трансвертера)
и TXProfByMode у слайса (сама разъезжается по контекстам — записи слайсов
хранятся отдельно для КВ и трансвертера).

Ключ — режим САМОГО ФЛАГА (FlagMode): у главного FMode, у слайса свой. Выбор в
пилюле пишется в ячейку текущего режима, поэтому смена модуляции достаёт
профиль, с которым в этом виде работали в прошлый раз.

Применение — в трёх точках, все ДО пуша режима в WDSP (ApplyTXChainSettings
внутри SetTXMode перешьёт цепь уже новыми значениями): SetTxSlice, SetMode
(если передаём с главного) и SetSliceMode (если источник — этот слайс).

Персист: tx_prof_modes в записи слайса, main_prof_modes в секции tx_profiles.
Чтение защитное — индекс вне текущего списка профилей читается как -1.
Round-trip проверен прогоном: обе таблицы переживают запись/чтение, незаданные
режимы -1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 17:58:46 +03:00
co-authored by Claude Opus 5
parent 013d0406ac
commit 36af031767
3 changed files with 143 additions and 30 deletions
+8 -2
View File
@@ -1570,6 +1570,10 @@ begin
SyncSpecViewFreq; // иначе полоса фильтра рисуется под старый режим SyncSpecViewFreq; // иначе полоса фильтра рисуется под старый режим
UpdateTXControlAvailability; UpdateTXControlAvailability;
UpdateDMRStatus; UpdateDMRStatus;
// Пилюля профиля во флаге A: привязка ключуется режимом, значит на
// смене модуляции она показывает уже другой профиль. Флаг главного
// приёмника живёт только на пане 0 — как в RefreshTxIndicators.
FPan.PushMainFlagExtState;
end; end;
rfFilter: rfFilter:
begin begin
@@ -7176,7 +7180,7 @@ begin
Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel; Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel;
Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName; Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName;
Cfg.Pans[i].Slices[n].InDevName := FController.FSlices[s].InDevName; Cfg.Pans[i].Slices[n].InDevName := FController.FSlices[s].InDevName;
Cfg.Pans[i].Slices[n].TXProfile := FController.FSlices[s].TXProfile; Cfg.Pans[i].Slices[n].TXProfByMode := FController.FSlices[s].TXProfByMode;
Inc(n); Inc(n);
end; end;
end; end;
@@ -7203,7 +7207,9 @@ begin
FController.FAudioIn.FindDeviceByName(SC.InDevName), SC.InDevName); FController.FAudioIn.FindDeviceByName(SC.InDevName), SC.InDevName);
if SC.Muted then FController.SetSliceMute(Id, True); if SC.Muted then FController.SetSliceMute(Id, True);
FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx); FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx);
FController.SetSliceTXProfile(Id, SC.TXProfile); // «этот слайс звучит вот так» // Таблица «режим → профиль» этого слайса целиком (не через SetSliceTXProfile:
// тот пишет только ячейку текущего режима).
FController.SetSliceTXProfileTable(Id, SC.TXProfByMode);
if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel); if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel);
P.CreateSliceFlag(Id); P.CreateSliceFlag(Id);
FActiveSliceId := Id; FActiveSliceId := Id;
+77 -16
View File
@@ -178,9 +178,10 @@ type
InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий) InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий)
InDevName: string; InDevName: string;
AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула
// TX-профиль слайса: индекс в FTXProfiles или -1 = «не переключать». // TX-профиль слайса ПО РЕЖИМУ: индекс в FTXProfiles или -1 = «не
// Применяется, когда слайс становится TX-источником (SetTxSlice). // переключать». Применяется, когда слайс становится TX-источником
TXProfile: Integer; // (SetTxSlice) и когда он меняет режим, будучи источником.
TXProfByMode: TTXProfModeTable;
DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса
end; end;
@@ -599,9 +600,14 @@ type
procedure RenameTXProfile(Idx: Integer; const AName: string); procedure RenameTXProfile(Idx: Integer; const AName: string);
procedure DeleteTXProfile(Idx: Integer); procedure DeleteTXProfile(Idx: Integer);
procedure ResetTXProfilesToFactory; // заводской набор заново procedure ResetTXProfilesToFactory; // заводской набор заново
// Профиль на TX-источник: -1 = не переключать (звучит активный). // Профиль на TX-источник, ключ — режим этого флага (-1 = не переключать,
// звучит активный). Таблица у каждого флага своя: главный в FTXProfiles,
// слайсы в своих записях.
function FlagMode(SliceId: Integer): Integer;
function SliceTXProfile(SliceId: Integer): Integer; function SliceTXProfile(SliceId: Integer): Integer;
procedure SetSliceTXProfile(SliceId, ProfIdx: Integer); procedure SetSliceTXProfile(SliceId, ProfIdx: Integer);
function SliceTXProfileTable(SliceId: Integer): TTXProfModeTable;
procedure SetSliceTXProfileTable(SliceId: Integer; const T: TTXProfModeTable);
procedure RemapTXProfileBindings(Deleted: Integer); // после удаления/замены списка procedure RemapTXProfileBindings(Deleted: Integer); // после удаления/замены списка
procedure ApplyBoundTXProfile; // профиль текущего TX-источника → в эфир procedure ApplyBoundTXProfile; // профиль текущего TX-источника → в эфир
procedure SaveTXProfilesToConfig; procedure SaveTXProfilesToConfig;
@@ -2156,7 +2162,7 @@ begin
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
FSlices[slot].FMSQLevel := 40; FSlices[slot].FMSQLevel := 40;
FSlices[slot].TXProfile := -1; // звучит тот профиль, что активен ClearTXProfModeTable(FSlices[slot].TXProfByMode); // звучит активный профиль
FSlices[slot].NRMode := 0; // DSP слайса стартует выключенным FSlices[slot].NRMode := 0; // DSP слайса стартует выключенным
FSlices[slot].NBMode := 0; FSlices[slot].NBMode := 0;
FSlices[slot].SNB := False; FSlices[slot].SNB := False;
@@ -2231,6 +2237,8 @@ begin
// Этот слайс — источник передачи: передающий тракт идёт за его режимом // Этот слайс — источник передачи: передающий тракт идёт за его режимом
// (иначе после смены режима в эфир ушёл бы прежний вид модуляции). Вход FM // (иначе после смены режима в эфир ушёл бы прежний вид модуляции). Вход FM
// RAW — внешний PCM, поэтому заодно пересматриваем источник модуляции. // RAW — внешний PCM, поэтому заодно пересматриваем источник модуляции.
// Профиль этого слайса в НОВОМ режиме — до пуша цепи (см. SetMode).
if (FTxSliceId = Id) and (Mode <> OldMode) then ApplyBoundTXProfile;
if (FTxSliceId = Id) and (Mode <> OldMode) and if (FTxSliceId = Id) and (Mode <> OldMode) and
FWDSPReady and Assigned(FDSPEngine) then FWDSPReady and Assigned(FDSPEngine) then
begin begin
@@ -3996,6 +4004,10 @@ begin
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR); if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
ApplyModeDefaults; // дефолтный фильтр/девиация под режим ApplyModeDefaults; // дефолтный фильтр/девиация под режим
FBandCache[FCurrentBand].Mode := FMode; FBandCache[FCurrentBand].Mode := FMode;
// Профиль привязан к паре «флаг + модуляция»: если передаём с главного,
// новый режим приносит свой профиль. До пуша в WDSP — ApplyTXChainSettings
// внутри SetTXMode ниже перешьёт цепь уже с новыми значениями.
if FTxSliceId = 0 then ApplyBoundTXProfile;
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
begin begin
FDSPEngine.SetMode(FMode); FDSPEngine.SetMode(FMode);
@@ -5074,43 +5086,61 @@ procedure TRadioController.RemapTXProfileBindings(Deleted: Integer);
else if B > Deleted then Dec(B); else if B > Deleted then Dec(B);
end; end;
var i: Integer; var i, m: Integer;
begin begin
Fix(FTXProfiles.MainBind); for m := 0 to CFG_MODE_MAX do Fix(FTXProfiles.MainByMode[m]);
for i := 0 to High(FSlices) do for i := 0 to High(FSlices) do
if FSlices[i].Active then Fix(FSlices[i].TXProfile); if FSlices[i].Active then
for m := 0 to CFG_MODE_MAX do Fix(FSlices[i].TXProfByMode[m]);
end;
function TRadioController.FlagMode(SliceId: Integer): Integer;
// Режим, в котором сейчас стоит этот флаг: у главного — FMode, у слайса — свой.
var idx: Integer;
begin
Result := FMode;
if SliceId > 0 then
begin
idx := FindSliceIndex(SliceId);
if idx >= 0 then Result := FSlices[idx].Mode;
end;
Result := EnsureRange(Result, 0, CFG_MODE_MAX);
end; end;
function TRadioController.SliceTXProfile(SliceId: Integer): Integer; function TRadioController.SliceTXProfile(SliceId: Integer): Integer;
// Привязка «этот источник передачи → этот профиль». -1 = не переключать. // Привязка «этот источник передачи В ЭТОМ РЕЖИМЕ → этот профиль».
// -1 = не переключать. Ключ — режим самого флага, поэтому смена модуляции
// сама достаёт профиль, с которым в этом виде работали в прошлый раз.
var idx: Integer; var idx: Integer;
begin begin
Result := -1; Result := -1;
if SliceId <= 0 then if SliceId <= 0 then
Result := FTXProfiles.MainBind Result := FTXProfiles.MainByMode[FlagMode(SliceId)]
else else
begin begin
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if idx >= 0 then Result := FSlices[idx].TXProfile; if idx >= 0 then Result := FSlices[idx].TXProfByMode[FlagMode(SliceId)];
end; end;
if (Result < 0) or (Result >= FTXProfiles.Count) then Result := -1; if (Result < 0) or (Result >= FTXProfiles.Count) then Result := -1;
end; end;
procedure TRadioController.SetSliceTXProfile(SliceId, ProfIdx: Integer); procedure TRadioController.SetSliceTXProfile(SliceId, ProfIdx: Integer);
var idx: Integer; // Пишем в ячейку ТЕКУЩЕГО режима этого флага: выбор запоминается за модуляцией.
var idx, m: Integer;
begin begin
if (ProfIdx < -1) or (ProfIdx >= FTXProfiles.Count) then ProfIdx := -1; if (ProfIdx < -1) or (ProfIdx >= FTXProfiles.Count) then ProfIdx := -1;
m := FlagMode(SliceId);
if SliceId <= 0 then if SliceId <= 0 then
begin begin
if FTXProfiles.MainBind = ProfIdx then Exit; if FTXProfiles.MainByMode[m] = ProfIdx then Exit;
FTXProfiles.MainBind := ProfIdx; FTXProfiles.MainByMode[m] := ProfIdx;
SaveTXProfilesToConfig; SaveTXProfilesToConfig;
end end
else else
begin begin
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if (idx < 0) or (FSlices[idx].TXProfile = ProfIdx) then Exit; if (idx < 0) or (FSlices[idx].TXProfByMode[m] = ProfIdx) then Exit;
FSlices[idx].TXProfile := ProfIdx; FSlices[idx].TXProfByMode[m] := ProfIdx;
// Персист слайсов делает MainForm на общем сохранении панов — здесь только // Персист слайсов делает MainForm на общем сохранении панов — здесь только
// состояние. Привязку главного пишем сразу: она живёт в секции профилей. // состояние. Привязку главного пишем сразу: она живёт в секции профилей.
end; end;
@@ -5120,6 +5150,37 @@ begin
Changed(rfTXProfile); Changed(rfTXProfile);
end; end;
procedure TRadioController.SetSliceTXProfileTable(SliceId: Integer;
const T: TTXProfModeTable);
// Восстановление сохранённой таблицы целиком (персист слайсов/панов).
var idx, m: Integer;
begin
if SliceId <= 0 then
begin
FTXProfiles.MainByMode := T;
Exit;
end;
idx := FindSliceIndex(SliceId);
if idx < 0 then Exit;
for m := 0 to CFG_MODE_MAX do
if (T[m] >= 0) and (T[m] < FTXProfiles.Count) then
FSlices[idx].TXProfByMode[m] := T[m]
else
FSlices[idx].TXProfByMode[m] := -1;
end;
function TRadioController.SliceTXProfileTable(SliceId: Integer): TTXProfModeTable;
var idx: Integer;
begin
ClearTXProfModeTable(Result);
if SliceId <= 0 then Result := FTXProfiles.MainByMode
else
begin
idx := FindSliceIndex(SliceId);
if idx >= 0 then Result := FSlices[idx].TXProfByMode;
end;
end;
procedure TRadioController.ApplyBoundTXProfile; procedure TRadioController.ApplyBoundTXProfile;
// Профиль текущего TX-источника → в эфир. Ничего не делает, если источник ни к // Профиль текущего TX-источника → в эфир. Ничего не делает, если источник ни к
// чему не привязан (-1) или нужный профиль уже активен. // чему не привязан (-1) или нужный профиль уже активен.
+58 -12
View File
@@ -164,6 +164,10 @@ type
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается // Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
// один раз при выходе в Run. Слайс = приёмный WDSP-канал внутри пана; частоты // один раз при выходе в Run. Слайс = приёмный WDSP-канал внутри пана; частоты
// абсолютные (видимые). См. [[project_slices_plan]]. // абсолютные (видимые). См. [[project_slices_plan]].
// Привязка «режим → индекс TX-профиля» (-1 = не переключать). Такая таблица
// есть у каждого флага: у главного в TTXProfileList, у слайса в TSliceCfg.
TTXProfModeTable = array[0..CFG_MODE_MAX] of Integer;
TSliceCfg = record TSliceCfg = record
TargetHz: Double; // абсолютная видимая частота слайса TargetHz: Double; // абсолютная видимая частота слайса
Mode: Integer; Mode: Integer;
@@ -177,7 +181,9 @@ type
FMSQLevel: Integer; FMSQLevel: Integer;
DevName: string; // имя PortAudio-устройства вывода ('' = default) DevName: string; // имя PortAudio-устройства вывода ('' = default)
InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход) InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход)
TXProfile: Integer; // TX-профиль слайса: -1 = какой активен, тот и звучит // TX-профиль слайса ПО РЕЖИМУ: -1 = какой активен, тот и звучит. Таблица,
// а не одно значение: слайс на FT8 и слайс голосом помнят каждый своё.
TXProfByMode: TTXProfModeTable;
end; end;
TPanCfg = record TPanCfg = record
@@ -350,9 +356,12 @@ type
TTXProfileList = record TTXProfileList = record
Count: Integer; Count: Integer;
ActiveIdx: Integer; ActiveIdx: Integer;
// Привязка профиля к ГЛАВНОМУ приёмнику (слайс A). У слайсов B..G то же // Привязка профиля к ГЛАВНОМУ приёмнику (слайс A) ПО РЕЖИМУ; -1 = ничего
// самое лежит в TSliceCfg.TXProfile; -1 = ничего не переключать. // не переключать. У слайсов B..G своя такая же в TSliceCfg.TXProfByMode.
MainBind: Integer; // Таблица главного одна на устройство — общая для КВ и трансвертера
// (решение пользователя); у слайсов она сама разъезжается по контекстам,
// потому что их записи хранятся отдельно для КВ и трансвертера.
MainByMode: TTXProfModeTable;
Items: array[0..TXPROF_MAX-1] of TTXProfile; Items: array[0..TXPROF_MAX-1] of TTXProfile;
end; end;
@@ -701,6 +710,11 @@ procedure TXProfileApply(const P: TTXProfile; var T: TTXSettings; var DrivePct:
// (иначе переключение на «ESSB» сбрасывало бы Mic In/Line In и drive). // (иначе переключение на «ESSB» сбрасывало бы Mic In/Line In и drive).
procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer; procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
out L: TTXProfileList); out L: TTXProfileList);
// Таблица «режим → профиль» ↔ JSON-массив. Все элементы вне 0..TXPROF_MAX-1
// читаются как -1 («не переключать»), поэтому чужой/старый конфиг безопасен.
procedure ClearTXProfModeTable(var T: TTXProfModeTable);
function ModeTableToJSON(const T: TTXProfModeTable): TJSONArray;
procedure JSONToModeTable(Node: TJSONData; var T: TTXProfModeTable);
// Сколько пресетов реально есть у режима (VAR1/VAR2 идут сверх этого числа). // Сколько пресетов реально есть у режима (VAR1/VAR2 идут сверх этого числа).
function FilterPresetCount(Mode: Integer): Integer; function FilterPresetCount(Mode: Integer): Integer;
@@ -1866,6 +1880,34 @@ begin
DrivePct := EnsureRange(P.DrivePercent, 0, 100); DrivePct := EnsureRange(P.DrivePercent, 0, 100);
end; end;
procedure ClearTXProfModeTable(var T: TTXProfModeTable);
var i: Integer;
begin
for i := 0 to CFG_MODE_MAX do T[i] := -1;
end;
function ModeTableToJSON(const T: TTXProfModeTable): TJSONArray;
var i: Integer;
begin
Result := TJSONArray.Create;
for i := 0 to CFG_MODE_MAX do Result.Add(T[i]);
end;
procedure JSONToModeTable(Node: TJSONData; var T: TTXProfModeTable);
var
Arr: TJSONArray;
i, V: Integer;
begin
ClearTXProfModeTable(T);
if not (Node is TJSONArray) then Exit;
Arr := TJSONArray(Node);
for i := 0 to Min(CFG_MODE_MAX, Arr.Count - 1) do
begin
V := Arr.Integers[i];
if (V >= 0) and (V < TXPROF_MAX) then T[i] := V;
end;
end;
procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer; procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
out L: TTXProfileList); out L: TTXProfileList);
const const
@@ -1942,7 +1984,7 @@ begin
FillChar(L, SizeOf(L), 0); FillChar(L, SizeOf(L), 0);
L.Count := 0; L.Count := 0;
L.ActiveIdx := 0; L.ActiveIdx := 0;
L.MainBind := -1; // главный приёмник профиль не переключает ClearTXProfModeTable(L.MainByMode); // главный ничего не переключает
// «Default» — текущее состояние устройства (микрофонный вход, полоса, // «Default» — текущее состояние устройства (микрофонный вход, полоса,
// динамика, мощность), но с заводским EQ: после миграции старого конфига // динамика, мощность), но с заводским EQ: после миграции старого конфига
// звук в эфире не меняется, а сброс к заводским возвращает ровный тембр. // звук в эфире не меняется, а сброс к заводским возвращает ровный тембр.
@@ -1975,7 +2017,7 @@ begin
FillChar(L, SizeOf(L), 0); FillChar(L, SizeOf(L), 0);
L.Count := 0; L.Count := 0;
L.ActiveIdx := 0; L.ActiveIdx := 0;
L.MainBind := -1; ClearTXProfModeTable(L.MainByMode);
Result := False; Result := False;
MacStr := MacToStr(MAC); MacStr := MacToStr(MAC);
if FRoot.Find(MacStr) = nil then Exit; if FRoot.Find(MacStr) = nil then Exit;
@@ -2036,8 +2078,9 @@ begin
if L.Count = 0 then Exit; // пустой список = как будто секции нет if L.Count = 0 then Exit; // пустой список = как будто секции нет
L.ActiveIdx := JI(Root, 'active', 0); L.ActiveIdx := JI(Root, 'active', 0);
if (L.ActiveIdx < 0) or (L.ActiveIdx >= L.Count) then L.ActiveIdx := 0; if (L.ActiveIdx < 0) or (L.ActiveIdx >= L.Count) then L.ActiveIdx := 0;
L.MainBind := JI(Root, 'main_bind', -1); JSONToModeTable(Root.Find('main_prof_modes'), L.MainByMode);
if (L.MainBind < -1) or (L.MainBind >= L.Count) then L.MainBind := -1; for i := 0 to CFG_MODE_MAX do
if L.MainByMode[i] >= L.Count then L.MainByMode[i] := -1;
Result := True; Result := True;
end; end;
@@ -2050,7 +2093,9 @@ var
begin begin
Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx_profiles'); Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx_profiles');
JW(Root, 'active', EnsureRange(L.ActiveIdx, 0, Max(0, L.Count - 1))); 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('main_prof_modes');
if Idx >= 0 then Root.Delete(Idx);
Root.Add('main_prof_modes', ModeTableToJSON(L.MainByMode));
// Список пересоздаётся целиком: удаление профиля не должно оставлять хвост. // Список пересоздаётся целиком: удаление профиля не должно оставлять хвост.
Idx := Root.IndexOfName('list'); Idx := Root.IndexOfName('list');
if Idx >= 0 then Root.Delete(Idx); if Idx >= 0 then Root.Delete(Idx);
@@ -2741,7 +2786,8 @@ begin
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel); SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName); SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName); SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName);
SliceObj.Add('tx_profile', P.Pans[i].Slices[j].TXProfile); SliceObj.Add('tx_prof_modes',
ModeTableToJSON(P.Pans[i].Slices[j].TXProfByMode));
end; end;
end; end;
end; end;
@@ -2822,8 +2868,8 @@ begin
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30); 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].DevName := JS(SliceObj, 'dev_name', '');
P.Pans[id].Slices[n].InDevName := JS(SliceObj, 'in_dev_name', ''); P.Pans[id].Slices[n].InDevName := JS(SliceObj, 'in_dev_name', '');
P.Pans[id].Slices[n].TXProfile := EnsureRange( JSONToModeTable(SliceObj.Find('tx_prof_modes'),
JI(SliceObj, 'tx_profile', -1), -1, TXPROF_MAX - 1); P.Pans[id].Slices[n].TXProfByMode);
end; end;
end; end;
end; end;