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
+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-источником, без него — передаёт,