wip(audio): выбор устройства ВВОДА на каждом слайсе + вкладки OUT/IN в AUD

Паритет с уже существующим выбором вывода: на каждом слайсе своё устройство
входа (микрофон/виртуальный кабель).

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:52:24 +03:00
co-authored by Claude Opus 4.8
parent 317e9b732c
commit 4aa180e7b2
9 changed files with 493 additions and 49 deletions
+120 -14
View File
@@ -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