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
+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
// ---------------------------------------------------------------------------