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.