From: lx-s Date: Tue, 18 Aug 2026 09:32:24 +0000 (+0200) Subject: Save Device-IDs instead of friendly names X-Git-Tag: 2.6.0.0~16 X-Git-Url: https://repos.lx-s.de/gitweb/?a=commitdiff_plain;h=d3031e715a213d351e69cd26e8e3e1c44bc9cd19;p=winmute.git Save Device-IDs instead of friendly names Friendly Names might change, depending on driver/firmware updates. Device-IDs are resilient against this and always unique. On start old device names are migrated to ids if possible. --- diff --git a/WinMute/MuteControl.cpp b/WinMute/MuteControl.cpp index 7a565d9..200c306 100644 --- a/WinMute/MuteControl.cpp +++ b/WinMute/MuteControl.cpp @@ -545,7 +545,7 @@ void MuteControl::NotifyAudioDeviceArrived() } void MuteControl::SetManagedEndpoints( - const std::vector& endpoints, bool isAllowList) + const std::vector& endpoints, bool isAllowList) { winAudio_->MuteSpecificEndpoints(true); winAudio_->SetManagedEndpoints(endpoints, isAllowList); diff --git a/WinMute/MuteControl.h b/WinMute/MuteControl.h index 07e9beb..400bec1 100644 --- a/WinMute/MuteControl.h +++ b/WinMute/MuteControl.h @@ -100,7 +100,7 @@ class MuteControl { void NotifyAudioDeviceArrived(); - void SetManagedEndpoints(const std::vector& endpoints, + void SetManagedEndpoints(const std::vector& endpoints, bool isAllowList); void ClearManagedEndpoints(); diff --git a/WinMute/Settings_ManageEndpointsDlg.cpp b/WinMute/Settings_ManageEndpointsDlg.cpp index 119d702..7e8b3cd 100644 --- a/WinMute/Settings_ManageEndpointsDlg.cpp +++ b/WinMute/Settings_ManageEndpointsDlg.cpp @@ -31,57 +31,42 @@ POSSIBILITY OF SUCH DAMAGE. ----------------------------------------------------------------------------- */ -#include "common.h" #include #include #include -static constexpr int ENDPOINT_NAME_MAX_LEN = 200; +#include "common.h" -struct EndpointData { - std::wstring name; +// Passed as lParam to the add/edit dialog; owned by the calling dialog. +// +// The combo box is pick-only, so every entry created here is bound to a real +// endpoint id. The one exception is an entry carried over from an older +// version (or created before the device list included it): that name is added +// to the list as-is so editing does not silently discard it. +struct EndpointAddCtx { + ManagedEndpoint* data = nullptr; + std::vector candidates; }; -static bool GetAudioEndpoints(std::vector& endpoints) -{ - WMLog& log = WMLog::GetInstance(); +// Item data of the "keep this legacy name" entry. Every other item carries an +// index into EndpointAddCtx::candidates -- the combo box is sorted, so the +// selection index on its own says nothing about which device was picked. +static constexpr LONG_PTR ENDPOINT_ITEM_LEGACY_NAME = -1; - CComPtr deviceEnumerator; - if (FAILED(deviceEnumerator.CoCreateInstance( - __uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER))) - { - 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 | DEVICE_STATE_UNPLUGGED, - &audioEndpoints))) - { - return false; - } - - UINT epCount = 0; - if (FAILED(audioEndpoints->GetCount(&epCount))) { - return false; - } - - for (UINT i = 0; i < epCount; ++i) { - CComPtr device = nullptr; - if (FAILED(audioEndpoints->Item(i, &device))) { - log.LogError(L"Failed to get audio endpoint #{}", i); - continue; - } +// State of the endpoint list dialog. The list box only renders friendly +// names, so the endpoint ids have to be tracked alongside it. +struct ManageEndpointsDlgState { + WMSettings* settings = nullptr; + std::vector entries; +}; - const auto deviceName = GetAudioDeviceName(device); - if (deviceName) { - endpoints.push_back(*deviceName); - } +static void RebuildEndpointList(HWND hList, + const std::vector& entries) +{ + ListBox_ResetContent(hList); + for (const auto& entry : entries) { + ListBox_AddString(hList, entry.name.c_str()); } - - return true; } static void LoadManageEndpointsAddDlgTranslation(HWND hDlg, bool isEdit) @@ -98,11 +83,6 @@ static void LoadManageEndpointsAddDlgTranslation(HWND hDlg, bool isEdit) "settings.bluetooth.add-edit.device-name-label"); i18n.SetItemText(hDlg, IDOK, "settings.btn-save"); i18n.SetItemText(hDlg, IDCANCEL, "settings.btn-cancel"); - - const auto placeholder = i18n.GetTranslationW( - "settings.mute.manage-endpoints.add-edit.endpoint-name-placeholder"); - ComboBox_SetCueBannerText(GetDlgItem(hDlg, IDC_ENDPOINT_NAME), - placeholder.c_str()); } static INT_PTR CALLBACK Settings_EndpointAddDlgProc(HWND hDlg, UINT msg, @@ -112,67 +92,87 @@ static INT_PTR CALLBACK Settings_EndpointAddDlgProc(HWND hDlg, UINT msg, switch (msg) { case WM_INITDIALOG: { HWND hEndpointName = GetDlgItem(hDlg, IDC_ENDPOINT_NAME); - EndpointData* endpointData = - reinterpret_cast(lParam); - if (endpointData == nullptr) { + EndpointAddCtx* ctx = reinterpret_cast(lParam); + if (ctx == nullptr || ctx->data == nullptr) { return FALSE; } - SetWindowLongPtr(hDlg, DWLP_USER, - reinterpret_cast(endpointData)); + SetWindowLongPtr(hDlg, DWLP_USER, reinterpret_cast(ctx)); - LoadManageEndpointsAddDlgTranslation( - hDlg, endpointData->name.length() != 0); - if (endpointData->name.length() != 0) { - SetWindowTextW(GetDlgItem(hDlg, IDC_ENDPOINT_NAME), - endpointData->name.c_str()); - } + const bool isEdit = ctx->data->name.length() != 0; + LoadManageEndpointsAddDlgTranslation(hDlg, isEdit); - // Disable save button until at least one string change is made + // Disable save button until a device has been picked EnableWindow(GetDlgItem(hDlg, IDOK), FALSE); - // Fill Combobox - std::vector audioEndpoints; - if (GetAudioEndpoints(audioEndpoints)) { - for (const auto& devName : audioEndpoints) { - ComboBox_AddString(hEndpointName, devName.c_str()); + // The combo box sorts, so remember which device each item is via + // its item data rather than relying on the insertion order. + if (EnumerateAudioEndpoints(ctx->candidates)) { + for (size_t i = 0; i < ctx->candidates.size(); ++i) { + const int pos = ComboBox_AddString( + hEndpointName, ctx->candidates[i].name.c_str()); + if (pos >= 0) { + ComboBox_SetItemData(hEndpointName, pos, + static_cast(i)); + } } } - Edit_LimitText(hEndpointName, ENDPOINT_NAME_MAX_LEN); - if (GetDlgCtrlID(reinterpret_cast(wParam)) != - IDC_ENDPOINT_NAME) - { - SetFocus(hEndpointName); - return FALSE; + if (isEdit) { + int pos = ComboBox_FindStringExact(hEndpointName, -1, + ctx->data->name.c_str()); + if (pos == CB_ERR) { + // The entry refers to a device that is not around (or was + // stored by name only). Offer it so it can be kept. + pos = ComboBox_AddString(hEndpointName, + ctx->data->name.c_str()); + if (pos >= 0) { + ComboBox_SetItemData(hEndpointName, pos, + ENDPOINT_ITEM_LEGACY_NAME); + } + } + if (pos >= 0) { + ComboBox_SetCurSel(hEndpointName, pos); + } } - return TRUE; + + SetFocus(hEndpointName); + return FALSE; } case WM_COMMAND: if (LOWORD(wParam) == IDC_ENDPOINT_NAME) { - HWND hDevName = GetDlgItem(hDlg, IDC_ENDPOINT_NAME); - if (HIWORD(wParam) == CBN_EDITUPDATE) { - const int textLen = Edit_GetTextLength(hDevName); - EnableWindow(GetDlgItem(hDlg, IDOK), textLen > 0); - } else if (HIWORD(wParam) == CBN_SELCHANGE) { - const auto curSelIdx = - SendMessage(hDevName, CB_GETCURSEL, 0, 0); - EnableWindow(GetDlgItem(hDlg, IDOK), curSelIdx != CB_ERR); + if (HIWORD(wParam) == CBN_SELCHANGE) { + const int sel = + ComboBox_GetCurSel(GetDlgItem(hDlg, IDC_ENDPOINT_NAME)); + EnableWindow(GetDlgItem(hDlg, IDOK), sel != CB_ERR); } } else if (LOWORD(wParam) == IDOK) { HWND hEpName = GetDlgItem(hDlg, IDC_ENDPOINT_NAME); - const int textLen = Edit_GetTextLength(hEpName); - if (textLen != 0) { - wchar_t epNameBuf[ENDPOINT_NAME_MAX_LEN + 1] = {'L\0'}; - EndpointData* epData = reinterpret_cast( - GetWindowLongPtr(hDlg, DWLP_USER)); - if (epData != nullptr) { - Edit_GetText(hEpName, epNameBuf, ARRAY_SIZE(epNameBuf)); - epData->name = epNameBuf; - EndDialog(hDlg, 0); - } else { - EndDialog(hDlg, 1); + EndpointAddCtx* ctx = reinterpret_cast( + GetWindowLongPtr(hDlg, DWLP_USER)); + const int sel = ComboBox_GetCurSel(hEpName); + if (ctx == nullptr || ctx->data == nullptr || sel == CB_ERR) { + EndDialog(hDlg, 1); + return FALSE; + } + const LONG_PTR item = + static_cast(ComboBox_GetItemData(hEpName, sel)); + if (item != ENDPOINT_ITEM_LEGACY_NAME && + static_cast(item) < ctx->candidates.size()) + { + ctx->data->name = ctx->candidates[item].name; + ctx->data->id = ctx->candidates[item].id; + } else { + // Kept an entry whose device is not currently around, so + // there is no id to bind; it stays matched by name. + const int len = ComboBox_GetLBTextLen(hEpName, sel); + if (len > 0) { + std::wstring name(static_cast(len), L'\0'); + ComboBox_GetLBText(hEpName, sel, name.data()); + ctx->data->name = name; } + ctx->data->id.clear(); } + EndDialog(hDlg, 0); } else if (LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, 1); } @@ -186,21 +186,6 @@ static INT_PTR CALLBACK Settings_EndpointAddDlgProc(HWND hDlg, UINT msg, return FALSE; } -static std::vector ExportEndpointNameList(HWND hList) -{ - std::vector items; - DWORD itemCount = ListBox_GetCount(hList); - for (DWORD i = 0; i < itemCount; ++i) { - wchar_t textBuf[ENDPOINT_NAME_MAX_LEN + 1] = {0}; - DWORD textLen = ListBox_GetTextLen(hList, i); - if (textLen < ARRAY_SIZE(textBuf)) { - ListBox_GetText(hList, i, textBuf); - items.push_back(textBuf); - } - } - return items; -} - static void LoadManageEndpointsDlgTranslation(HWND hDlg) { WMi18n& i18n = WMi18n::GetInstance(); @@ -239,8 +224,10 @@ INT_PTR CALLBACK Settings_ManageEndpointsDlgProc(HWND hDlg, UINT msg, WMSettings* settings = reinterpret_cast(lParam); assert(settings != nullptr); + auto* state = new ManageEndpointsDlgState(); + state->settings = settings; SetWindowLongPtr(hDlg, DWLP_USER, - reinterpret_cast(settings)); + reinterpret_cast(state)); DWORD endpointMode = settings->QueryValue( SettingsKey::MUTE_INDIVIDUAL_ENDPOINTS_MODE); @@ -262,16 +249,25 @@ INT_PTR CALLBACK Settings_ManageEndpointsDlgProc(HWND hDlg, UINT msg, } HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST); - const auto devices = settings->GetManagedAudioEndpoints(); - for (const auto& dev : devices) { - ListBox_AddString(hList, dev.c_str()); - } + state->entries = settings->GetManagedAudioEndpoints(); + RebuildEndpointList(hList, state->entries); Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL), ListBox_GetCount(hList) > 0); return TRUE; } + case WM_NCDESTROY: { + delete reinterpret_cast( + GetWindowLongPtr(hDlg, DWLP_USER)); + SetWindowLongPtr(hDlg, DWLP_USER, 0); + break; + } case WM_COMMAND: { + auto* state = reinterpret_cast( + GetWindowLongPtr(hDlg, DWLP_USER)); + if (state == nullptr) { + return FALSE; + } if (LOWORD(wParam) == IDC_ENDPOINT_LIST) { HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST); if (HIWORD(wParam) == LBN_SELCHANGE || @@ -292,19 +288,23 @@ INT_PTR CALLBACK Settings_ManageEndpointsDlgProc(HWND hDlg, UINT msg, entrySelected); } } else if (LOWORD(wParam) == IDC_ENDPOINT_ADD) { - EndpointData epData; + ManagedEndpoint epData; + EndpointAddCtx ctx; + ctx.data = &epData; if (DialogBoxParam(nullptr, MAKEINTRESOURCE(IDD_MANAGE_ENDPOINTS_ADD), hDlg, Settings_EndpointAddDlgProc, - reinterpret_cast(&epData)) == 0) + reinterpret_cast(&ctx)) == 0) { - std::vector devices = ExportEndpointNameList( - GetDlgItem(hDlg, IDC_ENDPOINT_LIST)); - if (std::find(begin(devices), end(devices), epData.name) == - end(devices)) - { - ListBox_AddString(GetDlgItem(hDlg, IDC_ENDPOINT_LIST), - epData.name.c_str()); + const bool duplicate = std::any_of( + state->entries.begin(), state->entries.end(), + [&epData](const ManagedEndpoint& e) { + return e.name == epData.name; + }); + if (!duplicate) { + state->entries.push_back(epData); + RebuildEndpointList(GetDlgItem(hDlg, IDC_ENDPOINT_LIST), + state->entries); HWND hRemoveAll = GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL); if (!IsWindowEnabled(hRemoveAll)) { @@ -314,68 +314,72 @@ INT_PTR CALLBACK Settings_ManageEndpointsDlgProc(HWND hDlg, UINT msg, } } else if (LOWORD(wParam) == IDC_ENDPOINT_EDIT) { HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST); - const WPARAM sel = ListBox_GetCurSel(hList); - if (sel != LB_ERR) { - const int len = ListBox_GetTextLen(hList, sel); - wchar_t* textBuf = nullptr; - if (len != LB_ERR) { - if ((textBuf = - new wchar_t[static_cast(len) + 1]) != - nullptr) - { - ListBox_GetText(hList, sel, textBuf); - - EndpointData epData; - epData.name = textBuf; - delete[] textBuf; - - if (DialogBoxParam( - nullptr, - MAKEINTRESOURCE(IDD_MANAGE_ENDPOINTS_ADD), - hDlg, Settings_EndpointAddDlgProc, - reinterpret_cast(&epData)) == 0) + const int sel = ListBox_GetCurSel(hList); + if (sel != LB_ERR && + static_cast(sel) < state->entries.size()) + { + ManagedEndpoint epData = state->entries[sel]; + + EndpointAddCtx ctx; + ctx.data = &epData; + if (DialogBoxParam( + nullptr, MAKEINTRESOURCE(IDD_MANAGE_ENDPOINTS_ADD), + hDlg, Settings_EndpointAddDlgProc, + reinterpret_cast(&ctx)) == 0) + { + // If the edit turned this entry into a duplicate of + // another one, drop it instead of keeping both. + bool duplicate = false; + for (size_t i = 0; i < state->entries.size(); ++i) { + if (static_cast(i) != sel && + state->entries[i].name == epData.name) { - std::vector networks = - ExportEndpointNameList( - GetDlgItem(hDlg, IDC_WIFI_LIST)); - if (std::find(begin(networks), end(networks), - epData.name) == end(networks)) - { - ListBox_InsertString(hList, sel, - epData.name.c_str()); - ListBox_DeleteString(hList, sel + 1); - } else { - ListBox_DeleteString(hList, sel); - } + duplicate = true; + break; } } + if (duplicate) { + state->entries.erase(state->entries.begin() + sel); + } else { + state->entries[sel] = epData; + } + // Rebuilding drops the selection, so restore it. + RebuildEndpointList(hList, state->entries); + if (static_cast(sel) < state->entries.size()) { + ListBox_SetCurSel(hList, sel); + } else { + Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT), + FALSE); + Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE), + FALSE); + } } } } else if (LOWORD(wParam) == IDC_ENDPOINT_REMOVE) { HWND hList = GetDlgItem(hDlg, IDC_ENDPOINT_LIST); - const WPARAM sel = ListBox_GetCurSel(hList); - if (sel != LB_ERR) { - ListBox_DeleteString(hList, sel); + const int sel = ListBox_GetCurSel(hList); + if (sel != LB_ERR && + static_cast(sel) < state->entries.size()) + { + state->entries.erase(state->entries.begin() + sel); + // Nothing is selected after the rebuild. + RebuildEndpointList(hList, state->entries); + Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT), FALSE); + Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE), FALSE); if (ListBox_GetCount(hList) == 0) { - Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT), - FALSE); - Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE), - FALSE); Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL), FALSE); } } } else if (LOWORD(wParam) == IDC_ENDPOINT_REMOVEALL) { + state->entries.clear(); ListBox_ResetContent(GetDlgItem(hDlg, IDC_ENDPOINT_LIST)); Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_EDIT), FALSE); Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVE), FALSE); Button_Enable(GetDlgItem(hDlg, IDC_ENDPOINT_REMOVEALL), FALSE); } else if (LOWORD(wParam) == IDOK) { - WMSettings* settings = reinterpret_cast( - GetWindowLongPtr(hDlg, DWLP_USER)); - std::vector devices = - ExportEndpointNameList(GetDlgItem(hDlg, IDC_ENDPOINT_LIST)); - settings->StoreManagedAudioEndpoints(devices); + WMSettings* settings = state->settings; + settings->StoreManagedAudioEndpoints(state->entries); if (Button_GetCheck( GetDlgItem(hDlg, IDC_ENDPOINT_LIST_IS_ALLOWLIST))) { diff --git a/WinMute/Utility.cpp b/WinMute/Utility.cpp index ca74d7e..92ef18f 100644 --- a/WinMute/Utility.cpp +++ b/WinMute/Utility.cpp @@ -162,6 +162,48 @@ std::optional GetAudioDeviceId(const CComPtr& devicePtr return id; } +bool EnumerateAudioEndpoints(std::vector& endpoints) +{ + WMLog& log = WMLog::GetInstance(); + + CComPtr deviceEnumerator; + if (FAILED(deviceEnumerator.CoCreateInstance( + __uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER))) + { + return false; + } + // Must match the states VistaAudio manages, otherwise endpoints that are + // currently unplugged (a sleeping monitor's HDMI/DisplayPort audio) would + // be invisible here. + CComPtr audioEndpoints; + if (FAILED(deviceEnumerator->EnumAudioEndpoints( + eRender, DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED, + &audioEndpoints))) + { + return false; + } + + UINT epCount = 0; + if (FAILED(audioEndpoints->GetCount(&epCount))) { + return false; + } + + for (UINT i = 0; i < epCount; ++i) { + CComPtr device = nullptr; + if (FAILED(audioEndpoints->Item(i, &device))) { + log.LogError(L"Failed to get audio endpoint #{}", i); + continue; + } + const auto deviceName = GetAudioDeviceName(device); + const auto deviceId = GetAudioDeviceId(device); + if (deviceName && deviceId) { + endpoints.push_back({*deviceId, *deviceName}); + } + } + + return true; +} + // ========================================================================= // String Conversion // ========================================================================= diff --git a/WinMute/Utility.h b/WinMute/Utility.h index 5d3208d..12ceb51 100644 --- a/WinMute/Utility.h +++ b/WinMute/Utility.h @@ -71,6 +71,10 @@ std::optional GetAudioDeviceName( std::optional GetAudioDeviceId(const CComPtr& devicePtr); +// Lists every render endpoint WinMute is willing to manage, with both its id +// and its friendly name. Requires an initialized COM apartment. +bool EnumerateAudioEndpoints(std::vector& endpoints); + // ============================================================================= // COM Helper template diff --git a/WinMute/VistaAudio.cpp b/WinMute/VistaAudio.cpp index ba6a283..00d5a53 100644 --- a/WinMute/VistaAudio.cpp +++ b/WinMute/VistaAudio.cpp @@ -252,7 +252,7 @@ bool VistaAudio::AllEndpointsMuted() } bool anyManaged = false; for (auto& e : endpoints_) { - if (!IsEndpointManaged(e->deviceName)) { + if (!IsEndpointManaged(*e)) { continue; } anyManaged = true; @@ -329,7 +329,7 @@ bool VistaAudio::RestoreMuteStatus() std::set presentIds; for (auto& e : endpoints_) { presentIds.insert(e->deviceId); - if (!IsEndpointManaged(e->deviceName)) { + if (!IsEndpointManaged(*e)) { log.LogInfo(L"Skipping Endpoint {}", e->deviceName); continue; } @@ -388,7 +388,7 @@ void VistaAudio::RestoreArrivedEndpoints() if (pendingRestoreIds_.count(e->deviceId) == 0) { continue; } - if (!IsEndpointManaged(e->deviceName)) { + if (!IsEndpointManaged(*e)) { log.LogInfo(L"Skipping Endpoint {}", e->deviceName); pendingRestoreIds_.erase(e->deviceId); continue; @@ -405,7 +405,7 @@ void VistaAudio::SetMute(bool mute) if (CheckForReInit()) { for (auto& e : endpoints_) { BOOL isMuted = !mute; - if (!IsEndpointManaged(e->deviceName)) { + if (!IsEndpointManaged(*e)) { log.LogInfo(L"Skipping Endpoint {}", e->deviceName); continue; } @@ -423,16 +423,21 @@ void VistaAudio::SetMute(bool mute) } } -bool VistaAudio::IsEndpointManaged(const std::wstring& endpointName) const +bool VistaAudio::IsEndpointManaged(const Endpoint& ep) const { if (!muteSpecificEndpoints_) { return true; } + // An entry that carries an id identifies exactly one device, so the + // friendly name is ignored for it. const bool inManagedEndpoints = - std::find(std::begin(managedEndpointNames_), - std::end(managedEndpointNames_), - endpointName) != std::end(managedEndpointNames_); + std::any_of(std::begin(managedEndpoints_), std::end(managedEndpoints_), + [&ep](const ManagedEndpoint& managed) { + return managed.id.empty() + ? managed.name == ep.deviceName + : managed.id == ep.deviceId; + }); // ------------+----------+-------------+ // | In List | Not in List | @@ -449,9 +454,9 @@ void VistaAudio::MuteSpecificEndpoints(bool muteSpecific) muteSpecificEndpoints_ = muteSpecific; } -void VistaAudio::SetManagedEndpoints(const std::vector& endpoints, - bool isAllowList) +void VistaAudio::SetManagedEndpoints( + const std::vector& endpoints, bool isAllowList) { - managedEndpointNames_ = endpoints; + managedEndpoints_ = endpoints; muteSpecificEndpointsAllowList_ = isAllowList; } diff --git a/WinMute/WMSettings.cpp b/WinMute/WMSettings.cpp index 9ce3334..cea0578 100644 --- a/WinMute/WMSettings.cpp +++ b/WinMute/WMSettings.cpp @@ -137,6 +137,9 @@ static const wchar_t* KeyToStr(SettingsKey key) case SettingsKey::CHECK_FOR_BETA_UPDATE: keyStr = L"CheckForBetaUpdate"; break; + case SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED: + keyStr = L"ManagedEndpointsIdMigrated"; + break; case SettingsKey::ENABLE_GLOBAL_MUTE_HOTKEY: keyStr = L"EnableGlobalMuteHotkey"; break; @@ -202,10 +205,42 @@ static DWORD GetDefaultSetting(SettingsKey key) return 0; case SettingsKey::CHECK_FOR_BETA_UPDATE: return 0; + case SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED: + return 0; } return 0; } +// Serialized form of a list entry: "|", or just +// "" when no id is bound. Endpoint ids always start with '{' +// (e.g. "{0.0.0.00000000}.{9f81ed6c-...}"), which is what tells the two apart +// -- a friendly name may well contain a '|', so the separator alone is not a +// reliable discriminator. +static std::wstring SerializeManagedEndpoint(const ManagedEndpoint& ep) +{ + return ep.id.empty() ? ep.name : ep.id + L"|" + ep.name; +} + +static ManagedEndpoint ParseManagedEndpoint(const std::wstring& stored) +{ + if (!stored.empty() && stored.front() == L'{') { + const size_t sep = stored.find(L'|'); + if (sep != std::wstring::npos) { + return {stored.substr(0, sep), stored.substr(sep + 1)}; + } + } + return {L"", stored}; +} + +static void NormalizeEndpointList(std::vector& items) +{ + if (items.size() > 1) { + std::sort(std::begin(items), std::end(items)); + auto it = std::unique(std::begin(items), std::end(items)); + items.resize(std::distance(std::begin(items), it)); + } +} + template static void NormalizeStringList(std::vector>& items) { @@ -793,7 +828,7 @@ std::vector> WMSettings::GetQuietHoursTimes() } bool WMSettings::StoreManagedAudioEndpoints( - std::vector& endpoints) + std::vector& endpoints) { // Clear all stored keys for (;;) { @@ -816,13 +851,13 @@ bool WMSettings::StoreManagedAudioEndpoints( } } - NormalizeStringList(endpoints); + NormalizeEndpointList(endpoints); for (size_t i = 0; i < endpoints.size(); ++i) { wchar_t valueName[25]; StringCchPrintfW(valueName, ARRAY_SIZE(valueName), L"Endpoint %03zu", i + 1); - const std::wstring& v = endpoints[i]; + const std::wstring v = SerializeManagedEndpoint(endpoints[i]); DWORD regError = RegSetValueEx(hAudioEndpointsKey_, valueName, 0, REG_SZ, reinterpret_cast(v.c_str()), @@ -836,12 +871,14 @@ bool WMSettings::StoreManagedAudioEndpoints( return true; } -std::vector WMSettings::GetManagedAudioEndpoints() const +std::vector WMSettings::GetManagedAudioEndpoints() const { - std::vector devices; + std::vector devices; for (int valIdx = 0;; ++valIdx) { wchar_t valueName[260] = {0}; - wchar_t dataBuf[260] = {0}; + // Holds "|": an endpoint id runs to about + // 55 characters and the name is capped at ENDPOINT_NAME_MAX_LEN (200), + wchar_t dataBuf[512] = {0}; DWORD valueSize = ARRAY_SIZE(valueName); DWORD valType = 0; DWORD dataLen = sizeof(dataBuf) - sizeof(wchar_t); @@ -855,9 +892,64 @@ std::vector WMSettings::GetManagedAudioEndpoints() const ShowWindowsError(L"RegEnumValue", regError); return {}; } else { - devices.push_back(dataBuf); + devices.push_back(ParseManagedEndpoint(dataBuf)); } } - NormalizeStringList(devices); + NormalizeEndpointList(devices); return devices; } + +void WMSettings::MigrateManagedEndpointIds() +{ + WMLog& log = WMLog::GetInstance(); + + if (QueryValue(SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED) != 0) { + return; + } + + auto endpoints = GetManagedAudioEndpoints(); + const bool anyUnbound = + std::any_of(endpoints.begin(), endpoints.end(), + [](const ManagedEndpoint& ep) { return ep.id.empty(); }); + if (!anyUnbound) { + SetValue(SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED, 1); + return; + } + + std::vector present; + if (!EnumerateAudioEndpoints(present)) { + log.LogError( + L"Could not enumerate audio endpoints;" + L" postponing endpoint id migration"); + return; + } + + int bound = 0; + for (auto& ep : endpoints) { + if (!ep.id.empty()) { + continue; + } + const auto match = + std::find_if(present.begin(), present.end(), + [&ep](const ManagedEndpoint& dev) { + return dev.name == ep.name; + }); + if (match != present.end()) { + ep.id = match->id; + ++bound; + log.LogInfo(L"Bound managed endpoint \"{}\" to id {}", ep.name, + ep.id); + } else { + log.LogInfo( + L"Managed endpoint \"{}\" is not present;" + L" it stays matched by name", + ep.name); + } + } + + if (bound == 0 || StoreManagedAudioEndpoints(endpoints)) { + SetValue(SettingsKey::MANAGED_ENDPOINTS_ID_MIGRATED, 1); + log.LogInfo(L"Endpoint id migration finished ({} of {} entries bound)", + bound, endpoints.size()); + } +} diff --git a/WinMute/WMSettings.h b/WinMute/WMSettings.h index 0c26769..f81dd6b 100644 --- a/WinMute/WMSettings.h +++ b/WinMute/WMSettings.h @@ -71,7 +71,12 @@ enum class SettingsKey { CHECK_FOR_UPDATE, CHECK_FOR_BETA_UPDATE, ENABLE_GLOBAL_MUTE_HOTKEY, - GLOBAL_MUTE_HOTKEY + GLOBAL_MUTE_HOTKEY, + // Set once the one-time "resolve stored endpoint names to endpoint ids" + // pass has run. This is deliberately not folded into SETTINGS_VERSION: + // MigrateSettings() runs from WMSettings::Init(), which happens before + // CoInitializeEx(), so it cannot enumerate audio devices. + MANAGED_ENDPOINTS_ID_MIGRATED }; class WMSettings { @@ -94,8 +99,13 @@ class WMSettings { bool StoreBluetoothDevices(std::vector& networks); std::vector GetBluetoothDevices() const; - bool StoreManagedAudioEndpoints(std::vector& endpoints); - std::vector GetManagedAudioEndpoints() const; + bool StoreManagedAudioEndpoints(std::vector& endpoints); + std::vector GetManagedAudioEndpoints() const; + + // Binds stored name-only entries to the endpoint id of a device with that + // name, for every such device present right now. Requires COM, so it must + // be called after CoInitializeEx. Runs at most once per installation. + void MigrateManagedEndpointIds(); bool StoreQuietHoursTimes( const std::vector>& times); diff --git a/WinMute/WinAudio.h b/WinMute/WinAudio.h index d6df7c0..5ff27e0 100644 --- a/WinMute/WinAudio.h +++ b/WinMute/WinAudio.h @@ -49,8 +49,8 @@ class WinAudio { virtual void RestoreArrivedEndpoints() = 0; virtual void SetMute(bool mute) = 0; virtual void MuteSpecificEndpoints(bool muteSpecific) = 0; - virtual void SetManagedEndpoints(const std::vector& endpoints, - bool isAllowList) = 0; + virtual void SetManagedEndpoints( + const std::vector& endpoints, bool isAllowList) = 0; virtual ~WinAudio() noexcept {}; }; @@ -89,7 +89,7 @@ class VistaAudio : public WinAudio { void SetMute(bool mute) override; void MuteSpecificEndpoints(bool muteSpecific) override; - void SetManagedEndpoints(const std::vector& endpoints, + void SetManagedEndpoints(const std::vector& endpoints, bool isAllowList) override; private: @@ -97,7 +97,7 @@ class VistaAudio : public WinAudio { bool CheckForReInit(); bool LoadAllEndpoints(); - bool IsEndpointManaged(const std::wstring& endpointName) const; + bool IsEndpointManaged(const Endpoint& ep) const; bool RestoreEndpoint(const Endpoint& ep, bool wasMuted); std::vector> endpoints_; @@ -119,7 +119,7 @@ class VistaAudio : public WinAudio { bool muteSpecificEndpointsAllowList_; HWND hParent_; - std::vector managedEndpointNames_; + std::vector managedEndpoints_; // non copy-able VistaAudio(const VistaAudio& other) = delete; diff --git a/WinMute/WinMute.cpp b/WinMute/WinMute.cpp index 41c96a9..e4e78d8 100644 --- a/WinMute/WinMute.cpp +++ b/WinMute/WinMute.cpp @@ -473,6 +473,9 @@ bool WinMute::LoadSettings() if (!settings_.QueryValue(SettingsKey::MUTE_INDIVIDUAL_ENDPOINTS)) { muteCtrl_.ClearManagedEndpoints(); } else { + // Not done in WMSettings::Init: this needs COM, which is only + // initialized once WinMute itself starts up. + settings_.MigrateManagedEndpointIds(); const auto endpoints = settings_.GetManagedAudioEndpoints(); const bool isAllowList = settings_.QueryValue(SettingsKey::MUTE_INDIVIDUAL_ENDPOINTS_MODE) == @@ -593,6 +596,8 @@ void WinMute::LoadMainMenuText() i18n_.GetTranslationW("traymenu.mute-on-logout"); menuText[ID_TRAYMENU_MUTE] = i18n_.GetTranslationW("traymenu.mute-all-devices"); + menuText[ID_TRAYMENU_SHOWLOG] = + i18n_.GetTranslationW("traymenu.show-log"); menuText[ID_TRAYMENU_SETTINGS] = i18n_.GetTranslationW("traymenu.settings"); menuText[ID_TRAYMENU_EXIT] = i18n_.GetTranslationW("traymenu.exit"); diff --git a/WinMute/WinMute.rc b/WinMute/WinMute.rc index 7583c4f..fc9f538 100644 --- a/WinMute/WinMute.rc +++ b/WinMute/WinMute.rc @@ -25,18 +25,18 @@ LANGUAGE LANG_GERMAN, SUBLANG_GERMAN // TEXTINCLUDE // -1 TEXTINCLUDE +1 TEXTINCLUDE BEGIN "resource.h\0" END -2 TEXTINCLUDE +2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END -3 TEXTINCLUDE +3 TEXTINCLUDE BEGIN "\r\n" "\0" @@ -226,24 +226,24 @@ BEGIN PUSHBUTTON "Cancel",IDCANCEL,144,41,50,14 END -IDD_MANAGE_ENDPOINTS DIALOGEX 0, 0, 241, 200 +IDD_MANAGE_ENDPOINTS DIALOGEX 0, 0, 246, 201 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Manage Endpoints" FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN - GROUPBOX "List behaviour",IDC_GROUP_LIST_BEHAVIOUR,7,7,227,41 + GROUPBOX "List behaviour",IDC_GROUP_LIST_BEHAVIOUR,7,7,232,41 CONTROL "Mute only listed endpoints",IDC_ENDPOINT_LIST_IS_ALLOWLIST, "Button",BS_AUTORADIOBUTTON,13,19,209,10 CONTROL "Mute all but the listed endpoints",IDC_ENDPOINT_LIST_IS_BLOCKLIST, "Button",BS_AUTORADIOBUTTON,13,32,207,10 - GROUPBOX "Endpoints",IDC_GROUP_ENDPOINTS,7,48,227,128 + GROUPBOX "Endpoints",IDC_GROUP_ENDPOINTS,7,48,232,128 LISTBOX IDC_ENDPOINT_LIST,13,60,156,110,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP - PUSHBUTTON "Add",IDC_ENDPOINT_ADD,175,60,59,16 - PUSHBUTTON "Edit",IDC_ENDPOINT_EDIT,175,78,59,16 - PUSHBUTTON "Remove",IDC_ENDPOINT_REMOVE,175,97,59,16 - PUSHBUTTON "Remove all",IDC_ENDPOINT_REMOVEALL,175,155,59,16 - DEFPUSHBUTTON "Save",IDOK,133,179,50,14 - PUSHBUTTON "Cancel",IDCANCEL,184,179,50,14 + PUSHBUTTON "Add",IDC_ENDPOINT_ADD,176,60,59,16 + PUSHBUTTON "Edit",IDC_ENDPOINT_EDIT,176,78,59,16 + PUSHBUTTON "Remove",IDC_ENDPOINT_REMOVE,176,97,59,16 + PUSHBUTTON "Remove all",IDC_ENDPOINT_REMOVEALL,176,155,59,16 + DEFPUSHBUTTON "Save",IDOK,133,180,50,14 + PUSHBUTTON "Cancel",IDCANCEL,189,180,50,14 END IDD_MANAGE_ENDPOINTS_ADD DIALOGEX 0, 0, 201, 62 @@ -252,7 +252,7 @@ CAPTION "Add Endpoint" FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN LTEXT "Endpoint name:",IDC_ENDPOINT_NAME_LABEL,7,7,187,14 - COMBOBOX IDC_ENDPOINT_NAME,7,22,187,17,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_ENDPOINT_NAME,7,22,187,17,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP DEFPUSHBUTTON "Save",IDOK,91,41,50,14 PUSHBUTTON "Cancel",IDCANCEL,144,41,50,14 END @@ -381,9 +381,9 @@ BEGIN IDD_MANAGE_ENDPOINTS, DIALOG BEGIN LEFTMARGIN, 7 - RIGHTMARGIN, 234 + RIGHTMARGIN, 239 TOPMARGIN, 7 - BOTTOMMARGIN, 193 + BOTTOMMARGIN, 194 END IDD_MANAGE_ENDPOINTS_ADD, DIALOG @@ -538,6 +538,8 @@ BEGIN MENUITEM SEPARATOR MENUITEM "Mute all devices", ID_TRAYMENU_MUTE MENUITEM SEPARATOR + MENUITEM "Show Log...", ID_TRAYMENU_SHOWLOG + MENUITEM SEPARATOR MENUITEM "Settings...", ID_TRAYMENU_SETTINGS MENUITEM SEPARATOR MENUITEM "Exit", ID_TRAYMENU_EXIT @@ -597,3 +599,4 @@ END ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED + diff --git a/WinMute/WinMute.vcxproj b/WinMute/WinMute.vcxproj index 42ed7b5..c99fe99 100644 --- a/WinMute/WinMute.vcxproj +++ b/WinMute/WinMute.vcxproj @@ -121,6 +121,7 @@ + diff --git a/WinMute/common.h b/WinMute/common.h index edf060c..78e5c04 100644 --- a/WinMute/common.h +++ b/WinMute/common.h @@ -116,6 +116,8 @@ namespace fs = std::filesystem; #include #pragma warning(default : 4201) +#include "ManagedEndpoint.hpp" + #include "BluetoothDetector.h" #include "MediaController.h" #include "MuteControl.h" diff --git a/WinMute/resource.h b/WinMute/resource.h index 60a1979..84fed3f 100644 --- a/WinMute/resource.h +++ b/WinMute/resource.h @@ -177,13 +177,15 @@ #define ID_TRAYMENU_MUTEONSCREE 40039 #define ID_TRAYMENU_LABEL_MUTEON_NO_RESTORE 40040 #define ID_TRAYMENU_LOG 40041 +#define ID_TRAYMENU_SHOWLOG 40042 +#define ID_TRAYMENU_40043 40043 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 146 -#define _APS_NEXT_COMMAND_VALUE 40042 +#define _APS_NEXT_COMMAND_VALUE 40044 #define _APS_NEXT_CONTROL_VALUE 1083 #define _APS_NEXT_SYMED_VALUE 101 #endif