feat(windows): per-app screen-audio sharing (include/exclude)
Adds "only selected apps" and "all apps except selected" screen-audio modes to the Windows client, alongside the existing entire-desktop path. Per-app capture uses WASAPI process loopback (AUDCLNT_ACTIVATIONTYPE_ PROCESS_LOOPBACK) via ProcessLoopbackCapture, mixed by ProcessAudioMixer and fed to the core through vc_stream_feed_pcm (external_feed=1 so the core skips its own loopback device). Init must pass AUDCLNT_STREAMFLAGS_LOOPBACK | EVENTCALLBACK | AUTOCONVERTPCM; the LOOPBACK flag is what makes the virtual endpoint deliver rendered audio (without it every buffer is flagged SILENT) and AUTOCONVERTPCM resamples the app's native format to 48k s16. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
166
clients/windows/VoiceCat.App/Forms/AppAudioPickerDialog.cs
Normal file
166
clients/windows/VoiceCat.App/Forms/AppAudioPickerDialog.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using VoiceCat.App.Audio;
|
||||
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Modal dialog for selecting which apps' audio to share.
|
||||
/// Returns the chosen <see cref="AppAudioScope"/> via <see cref="ChosenScope"/>,
|
||||
/// or <see cref="DialogResult.Cancel"/> if the user dismissed without sharing.
|
||||
/// </summary>
|
||||
public sealed class AppAudioPickerDialog : Form
|
||||
{
|
||||
private readonly RadioButton _rdoAll;
|
||||
private readonly RadioButton _rdoOnly;
|
||||
private readonly RadioButton _rdoExcept;
|
||||
private readonly ListView _appList;
|
||||
private readonly Label _lblApps;
|
||||
|
||||
// Snapshot taken when the dialog opens (refresh on open, not on every check change).
|
||||
private IReadOnlyList<AudioAppInfo> _apps = [];
|
||||
|
||||
public AppAudioScope? ChosenScope { get; private set; }
|
||||
|
||||
public AppAudioPickerDialog()
|
||||
{
|
||||
// ── Radio buttons ───────────────────────────────────────────────────
|
||||
_rdoAll = new RadioButton
|
||||
{
|
||||
Text = "&Entire desktop",
|
||||
Checked = true,
|
||||
Location = new Point(12, 12),
|
||||
Size = new Size(360, 20),
|
||||
TabIndex = 0,
|
||||
};
|
||||
_rdoOnly = new RadioButton
|
||||
{
|
||||
Text = "&Only selected apps",
|
||||
Location = new Point(12, 36),
|
||||
Size = new Size(360, 20),
|
||||
TabIndex = 1,
|
||||
};
|
||||
_rdoExcept = new RadioButton
|
||||
{
|
||||
Text = "All apps e&xcept selected",
|
||||
Location = new Point(12, 60),
|
||||
Size = new Size(360, 20),
|
||||
TabIndex = 2,
|
||||
};
|
||||
|
||||
_rdoAll.CheckedChanged += OnModeChanged;
|
||||
_rdoOnly.CheckedChanged += OnModeChanged;
|
||||
_rdoExcept.CheckedChanged += OnModeChanged;
|
||||
|
||||
// ── App list ────────────────────────────────────────────────────────
|
||||
_lblApps = new Label
|
||||
{
|
||||
Text = "Apps with active audio sessions:",
|
||||
AutoSize = true,
|
||||
Location = new Point(12, 90),
|
||||
Visible = false,
|
||||
TabIndex = 3,
|
||||
};
|
||||
|
||||
_appList = new ListView
|
||||
{
|
||||
Location = new Point(12, 112),
|
||||
Size = new Size(360, 180),
|
||||
CheckBoxes = true,
|
||||
View = View.List,
|
||||
Visible = false,
|
||||
TabIndex = 4,
|
||||
FullRowSelect = true,
|
||||
};
|
||||
|
||||
// ── Buttons ─────────────────────────────────────────────────────────
|
||||
var btnShare = new Button
|
||||
{
|
||||
Text = "&Share",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(216, 308),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 5,
|
||||
};
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(297, 308),
|
||||
Size = new Size(75, 27),
|
||||
TabIndex = 6,
|
||||
};
|
||||
btnShare.Click += OnShareClick;
|
||||
|
||||
AcceptButton = btnShare;
|
||||
CancelButton = btnCancel;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(384, 348);
|
||||
Controls.AddRange([_rdoAll, _rdoOnly, _rdoExcept, _lblApps, _appList, btnShare, btnCancel]);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Share App Audio";
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
RefreshAppList();
|
||||
}
|
||||
|
||||
private void RefreshAppList()
|
||||
{
|
||||
_apps = AudioSessionEnumerator.GetAudioApps();
|
||||
_appList.Items.Clear();
|
||||
foreach (var app in _apps)
|
||||
_appList.Items.Add(new ListViewItem($"{app.DisplayName} (PID {app.Pid})") { Tag = app.Pid });
|
||||
}
|
||||
|
||||
private void OnModeChanged(object? sender, EventArgs e)
|
||||
{
|
||||
bool showList = _rdoOnly.Checked || _rdoExcept.Checked;
|
||||
_lblApps.Visible = showList;
|
||||
_appList.Visible = showList;
|
||||
|
||||
if (showList && _appList.Items.Count == 0)
|
||||
RefreshAppList();
|
||||
}
|
||||
|
||||
private void OnShareClick(object? sender, EventArgs e)
|
||||
{
|
||||
if (_rdoAll.Checked)
|
||||
{
|
||||
ChosenScope = new EntireDesktop();
|
||||
return;
|
||||
}
|
||||
|
||||
var checkedPids = new List<int>();
|
||||
var checkedNames = new List<string>();
|
||||
foreach (ListViewItem item in _appList.CheckedItems)
|
||||
{
|
||||
if (item.Tag is int pid)
|
||||
{
|
||||
checkedPids.Add(pid);
|
||||
checkedNames.Add(item.Text);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkedPids.Count == 0)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Select at least one app, or choose 'Entire desktop'.",
|
||||
"No apps selected",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
DialogResult = DialogResult.None; // prevent close
|
||||
return;
|
||||
}
|
||||
|
||||
ChosenScope = _rdoOnly.Checked
|
||||
? new OnlyApps(checkedPids, checkedNames)
|
||||
: new AllExceptApps(checkedPids, checkedNames);
|
||||
}
|
||||
|
||||
/// <summary>All apps that were visible in the list when the user clicked Share.</summary>
|
||||
public IReadOnlyList<AudioAppInfo> VisibleApps => _apps;
|
||||
}
|
||||
Reference in New Issue
Block a user