From 14d27c2d58120dd2b5b93e78ea6f18ffaa194b14 Mon Sep 17 00:00:00 2001 From: Tony Prime Date: Sun, 12 Jul 2026 20:10:08 +0200 Subject: [PATCH 1/3] 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 38bce01ca86..03a7cb35a32 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -599,9 +599,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; @@ -632,7 +633,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; } @@ -744,8 +751,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); @@ -833,6 +843,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. }; @@ -924,6 +935,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. * @@ -938,7 +978,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; } @@ -1356,7 +1414,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 655c7c7eb75fa306226f54e9b21bad7a617792a0 Mon Sep 17 00:00:00 2001 From: Tony Prime Date: Mon, 13 Jul 2026 11:51:29 +0200 Subject: [PATCH 2/3] 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 03a7cb35a32..6a75aa8d377 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -956,8 +956,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; } From 70c25b57c7dc5bd7aff7edd660e054b108d8671f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:55:14 -0400 Subject: [PATCH 3/3] test: windows audio sink precedence This change adds a shared sink-selection helper so the assigned capture sink takes precedence over the configured sink when present. It also centralizes the Windows endpoint lookup and covers the default-device-change handling with focused unit tests for sink resolution and reinit behavior. --- src/platform/windows/audio.cpp | 116 ++++++- tests/unit/platform/windows/test_audio.cpp | 365 +++++++++++++++++++++ 2 files changed, 475 insertions(+), 6 deletions(-) create mode 100644 tests/unit/platform/windows/test_audio.cpp diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 6a75aa8d377..9ace2a6b338 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -6,6 +6,7 @@ // standard includes #include +#include // platform includes #include @@ -633,12 +634,7 @@ namespace platf::audio { return -1; } - follows_default_device = !capture_device; - if (follows_default_device) { - device = default_device(device_enum); - } else { - device = std::move(capture_device); - } + select_capture_device(std::move(capture_device)); if (!device) { return -1; @@ -715,6 +711,20 @@ namespace platf::audio { return 0; } + /** + * @brief Select the endpoint used by this capture stream. + * + * @param capture_device Explicit endpoint to capture, or an empty pointer to follow the default endpoint. + */ + void select_capture_device(device_t capture_device) { + follows_default_device = !capture_device; + if (follows_default_device) { + device = default_device(device_enum); + } else { + device = std::move(capture_device); + } + } + ~mic_wasapi_t() override { if (device_enum) { device_enum->UnregisterEndpointNotificationCallback(&endpt_notification); @@ -1415,6 +1425,100 @@ namespace platf::audio { audio::device_enum_t device_enum; ///< Device enumerator used to query and watch audio endpoints. std::string assigned_sink; ///< Sink assigned while Sunshine captures host audio, captured directly by the microphone. }; + +#ifdef SUNSHINE_TESTS + namespace tests { + /** + * @brief Resolve a sink through the production Windows endpoint lookup. + * + * @param sink Sink name, virtual sink descriptor, or device identifier. + * @param device_enum Device enumerator supplied by the test. + * @return `true` when the sink resolves to an active endpoint. + */ + bool sink_device_available(const std::string &sink, IMMDeviceEnumerator *device_enum) { + audio_control_t control; + device_enum->AddRef(); + control.device_enum.reset(device_enum); + return static_cast(control.get_sink_device(sink)); + } + + /** + * @brief Exercise microphone creation with controlled assigned and configured sinks. + * + * @param assigned_sink Sink selected by the shared audio context. + * @param configured_sink Sink configured by the user. + * @param device_enum Device enumerator supplied by the test. + * @return `true` when microphone initialization succeeds. + */ + bool microphone_available(const std::string &assigned_sink, const std::string &configured_sink, IMMDeviceEnumerator *device_enum) { + audio_control_t control; + device_enum->AddRef(); + control.device_enum.reset(device_enum); + control.assigned_sink = assigned_sink; + + auto previous_configured_sink = std::exchange(config::audio.sink, configured_sink); + auto microphone = control.microphone(nullptr, 2, 48000, 240, false, false); + config::audio.sink = std::move(previous_configured_sink); + return static_cast(microphone); + } + + /** + * @brief Select a default or explicit capture endpoint through the production selection path. + * + * @param device_enum Device enumerator supplied by the test. + * @param capture_device Explicit endpoint, or `nullptr` to select the default endpoint. + * @return `true` when capture follows the default endpoint. + */ + bool capture_follows_default_device(IMMDeviceEnumerator *device_enum, IMMDevice *capture_device) { + mic_wasapi_t microphone; + device_enum->AddRef(); + microphone.device_enum.reset(device_enum); + + device_t selected_device; + if (capture_device) { + capture_device->AddRef(); + selected_device.reset(capture_device); + } + + microphone.select_capture_device(std::move(selected_device)); + return microphone.follows_default_device; + } + + /** + * @brief Exercise the production default-device-change path without live audio hardware. + * + * @param follows_default_device Whether the capture follows the default render endpoint. + * @param install_callback Whether to install a default-device-change callback. + * @param render_device_changed Whether to signal a render rather than capture endpoint change. + * @param callback_count Receives the number of callback invocations. + * @return Capture result produced after processing the notification. + */ + capture_e simulate_default_device_change(bool follows_default_device, bool install_callback, bool render_device_changed, int &callback_count) { + mic_wasapi_t mic; + mic.audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr)); + mic.default_latency_ms = 0; + mic.sample_buf = util::buffer_t {1}; + mic.sample_buf_pos = std::begin(mic.sample_buf); + mic.continuous_audio = false; + mic.follows_default_device = follows_default_device; + + if (install_callback) { + mic.default_endpt_changed_cb = [&callback_count] { + ++callback_count; + }; + } + + mic.endpt_notification.OnDefaultDeviceChanged( + render_device_changed ? eRender : eCapture, + eConsole, + nullptr + ); + + std::vector sample(1); + return mic.sample(sample); + } + } // namespace tests +#endif } // namespace platf::audio namespace platf { diff --git a/tests/unit/platform/windows/test_audio.cpp b/tests/unit/platform/windows/test_audio.cpp new file mode 100644 index 00000000000..081b5b0d571 --- /dev/null +++ b/tests/unit/platform/windows/test_audio.cpp @@ -0,0 +1,365 @@ +/** + * @file tests/unit/platform/windows/test_audio.cpp + * @brief Tests for Windows audio sink selection and endpoint-change handling. + */ + +#include "../../../tests_common.h" + +#ifdef _WIN32 + #include "src/platform/common.h" + + #include + #include + #include + +namespace platf::audio::tests { + bool sink_device_available(const std::string &sink, IMMDeviceEnumerator *device_enum); + bool microphone_available(const std::string &assigned_sink, const std::string &configured_sink, IMMDeviceEnumerator *device_enum); + bool capture_follows_default_device(IMMDeviceEnumerator *device_enum, IMMDevice *capture_device); + capture_e simulate_default_device_change(bool follows_default_device, bool install_callback, bool render_device_changed, int &callback_count); +} // namespace platf::audio::tests + +namespace { + class fake_property_store_t final: public IPropertyStore { + public: + explicit fake_property_store_t(std::wstring friendly_name): + friendly_name {std::move(friendly_name)} { + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface + *object = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { + return 1; + } + + ULONG STDMETHODCALLTYPE Release() override { + return 1; + } + + HRESULT STDMETHODCALLTYPE GetCount(DWORD *property_count) override { + *property_count = friendly_name.empty() ? 0 : 1; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetAt(DWORD, PROPERTYKEY *) override { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY, PROPVARIANT *value) override { + if (friendly_name.empty()) { + return E_NOTIMPL; + } + + const auto byte_count = (friendly_name.size() + 1) * sizeof(wchar_t); + value->pwszVal = static_cast(CoTaskMemAlloc(byte_count)); + if (!value->pwszVal) { + return E_OUTOFMEMORY; + } + + std::memcpy(value->pwszVal, friendly_name.c_str(), byte_count); + value->vt = VT_LPWSTR; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE Commit() override { + return E_NOTIMPL; + } + + std::wstring friendly_name; + }; + + class fake_device_t final: public IMMDevice { + public: + fake_device_t(std::wstring id, std::wstring friendly_name): + id {std::move(id)}, + properties {std::move(friendly_name)} { + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface + *object = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { + return 1; + } + + ULONG STDMETHODCALLTYPE Release() override { + return 1; + } + + HRESULT STDMETHODCALLTYPE Activate(REFIID, DWORD, PROPVARIANT *, void **) override { // NOSONAR(cpp:S5008): required by the Windows COM interface + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD, IPropertyStore **property_store) override { + properties.AddRef(); + *property_store = &properties; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetId(LPWSTR *device_id) override { + const auto byte_count = (id.size() + 1) * sizeof(wchar_t); + *device_id = static_cast(CoTaskMemAlloc(byte_count)); + if (!*device_id) { + return E_OUTOFMEMORY; + } + + std::memcpy(*device_id, id.c_str(), byte_count); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetState(DWORD *device_state) override { + if (FAILED(state_status)) { + return state_status; + } + + *device_state = state; + return S_OK; + } + + std::wstring id; + fake_property_store_t properties; + HRESULT state_status = S_OK; + DWORD state = DEVICE_STATE_ACTIVE; + }; + + class fake_device_collection_t final: public IMMDeviceCollection { + public: + explicit fake_device_collection_t(fake_device_t &device): + device {device} { + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface + *object = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { + return 1; + } + + ULONG STDMETHODCALLTYPE Release() override { + return 1; + } + + HRESULT STDMETHODCALLTYPE GetCount(UINT *device_count) override { + *device_count = 1; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Item(UINT index, IMMDevice **item) override { + if (index != 0) { + return E_INVALIDARG; + } + + device.AddRef(); + *item = &device; + return S_OK; + } + + fake_device_t &device; + }; + + class fake_device_enumerator_t final: public IMMDeviceEnumerator { + public: + explicit fake_device_enumerator_t(std::wstring id, std::wstring friendly_name = {}): + device {std::move(id), std::move(friendly_name)}, + collection {device} { + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface + *object = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { + return 1; + } + + ULONG STDMETHODCALLTYPE Release() override { + return 1; + } + + HRESULT STDMETHODCALLTYPE EnumAudioEndpoints(EDataFlow, DWORD, IMMDeviceCollection **devices) override { + if (FAILED(enumeration_status)) { + return enumeration_status; + } + + collection.AddRef(); + *devices = &collection; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetDefaultAudioEndpoint(EDataFlow, ERole, IMMDevice **resolved_device) override { + ++get_default_device_calls; + device.AddRef(); + *resolved_device = &device; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetDevice(LPCWSTR device_id, IMMDevice **resolved_device) override { + ++get_device_calls; + last_requested_id = device_id; + if (FAILED(get_device_status)) { + return get_device_status; + } + if (last_requested_id != device.id) { + return HRESULT_FROM_WIN32(ERROR_NOT_FOUND); + } + + device.AddRef(); + *resolved_device = &device; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE RegisterEndpointNotificationCallback(IMMNotificationClient *) override { + return S_OK; + } + + HRESULT STDMETHODCALLTYPE UnregisterEndpointNotificationCallback(IMMNotificationClient *) override { + return S_OK; + } + + fake_device_t device; + fake_device_collection_t collection; + HRESULT enumeration_status = S_OK; + HRESULT get_device_status = S_OK; + int get_device_calls = 0; + int get_default_device_calls = 0; + std::wstring last_requested_id; + }; +} // namespace + +TEST(WindowsAudioTest, AssignedSinkTakesPriorityOverConfiguredSink) { + fake_device_enumerator_t enumerator {L"assigned-id"}; + + EXPECT_FALSE(platf::audio::tests::microphone_available("assigned-id", "configured-id", &enumerator)); + EXPECT_EQ(enumerator.get_device_calls, 1); + EXPECT_EQ(enumerator.last_requested_id, L"assigned-id"); +} + +TEST(WindowsAudioTest, ConfiguredSinkIsUsedWhenNoSinkWasAssigned) { + fake_device_enumerator_t enumerator {L"configured-id"}; + + EXPECT_FALSE(platf::audio::tests::microphone_available({}, "configured-id", &enumerator)); + EXPECT_EQ(enumerator.get_device_calls, 1); + EXPECT_EQ(enumerator.last_requested_id, L"configured-id"); +} + +TEST(WindowsAudioTest, DefaultDeviceIsUsedWhenNoSinkWasRequested) { + fake_device_enumerator_t enumerator {L"endpoint-id"}; + + platf::audio::tests::microphone_available({}, {}, &enumerator); + EXPECT_EQ(enumerator.get_device_calls, 0); +} + +TEST(WindowsAudioTest, DefaultCaptureSelectsDefaultEndpoint) { + fake_device_enumerator_t enumerator {L"endpoint-id"}; + + EXPECT_TRUE(platf::audio::tests::capture_follows_default_device(&enumerator, nullptr)); + EXPECT_EQ(enumerator.get_default_device_calls, 1); +} + +TEST(WindowsAudioTest, PinnedCaptureKeepsExplicitEndpoint) { + fake_device_enumerator_t enumerator {L"endpoint-id"}; + + EXPECT_FALSE(platf::audio::tests::capture_follows_default_device(&enumerator, &enumerator.device)); + EXPECT_EQ(enumerator.get_default_device_calls, 0); +} + +TEST(WindowsAudioTest, MicrophoneRejectsUnresolvedSink) { + fake_device_enumerator_t enumerator {L"endpoint-id"}; + + EXPECT_FALSE(platf::audio::tests::microphone_available("unknown", {}, &enumerator)); + EXPECT_EQ(enumerator.get_device_calls, 0); +} + +TEST(WindowsAudioTest, ResolvesVirtualSinkDescriptorToActiveEndpoint) { + fake_device_enumerator_t enumerator {L"endpoint-id"}; + + EXPECT_TRUE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &enumerator)); + EXPECT_EQ(enumerator.get_device_calls, 1); + EXPECT_EQ(enumerator.last_requested_id, L"endpoint-id"); +} + +TEST(WindowsAudioTest, ResolvesDeviceIdentifiersAndFriendlyNames) { + fake_device_enumerator_t id_enumerator {L"endpoint-id"}; + EXPECT_TRUE(platf::audio::tests::sink_device_available("endpoint-id", &id_enumerator)); + + fake_device_enumerator_t name_enumerator {L"endpoint-id", L"Friendly Endpoint"}; + EXPECT_TRUE(platf::audio::tests::sink_device_available("Friendly Endpoint", &name_enumerator)); +} + +TEST(WindowsAudioTest, RejectsUnknownOrUnenumerableSinks) { + fake_device_enumerator_t unknown_enumerator {L"endpoint-id"}; + EXPECT_FALSE(platf::audio::tests::sink_device_available("unknown", &unknown_enumerator)); + EXPECT_EQ(unknown_enumerator.get_device_calls, 0); + + fake_device_enumerator_t failed_enumerator {L"endpoint-id"}; + failed_enumerator.enumeration_status = E_FAIL; + EXPECT_FALSE(platf::audio::tests::sink_device_available("endpoint-id", &failed_enumerator)); + EXPECT_EQ(failed_enumerator.get_device_calls, 0); +} + +TEST(WindowsAudioTest, RejectsUnavailableResolvedEndpoints) { + fake_device_enumerator_t missing_enumerator {L"endpoint-id"}; + missing_enumerator.get_device_status = E_FAIL; + EXPECT_FALSE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &missing_enumerator)); + + fake_device_enumerator_t state_failure_enumerator {L"endpoint-id"}; + state_failure_enumerator.device.state_status = E_FAIL; + EXPECT_FALSE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &state_failure_enumerator)); + + fake_device_enumerator_t inactive_enumerator {L"endpoint-id"}; + inactive_enumerator.device.state = DEVICE_STATE_DISABLED; + EXPECT_FALSE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &inactive_enumerator)); +} + +TEST(WindowsAudioTest, DefaultFollowingCaptureReinitializesAfterRenderEndpointChange) { + int callback_count = 0; + + EXPECT_EQ( + platf::audio::tests::simulate_default_device_change(true, true, true, callback_count), + platf::capture_e::reinit + ); + EXPECT_EQ(callback_count, 1); +} + +TEST(WindowsAudioTest, PinnedCaptureContinuesAfterRenderEndpointChange) { + int callback_count = 0; + + EXPECT_EQ( + platf::audio::tests::simulate_default_device_change(false, true, true, callback_count), + platf::capture_e::timeout + ); + EXPECT_EQ(callback_count, 1); +} + +TEST(WindowsAudioTest, CaptureEndpointChangeDoesNotTriggerRenderCallback) { + int callback_count = 0; + + EXPECT_EQ( + platf::audio::tests::simulate_default_device_change(false, true, false, callback_count), + platf::capture_e::timeout + ); + EXPECT_EQ(callback_count, 0); +} + +TEST(WindowsAudioTest, DefaultChangeWithoutCallbackStillReinitializes) { + int callback_count = 0; + + EXPECT_EQ( + platf::audio::tests::simulate_default_device_change(true, false, true, callback_count), + platf::capture_e::reinit + ); + EXPECT_EQ(callback_count, 0); +} +#endif