From 4aa180e7b299d080f3a38cac74d59ee7b1bc5503 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 21 Jul 2026 21:52:24 +0300 Subject: [PATCH] =?UTF-8?q?wip(audio):=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=20?= =?UTF-8?q?=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0=20?= =?UTF-8?q?=D0=92=D0=92=D0=9E=D0=94=D0=90=20=D0=BD=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=B6=D0=B4=D0=BE=D0=BC=20=D1=81=D0=BB=D0=B0=D0=B9=D1=81=D0=B5?= =?UTF-8?q?=20+=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20OUT/IN=20?= =?UTF-8?q?=D0=B2=20AUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Паритет с уже существующим выбором вывода: на каждом слайсе своё устройство входа (микрофон/виртуальный кабель). - RadioController: TCtrlSlice += InDeviceIndex/InDevName/AudioIn (ссылка, не владение); mic-пул FMicPool с refcount (Acquire/ReleaseMicInput) — один открытый TAudioInput на уникальное PA-устройство, смена TX-слайса не переоткрывает поток; ActiveMicInput/ActiveMicInDevName для PullSoundCardMic, Flush и DefaultMicSource; SetMainMicDevice перепривязывает ссылки слайсов. - VfoOverlay: во fly-out AUD ряд вкладок OUT | IN, событие OnAudioInDevice. - PanafallPanel: проброс списка входных устройств во флаги слайсов и главный. - Settings: персист in_dev_name. НЕ ГОТОВО: зависание GUI при выборе на слайсе того же входа, что в настройках (пул открывает поток вместо шортката на общий FAudioIn). В коде оставлена временная диагностика [MIC] в stderr. Также подозрение на замедление потока данных от радио — проверяем сравнением с main. Со мной в коммите временные тестовые программы PortAudio (other/pa*.lpr). Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 41 ++++--- PanafallPanel.pas | 24 ++++ RadioController.pas | 260 ++++++++++++++++++++++++++++++++++++++++---- Settings.pas | 5 +- VfoOverlay.pas | 134 ++++++++++++++++++++--- ewsdr.lpi | 5 +- other/padump.lpr | 25 +++++ other/patest2.lpr | 19 ++++ other/patest3.lpr | 29 +++++ 9 files changed, 493 insertions(+), 49 deletions(-) create mode 100644 other/padump.lpr create mode 100644 other/patest2.lpr create mode 100644 other/patest3.lpr diff --git a/MainForm.pas b/MainForm.pas index 1efe48d..c07556c 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -599,6 +599,7 @@ type procedure OnVfoOverlayVolume(Vol: Integer); procedure OnVfoOverlayMute(Sender: TObject); procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string); + procedure OnVfoOverlayAudioInDevice(DevIndex: Integer; const DevName: string); procedure OnVfoOverlaySplit(Sender: TObject); procedure OnVfoOverlayTxSelect(Sender: TObject); procedure OnVfoOverlayRxMuteTx(Sender: TObject); // RXM — приём слайса на TX @@ -3838,8 +3839,10 @@ function TMainForm.DefaultMicSource: TTXMicSource; // иначе используем HW mic-стрим от трансивера. // HW PTT всегда переключает на txmsRadio непосредственно в ApplyMOX. begin - if FController.FAudioInDevName <> '' then Result := txmsSoundCard - else Result := txmsRadio; + // Учитываем вход TX-слайса: у него может быть свой (виртуальный кабель), + // даже когда общего входа нет — иначе взяли бы микрофон радио. + if FController.ActiveMicInDevName <> '' then Result := txmsSoundCard + else Result := txmsRadio; end; // ApplyTXSettingsToDSP переехал в TRadioController (вся TX-цепь над FTXSettings). @@ -5739,6 +5742,20 @@ begin FPan.PushMainFlagAudioDevices; // обновить подсветку текущего устройства end; +procedure TMainForm.OnVfoOverlayAudioInDevice(DevIndex: Integer; const DevName: string); +// Вкладка IN во флаге: откуда слайс берёт модуляцию на передаче. У флага +// главного VFO это общий вход приложения (то же поле, что в настройках). +begin + if CurSliceId <> 0 then + begin + FController.SetSliceInDevice(CurSliceId, DevIndex, DevName); + PushFlagStateAllPans(CurSliceId); + Exit; + end; + ApplyAudioInputDevice(DevIndex, DevName); + FPan.PushMainFlagAudioDevices; +end; + procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject); var P: TPanafallPanel; begin @@ -5797,6 +5814,7 @@ begin O.OnVolume := OnVfoOverlayVolume; O.OnMute := OnVfoOverlayMute; O.OnAudioDevice := OnVfoOverlayAudioDevice; + O.OnAudioInDevice := OnVfoOverlayAudioInDevice; O.OnSplit := OnVfoOverlaySplit; O.OnTxSelect := OnVfoOverlayTxSelect; O.OnRxMuteTx := OnVfoOverlayRxMuteTx; @@ -6742,6 +6760,7 @@ begin Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn; Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel; Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName; + Cfg.Pans[i].Slices[n].InDevName := FController.FSlices[s].InDevName; Inc(n); end; end; @@ -6762,6 +6781,10 @@ begin TWDSPAGCMode(EnsureRange(SC.AGC, 0, Ord(High(TWDSPAGCMode)))), EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId); if Id < 0 then Exit; + // Свой вход слайса (имя устройства → PA-индекс текущей сессии). + if SC.InDevName <> '' then + FController.SetSliceInDevice(Id, + FController.FAudioIn.FindDeviceByName(SC.InDevName), SC.InDevName); if SC.Muted then FController.SetSliceMute(Id, True); FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx); if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel); @@ -8176,18 +8199,10 @@ begin end; procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string); +// Общий вход: контроллер сам переоткрывает поток и перепривязывает к нему +// слайсы, которые ссылались на общее устройство (mic-пул). begin - if FController.FAudioIn.IsOpen then - FController.FAudioIn.Close; - FController.FAudioIn.DeviceIndex := DevIndex; - FController.FAudioInDevName := DevName; - if DevIndex >= 0 then - begin - try - FController.FAudioIn.Open; - except - end; - end; + FController.SetMainMicDevice(DevIndex, DevName); end; procedure TMainForm.ApplyAudioBufferSize(BufferSize: Integer); diff --git a/PanafallPanel.pas b/PanafallPanel.pas index c2c5280..35027a7 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -1353,6 +1353,20 @@ begin Names.Free; end; end; + // Устройства ВВОДА + свой вход слайса ('' = общий вход приложения). + if Assigned(FController.FAudioIn) then + begin + Names := TStringList.Create; + try + Cnt := 0; + FController.FAudioIn.EnumInputDevices(Names, Indices, Cnt); + SetLength(Idx, Cnt); + for I := 0 to Cnt - 1 do Idx[I] := Indices[I]; + O.SetAudioInDevices(Names, Idx, S.InDevName); + finally + Names.Free; + end; + end; end; procedure TPanafallPanel.PushAllSliceFlagStates; @@ -1390,6 +1404,16 @@ begin SetLength(Idx, Cnt); for I := 0 to Cnt - 1 do Idx[I] := Indices[I]; FMainFlag.SetAudioDevices(Names, Idx, FController.FAudioOutDevName); + // Вход главного VFO = общее устройство ввода (то же, что в Settings). + if Assigned(FController.FAudioIn) then + begin + Cnt := 0; + Names.Clear; + FController.FAudioIn.EnumInputDevices(Names, Indices, Cnt); + SetLength(Idx, Cnt); + for I := 0 to Cnt - 1 do Idx[I] := Indices[I]; + FMainFlag.SetAudioInDevices(Names, Idx, FController.FAudioInDevName); + end; finally Names.Free; end; diff --git a/RadioController.pas b/RadioController.pas index b91d18e..debb2e3 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -162,9 +162,14 @@ type RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM) FMSQLevel: Integer; // порог squelch 0..100 - DeviceIndex: Integer; // PortAudio-индекс (-1 = default) + DeviceIndex: Integer; // PortAudio-индекс вывода (-1 = default) DevName: string; Audio: TAudioOutput; // владеет; свой поток вывода + // Вход слайса (mic/виртуальный кабель) — источник модуляции, когда именно + // этот слайс выбран TX-источником. -1/'' = брать общий вход (FAudioIn). + InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий) + InDevName: string; + AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса end; @@ -190,6 +195,11 @@ type procedure OnSliceDMRAudioReady(SliceId: Integer; const PCM: array of Single; Count: Integer); function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче? + // Пул входных потоков: захват/освобождение ссылки на устройство ввода. + function AcquireMicInput(DevIndex: Integer): TAudioInput; + procedure ReleaseMicInput(var AI: TAudioInput); + procedure SetSliceInDeviceIdx(Idx, DevIndex: Integer; const DevName: string); + procedure FreeMicPool; // финальная зачистка (FreeEngines) procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра // --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) --- procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст @@ -230,6 +240,18 @@ type // TargetHz абсолютна; ShiftHz в движок = TargetHz - FCenterFreq. FSlices: array[0..MAX_SLICES-1] of TCtrlSlice; FNextSliceId: Integer; // монотонный генератор id слайсов + + // ---- Пул входных потоков (mic) ---- + // Каждому УНИКАЛЬНОМУ устройству ввода — один открытый TAudioInput на всё + // приложение; слайсы держат на него ссылку по refcount. Потоки живут, пока + // на них кто-то ссылается: переключение TX-источника = смена указателя, + // без Close/Open (иначе на старте передачи терялись бы первые сэмплы). + // Читатель у кольца всегда один — TX-источник в данный момент только один. + FMicPool: array of record + DevIdx: Integer; + Refs: Integer; + AI: TAudioInput; + end; FSettings: TSettingsManager; FChannelStore: TChannelStore; FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered) @@ -642,6 +664,14 @@ type // hardwaregain у AD936x) — входит в оба офсета выше. function FrontEndCalDB: Double; procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string); + // Устройство ВВОДА слайса (источник модуляции, когда слайс = TX-источник). + // DevIndex=-1 / DevName='' — вернуться на общий вход (FAudioIn). + procedure SetSliceInDevice(Id, DevIndex: Integer; const DevName: string); + // Общее устройство ввода (главный VFO = слайс A + фолбэк для слайсов). + procedure SetMainMicDevice(DevIndex: Integer; const DevName: string); + // Поток/имя устройства ввода активного TX-источника (слайс или общий). + function ActiveMicInput: TAudioInput; + function ActiveMicInDevName: string; function SliceCount: Integer; function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean; // Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс. @@ -1059,8 +1089,10 @@ begin FSlices[i].Audio.Close; FreeAndNil(FSlices[i].Audio); end; + FSlices[i].AudioIn := nil; // владелец — mic-пул (FreeMicPool ниже) FSlices[i].Active := False; end; + FreeMicPool; if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end; if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end; if Assigned(FDSPEngine) then @@ -1922,6 +1954,10 @@ begin FSlices[slot].DeviceIndex := DevIndex; FSlices[slot].DevName := DevName; FSlices[slot].Audio := AO; + // Вход слайса по умолчанию общий (FAudioIn); свой ставится SetSliceInDevice. + FSlices[slot].InDeviceIndex := -1; + FSlices[slot].InDevName := ''; + FSlices[slot].AudioIn := nil; FSlices[slot].DMR := nil; if Mode = MODE_DMR then FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id); @@ -1944,6 +1980,9 @@ begin FSlices[idx].Audio.Close; FreeAndNil(FSlices[idx].Audio); end; + ReleaseMicInput(FSlices[idx].AudioIn); // поток закроется, когда уйдёт последняя ссылка + FSlices[idx].InDeviceIndex := -1; + FSlices[idx].InDevName := ''; FSlices[idx].Active := False; Changed(rfDevice); end; @@ -2150,6 +2189,182 @@ begin end; end; +// --- Пул входных потоков ----------------------------------------------------- + +procedure MicDbg(const S: string); +// ВРЕМЕННО: диагностика mic-пула в stderr (поиск причины зависания/отказа +// Pa_OpenStream при выборе входа на слайсе). Снять после разбора. +begin + WriteLn(StdErr, '[MIC] ', S); + Flush(StdErr); +end; + +function TRadioController.AcquireMicInput(DevIndex: Integer): TAudioInput; +// Ссылка на открытый поток устройства DevIndex (создаёт при первом обращении). +// DevIndex<0 = «своего входа нет» → nil (читатель упадёт на общий FAudioIn). +var + i, n: Integer; + AI: TAudioInput; +begin + Result := nil; + if Assigned(FAudioIn) then + MicDbg(Format('Acquire(dev=%d): main FAudioIn dev=%d open=%s name="%s"', + [DevIndex, FAudioIn.DeviceIndex, BoolToStr(FAudioIn.IsOpen, True), + FAudioInDevName])) + else + MicDbg(Format('Acquire(dev=%d): FAudioIn=nil', [DevIndex])); + if DevIndex < 0 then Exit; + // Общий вход уже открыт на этом же устройстве — переиспользуем его: второй + // Open того же PA-устройства на эксклюзивном ALSA-hw просто не прошёл бы. + if Assigned(FAudioIn) and (FAudioIn.DeviceIndex = DevIndex) then + begin + MicDbg(' -> общий FAudioIn (без открытия)'); + Exit(FAudioIn); + end; + for i := 0 to High(FMicPool) do + if FMicPool[i].DevIdx = DevIndex then + begin + MicDbg(Format(' -> пул[%d] refs=%d open=%s', + [i, FMicPool[i].Refs, BoolToStr(FMicPool[i].AI.IsOpen, True)])); + // Запись с Refs=0 — поток закрыт, но объект жив (см. ReleaseMicInput). + if (FMicPool[i].Refs <= 0) and FLocalAudio and + (not FMicPool[i].AI.IsOpen) then + try FMicPool[i].AI.Open; except end; + Inc(FMicPool[i].Refs); + Exit(FMicPool[i].AI); + end; + AI := TAudioInput.Create(48000); + AI.DeviceIndex := DevIndex; + if FLocalAudio then + begin + MicDbg(Format(' новый поток: Open(dev=%d) ...', [DevIndex])); + try + MicDbg(Format(' Open(dev=%d) = %s err="%s"', + [DevIndex, BoolToStr(AI.Open, True), AI.LastError])); + except + on E: Exception do MicDbg(' Open исключение: ' + E.Message); + end; + end; + n := Length(FMicPool); + SetLength(FMicPool, n + 1); + FMicPool[n].DevIdx := DevIndex; + FMicPool[n].Refs := 1; + FMicPool[n].AI := AI; + Result := AI; +end; + +procedure TRadioController.ReleaseMicInput(var AI: TAudioInput); +// Последняя ссылка ушла — поток ЗАКРЫВАЕМ, но объект оставляем в пуле до +// выхода: TX-поток (PullSoundCardMic) мог взять указатель прямо сейчас, а +// освобождение памяти под ним = падение. Повторный выбор устройства просто +// переоткроет ту же запись. +var i: Integer; +begin + if AI = nil then Exit; + if AI = FAudioIn then begin AI := nil; Exit; end; // общий вход не из пула + for i := 0 to High(FMicPool) do + if FMicPool[i].AI = AI then + begin + Dec(FMicPool[i].Refs); + if FMicPool[i].Refs <= 0 then + begin + FMicPool[i].Refs := 0; + FMicPool[i].AI.Close; + end; + Break; + end; + AI := nil; +end; + +procedure TRadioController.FreeMicPool; +var i: Integer; +begin + for i := 0 to High(FMicPool) do + if FMicPool[i].AI <> nil then + begin + FMicPool[i].AI.Close; + FMicPool[i].AI.Free; + end; + SetLength(FMicPool, 0); +end; + +procedure TRadioController.SetSliceInDevice(Id, DevIndex: Integer; + const DevName: string); +begin + SetSliceInDeviceIdx(FindSliceIndex(Id), DevIndex, DevName); +end; + +procedure TRadioController.SetSliceInDeviceIdx(Idx, DevIndex: Integer; + const DevName: string); +begin + if (Idx < 0) or (Idx >= MAX_SLICES) or (not FSlices[Idx].Active) then Exit; + MicDbg(Format('SetSliceInDevice: slot=%d id=%d dev=%d name="%s"', + [Idx, FSlices[Idx].Id, DevIndex, DevName])); + if DevName = '' then DevIndex := -1; + ReleaseMicInput(FSlices[Idx].AudioIn); + FSlices[Idx].InDeviceIndex := DevIndex; + FSlices[Idx].InDevName := DevName; + FSlices[Idx].AudioIn := AcquireMicInput(DevIndex); + // Слайс сейчас источник TX — источник модуляции мог сменить тип + // (звуковуха ↔ микрофон радио). + if (FTxSliceId = FSlices[Idx].Id) and FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetTXMicSource(DefaultMicSource); +end; + +procedure TRadioController.SetMainMicDevice(DevIndex: Integer; + const DevName: string); +var i: Integer; +begin + MicDbg(Format('SetMainMicDevice: dev=%d name="%s"', [DevIndex, DevName])); + if Assigned(FAudioIn) then + begin + if FAudioIn.IsOpen then FAudioIn.Close; + FAudioIn.DeviceIndex := DevIndex; + if (DevIndex >= 0) and FLocalAudio then + try + MicDbg(Format(' main Open(dev=%d) = %s err="%s"', + [DevIndex, BoolToStr(FAudioIn.Open, True), FAudioIn.LastError])); + except + on E: Exception do MicDbg(' main Open исключение: ' + E.Message); + end; + end; + FAudioInDevName := DevName; + // Слайсы могли ссылаться на общий вход (или держать свой поток на устройстве, + // которое теперь стало общим) — перепривязать все ссылки к новой картине. + for i := 0 to MAX_SLICES - 1 do + if FSlices[i].Active then + begin + ReleaseMicInput(FSlices[i].AudioIn); + FSlices[i].AudioIn := AcquireMicInput(FSlices[i].InDeviceIndex); + end; + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetTXMicSource(DefaultMicSource); +end; + +function TRadioController.ActiveMicInput: TAudioInput; +// Вход TX-источника: свой поток слайса, иначе общий (слайс без выбора). +var idx: Integer; +begin + Result := FAudioIn; + if FTxSliceId = 0 then Exit; + idx := FindSliceIndex(FTxSliceId); + // Поток слайса мог не открыться (устройство занято/исчезло) — тогда лучше + // передавать с общего входа, чем молчать в эфир. + if (idx >= 0) and (FSlices[idx].AudioIn <> nil) and + FSlices[idx].AudioIn.IsOpen then + Result := FSlices[idx].AudioIn; +end; + +function TRadioController.ActiveMicInDevName: string; +var idx: Integer; +begin + Result := FAudioInDevName; + if FTxSliceId = 0 then Exit; + idx := FindSliceIndex(FTxSliceId); + if (idx >= 0) and (FSlices[idx].InDevName <> '') then + Result := FSlices[idx].InDevName; +end; + function TRadioController.SliceCount: Integer; var i: Integer; begin @@ -2227,15 +2442,17 @@ function TRadioController.PullSoundCardMic(MaxN: Integer): Integer; // пачкой в локальный буфер — экономим будилки семафора. var i, n: Integer; + AI: TAudioInput; Buf: array[0..2047] of Double; begin Result := 0; - if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit; - n := FAudioIn.Available; + AI := ActiveMicInput; // вход TX-слайса (свой) либо общий + if (AI = nil) or (not AI.IsOpen) then Exit; + n := AI.Available; if n > MaxN then n := MaxN; if n > Length(Buf) then n := Length(Buf); for i := 0 to n - 1 do - Buf[i] := FAudioIn.ReadSample; + Buf[i] := AI.ReadSample; if n > 0 then FDSPEngine.PushTXMicSamplesD(Buf, n); Result := n; @@ -2365,7 +2582,15 @@ begin FTxSliceId := SliceId; // Передающий тракт под режим нового источника (idle-канал, применится к PTT). if FWDSPReady and Assigned(FDSPEngine) then + begin FDSPEngine.SetTXMode(ActiveTXMode); + // Источник модуляции — вход НОВОГО слайса (у него может быть свой). + FDSPEngine.SetTXMicSource(DefaultMicSource); + end; + // Кольцо нового входа копило звук всё время, пока он не был источником: + // без сброса первые ~200мс передачи ушли бы в эфир «из прошлого». + if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then + ActiveMicInput.Flush; FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx @@ -3416,7 +3641,8 @@ begin begin FWebMicActive := False; FDSPEngine.SetTXMicSource(DefaultMicSource); - if Assigned(FAudioIn) and FAudioIn.IsOpen then FAudioIn.Flush; + if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then + ActiveMicInput.Flush; end; // Если TUN активен — пересчитываем знак частоты под новый режим if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); @@ -4230,9 +4456,11 @@ begin end; function TRadioController.DefaultMicSource: TTXMicSource; +// Источник модуляции решает ТЕКУЩИЙ TX-слайс: у него может быть свой вход +// (виртуальный кабель), даже когда общего входа нет, и наоборот. begin - if FAudioInDevName <> '' then Result := txmsSoundCard - else Result := txmsRadio; + if ActiveMicInDevName <> '' then Result := txmsSoundCard + else Result := txmsRadio; end; function TRadioController.BuildMicLineSelectByte: Byte; @@ -4416,9 +4644,9 @@ begin // звук бёрстом уходит в тракт: стоячая задержка ~190мс в DUC-очереди // + в эфир передаётся аудио, записанное ДО нажатия PTT (на TUN оно // складывается с тоном поверх полной шкалы → клип-всплеск). - if FTransmitting and (not WasTransmitting) and Assigned(FAudioIn) - and FAudioIn.IsOpen then - FAudioIn.Flush; + if FTransmitting and (not WasTransmitting) and Assigned(ActiveMicInput) + and ActiveMicInput.IsOpen then + ActiveMicInput.Flush; // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы // TX leakage не накапливался в RXA pipeline / FFT-истории анализатора. FDSPEngine.KeepRXDuringTX := FDisplayDuplex; @@ -5254,16 +5482,10 @@ begin // Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT — // здесь только открываем устройство. if FLoadedGlobal.AudioInDevice <> '' then - begin - FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice); - FAudioInDevName := FLoadedGlobal.AudioInDevice; - FAudioIn.Open; - end + SetMainMicDevice(FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice), + FLoadedGlobal.AudioInDevice) else - begin - FAudioIn.DeviceIndex := -1; - FAudioInDevName := ''; - end; + SetMainMicDevice(-1, ''); end; end; diff --git a/Settings.pas b/Settings.pas index 8b0ae1d..6f98b36 100644 --- a/Settings.pas +++ b/Settings.pas @@ -122,7 +122,8 @@ type RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл) FMSQOn: Boolean; FMSQLevel: Integer; - DevName: string; // имя PortAudio-устройства ('' = default) + DevName: string; // имя PortAudio-устройства вывода ('' = default) + InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход) end; TPanCfg = record @@ -1972,6 +1973,7 @@ begin SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn); 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); end; end; end; @@ -2051,6 +2053,7 @@ begin P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False); 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', ''); end; end; end; diff --git a/VfoOverlay.pas b/VfoOverlay.pas index d7f1827..2dfa2e7 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -94,6 +94,11 @@ type FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default) FCurDev: string; // имя текущего устройства ('' = default) + FInDevNames: TStringList; // имена устройств ВВОДА (микрофон/кабель) + FInDevIdx: array of Integer; // их PortAudio-индексы + FCurInDev: string; // текущий вход слайса ('' = общий вход) + FAudInTab: Boolean; // AUD-панель показывает вход (IN), а не выход + FPanel: TFlyPanel; // открытая fly-out панель FAudScroll: Integer; // прокрутка списка аудио-устройств (верхний индекс) FVolTrack: TRect; // трек громкости (локальные координаты) @@ -108,6 +113,7 @@ type FOnVolume: TVolumeEvent; FOnMute: TNotifyEvent; FOnAudioDevice: TAudioDeviceEvent; + FOnAudioInDevice: TAudioDeviceEvent; FOnSplit: TNotifyEvent; FOnTxSelect: TNotifyEvent; FOnRxMuteTx: TNotifyEvent; @@ -146,6 +152,11 @@ type procedure DrawModePanel(C: TCanvas; X, Y, RowW: Integer); procedure DrawDspPanel(C: TCanvas; X, Y, RowW: Integer); procedure DrawAudPanel(C: TCanvas; X, Y, RowW: Integer); + // Строки активной вкладки AUD (OUT — устройства вывода, IN — ввода; + // у IN первая строка псевдо: «общий вход» / «нет»). + function AudRowCount: Integer; + function AudRowName(I: Integer): string; + function AudRowCurrent(I: Integer): Boolean; procedure RegisterHit(const HR: TRect; HV: Integer); procedure ApplyThemeColors; // раскладывает FTheme → структурные CLR_* function FormatFreq(Hz: Double): string; @@ -192,6 +203,9 @@ type procedure SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX) procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer; const CurName: string); + // Список устройств ВВОДА + текущий выбор ('' = общий вход приложения). + procedure SetAudioInDevices(Names: TStrings; const Indices: array of Integer; + const CurName: string); procedure UpdateSMeter(DB: Double); procedure UpdateVfo(AVfoHz: Double); procedure UpdateDMRStatus(const AText: string; Synced: Boolean); @@ -223,6 +237,7 @@ type property OnVolume: TVolumeEvent read FOnVolume write FOnVolume; property OnMute: TNotifyEvent read FOnMute write FOnMute; property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice; + property OnAudioInDevice: TAudioDeviceEvent read FOnAudioInDevice write FOnAudioInDevice; property OnSplit: TNotifyEvent read FOnSplit write FOnSplit; property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect; property OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx; @@ -262,6 +277,8 @@ const HIT_TX = -26; HIT_AUD_UP = -27; HIT_AUD_DN = -28; + HIT_AUD_TAB_OUT = -34; // вкладка «OUT» в AUD-панели + HIT_AUD_TAB_IN = -35; // вкладка «IN» HIT_SLICE_SEL = -29; // клик по бейджу буквы (выбрать слайс активным) HIT_CLOSE = -30; // × закрытия слайса (неглавные) HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели) @@ -481,6 +498,9 @@ begin FVolDrag := False; FDevNames := TStringList.Create; FCurDev := ''; + FInDevNames := TStringList.Create; + FCurInDev := ''; + FAudInTab := False; // AUD открывается на вкладке OUT FCacheBitmap := TBitmap.Create; FCacheBitmap.PixelFormat := pf32bit; FCacheDirty := True; @@ -520,6 +540,7 @@ end; destructor TVfoOverlay.Destroy; begin + FInDevNames.Free; FDevNames.Free; FCacheBitmap.Free; inherited Destroy; @@ -618,7 +639,9 @@ begin Result := ROW_H + IGAP + ROW_H; // toggles + AGC if IsFM then Inc(Result, IGAP + ROW_H); // + ряд SQL (только FM) end; - fpAud: Result := Min(Max(1, FDevNames.Count), AUD_MAX_VIS) * AUD_ROW_H; + // ряд вкладок OUT/IN + список устройств активной вкладки + fpAud: Result := ROW_H + IGAP + + Min(Max(1, AudRowCount), AUD_MAX_VIS) * AUD_ROW_H; else Result := 0; end; end; @@ -907,20 +930,63 @@ begin end; end; +function TVfoOverlay.AudRowCount: Integer; +begin + // У вкладки IN первая строка — «общий вход» (слайс) / «нет» (главный флаг). + if FAudInTab then Result := FInDevNames.Count + 1 + else Result := FDevNames.Count; +end; + +function TVfoOverlay.AudRowName(I: Integer): string; +begin + if not FAudInTab then Exit(FDevNames[I]); + if I = 0 then + begin + if FIsMain then Result := '(нет · мик радио)' + else Result := '(общий вход)'; + Exit; + end; + Result := FInDevNames[I - 1]; +end; + +function TVfoOverlay.AudRowCurrent(I: Integer): Boolean; +begin + if FAudInTab then + begin + if I = 0 then Exit(FCurInDev = ''); + Result := FInDevNames[I - 1] = FCurInDev; + end + else + // Пустое имя вывода = устройство по умолчанию (первая строка списка). + Result := (FDevNames[I] = FCurDev) or ((FCurDev = '') and (I = 0)); +end; + procedure TVfoOverlay.DrawAudPanel(C: TCanvas; X, Y, RowW: Integer); const ARROW_W = 15; var - I, Slot, VisN, RowWidth, MaxScroll, PanelH, ArrowX, MidY, CY: Integer; - HR: TRect; + I, Slot, VisN, RowWidth, MaxScroll, PanelH, ArrowX, MidY, CY, Cnt: Integer; + HR, TabR: TRect; Nm: string; Cur, Scrollable, CanUp, CanDn: Boolean; Tri: array[0..2] of TPoint; begin + // Ряд вкладок: OUT (куда отдаём звук) / IN (откуда берём модуляцию). + TabR := Rect(X, Y, X + (RowW - 4) div 2, Y + ROW_H); + DrawButton(C, TabR, 'OUT', not FAudInTab, + (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_AUD_TAB_OUT), 7); + RegisterHit(TabR, HIT_AUD_TAB_OUT); + TabR := Rect(TabR.Right + 4, Y, X + RowW, Y + ROW_H); + DrawButton(C, TabR, 'IN', FAudInTab, + (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_AUD_TAB_IN), 7); + RegisterHit(TabR, HIT_AUD_TAB_IN); + Inc(Y, ROW_H + IGAP); + C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Font.Style := []; - if FDevNames.Count = 0 then + Cnt := AudRowCount; + if Cnt = 0 then begin C.Font.Color := CLR_DIM; C.Brush.Style := bsClear; @@ -928,9 +994,9 @@ begin Exit; end; - VisN := Min(FDevNames.Count, AUD_MAX_VIS); - Scrollable := FDevNames.Count > AUD_MAX_VIS; - MaxScroll := FDevNames.Count - VisN; + VisN := Min(Cnt, AUD_MAX_VIS); + Scrollable := Cnt > AUD_MAX_VIS; + MaxScroll := Cnt - VisN; if FAudScroll > MaxScroll then FAudScroll := MaxScroll; if FAudScroll < 0 then FAudScroll := 0; if Scrollable then RowWidth := RowW - ARROW_W - 2 else RowWidth := RowW; @@ -939,8 +1005,8 @@ begin begin I := FAudScroll + Slot; HR := Rect(X, Y + Slot*AUD_ROW_H, X + RowWidth, Y + Slot*AUD_ROW_H + 16); - Nm := FDevNames[I]; - Cur := (Nm = FCurDev) or ((FCurDev = '') and (I = 0)); + Nm := AudRowName(I); + Cur := AudRowCurrent(I); if Cur then C.Brush.Color := CLR_ACCENT_D else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_DEV_BASE + I) then C.Brush.Color := CLR_BTN_HOT @@ -954,7 +1020,7 @@ begin if C.TextWidth(Nm) > RowWidth - 8 then while (Length(Nm) > 1) and (C.TextWidth(Nm + '…') > RowWidth - 8) do SetLength(Nm, Length(Nm) - 1); - if Nm <> FDevNames[I] then Nm := Nm + '…'; + if Nm <> AudRowName(I) then Nm := Nm + '…'; C.TextOut(HR.Left + 4, HR.Top + (16 - C.TextHeight('A')) div 2, Nm); RegisterHit(HR, HIT_DEV_BASE + I); end; @@ -1235,16 +1301,42 @@ begin HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end; HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll); RequestInvalidate; Exit; end; - HIT_AUD_DN: begin if FAudScroll < FDevNames.Count - AUD_MAX_VIS then + HIT_AUD_DN: begin if FAudScroll < AudRowCount - AUD_MAX_VIS then Inc(FAudScroll); RequestInvalidate; Exit; end; + HIT_AUD_TAB_OUT, + HIT_AUD_TAB_IN: + begin + if FAudInTab <> (HV = HIT_AUD_TAB_IN) then + begin + FAudInTab := HV = HIT_AUD_TAB_IN; + FAudScroll := 0; // список другой длины + Height := ComputeHeight; + RequestInvalidate; + end; + Exit; + end; end; - // Устройство аудио + // Устройство аудио (строка активной вкладки OUT/IN) if HV >= HIT_DEV_BASE then begin - NewMode := HV - HIT_DEV_BASE; // индекс в списке - if (NewMode >= 0) and (NewMode < FDevNames.Count) then + NewMode := HV - HIT_DEV_BASE; // индекс строки + if (NewMode < 0) or (NewMode >= AudRowCount) then Exit; + if FAudInTab then + begin + // Строка 0 = «общий вход» (слайс) / «нет устройства» (главный флаг). + if NewMode = 0 then FCurInDev := '' + else FCurInDev := FInDevNames[NewMode - 1]; + FPanel := fpNone; Height := ComputeHeight; + RequestInvalidate; + if Assigned(FOnAudioInDevice) then + begin + if NewMode = 0 then FOnAudioInDevice(-1, '') + else FOnAudioInDevice(FInDevIdx[NewMode - 1], FInDevNames[NewMode - 1]); + end; + end + else begin FCurDev := FDevNames[NewMode]; FPanel := fpNone; Height := ComputeHeight; @@ -1543,6 +1635,20 @@ begin RequestInvalidate; end; +procedure TVfoOverlay.SetAudioInDevices(Names: TStrings; + const Indices: array of Integer; const CurName: string); +var + I: Integer; +begin + FInDevNames.Assign(Names); + SetLength(FInDevIdx, FInDevNames.Count); + for I := 0 to FInDevNames.Count - 1 do + if I <= High(Indices) then FInDevIdx[I] := Indices[I] else FInDevIdx[I] := -1; + FCurInDev := CurName; + if FPanel = fpAud then Height := ComputeHeight; + RequestInvalidate; +end; + procedure TVfoOverlay.UpdateSMeter(DB: Double); begin if Abs(DB - FSMeterDB) > FSMeterThreshold then diff --git a/ewsdr.lpi b/ewsdr.lpi index 2c969dc..16adc0f 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -128,6 +128,7 @@ + diff --git a/other/padump.lpr b/other/padump.lpr new file mode 100644 index 0000000..81e593e --- /dev/null +++ b/other/padump.lpr @@ -0,0 +1,25 @@ +program padump; +{$MODE Delphi} +uses SysUtils, Classes, AudioInput, AudioOutput; +var + AI: TAudioInput; + AO: TAudioOutput; + Names: TStringList; + Idx: array[0..63] of Integer; + Cnt, i: Integer; +begin + AI := TAudioInput.Create(48000); + AO := TAudioOutput.Create(48000); + Names := TStringList.Create; + Cnt := 0; + AI.EnumInputDevices(Names, Idx, Cnt); + WriteLn('--- INPUT devices (', Cnt, ') ---'); + for i := 0 to Cnt - 1 do WriteLn(Format('%3d %s', [Idx[i], Names[i]])); + WriteLn('FindDeviceByName(Direwolf_TX_Output) = ', + AI.FindDeviceByName('Direwolf_TX_Output')); + Names.Clear; Cnt := 0; + AO.EnumOutputDevices(Names, Idx, Cnt); + WriteLn('--- OUTPUT devices (', Cnt, ') ---'); + for i := 0 to Cnt - 1 do WriteLn(Format('%3d %s', [Idx[i], Names[i]])); + Names.Free; AI.Free; AO.Free; +end. diff --git a/other/patest2.lpr b/other/patest2.lpr new file mode 100644 index 0000000..0f9fe6e --- /dev/null +++ b/other/patest2.lpr @@ -0,0 +1,19 @@ +program patest2; +{$MODE Delphi} +uses SysUtils, AudioInput; +var A, B: TAudioInput; +begin + A := TAudioInput.Create(48000); + A.DeviceIndex := 15; // DireWolf_TX_Output + WriteLn('open #1 ...'); Flush(Output); + WriteLn(' -> ', A.Open, ' err=', A.LastError); Flush(Output); + B := TAudioInput.Create(48000); + B.DeviceIndex := 15; // то же устройство вторым потоком + WriteLn('open #2 ...'); Flush(Output); + WriteLn(' -> ', B.Open, ' err=', B.LastError); Flush(Output); + Sleep(300); + WriteLn('avail #1=', A.Available, ' #2=', B.Available); Flush(Output); + B.Close; A.Close; + B.Free; A.Free; + WriteLn('done'); +end. diff --git a/other/patest3.lpr b/other/patest3.lpr new file mode 100644 index 0000000..a33ed78 --- /dev/null +++ b/other/patest3.lpr @@ -0,0 +1,29 @@ +program patest3; +{$MODE Delphi} +uses SysUtils, Classes, AudioInput; +var + E: TAudioInput; + A: TAudioInput; + Names: TStringList; + Idx: array[0..63] of Integer; + Cnt, i: Integer; + OK: Boolean; +begin + E := TAudioInput.Create(48000); + Names := TStringList.Create; + Cnt := 0; + E.EnumInputDevices(Names, Idx, Cnt); + for i := 0 to Cnt - 1 do + begin + WriteLn(Format('>>> try %3d %s', [Idx[i], Names[i]])); Flush(Output); + A := TAudioInput.Create(48000); + A.DeviceIndex := Idx[i]; + OK := A.Open; + WriteLn(Format(' %3d open=%s %s', [Idx[i], BoolToStr(OK, True), A.LastError])); + Flush(Output); + A.Close; + A.Free; + end; + Names.Free; E.Free; + WriteLn('ALL DONE'); +end.