]> repos.lx-s.de Git - winmute.git/commitdiff
Save Device-IDs instead of friendly names
authorlx-s <lx-s@users.noreply.github.com>
Tue, 18 Aug 2026 09:32:24 +0000 (11:32 +0200)
committerlx-s <lx-s@users.noreply.github.com>
Tue, 18 Aug 2026 09:32:24 +0000 (11:32 +0200)
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.

14 files changed:
WinMute/MuteControl.cpp
WinMute/MuteControl.h
WinMute/Settings_ManageEndpointsDlg.cpp
WinMute/Utility.cpp
WinMute/Utility.h
WinMute/VistaAudio.cpp
WinMute/WMSettings.cpp
WinMute/WMSettings.h
WinMute/WinAudio.h
WinMute/WinMute.cpp
WinMute/WinMute.rc
WinMute/WinMute.vcxproj
WinMute/common.h
WinMute/resource.h

index 7a565d9d18691e218810ac91b109d3972964297f..200c306d4947824f90e22b1b2b8bc5182e0c0f73 100644 (file)
@@ -545,7 +545,7 @@ void MuteControl::NotifyAudioDeviceArrived()
 }
 
 void MuteControl::SetManagedEndpoints(
-    const std::vector<std::wstring>& endpoints, bool isAllowList)
+    const std::vector<ManagedEndpoint>& endpoints, bool isAllowList)
 {
     winAudio_->MuteSpecificEndpoints(true);
     winAudio_->SetManagedEndpoints(endpoints, isAllowList);
index 07e9beb84cf42e46db7e35f6b0b446d391cd1967..400bec1178263411bd63fc1d14da70cec4de3c05 100644 (file)
@@ -100,7 +100,7 @@ class MuteControl {
 
     void NotifyAudioDeviceArrived();
 
-    void SetManagedEndpoints(const std::vector<std::wstring>& endpoints,
+    void SetManagedEndpoints(const std::vector<ManagedEndpoint>& endpoints,
                              bool isAllowList);
     void ClearManagedEndpoints();
 
index 119d702dd9371bdc3d9344db5c7ab95562ac3fa9..7e8b3cda632603644a99e3d156fdcebaf43e6572 100644 (file)
@@ -31,57 +31,42 @@ POSSIBILITY OF SUCH DAMAGE.
 -----------------------------------------------------------------------------
 */
 
-#include "common.h"
 #include <Functiondiscoverykeys_devpkey.h>
 #include <atlbase.h>
 #include <mmdeviceapi.h>
 
-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<ManagedEndpoint> candidates;
 };
 
-static bool GetAudioEndpoints(std::vector<std::wstring>& 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<IMMDeviceEnumerator> 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<IMMDeviceCollection> 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<IMMDevice> 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<ManagedEndpoint> entries;
+};
 
-        const auto deviceName = GetAudioDeviceName(device);
-        if (deviceName) {
-            endpoints.push_back(*deviceName);
-        }
+static void RebuildEndpointList(HWND hList,
+                                const std::vector<ManagedEndpoint>& 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<EndpointData*>(lParam);
-            if (endpointData == nullptr) {
+            EndpointAddCtx* ctx = reinterpret_cast<EndpointAddCtx*>(lParam);
+            if (ctx == nullptr || ctx->data == nullptr) {
                 return FALSE;
             }
-            SetWindowLongPtr(hDlg, DWLP_USER,
-                             reinterpret_cast<LONG_PTR>(endpointData));
+            SetWindowLongPtr(hDlg, DWLP_USER, reinterpret_cast<LONG_PTR>(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<std::wstring> 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<LONG_PTR>(i));
+                    }
                 }
             }
 
-            Edit_LimitText(hEndpointName, ENDPOINT_NAME_MAX_LEN);
-            if (GetDlgCtrlID(reinterpret_cast<HWND>(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<EndpointData*>(
-                        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<EndpointAddCtx*>(
+                    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<LONG_PTR>(ComboBox_GetItemData(hEpName, sel));
+                if (item != ENDPOINT_ITEM_LEGACY_NAME &&
+                    static_cast<size_t>(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<size_t>(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<std::wstring> ExportEndpointNameList(HWND hList)
-{
-    std::vector<std::wstring> 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<WMSettings*>(lParam);
             assert(settings != nullptr);
+            auto* state = new ManageEndpointsDlgState();
+            state->settings = settings;
             SetWindowLongPtr(hDlg, DWLP_USER,
-                             reinterpret_cast<LONG_PTR>(settings));
+                             reinterpret_cast<LONG_PTR>(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<ManageEndpointsDlgState*>(
+                GetWindowLongPtr(hDlg, DWLP_USER));
+            SetWindowLongPtr(hDlg, DWLP_USER, 0);
+            break;
+        }
         case WM_COMMAND: {
+            auto* state = reinterpret_cast<ManageEndpointsDlgState*>(
+                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<LPARAM>(&epData)) == 0)
+                                   reinterpret_cast<LPARAM>(&ctx)) == 0)
                 {
-                    std::vector<std::wstring> 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<size_t>(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<LPARAM>(&epData)) == 0)
+                const int sel = ListBox_GetCurSel(hList);
+                if (sel != LB_ERR &&
+                    static_cast<size_t>(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<LPARAM>(&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<int>(i) != sel &&
+                                state->entries[i].name == epData.name)
                             {
-                                std::vector<std::wstring> 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<size_t>(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<size_t>(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<WMSettings*>(
-                    GetWindowLongPtr(hDlg, DWLP_USER));
-                std::vector<std::wstring> 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)))
                 {
index ca74d7ee829c2ac067916da2683054d7ec8c3f1f..92ef18faf619200b581f55c391eb567a70ddbd51 100644 (file)
@@ -162,6 +162,48 @@ std::optional<std::wstring> GetAudioDeviceId(const CComPtr<IMMDevice>& devicePtr
     return id;
 }
 
+bool EnumerateAudioEndpoints(std::vector<ManagedEndpoint>& endpoints)
+{
+    WMLog& log = WMLog::GetInstance();
+
+    CComPtr<IMMDeviceEnumerator> 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<IMMDeviceCollection> 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<IMMDevice> 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
 // =========================================================================
index 5d3208d2e3f987f00a0c0cc8d3906435b3c9115e..12ceb515e5843dd8788fd906e5ab4a914b089486 100644 (file)
@@ -71,6 +71,10 @@ std::optional<std::wstring> GetAudioDeviceName(
 
 std::optional<std::wstring> GetAudioDeviceId(const CComPtr<IMMDevice>& 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<ManagedEndpoint>& endpoints);
+
 // =============================================================================
 // COM Helper
 template <class Interface>
index ba6a2835f3778c2615c0ce60daf93ef95d49f1b5..00d5a5386d40ff543ef02bb8afe431fab6182681 100644 (file)
@@ -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<std::wstring> 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<std::wstring>& endpoints,
-                                     bool isAllowList)
+void VistaAudio::SetManagedEndpoints(
+    const std::vector<ManagedEndpoint>& endpoints, bool isAllowList)
 {
-    managedEndpointNames_ = endpoints;
+    managedEndpoints_ = endpoints;
     muteSpecificEndpointsAllowList_ = isAllowList;
 }
index 9ce3334ab130de6c06424ea64f7d669e168f80af..cea0578b214a8e1208182a4b6a5e1a54744165a7 100644 (file)
@@ -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: "<endpoint id>|<friendly name>", or just
+// "<friendly name>" 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<ManagedEndpoint>& 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 <typename T>
 static void NormalizeStringList(std::vector<std::basic_string<T>>& items)
 {
@@ -793,7 +828,7 @@ std::vector<std::pair<DWORD, DWORD>> WMSettings::GetQuietHoursTimes()
 }
 
 bool WMSettings::StoreManagedAudioEndpoints(
-    std::vector<std::wstring>& endpoints)
+    std::vector<ManagedEndpoint>& 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<const BYTE*>(v.c_str()),
@@ -836,12 +871,14 @@ bool WMSettings::StoreManagedAudioEndpoints(
     return true;
 }
 
-std::vector<std::wstring> WMSettings::GetManagedAudioEndpoints() const
+std::vector<ManagedEndpoint> WMSettings::GetManagedAudioEndpoints() const
 {
-    std::vector<std::wstring> devices;
+    std::vector<ManagedEndpoint> devices;
     for (int valIdx = 0;; ++valIdx) {
         wchar_t valueName[260] = {0};
-        wchar_t dataBuf[260] = {0};
+        // Holds "<endpoint id>|<friendly name>": 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<std::wstring> 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<ManagedEndpoint> 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());
+    }
+}
index 0c2676911c9b71fc20714ddbc4593bc632e567af..f81dd6b29593118604b1bf84213e35ea5b46ede7 100644 (file)
@@ -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<std::wstring>& networks);
     std::vector<std::wstring> GetBluetoothDevices() const;
 
-    bool StoreManagedAudioEndpoints(std::vector<std::wstring>& endpoints);
-    std::vector<std::wstring> GetManagedAudioEndpoints() const;
+    bool StoreManagedAudioEndpoints(std::vector<ManagedEndpoint>& endpoints);
+    std::vector<ManagedEndpoint> 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<std::pair<DWORD, DWORD>>& times);
index d6df7c029ac99af9795f3c1c12f743ac326124df..5ff27e05ef1c59c53f6e3bcbdc8ec2e828dd567a 100644 (file)
@@ -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<std::wstring>& endpoints,
-                                     bool isAllowList) = 0;
+    virtual void SetManagedEndpoints(
+        const std::vector<ManagedEndpoint>& 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<std::wstring>& endpoints,
+    void SetManagedEndpoints(const std::vector<ManagedEndpoint>& 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<std::unique_ptr<Endpoint>> endpoints_;
@@ -119,7 +119,7 @@ class VistaAudio : public WinAudio {
     bool muteSpecificEndpointsAllowList_;
     HWND hParent_;
 
-    std::vector<std::wstring> managedEndpointNames_;
+    std::vector<ManagedEndpoint> managedEndpoints_;
 
     // non copy-able
     VistaAudio(const VistaAudio& other) = delete;
index 41c96a9f201572c5a4aa8b88d6aa37af0775cf11..e4e78d8bc4c5fead3afc0e5e2b3203b28646a5fd 100644 (file)
@@ -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");
 
index 7583c4fe2b6d103aff0a3af8b111edffcb2afce9..fc9f5384181f19b8959c1d56520c72d6293eae70 100644 (file)
@@ -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
+
index 42ed7b55109086cd3a1c3b66bf1fe2f74fed6b17..c99fe99b52c907bbbcaf8f7f6bb91a860de05335 100644 (file)
     <ClInclude Include="BluetoothDetector.h" />
     <ClInclude Include="common.h" />
     <ClInclude Include="libs\json.hpp" />
+    <ClInclude Include="ManagedEndpoint.hpp" />
     <ClInclude Include="MediaController.h" />
     <ClInclude Include="resource.h" />
     <ClInclude Include="UpdateChecker.h" />
index edf060c731f0691fbb089808ac76ab87190b25d7..78e5c0405ea541b486ee08136e7e23cf49316432 100644 (file)
@@ -116,6 +116,8 @@ namespace fs = std::filesystem;
 #include <endpointvolume.h>
 #pragma warning(default : 4201)
 
+#include "ManagedEndpoint.hpp"
+
 #include "BluetoothDetector.h"
 #include "MediaController.h"
 #include "MuteControl.h"
index 60a19791207f27b0d7f7d19ecc3ae306d8a7c5b2..84fed3f5dd5ac51e3b2840591859ac1cfe86c9f5 100644 (file)
 #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