fix(windows): real native exclude + self-echo removal for screen audio
"All apps except selected" previously captured the complement of a frozen app snapshot in INCLUDE mode (missed late-launched apps and system sounds, wasted captures on silent windows). It now opens a single ProcessLoopbackCapture in EXCLUDE mode (AUDIOCLIENT_PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE) of the one chosen app — true system-mix-minus-one, dynamic so apps launched after sharing starts are included. The picker enforces single-selection in exclude mode (the activation params take one target PID). Adds an "Exclude VoiceCat's own audio (prevents echo)" checkbox (default on, entire-desktop only) that routes the desktop capture through the same EXCLUDE path targeting our own process id, killing the whole-device self-echo loop. No C++/ABI changes. Updates voice.md and PROGRESS.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,10 @@ namespace VoiceCat.App.Audio;
|
||||
// ── Scope types (mirror macOS ScreenAudioScope / ScreenAudioSelection) ────────
|
||||
|
||||
public abstract record AppAudioScope;
|
||||
public sealed record EntireDesktop : AppAudioScope;
|
||||
// ExcludeSelf=true captures the whole system render mix minus VoiceCat's own process tree
|
||||
// (kills the self-echo loop). Routed through the external-feed mixer as a single EXCLUDE
|
||||
// capture; ExcludeSelf=false keeps the core's whole-device loopback path.
|
||||
public sealed record EntireDesktop(bool ExcludeSelf = false) : AppAudioScope;
|
||||
public sealed record OnlyApps(IReadOnlyList<int> Pids, IReadOnlyList<string> Names) : AppAudioScope;
|
||||
public sealed record AllExceptApps(IReadOnlyList<int> Pids, IReadOnlyList<string> Names) : AppAudioScope;
|
||||
|
||||
|
||||
@@ -22,15 +22,14 @@ public sealed class ProcessAudioMixer : IDisposable
|
||||
private VoiceCatClient? _client;
|
||||
private uint _streamId;
|
||||
|
||||
public void Start(AppAudioScope scope, VoiceCatClient client, uint streamId,
|
||||
IReadOnlyList<AudioAppInfo> allApps)
|
||||
public void Start(AppAudioScope scope, VoiceCatClient client, uint streamId)
|
||||
{
|
||||
if (_running) return;
|
||||
_client = client;
|
||||
_streamId = streamId;
|
||||
|
||||
var pids = ResolvePids(scope, allApps);
|
||||
if (pids.Count == 0)
|
||||
var specs = ResolveCaptures(scope);
|
||||
if (specs.Count == 0)
|
||||
{
|
||||
// nothing to capture — scope resolved to empty set
|
||||
return;
|
||||
@@ -38,14 +37,15 @@ public sealed class ProcessAudioMixer : IDisposable
|
||||
|
||||
lock (_frameLock)
|
||||
{
|
||||
_latestFrames = new List<short[]>(new short[pids.Count][]);
|
||||
_latestFrames = new List<short[]>(new short[specs.Count][]);
|
||||
_activeChannels = Channels;
|
||||
}
|
||||
|
||||
for (int i = 0; i < pids.Count; i++)
|
||||
for (int i = 0; i < specs.Count; i++)
|
||||
{
|
||||
int captureIndex = i;
|
||||
var cap = new ProcessLoopbackCapture(pids[i], ProcessLoopbackCapture.Mode.Include);
|
||||
var (pid, mode) = specs[i];
|
||||
var cap = new ProcessLoopbackCapture(pid, mode);
|
||||
cap.PcmFrameReady += (pcm, spc, ch) => OnCaptureFrame(captureIndex, pcm, ch);
|
||||
_captures.Add(cap);
|
||||
}
|
||||
@@ -117,18 +117,21 @@ public sealed class ProcessAudioMixer : IDisposable
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
// For "AllExcept": enumerate all running audio apps and exclude the specified ones.
|
||||
// For "OnlyApps": use their PIDs directly.
|
||||
private static List<int> ResolvePids(AppAudioScope scope, IReadOnlyList<AudioAppInfo> allApps)
|
||||
// Resolve the scope to the WASAPI captures to open:
|
||||
// OnlyApps → one INCLUDE capture per selected process tree.
|
||||
// AllExceptApps → one EXCLUDE capture of the single selected process tree, which
|
||||
// natively captures the whole system render mix minus that tree
|
||||
// (dynamic — apps launched later are included automatically).
|
||||
private static List<(int pid, ProcessLoopbackCapture.Mode mode)> ResolveCaptures(AppAudioScope scope)
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
OnlyApps o => [.. o.Pids],
|
||||
AllExceptApps a =>
|
||||
allApps
|
||||
.Where(app => !a.Pids.Contains(app.Pid))
|
||||
.Select(app => app.Pid)
|
||||
.ToList(),
|
||||
OnlyApps o => o.Pids.Select(p => (p, ProcessLoopbackCapture.Mode.Include)).ToList(),
|
||||
AllExceptApps a when a.Pids.Count > 0 =>
|
||||
[(a.Pids[0], ProcessLoopbackCapture.Mode.Exclude)],
|
||||
// Entire desktop minus VoiceCat itself: EXCLUDE our own process tree.
|
||||
EntireDesktop { ExcludeSelf: true } =>
|
||||
[(Environment.ProcessId, ProcessLoopbackCapture.Mode.Exclude)],
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user