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:
@@ -1,3 +1,4 @@
|
||||
using VoiceCat.App.Audio;
|
||||
using VoiceCat.Interop;
|
||||
|
||||
namespace VoiceCat.App.Forms;
|
||||
@@ -22,6 +23,7 @@ public partial class MainForm : Form
|
||||
// Voice state
|
||||
private uint _micStreamId; // 0 = not started
|
||||
private uint _screenStreamId; // 0 = not sharing screen audio
|
||||
private ProcessAudioMixer? _screenMixer; // non-null only in per-app capture mode
|
||||
private Keys _pttKey = Keys.F8;
|
||||
private bool _serverMuted;
|
||||
private bool _serverDeafened;
|
||||
@@ -442,6 +444,7 @@ public partial class MainForm : Form
|
||||
_currentChannelId = 0;
|
||||
_micStreamId = 0;
|
||||
_screenStreamId = 0;
|
||||
_screenMixer?.Stop(); _screenMixer?.Dispose(); _screenMixer = null;
|
||||
txtCompose.Enabled = false;
|
||||
btnSend.Enabled = false;
|
||||
tsbJoinVoice.Enabled = false;
|
||||
@@ -622,29 +625,73 @@ public partial class MainForm : Form
|
||||
{
|
||||
if (_screenStreamId == 0)
|
||||
{
|
||||
var (result, streamId) = _client.StartStream(VcStreamKind.ScreenAudio, "Desktop audio");
|
||||
if (result == VcResult.Ok)
|
||||
{
|
||||
_screenStreamId = streamId;
|
||||
tsbScreenShare.Text = "Stop Screen Audio";
|
||||
_miScreenShare.Text = "Stop Screen &Audio";
|
||||
AddActivity("Started sharing screen audio");
|
||||
}
|
||||
else
|
||||
{
|
||||
AddActivity($"Failed to start screen audio: {result}");
|
||||
}
|
||||
StartScreenAudio();
|
||||
}
|
||||
else
|
||||
{
|
||||
_client.StopStream(_screenStreamId);
|
||||
_screenStreamId = 0;
|
||||
tsbScreenShare.Text = "Share Screen Audio";
|
||||
_miScreenShare.Text = "Share Screen &Audio";
|
||||
AddActivity("Stopped sharing screen audio");
|
||||
StopScreenAudio();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartScreenAudio()
|
||||
{
|
||||
using var picker = new AppAudioPickerDialog();
|
||||
if (picker.ShowDialog(this) != DialogResult.OK || picker.ChosenScope == null) return;
|
||||
|
||||
var scope = picker.ChosenScope;
|
||||
|
||||
if (scope is EntireDesktop)
|
||||
{
|
||||
// Existing whole-device WASAPI loopback path — core handles it.
|
||||
var (result, streamId) = _client.StartStream(VcStreamKind.ScreenAudio, "Desktop audio");
|
||||
if (result != VcResult.Ok)
|
||||
{
|
||||
AddActivity($"Failed to start screen audio: {result}");
|
||||
return;
|
||||
}
|
||||
_screenStreamId = streamId;
|
||||
AddActivity("Sharing screen audio: entire desktop");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Per-app path: suppress core loopback, C# mixer feeds PCM.
|
||||
var (result, streamId) = _client.StartStreamExternalFeed(VcStreamKind.ScreenAudio, "App audio");
|
||||
if (result != VcResult.Ok)
|
||||
{
|
||||
AddActivity($"Failed to start screen audio: {result}");
|
||||
return;
|
||||
}
|
||||
_screenStreamId = streamId;
|
||||
|
||||
_screenMixer = new ProcessAudioMixer();
|
||||
_screenMixer.Start(scope, _client, streamId, picker.VisibleApps);
|
||||
|
||||
string desc = scope switch
|
||||
{
|
||||
OnlyApps o => $"only {o.Pids.Count} app(s)",
|
||||
AllExceptApps a => $"all except {a.Pids.Count} app(s)",
|
||||
_ => "apps",
|
||||
};
|
||||
AddActivity($"Sharing screen audio: {desc}");
|
||||
}
|
||||
|
||||
tsbScreenShare.Text = "Stop Screen Audio";
|
||||
_miScreenShare.Text = "Stop Screen &Audio";
|
||||
}
|
||||
|
||||
private void StopScreenAudio()
|
||||
{
|
||||
_screenMixer?.Stop();
|
||||
_screenMixer?.Dispose();
|
||||
_screenMixer = null;
|
||||
|
||||
_client.StopStream(_screenStreamId);
|
||||
_screenStreamId = 0;
|
||||
tsbScreenShare.Text = "Share Screen Audio";
|
||||
_miScreenShare.Text = "Share Screen &Audio";
|
||||
AddActivity("Stopped sharing screen audio");
|
||||
}
|
||||
|
||||
private void TrkOutputVolume_Scroll(object? sender, EventArgs e) =>
|
||||
_client.SetOutputVolume(trkOutputVolume.Value / 100f);
|
||||
|
||||
@@ -1019,7 +1066,7 @@ public partial class MainForm : Form
|
||||
_client.EventReceived -= OnEvent;
|
||||
foreach (var win in _pmWindows.Values.ToList()) win.Close();
|
||||
_pmWindows.Clear();
|
||||
if (_screenStreamId != 0) _client.StopStream(_screenStreamId);
|
||||
if (_screenStreamId != 0) { _screenMixer?.Stop(); _screenMixer?.Dispose(); _screenMixer = null; _client.StopStream(_screenStreamId); }
|
||||
if (_micStreamId != 0) _client.StopStream(_micStreamId);
|
||||
_client.Disconnect();
|
||||
_client.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user