diff --git a/MainForm.pas b/MainForm.pas index 249e6b2..b8ba0dd 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -1570,6 +1570,10 @@ begin SyncSpecViewFreq; // иначе полоса фильтра рисуется под старый режим UpdateTXControlAvailability; UpdateDMRStatus; + // Пилюля профиля во флаге A: привязка ключуется режимом, значит на + // смене модуляции она показывает уже другой профиль. Флаг главного + // приёмника живёт только на пане 0 — как в RefreshTxIndicators. + FPan.PushMainFlagExtState; end; rfFilter: begin @@ -7176,7 +7180,7 @@ begin 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].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); end; end; @@ -7203,7 +7207,9 @@ begin FController.FAudioIn.FindDeviceByName(SC.InDevName), SC.InDevName); if SC.Muted then FController.SetSliceMute(Id, True); 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); P.CreateSliceFlag(Id); FActiveSliceId := Id; diff --git a/RadioController.pas b/RadioController.pas index abc3a59..af3a08b 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -178,9 +178,10 @@ type InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий) InDevName: string; AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула - // TX-профиль слайса: индекс в FTXProfiles или -1 = «не переключать». - // Применяется, когда слайс становится TX-источником (SetTxSlice). - TXProfile: Integer; + // TX-профиль слайса ПО РЕЖИМУ: индекс в FTXProfiles или -1 = «не + // переключать». Применяется, когда слайс становится TX-источником + // (SetTxSlice) и когда он меняет режим, будучи источником. + TXProfByMode: TTXProfModeTable; DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса end; @@ -599,9 +600,14 @@ type procedure RenameTXProfile(Idx: Integer; const AName: string); procedure DeleteTXProfile(Idx: Integer); procedure ResetTXProfilesToFactory; // заводской набор заново - // Профиль на TX-источник: -1 = не переключать (звучит активный). + // Профиль на TX-источник, ключ — режим этого флага (-1 = не переключать, + // звучит активный). Таблица у каждого флага своя: главный в FTXProfiles, + // слайсы в своих записях. + function FlagMode(SliceId: Integer): Integer; function SliceTXProfile(SliceId: Integer): Integer; procedure SetSliceTXProfile(SliceId, ProfIdx: Integer); + function SliceTXProfileTable(SliceId: Integer): TTXProfModeTable; + procedure SetSliceTXProfileTable(SliceId: Integer; const T: TTXProfModeTable); procedure RemapTXProfileBindings(Deleted: Integer); // после удаления/замены списка procedure ApplyBoundTXProfile; // профиль текущего TX-источника → в эфир procedure SaveTXProfilesToConfig; @@ -2156,7 +2162,7 @@ begin FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен FSlices[slot].FMSQLevel := 40; - FSlices[slot].TXProfile := -1; // звучит тот профиль, что активен + ClearTXProfModeTable(FSlices[slot].TXProfByMode); // звучит активный профиль FSlices[slot].NRMode := 0; // DSP слайса стартует выключенным FSlices[slot].NBMode := 0; FSlices[slot].SNB := False; @@ -2231,6 +2237,8 @@ begin // Этот слайс — источник передачи: передающий тракт идёт за его режимом // (иначе после смены режима в эфир ушёл бы прежний вид модуляции). Вход FM // RAW — внешний PCM, поэтому заодно пересматриваем источник модуляции. + // Профиль этого слайса в НОВОМ режиме — до пуша цепи (см. SetMode). + if (FTxSliceId = Id) and (Mode <> OldMode) then ApplyBoundTXProfile; if (FTxSliceId = Id) and (Mode <> OldMode) and FWDSPReady and Assigned(FDSPEngine) then begin @@ -3996,6 +4004,10 @@ begin if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR); ApplyModeDefaults; // дефолтный фильтр/девиация под режим FBandCache[FCurrentBand].Mode := FMode; + // Профиль привязан к паре «флаг + модуляция»: если передаём с главного, + // новый режим приносит свой профиль. До пуша в WDSP — ApplyTXChainSettings + // внутри SetTXMode ниже перешьёт цепь уже с новыми значениями. + if FTxSliceId = 0 then ApplyBoundTXProfile; if FWDSPReady and Assigned(FDSPEngine) then begin FDSPEngine.SetMode(FMode); @@ -5074,43 +5086,61 @@ procedure TRadioController.RemapTXProfileBindings(Deleted: Integer); else if B > Deleted then Dec(B); end; -var i: Integer; +var i, m: Integer; begin - Fix(FTXProfiles.MainBind); + for m := 0 to CFG_MODE_MAX do Fix(FTXProfiles.MainByMode[m]); 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; function TRadioController.SliceTXProfile(SliceId: Integer): Integer; -// Привязка «этот источник передачи → этот профиль». -1 = не переключать. +// Привязка «этот источник передачи В ЭТОМ РЕЖИМЕ → этот профиль». +// -1 = не переключать. Ключ — режим самого флага, поэтому смена модуляции +// сама достаёт профиль, с которым в этом виде работали в прошлый раз. var idx: Integer; begin Result := -1; if SliceId <= 0 then - Result := FTXProfiles.MainBind + Result := FTXProfiles.MainByMode[FlagMode(SliceId)] else begin idx := FindSliceIndex(SliceId); - if idx >= 0 then Result := FSlices[idx].TXProfile; + if idx >= 0 then Result := FSlices[idx].TXProfByMode[FlagMode(SliceId)]; end; if (Result < 0) or (Result >= FTXProfiles.Count) then Result := -1; end; procedure TRadioController.SetSliceTXProfile(SliceId, ProfIdx: Integer); -var idx: Integer; +// Пишем в ячейку ТЕКУЩЕГО режима этого флага: выбор запоминается за модуляцией. +var idx, m: Integer; begin if (ProfIdx < -1) or (ProfIdx >= FTXProfiles.Count) then ProfIdx := -1; + m := FlagMode(SliceId); if SliceId <= 0 then begin - if FTXProfiles.MainBind = ProfIdx then Exit; - FTXProfiles.MainBind := ProfIdx; + if FTXProfiles.MainByMode[m] = ProfIdx then Exit; + FTXProfiles.MainByMode[m] := ProfIdx; SaveTXProfilesToConfig; end else begin idx := FindSliceIndex(SliceId); - if (idx < 0) or (FSlices[idx].TXProfile = ProfIdx) then Exit; - FSlices[idx].TXProfile := ProfIdx; + if (idx < 0) or (FSlices[idx].TXProfByMode[m] = ProfIdx) then Exit; + FSlices[idx].TXProfByMode[m] := ProfIdx; // Персист слайсов делает MainForm на общем сохранении панов — здесь только // состояние. Привязку главного пишем сразу: она живёт в секции профилей. end; @@ -5120,6 +5150,37 @@ begin Changed(rfTXProfile); 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; // Профиль текущего TX-источника → в эфир. Ничего не делает, если источник ни к // чему не привязан (-1) или нужный профиль уже активен. diff --git a/Settings.pas b/Settings.pas index c3064f5..52caeac 100644 --- a/Settings.pas +++ b/Settings.pas @@ -164,6 +164,10 @@ type // Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается // один раз при выходе в Run. Слайс = приёмный WDSP-канал внутри пана; частоты // абсолютные (видимые). См. [[project_slices_plan]]. + // Привязка «режим → индекс TX-профиля» (-1 = не переключать). Такая таблица + // есть у каждого флага: у главного в TTXProfileList, у слайса в TSliceCfg. + TTXProfModeTable = array[0..CFG_MODE_MAX] of Integer; + TSliceCfg = record TargetHz: Double; // абсолютная видимая частота слайса Mode: Integer; @@ -177,7 +181,9 @@ type FMSQLevel: Integer; DevName: string; // имя PortAudio-устройства вывода ('' = default) InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход) - TXProfile: Integer; // TX-профиль слайса: -1 = какой активен, тот и звучит + // TX-профиль слайса ПО РЕЖИМУ: -1 = какой активен, тот и звучит. Таблица, + // а не одно значение: слайс на FT8 и слайс голосом помнят каждый своё. + TXProfByMode: TTXProfModeTable; end; TPanCfg = record @@ -350,9 +356,12 @@ type TTXProfileList = record Count: Integer; ActiveIdx: Integer; - // Привязка профиля к ГЛАВНОМУ приёмнику (слайс A). У слайсов B..G то же - // самое лежит в TSliceCfg.TXProfile; -1 = ничего не переключать. - MainBind: Integer; + // Привязка профиля к ГЛАВНОМУ приёмнику (слайс A) ПО РЕЖИМУ; -1 = ничего + // не переключать. У слайсов B..G своя такая же в TSliceCfg.TXProfByMode. + // Таблица главного одна на устройство — общая для КВ и трансвертера + // (решение пользователя); у слайсов она сама разъезжается по контекстам, + // потому что их записи хранятся отдельно для КВ и трансвертера. + MainByMode: TTXProfModeTable; Items: array[0..TXPROF_MAX-1] of TTXProfile; end; @@ -701,6 +710,11 @@ procedure TXProfileApply(const P: TTXProfile; var T: TTXSettings; var DrivePct: // (иначе переключение на «ESSB» сбрасывало бы Mic In/Line In и drive). procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer; 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 идут сверх этого числа). function FilterPresetCount(Mode: Integer): Integer; @@ -1866,6 +1880,34 @@ begin DrivePct := EnsureRange(P.DrivePercent, 0, 100); 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; out L: TTXProfileList); const @@ -1942,7 +1984,7 @@ begin FillChar(L, SizeOf(L), 0); L.Count := 0; L.ActiveIdx := 0; - L.MainBind := -1; // главный приёмник профиль не переключает + ClearTXProfModeTable(L.MainByMode); // главный ничего не переключает // «Default» — текущее состояние устройства (микрофонный вход, полоса, // динамика, мощность), но с заводским EQ: после миграции старого конфига // звук в эфире не меняется, а сброс к заводским возвращает ровный тембр. @@ -1975,7 +2017,7 @@ begin FillChar(L, SizeOf(L), 0); L.Count := 0; L.ActiveIdx := 0; - L.MainBind := -1; + ClearTXProfModeTable(L.MainByMode); Result := False; MacStr := MacToStr(MAC); if FRoot.Find(MacStr) = nil then Exit; @@ -2036,8 +2078,9 @@ 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; + JSONToModeTable(Root.Find('main_prof_modes'), L.MainByMode); + for i := 0 to CFG_MODE_MAX do + if L.MainByMode[i] >= L.Count then L.MainByMode[i] := -1; Result := True; end; @@ -2050,7 +2093,9 @@ 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('main_prof_modes'); + if Idx >= 0 then Root.Delete(Idx); + Root.Add('main_prof_modes', ModeTableToJSON(L.MainByMode)); // Список пересоздаётся целиком: удаление профиля не должно оставлять хвост. Idx := Root.IndexOfName('list'); if Idx >= 0 then Root.Delete(Idx); @@ -2741,7 +2786,8 @@ 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); + SliceObj.Add('tx_prof_modes', + ModeTableToJSON(P.Pans[i].Slices[j].TXProfByMode)); end; end; end; @@ -2822,8 +2868,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); + JSONToModeTable(SliceObj.Find('tx_prof_modes'), + P.Pans[id].Slices[n].TXProfByMode); end; end; end;