From 284c32310ca4297d341fc27ae91fb1ffb334831d Mon Sep 17 00:00:00 2001 From: lx-s Date: Tue, 18 Aug 2026 10:40:55 +0200 Subject: [PATCH] Restore mute state with delayed device wake-ups. Save mute states separately from devices so devices that need some time waking up (e.g. displays) get re-registered with winmute properly. Allow up to 60seconds for a device to wake up so to correctlry restore mute state. --- WinMute/MMNotificationClient.cpp | 39 +---- WinMute/MMNotificationClient.h | 9 +- WinMute/MuteControl.cpp | 67 +++++++- WinMute/MuteControl.h | 4 + WinMute/Settings_ManageEndpointsDlg.cpp | 6 +- WinMute/Utility.cpp | 13 ++ WinMute/Utility.h | 2 + WinMute/VistaAudio.cpp | 196 ++++++++++++++++++++---- WinMute/WinAudio.h | 21 ++- WinMute/WinMute.cpp | 3 + WinMute/common.h | 4 + 11 files changed, 290 insertions(+), 74 deletions(-) diff --git a/WinMute/MMNotificationClient.cpp b/WinMute/MMNotificationClient.cpp index f86acc7..cc2a5e7 100644 --- a/WinMute/MMNotificationClient.cpp +++ b/WinMute/MMNotificationClient.cpp @@ -40,7 +40,7 @@ POSSIBILITY OF SUCH DAMAGE. class WinAudio; MMNotificationClient::MMNotificationClient(WinAudio* notifyParent) - : ref_count_(1), deviceEnumerator_(nullptr), notifyParent_(notifyParent) + : ref_count_(1), notifyParent_(notifyParent) { } @@ -88,9 +88,9 @@ STDMETHODIMP_(HRESULT) MMNotificationClient::OnDeviceAdded(LPCWSTR pwstrDeviceId) { if (notifyParent_ && pwstrDeviceId != nullptr) { - const auto deviceName = GetDeviceNameFromId(pwstrDeviceId); - WMLog::GetInstance().LogInfo(L"Device \"{}\" added", deviceName); + WMLog::GetInstance().LogInfo(L"Device \"{}\" added", pwstrDeviceId); notifyParent_->ShouldReInit(); + notifyParent_->OnDeviceArrived(); } return S_OK; } @@ -99,8 +99,7 @@ STDMETHODIMP_(HRESULT) MMNotificationClient::OnDeviceRemoved(LPCWSTR pwstrDeviceId) { if (notifyParent_ && pwstrDeviceId != nullptr) { - const auto deviceName = GetDeviceNameFromId(pwstrDeviceId); - WMLog::GetInstance().LogInfo(L"Device \"{}\" removed", deviceName); + WMLog::GetInstance().LogInfo(L"Device \"{}\" removed", pwstrDeviceId); notifyParent_->ShouldReInit(); } return S_OK; @@ -127,10 +126,12 @@ MMNotificationClient::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, if (notify && notifyParent_) { // TODO: Check if output device - const auto deviceName = GetDeviceNameFromId(pwstrDeviceId); WMLog::GetInstance().LogInfo(L"Device \"{}\" status changed to {}", - deviceName, what); + pwstrDeviceId, what); notifyParent_->ShouldReInit(); + if (dwNewState == DEVICE_STATE_ACTIVE) { + notifyParent_->OnDeviceArrived(); + } } return S_OK; } @@ -140,27 +141,3 @@ MMNotificationClient::OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) { return S_OK; } - -std::wstring MMNotificationClient::GetDeviceNameFromId(LPCWSTR pwstrDeviceId) -{ - HRESULT hr = S_OK; - if (deviceEnumerator_ == nullptr) { - // Get enumerator for audio endpoint devices. - hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, - CLSCTX_INPROC_SERVER, - __uuidof(IMMDeviceEnumerator), - reinterpret_cast(&deviceEnumerator_)); - } - - CComPtr devicePtr = nullptr; - if (SUCCEEDED(hr)) { - hr = deviceEnumerator_->GetDevice(pwstrDeviceId, &devicePtr); - } - if (SUCCEEDED(hr)) { - const auto device_name = GetAudioDeviceName(devicePtr); - if (device_name) { - return *device_name; - } - } - return L"Unknown device"; -} diff --git a/WinMute/MMNotificationClient.h b/WinMute/MMNotificationClient.h index 0435a99..7d8d0f8 100644 --- a/WinMute/MMNotificationClient.h +++ b/WinMute/MMNotificationClient.h @@ -59,9 +59,12 @@ class MMNotificationClient : public IMMNotificationClient { const PROPERTYKEY key) override; private: + // These callbacks run on a WASAPI notification thread. Per the + // IMMNotificationClient contract they must not call back into the + // MMDevice API (no enumerator, no property stores) and must not block -- + // doing so risks a deadlock with the audio service. Raw endpoint IDs are + // logged here; the friendly names follow from the re-enumeration that the + // main thread performs right after. std::atomic ref_count_; - CComPtr deviceEnumerator_; WinAudio* notifyParent_; - - std::wstring GetDeviceNameFromId(LPCWSTR pwstrDeviceId); }; diff --git a/WinMute/MuteControl.cpp b/WinMute/MuteControl.cpp index 9cef180..7a565d9 100644 --- a/WinMute/MuteControl.cpp +++ b/WinMute/MuteControl.cpp @@ -47,6 +47,31 @@ static constexpr UINT_PTR BLUETOOTH_UNMUTE_TIMER_ID = 190502; static const wchar_t* MUTECONTROL_CLASS_NAME = L"WinMuteMuteControl"; +const wchar_t* MuteControl::MuteTypeToString(MuteType type) +{ + switch (type) { + case MuteTypeWorkstationLock: + return L"Workstation Lock"; + case MuteTypeRemoteSession: + return L"Remote Session"; + case MuteTypeDisplayStandby: + return L"Display Standby"; + case MuteTypeLidClose: + return L"Lid Close"; + case MuteTypeBluetoothDisconnect: + return L"Bluetooth Disconnect"; + case MuteTypeLogout: + return L"Logout"; + case MuteTypeSuspend: + return L"Suspend"; + case MuteTypeShutdown: + return L"Shutdown"; + case MuteTypeCount: + default: + return L"Unknown"; + } +} + void CALLBACK DelayedMuteTimerProc(HWND hWnd, UINT, UINT_PTR, DWORD) { MuteControl* muteCtrl = @@ -221,12 +246,16 @@ void MuteControl::RestoreVolume(bool withDelay) log.LogInfo(L"Volume Restore has been disabled"); return; } - const bool restore = !std::any_of( - muteConfig_.begin(), muteConfig_.end(), - [](const MuteConfig& conf) { return conf.shouldMute && conf.active; }); - if (!restore) { + const auto blocking = + std::find_if(muteConfig_.begin(), muteConfig_.end(), + [](const MuteConfig& conf) { + return conf.shouldMute && conf.active; + }); + if (blocking != muteConfig_.end()) { log.LogInfo( - L"Skipping restore since other mute event is currently active"); + L"Skipping restore since mute event \"{}\" is currently active", + MuteTypeToString(static_cast( + std::distance(muteConfig_.begin(), blocking)))); } else if (delayedMuteTimerId_ != 0) { KillTimer(hMuteCtrlWnd_, delayedMuteTimerId_); delayedMuteTimerId_ = 0; @@ -399,10 +428,19 @@ void MuteControl::NotifyRestoreCondition(MuteType type, bool active, } } } else { - if (muteConfig_[type].active) { + if (!muteConfig_[type].active) { + WMLog::GetInstance().LogInfo( + L"Ignoring end of \"{}\": it was never seen as started", + MuteTypeToString(type)); + } else { muteConfig_[type].active = false; if (muteConfig_[type].shouldMute) { RestoreVolume(withDelay); + } else { + WMLog::GetInstance().LogInfo( + L"Not restoring after \"{}\": muting for this event is" + L" disabled", + MuteTypeToString(type)); } } } @@ -489,6 +527,23 @@ void MuteControl::NotifyQuietHours(bool active) } } +void MuteControl::NotifyAudioDeviceArrived() +{ + if (!restoreVolume_) { + return; + } + // Only endpoints that went missing during a restore are eligible, and only + // while no mute event is active -- a device showing up mid-mute should stay + // as it is, not be unmuted behind the user's back. + const bool muteActive = std::any_of( + muteConfig_.begin(), muteConfig_.end(), + [](const MuteConfig& conf) { return conf.shouldMute && conf.active; }); + if (muteActive) { + return; + } + winAudio_->RestoreArrivedEndpoints(); +} + void MuteControl::SetManagedEndpoints( const std::vector& endpoints, bool isAllowList) { diff --git a/WinMute/MuteControl.h b/WinMute/MuteControl.h index 4264c82..07e9beb 100644 --- a/WinMute/MuteControl.h +++ b/WinMute/MuteControl.h @@ -98,6 +98,8 @@ class MuteControl { void NotifyQuietHours(bool active); + void NotifyAudioDeviceArrived(); + void SetManagedEndpoints(const std::vector& endpoints, bool isAllowList); void ClearManagedEndpoints(); @@ -153,6 +155,8 @@ class MuteControl { const TrayIcon* trayIcon_ = nullptr; + static const wchar_t* MuteTypeToString(MuteType type); + void NotifyRestoreCondition(MuteType type, bool active, bool withDelay = false); void SaveMuteStatus(); diff --git a/WinMute/Settings_ManageEndpointsDlg.cpp b/WinMute/Settings_ManageEndpointsDlg.cpp index d638c73..119d702 100644 --- a/WinMute/Settings_ManageEndpointsDlg.cpp +++ b/WinMute/Settings_ManageEndpointsDlg.cpp @@ -52,9 +52,13 @@ static bool GetAudioEndpoints(std::vector& endpoints) { return false; } + // Must match the states VistaAudio manages, otherwise endpoints that are + // currently unplugged (a sleeping monitor's HDMI/DisplayPort audio) could + // not be put on the allow/block list while they are asleep. CComPtr audioEndpoints; if (FAILED(deviceEnumerator->EnumAudioEndpoints( - eRender, DEVICE_STATE_ACTIVE, &audioEndpoints))) + eRender, DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED, + &audioEndpoints))) { return false; } diff --git a/WinMute/Utility.cpp b/WinMute/Utility.cpp index 017d109..ca74d7e 100644 --- a/WinMute/Utility.cpp +++ b/WinMute/Utility.cpp @@ -149,6 +149,19 @@ std::optional GetAudioDeviceName( return deviceName; } +std::optional GetAudioDeviceId(const CComPtr& devicePtr) +{ + PWSTR deviceId = nullptr; + if (FAILED(devicePtr->GetId(&deviceId))) { + WMLog::GetInstance().LogError( + L"Failed to get device id for audio endpoint"); + return std::nullopt; + } + std::wstring id{deviceId}; + CoTaskMemFree(deviceId); + return id; +} + // ========================================================================= // String Conversion // ========================================================================= diff --git a/WinMute/Utility.h b/WinMute/Utility.h index dd1ab25..5d3208d 100644 --- a/WinMute/Utility.h +++ b/WinMute/Utility.h @@ -69,6 +69,8 @@ bool LaunchBrowser(HWND hParent, const std::wstring& url); std::optional GetAudioDeviceName( const CComPtr& devicePtr); +std::optional GetAudioDeviceId(const CComPtr& devicePtr); + // ============================================================================= // COM Helper template diff --git a/WinMute/VistaAudio.cpp b/WinMute/VistaAudio.cpp index 2b70016..ba6a283 100644 --- a/WinMute/VistaAudio.cpp +++ b/WinMute/VistaAudio.cpp @@ -44,6 +44,29 @@ Endpoint::~Endpoint() } } +// Endpoints attached to a monitor (HDMI/DisplayPort, typically exposed by the +// GPU's audio driver) drop to DEVICE_STATE_UNPLUGGED while the display sleeps. +// They must stay under management across that transition, otherwise a mute +// applied before standby can never be undone afterwards. +static constexpr DWORD MANAGED_DEVICE_STATES = + DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED; + +static const wchar_t* DeviceStateToString(DWORD state) +{ + switch (state) { + case DEVICE_STATE_ACTIVE: + return L"active"; + case DEVICE_STATE_UNPLUGGED: + return L"unplugged"; + case DEVICE_STATE_DISABLED: + return L"disabled"; + case DEVICE_STATE_NOTPRESENT: + return L"not present"; + default: + return L"unknown"; + } +} + VistaAudio::VistaAudio() : deviceEnumerator_(nullptr), mmnAudioEvents_(nullptr), @@ -67,7 +90,7 @@ bool VistaAudio::LoadAllEndpoints() CComPtr audioEndpoints; HRESULT hr = deviceEnumerator_->EnumAudioEndpoints( - eRender, DEVICE_STATE_ACTIVE, &audioEndpoints); + eRender, MANAGED_DEVICE_STATES, &audioEndpoints); if (FAILED(hr)) { return false; } @@ -88,40 +111,55 @@ bool VistaAudio::LoadAllEndpoints() continue; } + const auto deviceId = GetAudioDeviceId(device); + if (!deviceId) { + log.LogError(L"Failed to get device id for audio endpoint #{}", i); + continue; + } + ep->deviceId = *deviceId; + const auto deviceName = GetAudioDeviceName(device); if (!deviceName) { log.LogError(L"Failed to get device name for audio endpoint #{}", i); continue; } else { - log.LogInfo(L"Found audio endpoint \"{}\"", *deviceName); + DWORD deviceState = 0; + if (FAILED(device->GetState(&deviceState))) { + deviceState = 0; + } + log.LogInfo(L"Found audio endpoint \"{}\" ({})", *deviceName, + DeviceStateToString(deviceState)); ep->deviceName = *deviceName; } + // Session notifications are a convenience (they trigger a re-init when + // a session drops), not a requirement for muting. An unplugged endpoint + // has no session manager, so a failure here must not disqualify it. CComPtr sessionManager2; if (FAILED(device->Activate( __uuidof(IAudioSessionManager2), CLSCTX_INPROC_SERVER, nullptr, reinterpret_cast(&sessionManager2)))) { - log.LogError(L"Failed to retrieve audio session manager for \"{}\"", - ep->deviceName); - continue; - } - - hr = sessionManager2->GetAudioSessionControl(nullptr, 0, - &ep->sessionCtrl); - if (FAILED(hr)) { - log.LogError(L"Failed to retrieve audio session manager for \"{}\"", - ep->deviceName); - continue; + log.LogInfo( + L"No audio session manager for \"{}\";" + L" continuing without session notifications", + ep->deviceName); + } else if (FAILED(sessionManager2->GetAudioSessionControl( + nullptr, 0, &ep->sessionCtrl))) + { + log.LogInfo( + L"No audio session control for \"{}\";" + L" continuing without session notifications", + ep->deviceName); + } else { + // Attach: the CComPtr takes over the initial reference from new, + // so the refcount stays balanced. + ep->wasapiAudioEvents.Attach(new VistaAudioSessionEvents(this)); + ep->sessionCtrl->RegisterAudioSessionNotification( + ep->wasapiAudioEvents); } - // Attach: the CComPtr takes over the initial reference from new, - // so the refcount stays balanced. - ep->wasapiAudioEvents.Attach(new VistaAudioSessionEvents(this)); - ep->sessionCtrl->RegisterAudioSessionNotification( - ep->wasapiAudioEvents); - hr = device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr, reinterpret_cast(&ep->endpointVolume)); @@ -181,11 +219,26 @@ void VistaAudio::OnAudioServiceShutdown() PostMessageW(hParent_, WM_WINMUTE_AUDIO_SERVICE_SHUTDOWN, 0, 0); } +void VistaAudio::OnDeviceArrived() +{ + // Called from a WASAPI notification thread. Only post here; all COM work + // has to happen on the main thread. + PostMessageW(hParent_, WM_WINMUTE_AUDIO_DEVICE_ARRIVED, 0, 0); +} + bool VistaAudio::CheckForReInit() { if (reInit_.exchange(false)) { Uninit(); - return Init(hParent_); + if (!Init(hParent_)) { + // Without re-arming, a single failed re-init would leave WinMute + // with an empty endpoint list and no way back: the flag is already + // consumed, so no later call would ever try again. + WMLog::GetInstance().LogError( + L"Audio re-initialization failed; will retry on next event"); + reInit_ = true; + return false; + } } return true; } @@ -217,12 +270,21 @@ bool VistaAudio::AllEndpointsMuted() return anyManaged; } +// How long after a restore a late-arriving endpoint is still considered part +// of that restore. Monitor-attached endpoints usually reappear within a few +// seconds of the display waking; anything later is a genuinely new device that +// must not be touched. +static constexpr auto LATE_RESTORE_WINDOW = std::chrono::seconds(60); + bool VistaAudio::SaveMuteStatus() { bool success = true; WMLog& log = WMLog::GetInstance(); if (CheckForReInit()) { + // A new mute cycle supersedes any restore still waiting for a device. + pendingRestoreIds_.clear(); + savedMuteState_.clear(); for (auto& e : endpoints_) { BOOL isMuted = FALSE; if (FAILED(e->endpointVolume->GetMute(&isMuted))) { @@ -230,13 +292,30 @@ bool VistaAudio::SaveMuteStatus() e->deviceName); success = false; } else { - e->wasMuted = isMuted; + savedMuteState_[e->deviceId] = !!isMuted; } } } return success; } +bool VistaAudio::RestoreEndpoint(const Endpoint& ep, bool wasMuted) +{ + WMLog& log = WMLog::GetInstance(); + + log.LogInfo(L"Restoring: Mute {} for \"{}\"", wasMuted ? L"true" : L"false", + ep.deviceName); + if (wasMuted) { + return true; + } + if (FAILED(ep.endpointVolume->SetMute(false, nullptr))) { + log.LogError(L"Failed to restore mute status to false for \"{}\"", + ep.deviceName); + return false; + } + return true; +} + bool VistaAudio::RestoreMuteStatus() { bool success = true; @@ -245,25 +324,81 @@ bool VistaAudio::RestoreMuteStatus() if (!CheckForReInit()) { return false; } + + pendingRestoreIds_.clear(); + std::set presentIds; for (auto& e : endpoints_) { + presentIds.insert(e->deviceId); if (!IsEndpointManaged(e->deviceName)) { log.LogInfo(L"Skipping Endpoint {}", e->deviceName); continue; } - log.LogInfo(L"Restoring: Mute {} for \"{}\"", - (e->wasMuted) ? L"true" : L"false", e->deviceName); - if (e->wasMuted != true) { - if (FAILED(e->endpointVolume->SetMute(false, nullptr))) { - log.LogError(L"Failed to restore mute status to {} for \"{}\"", - (e->wasMuted) ? L"true" : L"false", e->deviceName); - success = false; - } + const auto saved = savedMuteState_.find(e->deviceId); + if (saved == savedMuteState_.end()) { + // Appeared after the mute event; nothing was remembered for it. + log.LogInfo(L"No saved mute state for \"{}\"; leaving it alone", + e->deviceName); + continue; + } + if (!RestoreEndpoint(*e, saved->second)) { + success = false; } } + // Endpoints that were around when we muted but are gone now (a sleeping + // monitor's HDMI/DisplayPort audio, for example) keep the mute flag + // Windows persisted for them. Remember them so their reappearance within + // the next minute still completes the restore. + for (const auto& [deviceId, wasMuted] : savedMuteState_) { + if (presentIds.count(deviceId) == 0 && !wasMuted) { + pendingRestoreIds_.insert(deviceId); + } + } + if (!pendingRestoreIds_.empty()) { + restoreDeadline_ = std::chrono::steady_clock::now() + + LATE_RESTORE_WINDOW; + log.LogInfo( + L"{} endpoint(s) were not present at restore time." + L" Waiting up to {}s for them to reappear", + pendingRestoreIds_.size(), LATE_RESTORE_WINDOW.count()); + } + return success; } +void VistaAudio::RestoreArrivedEndpoints() +{ + WMLog& log = WMLog::GetInstance(); + + if (pendingRestoreIds_.empty()) { + return; + } + if (std::chrono::steady_clock::now() > restoreDeadline_) { + log.LogInfo( + L"Endpoint arrived, but the restore window has expired." + L" Discarding {} pending restore(s)", + pendingRestoreIds_.size()); + pendingRestoreIds_.clear(); + return; + } + if (!CheckForReInit()) { + return; + } + for (auto& e : endpoints_) { + if (pendingRestoreIds_.count(e->deviceId) == 0) { + continue; + } + if (!IsEndpointManaged(e->deviceName)) { + log.LogInfo(L"Skipping Endpoint {}", e->deviceName); + pendingRestoreIds_.erase(e->deviceId); + continue; + } + log.LogInfo(L"Endpoint \"{}\" reappeared after restore", e->deviceName); + RestoreEndpoint(*e, false); + pendingRestoreIds_.erase(e->deviceId); + } +} + void VistaAudio::SetMute(bool mute) { WMLog& log = WMLog::GetInstance(); @@ -281,8 +416,7 @@ void VistaAudio::SetMute(bool mute) if (!!isMuted != mute) { if (FAILED(e->endpointVolume->SetMute(mute, nullptr))) { log.LogError(L"Failed to set mute status to {} for \"{}\"", - (e->wasMuted) ? L"true" : L"false", - e->deviceName); + mute ? L"true" : L"false", e->deviceName); } } } diff --git a/WinMute/WinAudio.h b/WinMute/WinAudio.h index 6e4d553..d6df7c0 100644 --- a/WinMute/WinAudio.h +++ b/WinMute/WinAudio.h @@ -42,9 +42,11 @@ class WinAudio { virtual bool Init(HWND hParent) = 0; virtual void ShouldReInit() = 0; virtual void OnAudioServiceShutdown() = 0; + virtual void OnDeviceArrived() = 0; virtual bool AllEndpointsMuted() = 0; virtual bool SaveMuteStatus() = 0; virtual bool RestoreMuteStatus() = 0; + virtual void RestoreArrivedEndpoints() = 0; virtual void SetMute(bool mute) = 0; virtual void MuteSpecificEndpoints(bool muteSpecific) = 0; virtual void SetManagedEndpoints(const std::vector& endpoints, @@ -53,6 +55,10 @@ class WinAudio { }; struct Endpoint { + // Stable identity of the endpoint. The friendly name is for logging and + // for the user-facing allow/block list only -- it is neither unique nor + // guaranteed to survive a driver update. + std::wstring deviceId; std::wstring deviceName; CComPtr endpointVolume; CComPtr sessionCtrl; @@ -61,8 +67,6 @@ struct Endpoint { // it with a second owner. CComPtr wasapiAudioEvents; - bool wasMuted = false; - Endpoint() = default; ~Endpoint(); Endpoint(const Endpoint&) = delete; @@ -77,9 +81,11 @@ class VistaAudio : public WinAudio { bool Init(HWND hParent) override; void ShouldReInit() override; void OnAudioServiceShutdown() override; + void OnDeviceArrived() override; bool AllEndpointsMuted() override; bool SaveMuteStatus() override; bool RestoreMuteStatus() override; + void RestoreArrivedEndpoints() override; void SetMute(bool mute) override; void MuteSpecificEndpoints(bool muteSpecific) override; @@ -92,11 +98,22 @@ class VistaAudio : public WinAudio { bool LoadAllEndpoints(); bool IsEndpointManaged(const std::wstring& endpointName) const; + bool RestoreEndpoint(const Endpoint& ep, bool wasMuted); std::vector> endpoints_; CComPtr mmnAudioEvents_; CComPtr deviceEnumerator_; + // Saved mute state lives here, not on Endpoint: the endpoint objects are + // torn down and rebuilt on every re-init, which would otherwise discard + // the state saved just before a mute event (or between save and mute). + std::map savedMuteState_; + + // Endpoints that were saved but absent when the restore came in, plus the + // deadline until which their late arrival still triggers a restore. + std::set pendingRestoreIds_; + std::chrono::steady_clock::time_point restoreDeadline_{}; + std::atomic reInit_; bool muteSpecificEndpoints_; bool muteSpecificEndpointsAllowList_; diff --git a/WinMute/WinMute.cpp b/WinMute/WinMute.cpp index a71c631..41c96a9 100644 --- a/WinMute/WinMute.cpp +++ b/WinMute/WinMute.cpp @@ -1019,6 +1019,9 @@ LRESULT CALLBACK WinMute::WindowProc(HWND hWnd, UINT msg, WPARAM wParam, return OnQuietHours(hWnd, msg, wParam, lParam); case WM_WINMUTE_AUDIO_SERVICE_SHUTDOWN: return OnAudioServiceShutdown(hWnd, wParam, lParam); + case WM_WINMUTE_AUDIO_DEVICE_ARRIVED: + muteCtrl_.NotifyAudioDeviceArrived(); + return 0; case WM_DEVICECHANGE: return OnDeviceChange(hWnd, msg, wParam, lParam); case WM_WIFISTATUSCHANGED: diff --git a/WinMute/common.h b/WinMute/common.h index 975cc07..edf060c 100644 --- a/WinMute/common.h +++ b/WinMute/common.h @@ -79,6 +79,7 @@ POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include #include @@ -149,3 +150,6 @@ constexpr int WM_LOG_UPDATED = WM_USER + 302; constexpr int WM_WINMUTE_AUDIO_SERVICE_SHUTDOWN = WM_USER + 303; /* wParam = success. lParam = UpdateInfo*, ownership passes to the receiver */ constexpr int WM_WINMUTE_UPDATE_CHECK_DONE = WM_USER + 304; +/* An audio endpoint appeared. Posted from a WASAPI notification thread so the + actual work happens on the main thread. */ +constexpr int WM_WINMUTE_AUDIO_DEVICE_ARRIVED = WM_USER + 305; -- 2.43.0