feat(slices): аудио-устройства слайса на вкладке Slices (in/out на слот)

У каждого слайса B..G — свои Audio out / Audio in рядом с его CAT-портом.
Списки те же, что у главных комбо; пустой выбор (Default/None) = прежнее
поведение, общий выход/вход приложения.

Это устройство СЛОТА (буквы), а не шаблон:
  • смена настройки применяется к слайсу, стоящему на слоте, немедленно
    (SetSliceSlotAudio) — как и вся остальная форма настроек EWSDR;
  • слайс, который встанет на слот позже (создание/restore из персиста),
    получает то же устройство: слот сильнее аргумента AddSlice;
  • выбор во вкладке AUD флага пишется обратно в настройку слота
    (StoreSliceSlotAudio) — вкладка всегда показывает реальность, а не
    вторую, конкурирующую истину.

Хранятся ИМЕНА PortAudio-устройств, а не индексы: порядок перечисления
между запусками плавает. Имя устройства, которого сейчас нет в системе,
не теряется — комбо добавляет его в список и держит выбранным.

ApplySliceSlotAudio зовётся на старте и на смене устройства (rfDevice):
слайсы восстанавливаются позже, слот должен знать свои звуковухи заранее.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 14:33:42 +03:00
co-authored by Claude Opus 5
parent 2d15da0462
commit 20186ce132
5 changed files with 284 additions and 13 deletions
+52 -3
View File
@@ -581,7 +581,15 @@ type
procedure OnSliceCATSettingsChange(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean);
const AutoTX: array of Boolean;
const OutDev: array of string;
const InDev: array of string);
// Выбор устройства во флаге слайса = устройство его слота: зеркалим в
// конфиг, чтобы вкладка Slices показывала то же самое.
procedure StoreSliceSlotAudio(SliceId: Integer);
// Аудио слотов из конфига → контроллер (старт и смена устройства): слайсы
// восстанавливаются позже и должны встать на свои звуковухи.
procedure ApplySliceSlotAudio;
procedure OnAlexSettingsChange(const A: TAlexSettings);
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
procedure PushXvtrToWeb;
@@ -1127,6 +1135,7 @@ begin
FLightTheme := FController.FSettings.LoadTheme;
FCATAdapter := TCATAdapter.Create(FController);
FCATAdapter.ApplySettings(FCATLastGlobal);
ApplySliceSlotAudio;
FSMeterPeak := -130;
FSMeterMin := -130;
FSMeterAvg := -130;
@@ -1679,6 +1688,7 @@ begin
// возвращаем живое значение в persist-буфер, чтобы blob нёс актуальную тему.
FController.FLoadedGlobal.LightTheme := FLightTheme;
FCATAdapter.ApplySettings(FController.FLoadedGlobal);
ApplySliceSlotAudio;
RelabelBands; // HF ↔ VHF/UHF подписи под тип бэкенда
ApplyBackendCapsToUI; // пресеты sample rate + гейтинг полей
RebuildXvtrButtons;
@@ -5834,6 +5844,7 @@ begin
if CurSliceId <> 0 then
begin
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
StoreSliceSlotAudio(CurSliceId); // вкладка Slices показывает то же устройство
PushFlagStateAllPans(CurSliceId); // обновить подсветку текущего устройства слайса
Exit;
end;
@@ -5848,6 +5859,7 @@ begin
if CurSliceId <> 0 then
begin
FController.SetSliceInDevice(CurSliceId, DevIndex, DevName);
StoreSliceSlotAudio(CurSliceId);
PushFlagStateAllPans(CurSliceId);
Exit;
end;
@@ -8516,7 +8528,9 @@ begin
SF.LoadSliceCATSettings(
FCATLastGlobal.CATSliceEnabled,
FCATLastGlobal.CATSlicePort,
FCATLastGlobal.CATSliceAutoTX);
FCATLastGlobal.CATSliceAutoTX,
FCATLastGlobal.CATSliceOutDev,
FCATLastGlobal.CATSliceInDev);
// Перечисляем PA устройства
SF.RefreshAudioDevices(FController.FAudioOut, FController.FAudioIn);
SF.LoadVisibility(FController.FShowSpectrum, FController.FShowWaterfall, FController.FShowWideband, FController.FWidebandFill,
@@ -8622,7 +8636,9 @@ end;
procedure TMainForm.OnSliceCATSettingsChange(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean);
const AutoTX: array of Boolean;
const OutDev: array of string;
const InDev: array of string);
// CAT дополнительных слайсов. Хранится там же, где остальные CAT-настройки
// (корневая секция + device-blob), применяется тем же ApplySettings: адаптер
// поднимет/опустит порты эндпоинтов и положит Auto TX в контроллер.
@@ -8633,15 +8649,48 @@ begin
if i <= High(Enabled) then FCATLastGlobal.CATSliceEnabled[i] := Enabled[i];
if i <= High(Port) then FCATLastGlobal.CATSlicePort[i] := Port[i];
if i <= High(AutoTX) then FCATLastGlobal.CATSliceAutoTX[i] := AutoTX[i];
if i <= High(OutDev) then FCATLastGlobal.CATSliceOutDev[i] := OutDev[i];
if i <= High(InDev) then FCATLastGlobal.CATSliceInDev[i] := InDev[i];
// Аудио слота — сразу в контроллер: слайс, стоящий на слоте, переезжает
// на новое устройство, следующий получит его при создании.
FController.SetSliceSlotAudio(i, FCATLastGlobal.CATSliceOutDev[i],
FCATLastGlobal.CATSliceInDev[i]);
end;
FController.FLoadedGlobal.CATSliceEnabled := FCATLastGlobal.CATSliceEnabled;
FController.FLoadedGlobal.CATSlicePort := FCATLastGlobal.CATSlicePort;
FController.FLoadedGlobal.CATSliceAutoTX := FCATLastGlobal.CATSliceAutoTX;
FController.FLoadedGlobal.CATSliceOutDev := FCATLastGlobal.CATSliceOutDev;
FController.FLoadedGlobal.CATSliceInDev := FCATLastGlobal.CATSliceInDev;
FCATAdapter.ApplySettings(FCATLastGlobal);
FController.FSettings.SaveCATSettings(FCATLastGlobal);
FController.SaveGlobalSettings;
FController.FSettings.Save;
RefreshTxIndicators; // бейджи AutoTX на флагах слайсов — сразу
for i := 0 to MAX_PANS - 1 do // подсветка текущего устройства во флагах
if FPans[i] <> nil then FPans[i].PushAllSliceFlagStates;
end;
procedure TMainForm.ApplySliceSlotAudio;
var i: Integer;
begin
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
FController.SetSliceSlotAudio(i, FCATLastGlobal.CATSliceOutDev[i],
FCATLastGlobal.CATSliceInDev[i]);
end;
procedure TMainForm.StoreSliceSlotAudio(SliceId: Integer);
// Слот слайса и его устройства уже обновлены контроллером (SetSliceDevice/
// SetSliceInDevice) — здесь только зеркалим их в конфиг и сохраняем.
var Slot: Integer;
begin
Slot := FController.SliceSlotOf(SliceId);
if (Slot < 0) or (Slot >= CFG_CAT_SLICE_COUNT) then Exit;
FCATLastGlobal.CATSliceOutDev[Slot] := FController.SliceSlotOutDev(Slot);
FCATLastGlobal.CATSliceInDev[Slot] := FController.SliceSlotInDev(Slot);
FController.FLoadedGlobal.CATSliceOutDev := FCATLastGlobal.CATSliceOutDev;
FController.FLoadedGlobal.CATSliceInDev := FCATLastGlobal.CATSliceInDev;
FController.FSettings.SaveCATSettings(FCATLastGlobal);
FController.FSettings.Save;
end;
end.
+79 -3
View File
@@ -341,6 +341,11 @@ type
// назначает его TX-источником. Флаг живёт на слоте, а не на слайсе, потому
// что настройка привязана к букве (B..G) и переживает удаление слайса.
FSliceAutoTx: array[0..MAX_SLICES-1] of Boolean;
// Аудио-устройства слота (буквы B..G): '' = общий выход приложения / общий
// вход. Это НЕ шаблон, а привязка слота: применяется к живому слайсу сразу
// и достаётся новому слайсу, который встанет на этот слот.
FSliceOutDev: array[0..MAX_SLICES-1] of string;
FSliceInDev: array[0..MAX_SLICES-1] of string;
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
FDisplayDuplex: Boolean;
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
@@ -700,6 +705,11 @@ type
function SliceSlotAutoTx(Slot: Integer): Boolean;
procedure SetSliceSlotAutoTx(Slot: Integer; On_: Boolean);
function SliceAutoTx(Id: Integer): Boolean;
// Аудио-устройства слота. Сеттер применяет их к слайсу, который стоит на
// слоте прямо сейчас (имя → PA-индекс текущей сессии); '' = общий.
function SliceSlotOutDev(Slot: Integer): string;
function SliceSlotInDev(Slot: Integer): string;
procedure SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
// Заявка на передачу ОТ ИМЕНИ слайса (CAT-порт слайса). Единственная точка,
// где внешняя PTT может сменить TX-источник, поэтому здесь же — защита
// «в эфире одновременно только один слайс». Выполнять в потоке контроллера.
@@ -2018,6 +2028,8 @@ function TRadioController.AddSlice(TargetHz: Double;
DevIndex: Integer; const DevName: string; APanId: Integer): Integer;
var
i, slot, Id: Integer;
DevIndex_: Integer;
DevName_: string;
AO: TAudioOutput;
begin
Result := -1;
@@ -2032,6 +2044,20 @@ begin
if not FSlices[i].Active then begin slot := i; Break; end;
if slot < 0 then Exit; // нет свободных слотов
// Устройство слота (настройка вкладки Slices) сильнее того, что попросил
// вызывающий: слот — это буква флага, и звук у неё всегда один и тот же.
if FSliceOutDev[slot] <> '' then
begin
DevName_ := FSliceOutDev[slot];
DevIndex_ := -1;
if Assigned(FAudioOut) then DevIndex_ := FAudioOut.FindDeviceByName(DevName_);
end
else
begin
DevName_ := DevName;
DevIndex_ := DevIndex;
end;
Id := FNextSliceId;
if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi,
TargetHz - PanCenterHz(APanId), Vol, AGC,
@@ -2041,7 +2067,7 @@ begin
// Свой аудио-выход слайса на выбранной звуковухе.
AO := TAudioOutput.Create(48000);
AO.OutputBufferSize := FSettings.LoadAudioBufferSize;
AO.DeviceIndex := DevIndex;
AO.DeviceIndex := DevIndex_;
if FLocalAudio then AO.Open;
FSlices[slot].Active := True;
@@ -2064,8 +2090,8 @@ begin
FSlices[slot].NBMode := 0;
FSlices[slot].SNB := False;
FSlices[slot].ANF := False;
FSlices[slot].DeviceIndex := DevIndex;
FSlices[slot].DevName := DevName;
FSlices[slot].DeviceIndex := DevIndex_;
FSlices[slot].DevName := DevName_;
FSlices[slot].Audio := AO;
// Вход слайса по умолчанию общий (FAudioIn); свой ставится SetSliceInDevice.
FSlices[slot].InDeviceIndex := -1;
@@ -2075,6 +2101,11 @@ begin
if Mode = MODE_DMR then
FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id);
// Вход слота (настройка вкладки Slices) — тем же правилом, что и выход.
if (FSliceInDev[slot] <> '') and Assigned(FAudioIn) then
SetSliceInDeviceIdx(slot, FAudioIn.FindDeviceByName(FSliceInDev[slot]),
FSliceInDev[slot]);
Result := Id;
Changed(rfDevice);
end;
@@ -2326,6 +2357,7 @@ var idx: Integer;
begin
idx := FindSliceIndex(Id);
if idx < 0 then Exit;
FSliceOutDev[idx] := DevName; // выбор во флаге = устройство слота (одна истина)
FSlices[idx].DeviceIndex := DevIndex;
FSlices[idx].DevName := DevName;
if FSlices[idx].Audio <> nil then
@@ -2419,6 +2451,7 @@ procedure TRadioController.SetSliceInDeviceIdx(Idx, DevIndex: Integer;
begin
if (Idx < 0) or (Idx >= MAX_SLICES) or (not FSlices[Idx].Active) then Exit;
if DevName = '' then DevIndex := -1;
FSliceInDev[Idx] := DevName; // выбор во флаге = вход слота (одна истина)
ReleaseMicInput(FSlices[Idx].AudioIn);
FSlices[Idx].InDeviceIndex := DevIndex;
FSlices[Idx].InDevName := DevName;
@@ -2536,6 +2569,49 @@ begin
Result := SliceSlotAutoTx(FindSliceIndex(Id));
end;
function TRadioController.SliceSlotOutDev(Slot: Integer): string;
begin
if (Slot >= 0) and (Slot < MAX_SLICES) then Result := FSliceOutDev[Slot]
else Result := '';
end;
function TRadioController.SliceSlotInDev(Slot: Integer): string;
begin
if (Slot >= 0) and (Slot < MAX_SLICES) then Result := FSliceInDev[Slot]
else Result := '';
end;
procedure TRadioController.SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
// Настройка = устройство слота, поэтому применяем сразу: слайс, который стоит
// на слоте, переезжает на новое устройство (как выбор во вкладке AUD флага).
var Id: Integer;
begin
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit;
Id := SliceIdBySlot(Slot);
if FSliceOutDev[Slot] <> OutDev then
begin
FSliceOutDev[Slot] := OutDev;
if Id > 0 then
begin
if (OutDev <> '') and Assigned(FAudioOut) then
SetSliceDevice(Id, FAudioOut.FindDeviceByName(OutDev), OutDev)
else
SetSliceDevice(Id, -1, ''); // общий выход приложения
end;
end;
if FSliceInDev[Slot] <> InDev then
begin
FSliceInDev[Slot] := InDev;
if Id > 0 then
begin
if (InDev <> '') and Assigned(FAudioIn) then
SetSliceInDevice(Id, FAudioIn.FindDeviceByName(InDev), InDev)
else
SetSliceInDevice(Id, -1, ''); // общий вход приложения
end;
end;
end;
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean);
// PTT, пришедшая по CAT-порту слайса. Аналог «Auto Switch TX Slice» в SmartSDR
// CAT: с Auto TX слайс сам становится TX-источником, без него — передаёт,
+15
View File
@@ -460,6 +460,11 @@ type
// Auto TX (аналог «Auto Switch TX Slice» в SmartSDR CAT): команда TX по
// CAT-порту слайса сама назначает этот слайс источником передачи.
CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
// Аудио слота слайса: имена PortAudio-устройств ('' = общий выход
// приложения / общий вход). Имена, а не индексы: порядок перечисления
// устройств меняется от запуска к запуску.
CATSliceOutDev: array[0..CFG_CAT_SLICE_COUNT-1] of string;
CATSliceInDev: array[0..CFG_CAT_SLICE_COUNT-1] of string;
end;
TSettingsManager = class
@@ -665,6 +670,8 @@ begin
G.CATSliceEnabled[i] := False;
G.CATSlicePort[i] := CFG_CAT_SLICE_PORT0 + i;
G.CATSliceAutoTX[i] := False;
G.CATSliceOutDev[i] := '';
G.CATSliceInDev[i] := '';
end;
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
G.DitherEnabled := True;
@@ -923,6 +930,8 @@ begin
G.CATSliceEnabled[i] := JB(GObj,'cat_slice_en_' +IntToStr(i), False);
G.CATSlicePort[i] := JI(GObj,'cat_slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i);
G.CATSliceAutoTX[i] := JB(GObj,'cat_slice_autotx_'+IntToStr(i), False);
G.CATSliceOutDev[i] := JS(GObj,'cat_slice_out_' +IntToStr(i), '');
G.CATSliceInDev[i] := JS(GObj,'cat_slice_in_' +IntToStr(i), '');
end;
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
@@ -1062,6 +1071,8 @@ begin
JW(O,'cat_slice_en_' +IntToStr(i), G.CATSliceEnabled[i]);
JW(O,'cat_slice_port_' +IntToStr(i), G.CATSlicePort[i]);
JW(O,'cat_slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]);
JWS(O,'cat_slice_out_' +IntToStr(i), G.CATSliceOutDev[i]);
JWS(O,'cat_slice_in_' +IntToStr(i), G.CATSliceInDev[i]);
end;
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
JW(O,'cat_tcp_port',G.CATTcpPort);
@@ -1229,6 +1240,8 @@ begin
JW(O, 'slice_en_' +IntToStr(i), G.CATSliceEnabled[i]);
JW(O, 'slice_port_' +IntToStr(i), G.CATSlicePort[i]);
JW(O, 'slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]);
JWS(O,'slice_out_' +IntToStr(i), G.CATSliceOutDev[i]);
JWS(O,'slice_in_' +IntToStr(i), G.CATSliceInDev[i]);
end;
end;
@@ -1258,6 +1271,8 @@ begin
G.CATSliceEnabled[i] := JB(O, 'slice_en_' +IntToStr(i), False);
G.CATSlicePort[i] := JI(O, 'slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i);
G.CATSliceAutoTX[i] := JB(O, 'slice_autotx_'+IntToStr(i), False);
G.CATSliceOutDev[i] := JS(O, 'slice_out_' +IntToStr(i), '');
G.CATSliceInDev[i] := JS(O, 'slice_in_' +IntToStr(i), '');
end;
end;
+117 -6
View File
@@ -80,7 +80,9 @@ type
TOnSliceCATChange = procedure(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean) of object;
const AutoTX: array of Boolean;
const OutDev: array of string;
const InDev: array of string) of object;
TOnAlexChange = procedure(const A: TAlexSettings) of object;
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
@@ -346,6 +348,10 @@ type
FSliceCATEn: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatCheckBox;
FSliceCATPort: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatSpinEdit;
FSliceCATAutoTx: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatCheckBox;
// Аудио слота: списки — общие с главными комбо (RefreshAudioDevices),
// первый пункт «Default»/«None» = общий выход/вход приложения.
FSliceOutDev: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatComboBox;
FSliceInDev: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatComboBox;
// ---- Advanced tab controls ----
FChkDither: TFlatCheckBox;
@@ -473,6 +479,8 @@ type
procedure OnCATTcpChange(Sender: TObject);
procedure FireCATChange;
procedure OnSliceCATCtrlChange(Sender: TObject);
procedure SelectDevInCombo(Cmb: TFlatComboBox; const DevName: string);
procedure FillSliceDevCombos(AudioOut: TAudioOutput; AudioIn: TAudioInput);
procedure FireSliceCATChange;
procedure BtnCloseClick(Sender: TObject);
@@ -527,7 +535,9 @@ type
procedure LoadSliceCATSettings(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean);
const AutoTX: array of Boolean;
const OutDev: array of string;
const InDev: array of string);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
const SerPort: array of string;
@@ -592,6 +602,9 @@ const
BTN_H = 24;
GRP_TITLE_H = 22; // высота заголовка в group-панелях (title label + divider)
// Первый пункт комбо аудио слайса = «как у приложения» (пустое имя в конфиге).
SLICE_DEV_DEFAULT = 'Default (app output)';
SLICE_DEV_NONE = 'None (app input)';
CAT_BAUD_VALS: array[0..7] of Integer = (1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200);
CAT_BAUD_NAMES: array[0..7] of string = ('1200','2400','4800','9600','19200','38400','57600','115200');
@@ -2844,6 +2857,54 @@ begin
FLoading := False;
Names.Free;
end;
FillSliceDevCombos(AudioOut, AudioIn);
end;
procedure TSettingsForm.FillSliceDevCombos(AudioOut: TAudioOutput; AudioIn: TAudioInput);
// Списки устройств в комбо вкладки Slices — те же, что у главных комбо. Текущий
// выбор держим по ИМЕНИ: порядок перечисления PortAudio между запусками плавает.
var
Names: TStringList;
Idx: array[0..63] of Integer;
Cnt, i, k: Integer;
Cur: string;
begin
if FSliceOutDev[0] = nil then Exit; // вкладка ещё не построена
Names := TStringList.Create;
FLoading := True;
try
if AudioOut <> nil then
begin
Cnt := 0; Names.Clear;
AudioOut.EnumOutputDevices(Names, Idx, Cnt);
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
begin
Cur := FSliceOutDev[i].Text;
if FSliceOutDev[i].ItemIndex = 0 then Cur := '';
FSliceOutDev[i].Items.Clear;
FSliceOutDev[i].Items.Add(SLICE_DEV_DEFAULT);
for k := 0 to Cnt - 1 do FSliceOutDev[i].Items.Add(Names[k]);
SelectDevInCombo(FSliceOutDev[i], Cur);
end;
end;
if AudioIn <> nil then
begin
Cnt := 0; Names.Clear;
AudioIn.EnumInputDevices(Names, Idx, Cnt);
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
begin
Cur := FSliceInDev[i].Text;
if FSliceInDev[i].ItemIndex = 0 then Cur := '';
FSliceInDev[i].Items.Clear;
FSliceInDev[i].Items.Add(SLICE_DEV_NONE);
for k := 0 to Cnt - 1 do FSliceInDev[i].Items.Add(Names[k]);
SelectDevInCombo(FSliceInDev[i], Cur);
end;
end;
finally
FLoading := False;
Names.Free;
end;
end;
// ---------------------------------------------------------------------------
@@ -3560,9 +3621,10 @@ const
PAD = 18;
LW = 120;
CX = PAD + LW + 8;
GRP_W = 320;
GRP_H = 168;
GRP_W = 380;
GRP_H = 240;
GRP_GAP = 16;
CMB_W = 210;
R1 = 42;
STEP = 32;
var
@@ -3570,6 +3632,7 @@ var
Grp: TPanel;
Chk: TFlatCheckBox;
Spin: TFlatSpinEdit;
Cmb: TFlatComboBox;
begin
with TLabel.Create(Self) do
begin
@@ -3643,10 +3706,35 @@ begin
MakeLbl(Grp, 'CAT TX takes transmit to this slice',
PAD + 2, R1 + 2 * STEP + 24, GRP_W - PAD * 2 - 2);
// Аудио слота: не шаблон, а привязка буквы — применяется к слайсу, который
// стоит на слоте сейчас, и достаётся следующему.
MakeLbl(Grp, 'Audio out:', PAD, R1 + 3 * STEP + 26, LW);
Cmb := MakeCombo(Grp, CX, R1 + 3 * STEP + 20, CMB_W, OnSliceCATCtrlChange);
Cmb.Tag := i;
Cmb.Items.Add(SLICE_DEV_DEFAULT);
Cmb.ItemIndex := 0;
FSliceOutDev[i] := Cmb;
MakeLbl(Grp, 'Audio in:', PAD, R1 + 4 * STEP + 26, LW);
Cmb := MakeCombo(Grp, CX, R1 + 4 * STEP + 20, CMB_W, OnSliceCATCtrlChange);
Cmb.Tag := i;
Cmb.Items.Add(SLICE_DEV_NONE);
Cmb.ItemIndex := 0;
FSliceInDev[i] := Cmb;
end;
gy := 96 + ((CFG_CAT_SLICE_COUNT + 1) div 2) * (GRP_H + GRP_GAP);
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'Audio out/in belong to the slice letter: the setting moves the '
+ 'slice sitting on that slot right away and the next one inherits it.';
SetBounds(MARGIN, gy, 760, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
Inc(gy, 18);
with TLabel.Create(Self) do
begin
Parent := FPageSlices;
Caption := 'Only one slice transmits at a time: while anything is already on '
@@ -3676,6 +3764,7 @@ procedure TSettingsForm.FireSliceCATChange;
var
En, Auto: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
Prt: array[0..CFG_CAT_SLICE_COUNT-1] of Integer;
OutD, InD: array[0..CFG_CAT_SLICE_COUNT-1] of string;
i: Integer;
begin
if FLoading then Exit;
@@ -3685,8 +3774,13 @@ begin
En[i] := FSliceCATEn[i].Checked;
Prt[i] := FSliceCATPort[i].Value;
Auto[i] := FSliceCATAutoTx[i].Checked;
// Первый пункт списка — «общий выход/вход приложения» (пустое имя).
if FSliceOutDev[i].ItemIndex <= 0 then OutD[i] := ''
else OutD[i] := FSliceOutDev[i].Text;
if FSliceInDev[i].ItemIndex <= 0 then InD[i] := ''
else InD[i] := FSliceInDev[i].Text;
end;
FOnSliceCATChange(En, Prt, Auto);
FOnSliceCATChange(En, Prt, Auto, OutD, InD);
end;
procedure TSettingsForm.OnSliceCATCtrlChange(Sender: TObject);
@@ -3695,7 +3789,9 @@ begin FireSliceCATChange; end;
procedure TSettingsForm.LoadSliceCATSettings(
const Enabled: array of Boolean;
const Port: array of Integer;
const AutoTX: array of Boolean);
const AutoTX: array of Boolean;
const OutDev: array of string;
const InDev: array of string);
var i: Integer;
begin
FLoading := True;
@@ -3706,12 +3802,27 @@ begin
if (i <= High(Port)) and (Port[i] > 0) then
FSliceCATPort[i].Value := EnsureRange(Port[i], 1, 65535);
if i <= High(AutoTX) then FSliceCATAutoTx[i].Checked := AutoTX[i];
if i <= High(OutDev) then SelectDevInCombo(FSliceOutDev[i], OutDev[i]);
if i <= High(InDev) then SelectDevInCombo(FSliceInDev[i], InDev[i]);
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.SelectDevInCombo(Cmb: TFlatComboBox; const DevName: string);
// Выбор устройства по ИМЕНИ. Сохранённого устройства может не быть в системе
// (кабель не поднят, гарнитура не воткнута) — тогда добавляем имя в список,
// чтобы привязка слота не потерялась молча при первом же открытии настроек.
var i: Integer;
begin
if Cmb = nil then Exit;
if DevName = '' then begin Cmb.ItemIndex := 0; Exit; end;
i := Cmb.Items.IndexOf(DevName);
if i < 0 then i := Cmb.Items.Add(DevName);
Cmb.ItemIndex := i;
end;
// ---------------------------------------------------------------------------
// BuildAlexTab
// ---------------------------------------------------------------------------
+21 -1
View File
@@ -134,7 +134,27 @@ SmartSDR: Transmit Slice переключён, а не одолжен). Бейд
## 4. Настройки
Вкладка **Slices**: на каждый слайс B..G — `Enable CAT TCP server`, `Port`
(по умолчанию 19091, 19092, …) и `Auto TX`.
(по умолчанию 19091, 19092, …), `Auto TX`, `Audio out`, `Audio in`.
### 4.1 Аудио слота
`Audio out`/`Audio in` — это **устройство слота (буквы)**, а не шаблон:
* смена настройки применяется немедленно к слайсу, который стоит на слоте
сейчас (`TRadioController.SetSliceSlotAudio`);
* слайс, который встанет на слот позже (создание/восстановление из персиста),
получает то же устройство — оно сильнее того, что попросил вызывающий
`AddSlice`;
* выбор во вкладке `AUD` флага слайса пишется обратно в настройку слота
(`SetSliceDevice`/`SetSliceInDevice``MainForm.StoreSliceSlotAudio`), так
что вкладка всегда показывает реальное устройство — одна истина, два места
правки;
* пусто (`Default (app output)` / `None (app input)`) — как раньше: общий выход
приложения и общий вход.
Хранятся **имена** PortAudio-устройств, а не индексы: порядок перечисления между
запусками плавает. Имя отсутствующего сейчас устройства (кабель не поднят) не
теряется — комбо добавляет его в список и держит выбранным.
* Хранятся вместе с остальным CAT: корневая секция `cat` (`slice_en_N`,
`slice_port_N`, `slice_autotx_N`) + device-blob (`cat_slice_*`), не привязаны