From 9550ab4a7db6e7ddbb73dce04a4f121536006650 Mon Sep 17 00:00:00 2001 From: Tony Prime Date: Sun, 12 Jul 2026 20:10:08 +0200 Subject: [PATCH 1/2] fix(windows): capture the configured audio sink instead of the default device On Windows, the audio sink setting only switched the Windows default render device via IPolicyConfig::SetDefaultEndpoint, while WASAPI capture was always initialized against the default endpoint. Capture therefore silently recorded the wrong device whenever the default was not (or no longer) the configured sink: - the assigned sink is only applied by the first session of an audio context, so later sessions captured whatever the default happened to be - when the default device changed mid-session, capture followed the new default; the switch-back callback is only registered for virtual sinks Resolve the assigned (or configured) sink to its endpoint and open the loopback capture on that device directly, falling back to the default render device when no sink is set. When capture is pinned to an explicit sink, default-device change notifications no longer trigger a capture reinit, and a sink that cannot be resolved now fails capture initialization (retried by the session) instead of silently recording another device. Fixes #4865 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/platform/windows/audio.cpp | 70 +++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index e12dc9033df..d7b24ed4fec 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -601,9 +601,10 @@ namespace platf::audio { * @param frame_size Number of samples captured per audio frame. * @param channels_out Channels out. * @param continuous Whether silent audio should continue to be emitted. + * @param capture_device Endpoint device to capture from; the default render device is used when empty. * @return 0 on success; nonzero or negative platform status on failure. */ - int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out, bool continuous) { + int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out, bool continuous, device_t capture_device) { audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr)); if (!audio_event) { BOOST_LOG(error) << "Couldn't create Event handle"sv; @@ -634,7 +635,13 @@ namespace platf::audio { return -1; } - auto device = default_device(device_enum); + follows_default_device = !capture_device; + if (follows_default_device) { + device = default_device(device_enum); + } else { + device = std::move(capture_device); + } + if (!device) { return -1; } @@ -746,8 +753,11 @@ namespace platf::audio { (*default_endpt_changed_cb)(); } - // Reinitialize to pick up the new default device - return capture_e::reinit; + // Reinitialize to pick up the new default device, unless capture is + // pinned to an explicitly requested sink + if (follows_default_device) { + return capture_e::reinit; + } } status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE); @@ -836,6 +846,7 @@ namespace platf::audio { float *sample_buf_pos; ///< Current write position in `sample_buf`. int channels; ///< Number of channels in the capture format. bool continuous_audio; ///< Whether audio packets continue during silence. + bool follows_default_device; ///< Whether capture follows the default render device rather than an explicit sink. HANDLE mmcss_task_handle = nullptr; ///< MMCSS task handle for the audio capture thread. }; @@ -927,6 +938,35 @@ namespace platf::audio { return std::nullopt; } + /** + * @brief Resolve a sink name to the audio endpoint device it refers to. + * + * @param sink Sink name, virtual sink descriptor, or device identifier. + * @return Endpoint device to capture from, or an empty pointer if the sink couldn't be resolved. + */ + device_t get_sink_device(const std::string &sink) { + std::wstring device_id; + if (auto virtual_sink_info = extract_virtual_sink_info(sink)) { + device_id = virtual_sink_info->first; + } else if (auto matched = find_device_id(match_all_fields(utf_utils::from_utf8(sink)))) { + device_id = matched->second; + } else { + return nullptr; + } + + device_t device; + if (FAILED(device_enum->GetDevice(device_id.c_str(), &device))) { + return nullptr; + } + + DWORD device_state {}; + if (FAILED(device->GetState(&device_state)) || device_state != DEVICE_STATE_ACTIVE) { + return nullptr; + } + + return device; + } + /** * @brief Create a microphone capture stream for the requested layout. * @@ -941,7 +981,25 @@ namespace platf::audio { std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, [[maybe_unused]] bool host_audio_enabled) override { auto mic = std::make_unique(); - if (mic->init(sample_rate, frame_size, channels, continuous_audio)) { + // Prefer the sink that was assigned to this capture session since it accounts + // for the priority between virtual and configured sinks. + const auto &requested_sink = assigned_sink.empty() ? config::audio.sink : assigned_sink; + + // Capture the requested sink directly instead of relying on it being the default + // render device, so that capture keeps working when the default device differs + // from the sink or changes during the session. + device_t capture_device; + if (!requested_sink.empty()) { + capture_device = get_sink_device(requested_sink); + if (!capture_device) { + BOOST_LOG(error) << "Couldn't resolve audio sink ["sv << requested_sink << "] to a capture device"sv; + return nullptr; + } + + BOOST_LOG(info) << "Capturing audio from sink ["sv << requested_sink << ']'; + } + + if (mic->init(sample_rate, frame_size, channels, continuous_audio, std::move(capture_device))) { return nullptr; } @@ -1359,7 +1417,7 @@ namespace platf::audio { policy_t policy; ///< Windows policy configuration interface used to switch default audio devices. audio::device_enum_t device_enum; ///< Device enumerator used to query and watch audio endpoints. - std::string assigned_sink; ///< Virtual sink assigned while Sunshine captures host audio. + std::string assigned_sink; ///< Sink assigned while Sunshine captures host audio, captured directly by the microphone. }; } // namespace platf::audio From f15a863192d1cb7cda16aca66fd45a52522a32a0 Mon Sep 17 00:00:00 2001 From: Tony Prime Date: Mon, 13 Jul 2026 11:51:29 +0200 Subject: [PATCH 2/2] style(windows): declare device_state in if init-statement Addresses SonarQube finding cpp:S6004. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/platform/windows/audio.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index d7b24ed4fec..7368a1d2a65 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -959,8 +959,7 @@ namespace platf::audio { return nullptr; } - DWORD device_state {}; - if (FAILED(device->GetState(&device_state)) || device_state != DEVICE_STATE_ACTIVE) { + if (DWORD device_state {}; FAILED(device->GetState(&device_state)) || device_state != DEVICE_STATE_ACTIVE) { return nullptr; }