mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
+58
-12
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user