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:
@@ -207,6 +207,12 @@ typedef struct vc_stream_desc {
|
||||
vc_stream_kind kind;
|
||||
const char* device_id; /* NULL = default device for this kind */
|
||||
const char* label; /* human label, e.g. "Microphone" */
|
||||
/* If 1, the caller will feed PCM via vc_stream_feed_pcm; the core will NOT start its own
|
||||
* WASAPI loopback capture. Only meaningful for VC_STREAM_SCREEN_AUDIO on Windows. Callers
|
||||
* that brace-initialize this struct (tests, macOS) get 0 = auto-start loopback — no ABI
|
||||
* break. See clients/windows/VoiceCat.App/Audio/ProcessAudioMixer.cs for the Windows
|
||||
* per-app capture implementation that sets this. */
|
||||
int external_feed;
|
||||
} vc_stream_desc;
|
||||
|
||||
/* The effective Opus configuration in use for a stream — for a stream you own, this is
|
||||
|
||||
@@ -1160,8 +1160,9 @@ vc_result vc_client::stream_start(const vc_stream_desc& desc, uint32_t* out_stre
|
||||
if (ls.active.load(std::memory_order_acquire) || ls.pending) return VC_ERR_ALREADY;
|
||||
|
||||
uint32_t sid = next_local_stream_id_++;
|
||||
ls.stream_id = sid;
|
||||
ls.pending = true;
|
||||
ls.stream_id = sid;
|
||||
ls.pending = true;
|
||||
ls.external_feed = (desc.external_feed != 0);
|
||||
if (out_stream_id) *out_stream_id = sid;
|
||||
|
||||
req_id = next_req_id_++;
|
||||
@@ -1196,6 +1197,7 @@ void vc_client::handle_stream_announce_result(uint64_t req_id,
|
||||
bool ok_to_emit = false;
|
||||
int kind;
|
||||
int loopback_channels = 1; // only meaningful for SCREEN_AUDIO; set under the lock
|
||||
bool loopback_external = false;
|
||||
{
|
||||
std::lock_guard lk(local_streams_mu_);
|
||||
auto pit = pending_announce_kind_.find(req_id);
|
||||
@@ -1239,12 +1241,13 @@ void vc_client::handle_stream_announce_result(uint64_t req_id,
|
||||
// when the channel is stereo (real L/R, no downmix), mono otherwise. Captured under
|
||||
// the lock alongside the rest of the LocalStream setup; used below after unlock.
|
||||
if (kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO)) {
|
||||
loopback_channels = ls.effective_params.stereo ? 2 : 1;
|
||||
loopback_channels = ls.effective_params.stereo ? 2 : 1;
|
||||
loopback_external = ls.external_feed;
|
||||
}
|
||||
}
|
||||
|
||||
ensure_audio_running();
|
||||
if (kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO)) {
|
||||
if (kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO) && !loopback_external) {
|
||||
audio_engine_.start_loopback_capture(kind, loopback_channels);
|
||||
}
|
||||
|
||||
@@ -1260,7 +1263,8 @@ void vc_client::handle_stream_announce_result(uint64_t req_id,
|
||||
vc_result vc_client::stream_stop(uint32_t stream_id) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
|
||||
int stopped_kind = -1;
|
||||
int stopped_kind = -1;
|
||||
bool stopped_external = false;
|
||||
{
|
||||
std::lock_guard lk(local_streams_mu_);
|
||||
for (auto& [k, ls] : local_streams_) {
|
||||
@@ -1268,11 +1272,12 @@ vc_result vc_client::stream_stop(uint32_t stream_id) {
|
||||
}
|
||||
LocalStream* ls = find_local_stream_by_id(stream_id);
|
||||
if (!ls || !ls->active.load(std::memory_order_acquire)) return VC_ERR_INVALID_ARG;
|
||||
stopped_external = ls->external_feed;
|
||||
ls->active.store(false, std::memory_order_release);
|
||||
ls->encoder.destroy();
|
||||
}
|
||||
|
||||
if (stopped_kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO)) {
|
||||
if (stopped_kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO) && !stopped_external) {
|
||||
audio_engine_.stop_loopback_capture();
|
||||
}
|
||||
|
||||
|
||||
@@ -246,6 +246,11 @@ struct vc_client {
|
||||
// VC_STREAM_MIC. Set via vc_set_capture_channels; read by ensure_audio_running() to
|
||||
// configure AudioParams.capture_channels before the device opens. Defaults to 1 (mono).
|
||||
uint32_t capture_channels = 1;
|
||||
|
||||
// If true, the caller is feeding PCM via vc_stream_feed_pcm — skip start/stop of the
|
||||
// core's WASAPI loopback device. Set from vc_stream_desc::external_feed at stream_start
|
||||
// time and checked in handle_stream_announce_result / stream_stop.
|
||||
bool external_feed = false;
|
||||
};
|
||||
mutable std::mutex local_streams_mu_;
|
||||
std::unordered_map<int, LocalStream> local_streams_; // keyed by vc_stream_kind
|
||||
|
||||
Reference in New Issue
Block a user