diff --git a/MainForm.pas b/MainForm.pas index bf16323..5fc2b96 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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. diff --git a/RadioController.pas b/RadioController.pas index 3771a25..ba9b1d2 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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-источником, без него — передаёт, diff --git a/Settings.pas b/Settings.pas index dee4e04..fa8020a 100644 --- a/Settings.pas +++ b/Settings.pas @@ -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; diff --git a/SettingsForm.pas b/SettingsForm.pas index 3dab9b2..e7f8254 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -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 // --------------------------------------------------------------------------- diff --git a/doc/CAT_SLICES.md b/doc/CAT_SLICES.md index 316471f..19d80d7 100644 --- a/doc/CAT_SLICES.md +++ b/doc/CAT_SLICES.md @@ -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_*`), не привязаны