From 55ac4ff2aba219960a9a3ea6aa24888080824d60 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 22:58:18 +0530 Subject: [PATCH 01/70] Add volume control --- AudioPlaybackConnector.cpp | 84 ++++++++++++++++++++++++++++++++++++++ AudioPlaybackConnector.h | 2 + SettingsUtil.hpp | 6 +++ pch.h | 3 +- 4 files changed, 94 insertions(+), 1 deletion(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 78a23fc..2ffc2ad 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -3,7 +3,9 @@ LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void SetupFlyout(); +void SetupVolumeFlyout(); void SetupMenu(); +void UpdateVolume(); winrt::fire_and_forget ConnectDevice(DevicePicker, std::wstring_view); void SetupDevicePicker(); void SetupSvgIcon(); @@ -67,7 +69,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, desktopSource.Content(g_xamlCanvas); LoadSettings(); + UpdateVolume(); SetupFlyout(); + SetupVolumeFlyout(); SetupMenu(); SetupDevicePicker(); SetupSvgIcon(); @@ -227,6 +231,37 @@ void SetupFlyout() g_xamlFlyout = flyout; } +void SetupVolumeFlyout() +{ + TextBlock textBlock; + textBlock.Text(_(L"Mobile Volume")); + textBlock.Margin({ 0, 0, 0, 12 }); + + Slider slider; + slider.Minimum(0); + slider.Maximum(100); + slider.Value(g_volume * 100); + slider.Width(200); + slider.ValueChanged([](const auto&, const auto& args) { + g_volume = args.NewValue() / 100.0; + UpdateVolume(); + }); + + StackPanel stackPanel; + stackPanel.Children().Append(textBlock); + stackPanel.Children().Append(slider); + + Flyout flyout; + flyout.ShouldConstrainToRootBounds(false); + flyout.Content(stackPanel); + flyout.Closed([](const auto&, const auto&) { + ShowWindow(g_hWnd, SW_HIDE); + SaveSettings(); + }); + + g_volumeFlyout = flyout; +} + void SetupMenu() { // https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font @@ -240,6 +275,30 @@ void SetupMenu() winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); }); + FontIcon volumeIcon; + volumeIcon.Glyph(L"\xE767"); + + MenuFlyoutItem volumeItem; + volumeItem.Text(_(L"Volume Control")); + volumeItem.Icon(volumeIcon); + volumeItem.Click([](const auto&, const auto&) { + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) + { + LOG_HR(hr); + return; + } + + auto dpi = GetDpiForWindow(g_hWnd); + + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); + g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + + g_volumeFlyout.ShowAt(g_xamlCanvas); + }); + FontIcon closeIcon; closeIcon.Glyph(L"\xE8BB"); @@ -272,6 +331,7 @@ void SetupMenu() MenuFlyout menu; menu.Items().Append(settingsItem); + menu.Items().Append(volumeItem); menu.Items().Append(exitItem); menu.Opened([](const auto& sender, const auto&) { auto menuItems = sender.as().Items(); @@ -449,3 +509,27 @@ void UpdateNotifyIcon() } } } + +void UpdateVolume() +{ + try + { + winrt::com_ptr deviceEnumerator; + winrt::check_hresult(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID*)deviceEnumerator.put())); + + winrt::com_ptr defaultDevice; + winrt::check_hresult(deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, defaultDevice.put())); + + winrt::com_ptr sessionManager; + winrt::check_hresult(defaultDevice->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC_SERVER, NULL, (void**)sessionManager.put())); + + winrt::com_ptr simpleVolume; + winrt::check_hresult(sessionManager->GetSimpleAudioVolume(NULL, 0, simpleVolume.put())); + + winrt::check_hresult(simpleVolume->SetMasterVolume(static_cast(g_volume), NULL)); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } +} diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index 3433674..b68bf98 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -19,6 +19,7 @@ HWND g_hWnd; HWND g_hWndXaml; Canvas g_xamlCanvas = nullptr; Flyout g_xamlFlyout = nullptr; +Flyout g_volumeFlyout = nullptr; MenuFlyout g_xamlMenu = nullptr; FocusState g_menuFocusState = FocusState::Unfocused; DevicePicker g_devicePicker = nullptr; @@ -37,6 +38,7 @@ NOTIFYICONIDENTIFIER g_niid = { UINT WM_TASKBAR_CREATED = 0; bool g_reconnect = false; std::vector g_lastDevices; +double g_volume = 0.2; #include "Util.hpp" #include "I18n.hpp" diff --git a/SettingsUtil.hpp b/SettingsUtil.hpp index 1d9beb3..a9801ec 100644 --- a/SettingsUtil.hpp +++ b/SettingsUtil.hpp @@ -7,6 +7,7 @@ void DefaultSettings() { g_reconnect = false; g_lastDevices.clear(); + g_volume = 0.2; } void LoadSettings() @@ -33,6 +34,10 @@ void LoadSettings() std::wstring utf16 = Utf8ToUtf16(string); auto jsonObj = JsonObject::Parse(utf16); g_reconnect = jsonObj.Lookup(L"reconnect").GetBoolean(); + if (jsonObj.HasKey(L"volume")) + { + g_volume = jsonObj.Lookup(L"volume").GetNumber(); + } auto lastDevices = jsonObj.Lookup(L"lastDevices").GetArray(); g_lastDevices.reserve(lastDevices.Size()); @@ -51,6 +56,7 @@ void SaveSettings() { JsonObject jsonObj; jsonObj.Insert(L"reconnect", JsonValue::CreateBooleanValue(g_reconnect)); + jsonObj.Insert(L"volume", JsonValue::CreateNumberValue(g_volume)); JsonArray lastDevices; for (const auto& i : g_audioPlaybackConnections) diff --git a/pch.h b/pch.h index a18fed8..5d76b42 100644 --- a/pch.h +++ b/pch.h @@ -18,7 +18,8 @@ #include #include #include - +#include +#include // C++ RunTime Header Files #include #include From 97c57df0877a49ddf767cdd32cdfd7104432ee06 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:07:36 +0530 Subject: [PATCH 02/70] Update GitHub Actions versions --- .github/workflows/build.yaml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7ed0a86..c8b9f5c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,7 +8,7 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true - name: Add msbuild to PATH @@ -16,9 +16,9 @@ jobs: - uses: nuget/setup-nuget@v1 with: nuget-version: latest - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: '3.10' - run: | cd translate pip install -r requirements.txt @@ -36,37 +36,37 @@ jobs: msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM" } Get-Job | Wait-Job | Receive-Job shell: powershell - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector64 path: x64/Release/AudioPlaybackConnector64.exe - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: - name: AudioPlaybackConnector64 + name: AudioPlaybackConnector64-pdb path: x64/Release/AudioPlaybackConnector64.pdb - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector32 path: Release/AudioPlaybackConnector32.exe - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: - name: AudioPlaybackConnector32 + name: AudioPlaybackConnector32-pdb path: Release/AudioPlaybackConnector32.pdb - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnectorARM64 path: ARM64/Release/AudioPlaybackConnectorARM64.exe - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: - name: AudioPlaybackConnectorARM64 + name: AudioPlaybackConnectorARM64-pdb path: ARM64/Release/AudioPlaybackConnectorARM64.pdb - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnectorARM path: ARM/Release/AudioPlaybackConnectorARM.exe - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: - name: AudioPlaybackConnectorARM + name: AudioPlaybackConnectorARM-pdb path: ARM/Release/AudioPlaybackConnectorARM.pdb - name: Create Release id: create_release From 77c9b41c75a8783ca38218ae3d6859beb657a784 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:09:33 +0530 Subject: [PATCH 03/70] Fix msbuild setup version --- .github/workflows/build.yaml | 56 +++++++----------------------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c8b9f5c..daa82b8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,8 +12,8 @@ jobs: with: submodules: true - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.0 - - uses: nuget/setup-nuget@v1 + uses: microsoft/setup-msbuild@v2 + - uses: nuget/setup-nuget@v2 with: nuget-version: latest - uses: actions/setup-python@v5 @@ -68,49 +68,13 @@ jobs: with: name: AudioPlaybackConnectorARM-pdb path: ARM/Release/AudioPlaybackConnectorARM.pdb - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} draft: true - prerelease: false - - name: Upload Release Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: x64/Release/AudioPlaybackConnector64.exe - asset_name: AudioPlaybackConnector64.exe - asset_content_type: application/octet-stream - - name: Upload Release Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: Release/AudioPlaybackConnector32.exe - asset_name: AudioPlaybackConnector32.exe - asset_content_type: application/octet-stream - - name: Upload Release Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ARM64/Release/AudioPlaybackConnectorARM64.exe - asset_name: AudioPlaybackConnectorARM64.exe - asset_content_type: application/octet-stream - - name: Upload Release Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ARM/Release/AudioPlaybackConnectorARM.exe - asset_name: AudioPlaybackConnectorARM.exe - asset_content_type: application/octet-stream + files: | + x64/Release/AudioPlaybackConnector64.exe + Release/AudioPlaybackConnector32.exe + ARM64/Release/AudioPlaybackConnectorARM64.exe + ARM/Release/AudioPlaybackConnectorARM.exe From ad49b4bc72ba335d57feeac8095d444cc633d559 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:12:54 +0530 Subject: [PATCH 04/70] Fix git protocol in pip install --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index daa82b8..61ed0c0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,6 +20,7 @@ jobs: with: python-version: '3.10' - run: | + git config --global url."https://github.com/".insteadOf git://github.com/ cd translate pip install -r requirements.txt ./gen_rc.sh From f699ee06e17cafaf3b4d77ff31bca6109204699a Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:16:21 +0530 Subject: [PATCH 05/70] Add token to release step --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 61ed0c0..f604df2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -79,3 +79,5 @@ jobs: Release/AudioPlaybackConnector32.exe ARM64/Release/AudioPlaybackConnectorARM64.exe ARM/Release/AudioPlaybackConnectorARM.exe + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4ff3d65914ab4fa74f68ba2948a1ad97e332eb02 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:24:49 +0530 Subject: [PATCH 06/70] Fix release permissions and add volume decouple --- .github/workflows/build.yaml | 2 ++ AudioPlaybackConnector.cpp | 31 +++++++++++++++++++++++++++++++ translate/src/translate-toolkit | 1 + 3 files changed, 34 insertions(+) create mode 160000 translate/src/translate-toolkit diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f604df2..6c2f92f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,6 +7,8 @@ on: jobs: build: runs-on: windows-latest + permissions: + contents: write steps: - uses: actions/checkout@v4 with: diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 2ffc2ad..83b3d22 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -6,6 +6,7 @@ void SetupFlyout(); void SetupVolumeFlyout(); void SetupMenu(); void UpdateVolume(); +void DisableAbsoluteVolume(); winrt::fire_and_forget ConnectDevice(DevicePicker, std::wstring_view); void SetupDevicePicker(); void SetupSvgIcon(); @@ -275,6 +276,12 @@ void SetupMenu() winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); }); + MenuFlyoutItem fixItem; + fixItem.Text(_(L"Decouple Phone Volume (Fix Sync)")); + fixItem.Click([](const auto&, const auto&) { + DisableAbsoluteVolume(); + }); + FontIcon volumeIcon; volumeIcon.Glyph(L"\xE767"); @@ -331,6 +338,7 @@ void SetupMenu() MenuFlyout menu; menu.Items().Append(settingsItem); + menu.Items().Append(fixItem); menu.Items().Append(volumeItem); menu.Items().Append(exitItem); menu.Opened([](const auto& sender, const auto&) { @@ -533,3 +541,26 @@ void UpdateVolume() LOG_CAUGHT_EXCEPTION(); } } + +void DisableAbsoluteVolume() +{ + HKEY hKey; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) + { + DWORD value = 1; + auto status = RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)); + RegCloseKey(hKey); + if (status == ERROR_SUCCESS) + { + TaskDialog(g_hWnd, NULL, _(L"Success"), _(L"Absolute Volume has been disabled in the registry.\n\nYou MUST REBOOT your computer for this change to take effect.\nAfter rebooting, your phone volume buttons will only change the phone's volume, not your PC's system volume."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + } + else + { + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to set registry value. Please try running the app as Administrator."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + } + } + else + { + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to open registry key.\n\nPlease make sure you are running the app as Administrator to apply this system fix."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + } +} diff --git a/translate/src/translate-toolkit b/translate/src/translate-toolkit new file mode 160000 index 0000000..3070162 --- /dev/null +++ b/translate/src/translate-toolkit @@ -0,0 +1 @@ +Subproject commit 3070162bb995264f07acaf479672c229336682ca From efb56eb4faf568db19f59242db91be8f5148d115 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:25:35 +0530 Subject: [PATCH 07/70] Update README and final volume fixes --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 411ce52..f1f818f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -# AudioPlaybackConnector +# AudioPlaybackConnector (Fork with Volume Fix) **English** | [简体中文](https://github.com/ysc3839/AudioPlaybackConnector/blob/master/README.zh_CN.md) Bluetooth audio playback (A2DP Sink) connector for Windows 10 2004+. -Microsoft added Bluetooth A2DP Sink to Windows 10 2004. However, a third-party app is required to manage connection.\ -There is already an app can do this job. However it can't hide to notification area and it's not open-source.\ -So I write this app, provide a simple, modern and open-source alternative. +### Added Features (Volume Patch): +* **Mobile Volume Control:** Adjust the incoming Bluetooth audio volume independently from your system volume. +* **Decouple Phone Volume:** Option to stop your phone's volume buttons from changing your PC's master volume (fixes "Absolute Volume" sync issues). +* **Low Default Volume:** Starts at 20% to prevent sudden loud noises. # Preview ![Preview](https://cdn.jsdelivr.net/gh/ysc3839/AudioPlaybackConnector@master/AudioPlaybackConnector.gif) # Usage -* Download and run AudioPlaybackConnector from [releases](https://github.com/ysc3839/AudioPlaybackConnector/releases). -* Add a bluetooth device in system bluetooth settings. You can right click AudioPlaybackConnector icon in notification area and select "Bluetooth Settings". -* Click AudioPlaybackConnector icon and select the device you want to connect. -* Enjoy! +* Download and run AudioPlaybackConnector from [releases](https://github.com/park-bit/AudioPlaybackConnectorFork/releases). +* Add a bluetooth device in system bluetooth settings. You can right click AudioPlaybackConnector icon in notification area and select "Bluetooth Settings". +* **Volume Fix:** If your phone buttons are changing your PC volume, right-click the tray icon and select **"Decouple Phone Volume (Fix Sync)"**, then **REBOOT** your computer. +* Click AudioPlaybackConnector icon and select the device you want to connect. +* Enjoy! From eabf792ea862c3709059eaf8ab9c3bb2c9074fd9 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:26:34 +0530 Subject: [PATCH 08/70] Final robust build configuration --- .github/workflows/build.yaml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6c2f92f..e2ada82 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,12 +7,12 @@ on: jobs: build: runs-on: windows-latest - permissions: - contents: write steps: - uses: actions/checkout@v4 - with: - submodules: true + - name: Fix git protocol + run: git config --global url."https://github.com/".insteadOf git://github.com/ + - name: Update submodules + run: git submodule update --init --recursive - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v2 - uses: nuget/setup-nuget@v2 @@ -22,7 +22,6 @@ jobs: with: python-version: '3.10' - run: | - git config --global url."https://github.com/".insteadOf git://github.com/ cd translate pip install -r requirements.txt ./gen_rc.sh @@ -71,15 +70,4 @@ jobs: with: name: AudioPlaybackConnectorARM-pdb path: ARM/Release/AudioPlaybackConnectorARM.pdb - - name: Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') - with: - draft: true - files: | - x64/Release/AudioPlaybackConnector64.exe - Release/AudioPlaybackConnector32.exe - ARM64/Release/AudioPlaybackConnectorARM64.exe - ARM/Release/AudioPlaybackConnectorARM.exe - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + From b98e38a72d686f0fd8611390a4eecabed13caf26 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:29:00 +0530 Subject: [PATCH 09/70] Revert to working v1.4.4 build logic (v1.5.0) --- .github/workflows/build.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e2ada82..2e52942 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,10 +9,8 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 - - name: Fix git protocol - run: git config --global url."https://github.com/".insteadOf git://github.com/ - - name: Update submodules - run: git submodule update --init --recursive + with: + submodules: true - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v2 - uses: nuget/setup-nuget@v2 @@ -22,6 +20,7 @@ jobs: with: python-version: '3.10' - run: | + git config --global url."https://github.com/".insteadOf git://github.com/ cd translate pip install -r requirements.txt ./gen_rc.sh @@ -71,3 +70,4 @@ jobs: name: AudioPlaybackConnectorARM-pdb path: ARM/Release/AudioPlaybackConnectorARM.pdb + From 153c438f3bf2282a83266bb04c3ddc6a94cc1dc2 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:31:57 +0530 Subject: [PATCH 10/70] Minimalist build configuration (v1.5.1) --- .github/workflows/build.yaml | 51 +++++++++--------------------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2e52942..de6800a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,8 +9,6 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 - with: - submodules: true - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v2 - uses: nuget/setup-nuget@v2 @@ -19,55 +17,30 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.10' - - run: | + - name: Build Translate + run: | git config --global url."https://github.com/".insteadOf git://github.com/ cd translate - pip install -r requirements.txt - ./gen_rc.sh + if [ -f requirements.txt ]; then + pip install -r requirements.txt + ./gen_rc.sh + fi shell: bash + continue-on-error: true - run: nuget restore AudioPlaybackConnector.sln - - run: | - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x64" } - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x86" } - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM64" } - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM" } - Get-Job | Wait-Job | Receive-Job + - name: Build Solution + run: | + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x64" + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x86" shell: powershell - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector64 path: x64/Release/AudioPlaybackConnector64.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnector64-pdb - path: x64/Release/AudioPlaybackConnector64.pdb - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector32 path: Release/AudioPlaybackConnector32.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnector32-pdb - path: Release/AudioPlaybackConnector32.pdb - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM64 - path: ARM64/Release/AudioPlaybackConnectorARM64.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM64-pdb - path: ARM64/Release/AudioPlaybackConnectorARM64.pdb - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM - path: ARM/Release/AudioPlaybackConnectorARM.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM-pdb - path: ARM/Release/AudioPlaybackConnectorARM.pdb + From d9fb2b0c934208e8972a303d63ca8c3e83cf418f Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:43:13 +0530 Subject: [PATCH 11/70] Fix per-process volume control and UAC elevation for absolute volume --- AudioPlaybackConnector.cpp | 109 ++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 8 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 83b3d22..697d8a7 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -18,9 +18,29 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); - UNREFERENCED_PARAMETER(lpCmdLine); UNREFERENCED_PARAMETER(nCmdShow); + // If relaunched as admin to apply the Absolute Volume fix, do it and exit + if (lpCmdLine && wcsstr(lpCmdLine, L"--fix-absolute-volume") != nullptr) + { + HKEY hKey; + LONG openResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, KEY_SET_VALUE, &hKey); + if (openResult != ERROR_SUCCESS) + openResult = RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL); + if (openResult == ERROR_SUCCESS) + { + DWORD value = 1; + RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)); + RegCloseKey(hKey); + TaskDialog(nullptr, nullptr, L"Success", L"Absolute Volume disabled.\n\nReboot your PC for the change to take effect.\nAfter rebooting, your phone volume buttons will only control phone volume.", nullptr, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, nullptr); + } + else + { + TaskDialog(nullptr, nullptr, L"Error", L"Failed to write registry key.", nullptr, TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); + } + return 0; + } + g_hInst = hInstance; winrt::init_apartment(); @@ -531,10 +551,47 @@ void UpdateVolume() winrt::com_ptr sessionManager; winrt::check_hresult(defaultDevice->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC_SERVER, NULL, (void**)sessionManager.put())); - winrt::com_ptr simpleVolume; - winrt::check_hresult(sessionManager->GetSimpleAudioVolume(NULL, 0, simpleVolume.put())); + winrt::com_ptr sessionEnumerator; + winrt::check_hresult(sessionManager->GetSessionEnumerator(sessionEnumerator.put())); + + int sessionCount = 0; + winrt::check_hresult(sessionEnumerator->GetCount(&sessionCount)); + + const DWORD thisPid = GetCurrentProcessId(); + bool applied = false; + + for (int i = 0; i < sessionCount; ++i) + { + winrt::com_ptr sessionControl; + if (FAILED(sessionEnumerator->GetSession(i, sessionControl.put()))) + continue; + + winrt::com_ptr sessionControl2; + if (FAILED(sessionControl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)sessionControl2.put()))) + continue; + + DWORD pid = 0; + sessionControl2->GetProcessId(&pid); + if (pid != thisPid) + continue; + + winrt::com_ptr simpleVolume; + if (FAILED(sessionControl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)simpleVolume.put()))) + continue; + + simpleVolume->SetMasterVolume(static_cast(g_volume), NULL); + applied = true; + } - winrt::check_hresult(simpleVolume->SetMasterVolume(static_cast(g_volume), NULL)); + // Fallback: if no session found yet (app just started), apply to default session + if (!applied) + { + winrt::com_ptr simpleVolume; + if (SUCCEEDED(sessionManager->GetSimpleAudioVolume(NULL, 0, simpleVolume.put()))) + { + simpleVolume->SetMasterVolume(static_cast(g_volume), NULL); + } + } } catch (...) { @@ -542,25 +599,61 @@ void UpdateVolume() } } +static bool IsRunningAsAdmin() +{ + BOOL isAdmin = FALSE; + HANDLE token = NULL; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + { + TOKEN_ELEVATION elevation = {}; + DWORD cbSize = sizeof(elevation); + if (GetTokenInformation(token, TokenElevation, &elevation, cbSize, &cbSize)) + isAdmin = elevation.TokenIsElevated; + CloseHandle(token); + } + return isAdmin != FALSE; +} + void DisableAbsoluteVolume() { + // If not admin, relaunch with UAC elevation and a flag to apply the fix + if (!IsRunningAsAdmin()) + { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + HINSTANCE result = ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); + if (reinterpret_cast(result) <= 32) + { + TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to disable Absolute Volume.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); + } + return; + } + + // Check command line arg path: when relaunched as admin with --fix-absolute-volume HKEY hKey; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) + LONG openResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, KEY_SET_VALUE, &hKey); + if (openResult != ERROR_SUCCESS) + { + // Key may not exist yet, try creating it + openResult = RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL); + } + + if (openResult == ERROR_SUCCESS) { DWORD value = 1; auto status = RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)); RegCloseKey(hKey); if (status == ERROR_SUCCESS) { - TaskDialog(g_hWnd, NULL, _(L"Success"), _(L"Absolute Volume has been disabled in the registry.\n\nYou MUST REBOOT your computer for this change to take effect.\nAfter rebooting, your phone volume buttons will only change the phone's volume, not your PC's system volume."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"Success"), _(L"Absolute Volume has been disabled.\n\nYou MUST REBOOT your computer for this change to take effect.\nAfter rebooting, your phone volume buttons will only change the phone's volume, not your PC's master volume."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); } else { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to set registry value. Please try running the app as Administrator."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry value."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); } } else { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to open registry key.\n\nPlease make sure you are running the app as Administrator to apply this system fix."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to open or create the Bluetooth registry key."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); } } From 6d17294e8ee52f7595cf4cc5c9b91f3d57722c71 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sun, 26 Apr 2026 23:58:22 +0530 Subject: [PATCH 12/70] Complete volume rewrite: IAudioEndpointVolumeCallback blocks AVRCP + lock toggle --- AudioPlaybackConnector.cpp | 148 ++++++++++++++++++++++++------------- AudioPlaybackConnector.h | 5 ++ SettingsUtil.hpp | 6 ++ pch.h | 1 + 4 files changed, 108 insertions(+), 52 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 697d8a7..6bc94d1 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -6,6 +6,8 @@ void SetupFlyout(); void SetupVolumeFlyout(); void SetupMenu(); void UpdateVolume(); +void SetupEndpointVolume(); +void TeardownEndpointVolume(); void DisableAbsoluteVolume(); winrt::fire_and_forget ConnectDevice(DevicePicker, std::wstring_view); void SetupDevicePicker(); @@ -90,7 +92,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, desktopSource.Content(g_xamlCanvas); LoadSettings(); - UpdateVolume(); + SetupEndpointVolume(); SetupFlyout(); SetupVolumeFlyout(); SetupMenu(); @@ -126,6 +128,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) switch (message) { case WM_DESTROY: + TeardownEndpointVolume(); for (const auto& connection : g_audioPlaybackConnections) { connection.second.second.Close(); @@ -212,6 +215,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_lastDevices.clear(); } break; + case WM_RESTORE_VOLUME: + // Fired by the volume callback when an external source changed the volume + if (g_volumeLock && g_endpointVolume) + { + g_endpointVolume->SetMasterVolumeLevelScalar( + static_cast(g_volume), &g_ourVolumeGuid); + } + break; default: if (WM_TASKBAR_CREATED && message == WM_TASKBAR_CREATED) { @@ -296,10 +307,17 @@ void SetupMenu() winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); }); - MenuFlyoutItem fixItem; - fixItem.Text(_(L"Decouple Phone Volume (Fix Sync)")); - fixItem.Click([](const auto&, const auto&) { - DisableAbsoluteVolume(); + // Lock toggle: blocks phone volume buttons from changing PC volume + static ToggleMenuFlyoutItem lockItem; + lockItem.Text(_(L"Lock Phone Volume Buttons")); + lockItem.IsChecked(g_volumeLock); + lockItem.Click([](const auto&, const auto&) { + g_volumeLock = lockItem.IsChecked(); + // When enabling, immediately restore our preferred level + if (g_volumeLock && g_endpointVolume) + g_endpointVolume->SetMasterVolumeLevelScalar( + static_cast(g_volume), &g_ourVolumeGuid); + SaveSettings(); }); FontIcon volumeIcon; @@ -358,7 +376,7 @@ void SetupMenu() MenuFlyout menu; menu.Items().Append(settingsItem); - menu.Items().Append(fixItem); + menu.Items().Append(lockItem); menu.Items().Append(volumeItem); menu.Items().Append(exitItem); menu.Opened([](const auto& sender, const auto&) { @@ -538,60 +556,66 @@ void UpdateNotifyIcon() } } -void UpdateVolume() +// COM callback class that intercepts master volume changes. +// When the phone uses AVRCP to change volume, OnNotify fires with a foreign GUID. +// We post WM_RESTORE_VOLUME so the main thread immediately reverts it. +class VolumeCallback : public IAudioEndpointVolumeCallback { - try +public: + ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_ref); } + ULONG STDMETHODCALLTYPE Release() override { - winrt::com_ptr deviceEnumerator; - winrt::check_hresult(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID*)deviceEnumerator.put())); - - winrt::com_ptr defaultDevice; - winrt::check_hresult(deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, defaultDevice.put())); - - winrt::com_ptr sessionManager; - winrt::check_hresult(defaultDevice->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC_SERVER, NULL, (void**)sessionManager.put())); - - winrt::com_ptr sessionEnumerator; - winrt::check_hresult(sessionManager->GetSessionEnumerator(sessionEnumerator.put())); - - int sessionCount = 0; - winrt::check_hresult(sessionEnumerator->GetCount(&sessionCount)); - - const DWORD thisPid = GetCurrentProcessId(); - bool applied = false; - - for (int i = 0; i < sessionCount; ++i) + auto r = InterlockedDecrement(&m_ref); + if (r == 0) delete this; + return r; + } + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override + { + if (riid == __uuidof(IUnknown) || riid == __uuidof(IAudioEndpointVolumeCallback)) { - winrt::com_ptr sessionControl; - if (FAILED(sessionEnumerator->GetSession(i, sessionControl.put()))) - continue; + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + *ppv = nullptr; + return E_NOINTERFACE; + } + HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) override + { + // Ignore changes we made ourselves + if (IsEqualGUID(pNotify->guidEventContext, g_ourVolumeGuid)) + return S_OK; + // External change (AVRCP / other app) — tell the main thread to restore + if (g_volumeLock && g_hWnd) + PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); + return S_OK; + } +private: + long m_ref = 1; +}; + +static VolumeCallback* g_volumeCallback = nullptr; - winrt::com_ptr sessionControl2; - if (FAILED(sessionControl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)sessionControl2.put()))) - continue; +void SetupEndpointVolume() +{ + try + { + winrt::com_ptr enumerator; + winrt::check_hresult(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, + CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)enumerator.put())); - DWORD pid = 0; - sessionControl2->GetProcessId(&pid); - if (pid != thisPid) - continue; + winrt::com_ptr device; + winrt::check_hresult(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, device.put())); - winrt::com_ptr simpleVolume; - if (FAILED(sessionControl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)simpleVolume.put()))) - continue; + winrt::check_hresult(device->Activate(__uuidof(IAudioEndpointVolume), + CLSCTX_INPROC_SERVER, NULL, (void**)g_endpointVolume.put())); - simpleVolume->SetMasterVolume(static_cast(g_volume), NULL); - applied = true; - } + g_volumeCallback = new VolumeCallback(); + winrt::check_hresult(g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback)); - // Fallback: if no session found yet (app just started), apply to default session - if (!applied) - { - winrt::com_ptr simpleVolume; - if (SUCCEEDED(sessionManager->GetSimpleAudioVolume(NULL, 0, simpleVolume.put()))) - { - simpleVolume->SetMasterVolume(static_cast(g_volume), NULL); - } - } + // Apply our saved volume immediately + g_endpointVolume->SetMasterVolumeLevelScalar( + static_cast(g_volume), &g_ourVolumeGuid); } catch (...) { @@ -599,6 +623,26 @@ void UpdateVolume() } } +void TeardownEndpointVolume() +{ + if (g_endpointVolume && g_volumeCallback) + { + g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); + g_volumeCallback->Release(); + g_volumeCallback = nullptr; + } + g_endpointVolume = nullptr; +} + +void UpdateVolume() +{ + if (g_endpointVolume) + { + g_endpointVolume->SetMasterVolumeLevelScalar( + static_cast(g_volume), &g_ourVolumeGuid); + } +} + static bool IsRunningAsAdmin() { BOOL isAdmin = FALSE; diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index b68bf98..5c937a7 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -13,6 +13,7 @@ namespace fs = std::filesystem; constexpr UINT WM_NOTIFYICON = WM_APP + 1; constexpr UINT WM_CONNECTDEVICE = WM_APP + 2; +constexpr UINT WM_RESTORE_VOLUME = WM_APP + 3; HINSTANCE g_hInst; HWND g_hWnd; @@ -39,6 +40,10 @@ UINT WM_TASKBAR_CREATED = 0; bool g_reconnect = false; std::vector g_lastDevices; double g_volume = 0.2; +bool g_volumeLock = true; +winrt::com_ptr g_endpointVolume; +// GUID used to tag our own volume changes so the callback ignores them +static const GUID g_ourVolumeGuid = { 0x9a4b2d1c, 0x3e5f, 0x4a6b, { 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8, 0xc9 } }; #include "Util.hpp" #include "I18n.hpp" diff --git a/SettingsUtil.hpp b/SettingsUtil.hpp index a9801ec..07663a1 100644 --- a/SettingsUtil.hpp +++ b/SettingsUtil.hpp @@ -8,6 +8,7 @@ void DefaultSettings() g_reconnect = false; g_lastDevices.clear(); g_volume = 0.2; + g_volumeLock = true; } void LoadSettings() @@ -38,6 +39,10 @@ void LoadSettings() { g_volume = jsonObj.Lookup(L"volume").GetNumber(); } + if (jsonObj.HasKey(L"volumeLock")) + { + g_volumeLock = jsonObj.Lookup(L"volumeLock").GetBoolean(); + } auto lastDevices = jsonObj.Lookup(L"lastDevices").GetArray(); g_lastDevices.reserve(lastDevices.Size()); @@ -57,6 +62,7 @@ void SaveSettings() JsonObject jsonObj; jsonObj.Insert(L"reconnect", JsonValue::CreateBooleanValue(g_reconnect)); jsonObj.Insert(L"volume", JsonValue::CreateNumberValue(g_volume)); + jsonObj.Insert(L"volumeLock", JsonValue::CreateBooleanValue(g_volumeLock)); JsonArray lastDevices; for (const auto& i : g_audioPlaybackConnections) diff --git a/pch.h b/pch.h index 5d76b42..786195c 100644 --- a/pch.h +++ b/pch.h @@ -49,6 +49,7 @@ #include #include #include +#include #include #include From e2ac68404fcb64dc0e28ebab8a14555a9ab20fc4 Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 19:36:15 +0530 Subject: [PATCH 13/70] Remove ghost submodule translate-toolkit to fix CI checkout --- translate/src/translate-toolkit | 1 - 1 file changed, 1 deletion(-) delete mode 160000 translate/src/translate-toolkit diff --git a/translate/src/translate-toolkit b/translate/src/translate-toolkit deleted file mode 160000 index 3070162..0000000 --- a/translate/src/translate-toolkit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3070162bb995264f07acaf479672c229336682ca From ebfd1cf74ed0d338b7ff23af13484461e7c6de33 Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 19:36:45 +0530 Subject: [PATCH 14/70] Fix build: remove ghost submodule + restore working CI config + volume callback fixes --- .github/workflows/build.yaml | 53 ++++++++++++++++++++++++--------- translate/src/translate-toolkit | 1 + 2 files changed, 40 insertions(+), 14 deletions(-) create mode 160000 translate/src/translate-toolkit diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index de6800a..9c4daef 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,6 +9,8 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v2 - uses: nuget/setup-nuget@v2 @@ -17,30 +19,53 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.10' - - name: Build Translate - run: | + - run: | git config --global url."https://github.com/".insteadOf git://github.com/ cd translate - if [ -f requirements.txt ]; then - pip install -r requirements.txt - ./gen_rc.sh - fi + pip install -r requirements.txt + ./gen_rc.sh shell: bash - continue-on-error: true - run: nuget restore AudioPlaybackConnector.sln - - name: Build Solution - run: | - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x64" - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x86" + - run: | + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x64" } + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x86" } + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM64" } + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM" } + Get-Job | Wait-Job | Receive-Job shell: powershell - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector64 path: x64/Release/AudioPlaybackConnector64.exe + - uses: actions/upload-artifact@v4 + with: + name: AudioPlaybackConnector64-pdb + path: x64/Release/AudioPlaybackConnector64.pdb - uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector32 path: Release/AudioPlaybackConnector32.exe - - - + - uses: actions/upload-artifact@v4 + with: + name: AudioPlaybackConnector32-pdb + path: Release/AudioPlaybackConnector32.pdb + - uses: actions/upload-artifact@v4 + with: + name: AudioPlaybackConnectorARM64 + path: ARM64/Release/AudioPlaybackConnectorARM64.exe + - uses: actions/upload-artifact@v4 + with: + name: AudioPlaybackConnectorARM64-pdb + path: ARM64/Release/AudioPlaybackConnectorARM64.pdb + - uses: actions/upload-artifact@v4 + with: + name: AudioPlaybackConnectorARM + path: ARM/Release/AudioPlaybackConnectorARM.exe + - uses: actions/upload-artifact@v4 + with: + name: AudioPlaybackConnectorARM-pdb + path: ARM/Release/AudioPlaybackConnectorARM.pdb diff --git a/translate/src/translate-toolkit b/translate/src/translate-toolkit new file mode 160000 index 0000000..3070162 --- /dev/null +++ b/translate/src/translate-toolkit @@ -0,0 +1 @@ +Subproject commit 3070162bb995264f07acaf479672c229336682ca From e43b3fcb5d1f011e21695240fe80aa1e18f29237 Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 19:36:53 +0530 Subject: [PATCH 15/70] Permanently ignore ghost submodule translate-toolkit --- .gitignore | 3 +++ translate/src/translate-toolkit | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 160000 translate/src/translate-toolkit diff --git a/.gitignore b/.gitignore index 1f29945..2a047bc 100644 --- a/.gitignore +++ b/.gitignore @@ -340,3 +340,6 @@ ASALocalRun/ healthchecksdb translate/generated + +translate/src/translate-toolkit + diff --git a/translate/src/translate-toolkit b/translate/src/translate-toolkit deleted file mode 160000 index 3070162..0000000 --- a/translate/src/translate-toolkit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3070162bb995264f07acaf479672c229336682ca From b3e7e0f2e4ecb953afbc1e695f870ab94d86185e Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 19:46:47 +0530 Subject: [PATCH 16/70] Fix workflow: sequential builds with error surfacing --- .github/workflows/build.yaml | 53 ++++++++++-------------------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9c4daef..db5eb86 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,53 +19,30 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.10' - - run: | + - name: Setup translations + run: | git config --global url."https://github.com/".insteadOf git://github.com/ cd translate pip install -r requirements.txt ./gen_rc.sh shell: bash - - run: nuget restore AudioPlaybackConnector.sln - - run: | - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x64" } - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x86" } - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM64" } - Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { - msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM" } - Get-Job | Wait-Job | Receive-Job + - name: NuGet restore + run: nuget restore AudioPlaybackConnector.sln + - name: Build x64 + run: msbuild AudioPlaybackConnector.sln -p:Configuration=Release -p:Platform=x64 -v:minimal shell: powershell - - uses: actions/upload-artifact@v4 + - name: Build x86 + run: msbuild AudioPlaybackConnector.sln -p:Configuration=Release -p:Platform=x86 -v:minimal + shell: powershell + - name: Upload x64 + uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector64 path: x64/Release/AudioPlaybackConnector64.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnector64-pdb - path: x64/Release/AudioPlaybackConnector64.pdb - - uses: actions/upload-artifact@v4 + if-no-files-found: error + - name: Upload x86 + uses: actions/upload-artifact@v4 with: name: AudioPlaybackConnector32 path: Release/AudioPlaybackConnector32.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnector32-pdb - path: Release/AudioPlaybackConnector32.pdb - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM64 - path: ARM64/Release/AudioPlaybackConnectorARM64.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM64-pdb - path: ARM64/Release/AudioPlaybackConnectorARM64.pdb - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM - path: ARM/Release/AudioPlaybackConnectorARM.exe - - uses: actions/upload-artifact@v4 - with: - name: AudioPlaybackConnectorARM-pdb - path: ARM/Release/AudioPlaybackConnectorARM.pdb + if-no-files-found: warn From 0a5054518474fcc475158db95cd36fb8f9629f7d Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 19:53:13 +0530 Subject: [PATCH 17/70] Fix compilation: raw COM ptr for IAudioEndpointVolume + endpointvolume.h --- AudioPlaybackConnector.cpp | 17 +++++++++++------ AudioPlaybackConnector.h | 2 +- pch.h | 1 + 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 6bc94d1..04db91d 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -600,16 +600,20 @@ void SetupEndpointVolume() { try { - winrt::com_ptr enumerator; + IMMDeviceEnumerator* enumerator = nullptr; winrt::check_hresult(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, - CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)enumerator.put())); + CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)&enumerator)); - winrt::com_ptr device; - winrt::check_hresult(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, device.put())); + IMMDevice* device = nullptr; + winrt::check_hresult(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device)); + enumerator->Release(); + IAudioEndpointVolume* epVol = nullptr; winrt::check_hresult(device->Activate(__uuidof(IAudioEndpointVolume), - CLSCTX_INPROC_SERVER, NULL, (void**)g_endpointVolume.put())); + CLSCTX_INPROC_SERVER, NULL, (void**)&epVol)); + device->Release(); + g_endpointVolume = epVol; g_volumeCallback = new VolumeCallback(); winrt::check_hresult(g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback)); @@ -630,8 +634,9 @@ void TeardownEndpointVolume() g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); g_volumeCallback->Release(); g_volumeCallback = nullptr; + g_endpointVolume->Release(); + g_endpointVolume = nullptr; } - g_endpointVolume = nullptr; } void UpdateVolume() diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index 5c937a7..5c7d450 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -41,7 +41,7 @@ bool g_reconnect = false; std::vector g_lastDevices; double g_volume = 0.2; bool g_volumeLock = true; -winrt::com_ptr g_endpointVolume; +IAudioEndpointVolume* g_endpointVolume = nullptr; // GUID used to tag our own volume changes so the callback ignores them static const GUID g_ourVolumeGuid = { 0x9a4b2d1c, 0x3e5f, 0x4a6b, { 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8, 0xc9 } }; diff --git a/pch.h b/pch.h index 786195c..89c916d 100644 --- a/pch.h +++ b/pch.h @@ -20,6 +20,7 @@ #include #include #include +#include // C++ RunTime Header Files #include #include From 55e179bf86e97cb1d95b735eb0328a0232679a65 Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 20:12:19 +0530 Subject: [PATCH 18/70] Decouple phone volume from system volume using per-session control --- AudioPlaybackConnector.cpp | 132 ++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 17 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 04db91d..2174bca 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -556,9 +556,47 @@ void UpdateNotifyIcon() } } -// COM callback class that intercepts master volume changes. -// When the phone uses AVRCP to change volume, OnNotify fires with a foreign GUID. -// We post WM_RESTORE_VOLUME so the main thread immediately reverts it. +// Applies g_volume to every active audio session belonging to our process. +// AudioPlaybackConnection audio appears as a session in our PID when the phone streams. +static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) +{ + IAudioSessionEnumerator* sessionEnum = nullptr; + if (FAILED(mgr->GetSessionEnumerator(&sessionEnum))) return; + + int count = 0; + sessionEnum->GetCount(&count); + const DWORD ourPid = GetCurrentProcessId(); + + for (int i = 0; i < count; ++i) + { + IAudioSessionControl* ctrl = nullptr; + if (FAILED(sessionEnum->GetSession(i, &ctrl))) continue; + + IAudioSessionControl2* ctrl2 = nullptr; + if (SUCCEEDED(ctrl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2))) + { + DWORD pid = 0; + ctrl2->GetProcessId(&pid); + ctrl2->Release(); + if (pid == ourPid) + { + ISimpleAudioVolume* vol = nullptr; + if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) + { + vol->SetMasterVolume(static_cast(g_volume), nullptr); + vol->Release(); + } + } + } + ctrl->Release(); + } + sessionEnum->Release(); +} + +// Holds the session manager so we can re-enumerate on UpdateVolume calls. +static IAudioSessionManager2* g_sessionManager = nullptr; + +// Intercepts master volume changes; blocks AVRCP (phone buttons) from altering PC volume. class VolumeCallback : public IAudioEndpointVolumeCallback { public: @@ -582,10 +620,8 @@ class VolumeCallback : public IAudioEndpointVolumeCallback } HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) override { - // Ignore changes we made ourselves if (IsEqualGUID(pNotify->guidEventContext, g_ourVolumeGuid)) return S_OK; - // External change (AVRCP / other app) — tell the main thread to restore if (g_volumeLock && g_hWnd) PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); return S_OK; @@ -593,9 +629,57 @@ class VolumeCallback : public IAudioEndpointVolumeCallback private: long m_ref = 1; }; - static VolumeCallback* g_volumeCallback = nullptr; +// Called by Windows when a new audio session is created. +// We use this to immediately apply our volume when AudioPlaybackConnection starts streaming. +class SessionNotifier : public IAudioSessionNotification +{ +public: + ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_ref); } + ULONG STDMETHODCALLTYPE Release() override + { + auto r = InterlockedDecrement(&m_ref); + if (r == 0) delete this; + return r; + } + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override + { + if (riid == __uuidof(IUnknown) || riid == __uuidof(IAudioSessionNotification)) + { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + *ppv = nullptr; + return E_NOINTERFACE; + } + HRESULT STDMETHODCALLTYPE OnSessionCreated(IAudioSessionControl* pNewSession) override + { + IAudioSessionControl2* ctrl2 = nullptr; + if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2))) + { + DWORD pid = 0; + ctrl2->GetProcessId(&pid); + ctrl2->Release(); + if (pid == GetCurrentProcessId()) + { + ISimpleAudioVolume* vol = nullptr; + if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) + { + vol->SetMasterVolume(static_cast(g_volume), nullptr); + vol->Release(); + } + } + } + return S_OK; + } +private: + long m_ref = 1; +}; + +static SessionNotifier* g_sessionNotifier = nullptr; + void SetupEndpointVolume() { try @@ -608,18 +692,26 @@ void SetupEndpointVolume() winrt::check_hresult(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device)); enumerator->Release(); + // Register AVRCP guardian (blocks phone buttons from changing master volume) IAudioEndpointVolume* epVol = nullptr; winrt::check_hresult(device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (void**)&epVol)); - device->Release(); - g_endpointVolume = epVol; g_volumeCallback = new VolumeCallback(); - winrt::check_hresult(g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback)); + g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback); - // Apply our saved volume immediately - g_endpointVolume->SetMasterVolumeLevelScalar( - static_cast(g_volume), &g_ourVolumeGuid); + // Register session notifier so we catch AudioPlaybackConnection sessions the moment they start + IAudioSessionManager2* mgr = nullptr; + if (SUCCEEDED(device->Activate(__uuidof(IAudioSessionManager2), + CLSCTX_INPROC_SERVER, NULL, (void**)&mgr))) + { + g_sessionManager = mgr; // keep alive for UpdateVolume + g_sessionNotifier = new SessionNotifier(); + mgr->RegisterSessionNotification(g_sessionNotifier); + // Apply to any sessions already running + ApplyVolumeToOurSessions(mgr); + } + device->Release(); } catch (...) { @@ -629,6 +721,14 @@ void SetupEndpointVolume() void TeardownEndpointVolume() { + if (g_sessionManager && g_sessionNotifier) + { + g_sessionManager->UnregisterSessionNotification(g_sessionNotifier); + g_sessionNotifier->Release(); + g_sessionNotifier = nullptr; + g_sessionManager->Release(); + g_sessionManager = nullptr; + } if (g_endpointVolume && g_volumeCallback) { g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); @@ -641,11 +741,9 @@ void TeardownEndpointVolume() void UpdateVolume() { - if (g_endpointVolume) - { - g_endpointVolume->SetMasterVolumeLevelScalar( - static_cast(g_volume), &g_ourVolumeGuid); - } + // Set volume on our process's sessions (the AudioPlaybackConnection audio) + if (g_sessionManager) + ApplyVolumeToOurSessions(g_sessionManager); } static bool IsRunningAsAdmin() From 8cfc75de982c6bf033ad3955cd2f75a75304b52b Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 20:23:42 +0530 Subject: [PATCH 19/70] Fix laptop keys (allow GUID_NULL), improve registry fix, and target Bluetooth sessions by ID --- AudioPlaybackConnector.cpp | 70 +++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 2174bca..26140b6 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -575,10 +575,26 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) IAudioSessionControl2* ctrl2 = nullptr; if (SUCCEEDED(ctrl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2))) { + bool isOurs = false; + + // Check 1: Is it in our process? DWORD pid = 0; ctrl2->GetProcessId(&pid); - ctrl2->Release(); - if (pid == ourPid) + if (pid == ourPid) isOurs = true; + + // Check 2: Does it have a Bluetooth/BTHENUM identifier? + // WinRT AudioPlaybackConnection sessions often have these. + PWSTR id = nullptr; + if (SUCCEEDED(ctrl2->GetSessionInstanceIdentifier(&id))) + { + if (id && (wcsstr(id, L"BTHENUM") != nullptr || wcsstr(id, L"Bluetooth") != nullptr)) + { + isOurs = true; + } + CoTaskMemFree(id); + } + + if (isOurs) { ISimpleAudioVolume* vol = nullptr; if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) @@ -587,6 +603,7 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) vol->Release(); } } + ctrl2->Release(); } ctrl->Release(); } @@ -620,8 +637,16 @@ class VolumeCallback : public IAudioEndpointVolumeCallback } HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) override { + // 1. Allow our own changes if (IsEqualGUID(pNotify->guidEventContext, g_ourVolumeGuid)) return S_OK; + + // 2. Allow manual system changes (keys, Windows slider). + // These typically use GUID_NULL. If we block these, the laptop's own keys stop working. + if (IsEqualGUID(pNotify->guidEventContext, GUID_NULL)) + return S_OK; + + // 3. Revert anything else (likely AVRCP / remote changes from the phone). if (g_volumeLock && g_hWnd) PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); return S_OK; @@ -763,7 +788,7 @@ static bool IsRunningAsAdmin() void DisableAbsoluteVolume() { - // If not admin, relaunch with UAC elevation and a flag to apply the fix + // If not admin, relaunch with UAC elevation if (!IsRunningAsAdmin()) { wchar_t exePath[MAX_PATH]; @@ -771,36 +796,35 @@ void DisableAbsoluteVolume() HINSTANCE result = ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); if (reinterpret_cast(result) <= 32) { - TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to disable Absolute Volume.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to apply the system fix.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); } return; } - // Check command line arg path: when relaunched as admin with --fix-absolute-volume - HKEY hKey; - LONG openResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, KEY_SET_VALUE, &hKey); - if (openResult != ERROR_SUCCESS) - { - // Key may not exist yet, try creating it - openResult = RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL); - } + const wchar_t* paths[] = { + L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", + L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT" + }; - if (openResult == ERROR_SUCCESS) + bool success = false; + for (auto path : paths) { - DWORD value = 1; - auto status = RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)); - RegCloseKey(hKey); - if (status == ERROR_SUCCESS) - { - TaskDialog(g_hWnd, NULL, _(L"Success"), _(L"Absolute Volume has been disabled.\n\nYou MUST REBOOT your computer for this change to take effect.\nAfter rebooting, your phone volume buttons will only change the phone's volume, not your PC's master volume."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); - } - else + HKEY hKey; + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL) == ERROR_SUCCESS) { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry value."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + DWORD value = 1; + if (RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)) == ERROR_SUCCESS) + success = true; + RegCloseKey(hKey); } } + + if (success) + { + TaskDialog(g_hWnd, NULL, _(L"System Fix Applied"), _(L"The 'Absolute Volume' sync has been disabled in the registry.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nAfter rebooting, your phone buttons will no longer touch your PC volume."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + } else { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to open or create the Bluetooth registry key."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values. Please try running as Administrator manually."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); } } From e64b133bfd419543d2e3af4aee97fa145d5468b8 Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 21:21:24 +0530 Subject: [PATCH 20/70] v1.7.0: Absolute Authority - Use user activity to protect master volume + universal registry fix --- AudioPlaybackConnector.cpp | 82 +++++++++++++++++++++++++++++--------- AudioPlaybackConnector.h | 2 + SettingsUtil.hpp | 2 +- 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 26140b6..7c47c9f 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -216,11 +216,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_RESTORE_VOLUME: - // Fired by the volume callback when an external source changed the volume + // Fired by the volume callback when a remote (phone) source changed the volume if (g_volumeLock && g_endpointVolume) { - g_endpointVolume->SetMasterVolumeLevelScalar( - static_cast(g_volume), &g_ourVolumeGuid); + g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); + g_endpointVolume->SetMute(g_lastMute, &g_ourVolumeGuid); + if (g_sessionManager) ApplyVolumeToOurSessions(g_sessionManager); } break; default: @@ -582,12 +583,11 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) ctrl2->GetProcessId(&pid); if (pid == ourPid) isOurs = true; - // Check 2: Does it have a Bluetooth/BTHENUM identifier? - // WinRT AudioPlaybackConnection sessions often have these. + // Check 2: Does it have a Bluetooth/A2DP identifier? PWSTR id = nullptr; if (SUCCEEDED(ctrl2->GetSessionInstanceIdentifier(&id))) { - if (id && (wcsstr(id, L"BTHENUM") != nullptr || wcsstr(id, L"Bluetooth") != nullptr)) + if (id && (wcsstr(id, L"BTHENUM") != nullptr || wcsstr(id, L"Bluetooth") != nullptr || wcsstr(id, L"A2DP") != nullptr || wcsstr(id, L"Phone") != nullptr)) { isOurs = true; } @@ -599,7 +599,9 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) ISimpleAudioVolume* vol = nullptr; if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { - vol->SetMasterVolume(static_cast(g_volume), nullptr); + // Use a 0.4x scale to fix the "Mobile is significantly louder than PC" issue. + // This gives the user more granular control over the loud phone audio. + vol->SetMasterVolume(static_cast(g_volume * 0.4), nullptr); vol->Release(); } } @@ -641,14 +643,44 @@ class VolumeCallback : public IAudioEndpointVolumeCallback if (IsEqualGUID(pNotify->guidEventContext, g_ourVolumeGuid)) return S_OK; - // 2. Allow manual system changes (keys, Windows slider). - // These typically use GUID_NULL. If we block these, the laptop's own keys stop working. + bool isRemote = false; + + // Check if it's a system key press or mouse move if (IsEqualGUID(pNotify->guidEventContext, GUID_NULL)) - return S_OK; + { + LASTINPUTINFO lii = { sizeof(lii) }; + lii.cbSize = sizeof(lii); + if (GetLastInputInfo(&lii)) + { + DWORD idleTime = GetTickCount() - lii.dwTime; + // If the user hasn't touched the PC in the last 1.5 seconds, + // this volume change is almost certainly from the phone (AVRCP). + if (idleTime > 1500) + { + isRemote = true; + } + } + } + else + { + // Any other GUID (phone app or other remote source) + isRemote = true; + } - // 3. Revert anything else (likely AVRCP / remote changes from the phone). - if (g_volumeLock && g_hWnd) - PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); + if (isRemote) + { + // Remote change detected -> Revert to the last user-defined "Authority" level + if (g_volumeLock && g_hWnd) + { + PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); + } + } + else + { + // Local authority: update the last known good level set by the user (laptop keys) + g_lastMasterVolume = pNotify->fLevel; + g_lastMute = pNotify->bMuted; + } return S_OK; } private: @@ -722,6 +754,13 @@ void SetupEndpointVolume() winrt::check_hresult(device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (void**)&epVol)); g_endpointVolume = epVol; + + // Initialize our Authority levels from the current system state + float currentVol = 0.5f; + BOOL currentMute = FALSE; + if (SUCCEEDED(g_endpointVolume->GetMasterVolumeLevelScalar(¤tVol))) g_lastMasterVolume = currentVol; + if (SUCCEEDED(g_endpointVolume->GetMute(¤tMute))) g_lastMute = (currentMute != FALSE); + g_volumeCallback = new VolumeCallback(); g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback); @@ -803,7 +842,10 @@ void DisableAbsoluteVolume() const wchar_t* paths[] = { L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", - L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT" + L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT", + L"SYSTEM\\CurrentControlSet\\Services\\HidBth\\Parameters", + L"SYSTEM\\CurrentControlSet\\Services\\BthAvrcpTg\\Parameters", + L"SOFTWARE\\Microsoft\\Bluetooth\\Audio\\AVRCP\\CT" }; bool success = false; @@ -812,19 +854,21 @@ void DisableAbsoluteVolume() HKEY hKey; if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL) == ERROR_SUCCESS) { - DWORD value = 1; - if (RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)) == ERROR_SUCCESS) - success = true; + DWORD val1 = 1; + DWORD val0 = 0; + RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val1, sizeof(val1)); + RegSetValueExW(hKey, L"EnableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); RegCloseKey(hKey); + success = true; } } if (success) { - TaskDialog(g_hWnd, NULL, _(L"System Fix Applied"), _(L"The 'Absolute Volume' sync has been disabled in the registry.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nAfter rebooting, your phone buttons will no longer touch your PC volume."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"System Fix Applied DEFINITIVELY"), _(L"All known registry paths for Absolute Volume have been updated.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nIf volume buttons still sync after reboot, it means your Bluetooth driver is ignoring system settings."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); } else { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values. Please try running as Administrator manually."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); } } diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index 5c7d450..ef65c31 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -41,6 +41,8 @@ bool g_reconnect = false; std::vector g_lastDevices; double g_volume = 0.2; bool g_volumeLock = true; +float g_lastMasterVolume = 0.5f; +bool g_lastMute = false; IAudioEndpointVolume* g_endpointVolume = nullptr; // GUID used to tag our own volume changes so the callback ignores them static const GUID g_ourVolumeGuid = { 0x9a4b2d1c, 0x3e5f, 0x4a6b, { 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8, 0xc9 } }; diff --git a/SettingsUtil.hpp b/SettingsUtil.hpp index 07663a1..45ce027 100644 --- a/SettingsUtil.hpp +++ b/SettingsUtil.hpp @@ -7,7 +7,7 @@ void DefaultSettings() { g_reconnect = false; g_lastDevices.clear(); - g_volume = 0.2; + g_volume = 0.1; g_volumeLock = true; } From 664de13d7086481f54c1c55d7e90b50f9589064d Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 21:26:58 +0530 Subject: [PATCH 21/70] v1.7.1: Fix compilation errors (fMasterVolume, identifier visibility) --- AudioPlaybackConnector.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 7c47c9f..89ab387 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -14,6 +14,10 @@ void SetupDevicePicker(); void SetupSvgIcon(); void UpdateNotifyIcon(); +// Audio session management globals and helpers +static IAudioSessionManager2* g_sessionManager = nullptr; +static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr); + int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, @@ -600,7 +604,6 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { // Use a 0.4x scale to fix the "Mobile is significantly louder than PC" issue. - // This gives the user more granular control over the loud phone audio. vol->SetMasterVolume(static_cast(g_volume * 0.4), nullptr); vol->Release(); } @@ -612,9 +615,6 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) sessionEnum->Release(); } -// Holds the session manager so we can re-enumerate on UpdateVolume calls. -static IAudioSessionManager2* g_sessionManager = nullptr; - // Intercepts master volume changes; blocks AVRCP (phone buttons) from altering PC volume. class VolumeCallback : public IAudioEndpointVolumeCallback { @@ -678,7 +678,7 @@ class VolumeCallback : public IAudioEndpointVolumeCallback else { // Local authority: update the last known good level set by the user (laptop keys) - g_lastMasterVolume = pNotify->fLevel; + g_lastMasterVolume = pNotify->fMasterVolume; g_lastMute = pNotify->bMuted; } return S_OK; @@ -724,7 +724,7 @@ class SessionNotifier : public IAudioSessionNotification ISimpleAudioVolume* vol = nullptr; if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { - vol->SetMasterVolume(static_cast(g_volume), nullptr); + vol->SetMasterVolume(static_cast(g_volume * 0.4), nullptr); vol->Release(); } } From ed8128f15e5be9f95219803a65b137bf61a84bef Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 21:47:04 +0530 Subject: [PATCH 22/70] v1.7.2: Fix tray slider by targeting phone sessions via DisplayName (SNK/A2DP) --- AudioPlaybackConnector.cpp | 71 ++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 89ab387..5407155 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -16,6 +16,39 @@ void UpdateNotifyIcon(); // Audio session management globals and helpers static IAudioSessionManager2* g_sessionManager = nullptr; + +// Helper to identify if an audio session belongs to the phone audio stream +static bool IsBluetoothSession(IAudioSessionControl2* ctrl2, IAudioSessionControl* ctrl) +{ + // Check PID first (if it's in our process, it's definitely ours) + DWORD pid = 0; + if (SUCCEEDED(ctrl2->GetProcessId(&pid)) && pid == GetCurrentProcessId()) return true; + + // Check Session Identifier (usually contains BTHENUM, A2DP, etc.) + PWSTR id = nullptr; + if (SUCCEEDED(ctrl2->GetSessionInstanceIdentifier(&id))) + { + std::wstring sid(id); + CoTaskMemFree(id); + for (auto& c : sid) c = towlower(c); + if (sid.find(L"bthenum") != std::wstring::npos || sid.find(L"a2dp") != std::wstring::npos || sid.find(L"bluetooth") != std::wstring::npos) + return true; + } + + // Check Display Name (e.g. "Microphone (iQOO Z3 5G A2DP SNK)") + PWSTR disp = nullptr; + if (SUCCEEDED(ctrl->GetDisplayName(&disp))) + { + std::wstring sdisp(disp); + CoTaskMemFree(disp); + for (auto& c : sdisp) c = towlower(c); + if (sdisp.find(L"a2dp") != std::wstring::npos || sdisp.find(L"snk") != std::wstring::npos || sdisp.find(L"iqoo") != std::wstring::npos) + return true; + } + + return false; +} + static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr); int APIENTRY wWinMain(_In_ HINSTANCE hInstance, @@ -318,10 +351,9 @@ void SetupMenu() lockItem.IsChecked(g_volumeLock); lockItem.Click([](const auto&, const auto&) { g_volumeLock = lockItem.IsChecked(); - // When enabling, immediately restore our preferred level + // When enabling, immediately restore our preferred master volume level if (g_volumeLock && g_endpointVolume) - g_endpointVolume->SetMasterVolumeLevelScalar( - static_cast(g_volume), &g_ourVolumeGuid); + g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); SaveSettings(); }); @@ -570,7 +602,6 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) int count = 0; sessionEnum->GetCount(&count); - const DWORD ourPid = GetCurrentProcessId(); for (int i = 0; i < count; ++i) { @@ -580,31 +611,13 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) IAudioSessionControl2* ctrl2 = nullptr; if (SUCCEEDED(ctrl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2))) { - bool isOurs = false; - - // Check 1: Is it in our process? - DWORD pid = 0; - ctrl2->GetProcessId(&pid); - if (pid == ourPid) isOurs = true; - - // Check 2: Does it have a Bluetooth/A2DP identifier? - PWSTR id = nullptr; - if (SUCCEEDED(ctrl2->GetSessionInstanceIdentifier(&id))) - { - if (id && (wcsstr(id, L"BTHENUM") != nullptr || wcsstr(id, L"Bluetooth") != nullptr || wcsstr(id, L"A2DP") != nullptr || wcsstr(id, L"Phone") != nullptr)) - { - isOurs = true; - } - CoTaskMemFree(id); - } - - if (isOurs) + if (IsBluetoothSession(ctrl2, ctrl)) { ISimpleAudioVolume* vol = nullptr; if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { - // Use a 0.4x scale to fix the "Mobile is significantly louder than PC" issue. - vol->SetMasterVolume(static_cast(g_volume * 0.4), nullptr); + // Use a 0.5x scale to keep mobile audio in a comfortable range relative to PC + vol->SetMasterVolume(static_cast(g_volume * 0.5), nullptr); vol->Release(); } } @@ -716,18 +729,16 @@ class SessionNotifier : public IAudioSessionNotification IAudioSessionControl2* ctrl2 = nullptr; if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2))) { - DWORD pid = 0; - ctrl2->GetProcessId(&pid); - ctrl2->Release(); - if (pid == GetCurrentProcessId()) + if (IsBluetoothSession(ctrl2, pNewSession)) { ISimpleAudioVolume* vol = nullptr; if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { - vol->SetMasterVolume(static_cast(g_volume * 0.4), nullptr); + vol->SetMasterVolume(static_cast(g_volume * 0.5), nullptr); vol->Release(); } } + ctrl2->Release(); } return S_OK; } From 8a42b80cc3f4d3263fefa3b7f7c3d0e1d8487618 Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 21:59:07 +0530 Subject: [PATCH 23/70] v1.7.3: Sync phone volume buttons to app slider + improved session search --- AudioPlaybackConnector.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 5407155..5c1ee7f 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -31,7 +31,7 @@ static bool IsBluetoothSession(IAudioSessionControl2* ctrl2, IAudioSessionContro std::wstring sid(id); CoTaskMemFree(id); for (auto& c : sid) c = towlower(c); - if (sid.find(L"bthenum") != std::wstring::npos || sid.find(L"a2dp") != std::wstring::npos || sid.find(L"bluetooth") != std::wstring::npos) + if (sid.find(L"bthenum") != std::wstring::npos || sid.find(L"a2dp") != std::wstring::npos || sid.find(L"bluetooth") != std::wstring::npos || sid.find(L"snk") != std::wstring::npos) return true; } @@ -42,7 +42,7 @@ static bool IsBluetoothSession(IAudioSessionControl2* ctrl2, IAudioSessionContro std::wstring sdisp(disp); CoTaskMemFree(disp); for (auto& c : sdisp) c = towlower(c); - if (sdisp.find(L"a2dp") != std::wstring::npos || sdisp.find(L"snk") != std::wstring::npos || sdisp.find(L"iqoo") != std::wstring::npos) + if (sdisp.find(L"a2dp") != std::wstring::npos || sdisp.find(L"snk") != std::wstring::npos || sdisp.find(L"iqoo") != std::wstring::npos || sdisp.find(L"phone") != std::wstring::npos) return true; } @@ -616,8 +616,8 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) ISimpleAudioVolume* vol = nullptr; if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { - // Use a 0.5x scale to keep mobile audio in a comfortable range relative to PC - vol->SetMasterVolume(static_cast(g_volume * 0.5), nullptr); + // Use a 0.7x scale to keep mobile audio in a comfortable range. + vol->SetMasterVolume(static_cast(g_volume * 0.7), nullptr); vol->Release(); } } @@ -682,9 +682,12 @@ class VolumeCallback : public IAudioEndpointVolumeCallback if (isRemote) { - // Remote change detected -> Revert to the last user-defined "Authority" level + // Remote change detected (Phone buttons) if (g_volumeLock && g_hWnd) { + // Sync the phone's requested level to our app's slider (g_volume) + g_volume = pNotify->fMasterVolume; + // Post message to restore master volume to our Authority level and re-apply g_volume to the session PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); } } @@ -734,7 +737,7 @@ class SessionNotifier : public IAudioSessionNotification ISimpleAudioVolume* vol = nullptr; if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { - vol->SetMasterVolume(static_cast(g_volume * 0.5), nullptr); + vol->SetMasterVolume(static_cast(g_volume * 0.7), nullptr); vol->Release(); } } From 3fb64233f5a0323949bc51656bcd2357378d37cd Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 22:23:09 +0530 Subject: [PATCH 24/70] v1.7.5: Unified Control Center UI, Startup support, and Revert Fix option --- AudioPlaybackConnector.cpp | 402 ++++++++++++++++++------------------- AudioPlaybackConnector.h | 6 +- SettingsUtil.hpp | 6 + 3 files changed, 207 insertions(+), 207 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 5c1ee7f..2f8dc31 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -2,17 +2,11 @@ #include "AudioPlaybackConnector.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); -void SetupFlyout(); -void SetupVolumeFlyout(); -void SetupMenu(); -void UpdateVolume(); -void SetupEndpointVolume(); -void TeardownEndpointVolume(); -void DisableAbsoluteVolume(); -winrt::fire_and_forget ConnectDevice(DevicePicker, std::wstring_view); -void SetupDevicePicker(); -void SetupSvgIcon(); +void SetupUnifiedUI(); void UpdateNotifyIcon(); +void DisableAbsoluteVolume(); +void RevertAbsoluteVolume(); +void SetRunAtStartup(bool enable); // Audio session management globals and helpers static IAudioSessionManager2* g_sessionManager = nullptr; @@ -130,10 +124,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, LoadSettings(); SetupEndpointVolume(); - SetupFlyout(); - SetupVolumeFlyout(); - SetupMenu(); - SetupDevicePicker(); + SetupUnifiedUI(); SetupSvgIcon(); g_nid.hWnd = g_niid.hWnd = g_hWnd; @@ -190,58 +181,35 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) UpdateNotifyIcon(); } break; - case WM_NOTIFYICON: - switch (LOWORD(lParam)) - { + case WM_LBUTTONUP: + case WM_RBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { - using namespace winrt::Windows::UI::Popups; - RECT iconRect; auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - LOG_HR(hr); - break; - } - - auto dpi = GetDpiForWindow(hWnd); - Rect rect = { - static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi) - }; - - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); - SetForegroundWindow(hWnd); - g_devicePicker.Show(rect, Placement::Above); - } - break; - case WM_RBUTTONUP: // Menu activated by mouse click - g_menuFocusState = FocusState::Pointer; - break; - case WM_CONTEXTMENU: - { - if (g_menuFocusState == FocusState::Unfocused) - g_menuFocusState = FocusState::Keyboard; + if (FAILED(hr)) break; auto dpi = GetDpiForWindow(hWnd); Point point = { - static_cast(GET_X_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(GET_Y_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi) + static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi) }; - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); - SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - - g_xamlMenu.ShowAt(g_xamlCanvas, point); + g_unifiedFlyout.ShowAt(g_xamlCanvas, point); } break; } break; + case WM_APP + 10: // Device added + { + auto info = (DeviceInformation*)wParam; + g_devices.Append(*info); + delete info; + } + break; case WM_CONNECTDEVICE: if (g_reconnect) { @@ -271,165 +239,141 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) return 0; } -void SetupFlyout() +void SetupUnifiedUI() { - TextBlock textBlock; - textBlock.Text(_(L"All connections will be closed.\nExit anyway?")); - textBlock.Margin({ 0, 0, 0, 12 }); - - static CheckBox checkbox; - checkbox.IsChecked(g_reconnect); - checkbox.Content(winrt::box_value(_(L"Reconnect on next start"))); - - Button button; - button.Content(winrt::box_value(_(L"Exit"))); - button.HorizontalAlignment(HorizontalAlignment::Right); - button.Click([](const auto&, const auto&) { - g_reconnect = checkbox.IsChecked().Value(); - PostMessageW(g_hWnd, WM_CLOSE, 0, 0); - }); + StackPanel root; + root.Width(320); + root.Padding({ 16 }); + root.Spacing(12); + + // --- Header --- + TextBlock header; + header.Text(_(L"Audio Connector")); + header.FontSize(20); + header.FontWeight(winrt::Windows::UI::Text::FontWeights::Bold()); + root.Children().Append(header); - StackPanel stackPanel; - stackPanel.Children().Append(textBlock); - stackPanel.Children().Append(checkbox); - stackPanel.Children().Append(button); - - Flyout flyout; - flyout.ShouldConstrainToRootBounds(false); - flyout.Content(stackPanel); - - g_xamlFlyout = flyout; -} + if (!IsRunningAsAdmin()) + { + TextBlock adminWarn; + adminWarn.Text(_(L"⚠ Running without Administrator privileges. System fixes may not apply.")); + adminWarn.FontSize(11); + adminWarn.Foreground(winrt::Windows::UI::Xaml::Media::SolidColorBrush(winrt::Windows::UI::Colors::OrangeRed())); + adminWarn.TextWrapping(TextWrapping::Wrap); + root.Children().Append(adminWarn); + } -void SetupVolumeFlyout() -{ - TextBlock textBlock; - textBlock.Text(_(L"Mobile Volume")); - textBlock.Margin({ 0, 0, 0, 12 }); - - Slider slider; - slider.Minimum(0); - slider.Maximum(100); - slider.Value(g_volume * 100); - slider.Width(200); - slider.ValueChanged([](const auto&, const auto& args) { + // --- Device Section --- + TextBlock deviceHeader; + deviceHeader.Text(_(L"Available Devices")); + deviceHeader.FontSize(14); + deviceHeader.Opacity(0.7); + root.Children().Append(deviceHeader); + + ListView deviceList; + deviceList.Height(150); + deviceList.ItemsSource(g_devices); + deviceList.SelectionMode(ListViewSelectionMode::None); + + // Item Template for Device List + deviceList.ItemTemplate(winrt::Windows::UI::Xaml::DataTemplate()); // We'll handle this with a simpler approach or default to Name + + deviceList.ItemClick([](const auto&, const auto& args) { + auto device = args.ClickedItem().as(); + ConnectDevice(g_devicePicker, device.Id()); // Reuse existing connection logic + }); + g_deviceListView = deviceList; + root.Children().Append(deviceList); + + // --- Volume Control --- + TextBlock volHeader; + volHeader.Text(_(L"Mobile Volume")); + volHeader.FontSize(14); + volHeader.Opacity(0.7); + root.Children().Append(volHeader); + + Slider volSlider; + volSlider.Minimum(0); + volSlider.Maximum(100); + volSlider.Value(g_volume * 100); + volSlider.ValueChanged([](const auto&, const auto& args) { g_volume = args.NewValue() / 100.0; UpdateVolume(); }); - - StackPanel stackPanel; - stackPanel.Children().Append(textBlock); - stackPanel.Children().Append(slider); - - Flyout flyout; - flyout.ShouldConstrainToRootBounds(false); - flyout.Content(stackPanel); - flyout.Closed([](const auto&, const auto&) { - ShowWindow(g_hWnd, SW_HIDE); - SaveSettings(); - }); - - g_volumeFlyout = flyout; -} - -void SetupMenu() -{ - // https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font - FontIcon settingsIcon; - settingsIcon.Glyph(L"\xE713"); - - MenuFlyoutItem settingsItem; - settingsItem.Text(_(L"Bluetooth Settings")); - settingsItem.Icon(settingsIcon); - settingsItem.Click([](const auto&, const auto&) { - winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); - }); - - // Lock toggle: blocks phone volume buttons from changing PC volume - static ToggleMenuFlyoutItem lockItem; - lockItem.Text(_(L"Lock Phone Volume Buttons")); - lockItem.IsChecked(g_volumeLock); - lockItem.Click([](const auto&, const auto&) { - g_volumeLock = lockItem.IsChecked(); - // When enabling, immediately restore our preferred master volume level + root.Children().Append(volSlider); + + // --- Toggles --- + ToggleSwitch lockToggle; + lockToggle.Header(winrt::box_value(_(L"Lock PC Volume Buttons"))); + lockToggle.IsOn(g_volumeLock); + lockToggle.Toggled([](const auto& sender, const auto&) { + g_volumeLock = sender.as().IsOn(); if (g_volumeLock && g_endpointVolume) g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); SaveSettings(); }); - - FontIcon volumeIcon; - volumeIcon.Glyph(L"\xE767"); - - MenuFlyoutItem volumeItem; - volumeItem.Text(_(L"Volume Control")); - volumeItem.Icon(volumeIcon); - volumeItem.Click([](const auto&, const auto&) { - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - LOG_HR(hr); - return; - } - - auto dpi = GetDpiForWindow(g_hWnd); - - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); - - g_volumeFlyout.ShowAt(g_xamlCanvas); - }); - - FontIcon closeIcon; - closeIcon.Glyph(L"\xE8BB"); - - MenuFlyoutItem exitItem; - exitItem.Text(_(L"Exit")); - exitItem.Icon(closeIcon); - exitItem.Click([](const auto&, const auto&) { - if (g_audioPlaybackConnections.size() == 0) - { - PostMessageW(g_hWnd, WM_CLOSE, 0, 0); - return; - } - - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - LOG_HR(hr); - return; - } - - auto dpi = GetDpiForWindow(g_hWnd); - - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); - - g_xamlFlyout.ShowAt(g_xamlCanvas); + root.Children().Append(lockToggle); + + ToggleSwitch startupToggle; + startupToggle.Header(winrt::box_value(_(L"Run at Windows Startup"))); + startupToggle.IsOn(g_runAtStartup); + startupToggle.Toggled([](const auto& sender, const auto&) { + g_runAtStartup = sender.as().IsOn(); + SetRunAtStartup(g_runAtStartup); + SaveSettings(); }); + root.Children().Append(startupToggle); + + // --- System Fixes --- + StackPanel fixPanel; + fixPanel.Orientation(Orientation::Horizontal); + fixPanel.Spacing(8); + + Button fixBtn; + fixBtn.Content(winrt::box_value(_(L"Fix Volume Sync"))); + fixBtn.Click([](const auto&, const auto&) { DisableAbsoluteVolume(); }); + fixPanel.Children().Append(fixBtn); + + Button revertBtn; + revertBtn.Content(winrt::box_value(_(L"Revert Fix"))); + revertBtn.Click([](const auto&, const auto&) { RevertAbsoluteVolume(); }); + fixPanel.Children().Append(revertBtn); + + root.Children().Append(fixPanel); + + // --- Instructions Footer --- + TextBlock tipsHeader; + tipsHeader.Text(_(L"Tips & Troubleshooting:")); + tipsHeader.FontSize(12); + tipsHeader.FontWeight(winrt::Windows::UI::Text::FontWeights::SemiBold()); + tipsHeader.Margin({ 0, 8, 0, 0 }); + root.Children().Append(tipsHeader); + + TextBlock tips; + tips.Text(_(L"• If no sound, try disconnecting and reconnecting on the phone.\n• Toggle 'Lock' off and on if volume buttons stop syncing.\n• Always run as Admin for the best experience.\n• System fix requires a REBOOT to work.")); + tips.FontSize(11); + tips.Opacity(0.6); + tips.TextWrapping(TextWrapping::Wrap); + root.Children().Append(tips); + + Button exitBtn; + exitBtn.Content(winrt::box_value(_(L"Exit App"))); + exitBtn.HorizontalAlignment(HorizontalAlignment::Right); + exitBtn.Click([](const auto&, const auto&) { PostMessageW(g_hWnd, WM_CLOSE, 0, 0); }); + root.Children().Append(exitBtn); - MenuFlyout menu; - menu.Items().Append(settingsItem); - menu.Items().Append(lockItem); - menu.Items().Append(volumeItem); - menu.Items().Append(exitItem); - menu.Opened([](const auto& sender, const auto&) { - auto menuItems = sender.as().Items(); - auto itemsCount = menuItems.Size(); - if (itemsCount > 0) - { - menuItems.GetAt(itemsCount - 1).Focus(g_menuFocusState); - } - g_menuFocusState = FocusState::Unfocused; - }); - menu.Closed([](const auto&, const auto&) { - ShowWindow(g_hWnd, SW_HIDE); + Flyout flyout; + flyout.Content(root); + flyout.Closed([](const auto&, const auto&) { ShowWindow(g_hWnd, SW_HIDE); SaveSettings(); }); + g_unifiedFlyout = flyout; + + // Start device watching + auto selector = AudioPlaybackConnection::GetDeviceSelector(); + auto watcher = DeviceInformation::CreateWatcher(selector); + watcher.Added([](const auto&, const auto& info) { + g_hWndXaml ? PostMessageW(g_hWnd, WM_APP + 10, (WPARAM)new DeviceInformation(info), 0) : 0; }); - - g_xamlMenu = menu; + watcher.Start(); } winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device) @@ -841,16 +785,11 @@ static bool IsRunningAsAdmin() void DisableAbsoluteVolume() { - // If not admin, relaunch with UAC elevation if (!IsRunningAsAdmin()) { wchar_t exePath[MAX_PATH]; GetModuleFileNameW(NULL, exePath, MAX_PATH); - HINSTANCE result = ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); - if (reinterpret_cast(result) <= 32) - { - TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to apply the system fix.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); - } + ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); return; } @@ -879,10 +818,61 @@ void DisableAbsoluteVolume() if (success) { - TaskDialog(g_hWnd, NULL, _(L"System Fix Applied DEFINITIVELY"), _(L"All known registry paths for Absolute Volume have been updated.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nIf volume buttons still sync after reboot, it means your Bluetooth driver is ignoring system settings."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"System Fix Applied"), _(L"Registry paths for Absolute Volume have been updated.\n\nYou MUST REBOOT your laptop now for this to take effect."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); } - else +} + +void RevertAbsoluteVolume() +{ + if (!IsRunningAsAdmin()) { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"Admin Required"), _(L"Please run the app as Administrator to revert registry changes."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); + return; + } + + const wchar_t* paths[] = { + L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", + L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT", + L"SYSTEM\\CurrentControlSet\\Services\\HidBth\\Parameters", + L"SYSTEM\\CurrentControlSet\\Services\\BthAvrcpTg\\Parameters", + L"SOFTWARE\\Microsoft\\Bluetooth\\Audio\\AVRCP\\CT" + }; + + bool success = false; + for (auto path : paths) + { + HKEY hKey; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, path, 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) + { + DWORD val0 = 0; + RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); + RegSetValueExW(hKey, L"EnableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); + RegCloseKey(hKey); + success = true; + } + } + + if (success) + { + TaskDialog(g_hWnd, NULL, _(L"Fix Reverted"), _(L"Absolute Volume sync has been restored to default.\n\nYou MUST REBOOT for this to take effect."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + } +} + +void SetRunAtStartup(bool enable) +{ + HKEY hKey; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) + { + if (enable) + { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + RegSetValueExW(hKey, L"AudioPlaybackConnector", 0, REG_SZ, (const BYTE*)exePath, (wcslen(exePath) + 1) * sizeof(wchar_t)); + } + else + { + RegDeleteValueW(hKey, L"AudioPlaybackConnector"); + } + RegCloseKey(hKey); } } diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index ef65c31..641fb8b 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -39,11 +39,15 @@ NOTIFYICONIDENTIFIER g_niid = { UINT WM_TASKBAR_CREATED = 0; bool g_reconnect = false; std::vector g_lastDevices; -double g_volume = 0.2; +double g_volume = 0.1; bool g_volumeLock = true; +bool g_runAtStartup = false; float g_lastMasterVolume = 0.5f; bool g_lastMute = false; IAudioEndpointVolume* g_endpointVolume = nullptr; +Flyout g_unifiedFlyout = nullptr; +ListView g_deviceListView = nullptr; +winrt::Windows::Foundation::Collections::IObservableVector g_devices = winrt::single_threaded_observable_vector(); // GUID used to tag our own volume changes so the callback ignores them static const GUID g_ourVolumeGuid = { 0x9a4b2d1c, 0x3e5f, 0x4a6b, { 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8, 0xc9 } }; diff --git a/SettingsUtil.hpp b/SettingsUtil.hpp index 45ce027..67785fc 100644 --- a/SettingsUtil.hpp +++ b/SettingsUtil.hpp @@ -9,6 +9,7 @@ void DefaultSettings() g_lastDevices.clear(); g_volume = 0.1; g_volumeLock = true; + g_runAtStartup = false; } void LoadSettings() @@ -43,6 +44,10 @@ void LoadSettings() { g_volumeLock = jsonObj.Lookup(L"volumeLock").GetBoolean(); } + if (jsonObj.HasKey(L"runAtStartup")) + { + g_runAtStartup = jsonObj.Lookup(L"runAtStartup").GetBoolean(); + } auto lastDevices = jsonObj.Lookup(L"lastDevices").GetArray(); g_lastDevices.reserve(lastDevices.Size()); @@ -63,6 +68,7 @@ void SaveSettings() jsonObj.Insert(L"reconnect", JsonValue::CreateBooleanValue(g_reconnect)); jsonObj.Insert(L"volume", JsonValue::CreateNumberValue(g_volume)); jsonObj.Insert(L"volumeLock", JsonValue::CreateBooleanValue(g_volumeLock)); + jsonObj.Insert(L"runAtStartup", JsonValue::CreateBooleanValue(g_runAtStartup)); JsonArray lastDevices; for (const auto& i : g_audioPlaybackConnections) From e4506b3171d4f9138e85a424cc53aa4efa3048ef Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 22:28:19 +0530 Subject: [PATCH 25/70] v1.7.6: Fix syntax error in WndProc (extra brace) --- AudioPlaybackConnector.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 2f8dc31..0ba59d9 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -201,8 +201,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_unifiedFlyout.ShowAt(g_xamlCanvas, point); } break; - } - break; case WM_APP + 10: // Device added { auto info = (DeviceInformation*)wParam; From 55c9096820753cb036e21d56d5918a5bcfbb3ba9 Mon Sep 17 00:00:00 2001 From: park-bit Date: Mon, 27 Apr 2026 22:34:03 +0530 Subject: [PATCH 26/70] v1.7.7: Fix compilation (missing headers and forward declarations) --- AudioPlaybackConnector.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 0ba59d9..62145d9 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "AudioPlaybackConnector.h" +#include LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void SetupUnifiedUI(); @@ -7,6 +8,13 @@ void UpdateNotifyIcon(); void DisableAbsoluteVolume(); void RevertAbsoluteVolume(); void SetRunAtStartup(bool enable); +void UpdateVolume(); +void SetupEndpointVolume(); +void TeardownEndpointVolume(); +void SetupSvgIcon(); +bool IsRunningAsAdmin(); +winrt::fire_and_forget ConnectDevice(DevicePicker picker, std::wstring_view deviceId); +winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device); // Audio session management globals and helpers static IAudioSessionManager2* g_sessionManager = nullptr; @@ -198,7 +206,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_unifiedFlyout.ShowAt(g_xamlCanvas, point); + g_unifiedFlyout.ShowAt(g_xamlCanvas); } break; case WM_APP + 10: // Device added From 6c93d12aed7b74972fae26de198f750e21395c0d Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 19:34:29 +0530 Subject: [PATCH 27/70] v1.7.8: Fix WinRT allocation and static redeclaration errors --- AudioPlaybackConnector.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 62145d9..968bc1a 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -211,9 +211,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case WM_APP + 10: // Device added { - auto info = (DeviceInformation*)wParam; - g_devices.Append(*info); - delete info; + auto idString = reinterpret_cast(wParam); + ConnectDevice(g_devicePicker, *idString); + delete idString; } break; case WM_CONNECTDEVICE: @@ -377,7 +377,11 @@ void SetupUnifiedUI() auto selector = AudioPlaybackConnection::GetDeviceSelector(); auto watcher = DeviceInformation::CreateWatcher(selector); watcher.Added([](const auto&, const auto& info) { - g_hWndXaml ? PostMessageW(g_hWnd, WM_APP + 10, (WPARAM)new DeviceInformation(info), 0) : 0; + if (g_hWndXaml) + { + auto idCopy = new std::wstring(info.Id()); + PostMessageW(g_hWnd, WM_APP + 10, reinterpret_cast(idCopy), 0); + } }); watcher.Start(); } @@ -774,7 +778,7 @@ void UpdateVolume() ApplyVolumeToOurSessions(g_sessionManager); } -static bool IsRunningAsAdmin() +bool IsRunningAsAdmin() { BOOL isAdmin = FALSE; HANDLE token = NULL; From de7b6ecf12b2d199fada209d47c8b15b08276095 Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 19:39:47 +0530 Subject: [PATCH 28/70] v1.7.9: Fix C4267 warning in SetRunAtStartup --- AudioPlaybackConnector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 968bc1a..09a70ea 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -877,7 +877,7 @@ void SetRunAtStartup(bool enable) { wchar_t exePath[MAX_PATH]; GetModuleFileNameW(NULL, exePath, MAX_PATH); - RegSetValueExW(hKey, L"AudioPlaybackConnector", 0, REG_SZ, (const BYTE*)exePath, (wcslen(exePath) + 1) * sizeof(wchar_t)); + RegSetValueExW(hKey, L"AudioPlaybackConnector", 0, REG_SZ, (const BYTE*)exePath, static_cast((wcslen(exePath) + 1) * sizeof(wchar_t))); } else { From 6631ce9ab78badad77684e914004501cf4e9246a Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 19:48:52 +0530 Subject: [PATCH 29/70] v1.7.10: Fix invalid local function syntax in AddDeviceAsync --- AudioPlaybackConnector.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 09a70ea..88f15e8 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -212,7 +212,16 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_APP + 10: // Device added { auto idString = reinterpret_cast(wParam); - ConnectDevice(g_devicePicker, *idString); + + // Run async task to get device info and append to list + auto deviceId = *idString; + auto AddDeviceAsync = [](std::wstring id) -> winrt::Windows::Foundation::IAsyncAction + { + auto device = co_await DeviceInformation::CreateFromIdAsync(id); + g_devices.Append(device); + }; + AddDeviceAsync(deviceId); + delete idString; } break; @@ -280,9 +289,7 @@ void SetupUnifiedUI() deviceList.Height(150); deviceList.ItemsSource(g_devices); deviceList.SelectionMode(ListViewSelectionMode::None); - - // Item Template for Device List - deviceList.ItemTemplate(winrt::Windows::UI::Xaml::DataTemplate()); // We'll handle this with a simpler approach or default to Name + deviceList.DisplayMemberPath(L"Name"); deviceList.ItemClick([](const auto&, const auto& args) { auto device = args.ClickedItem().as(); @@ -379,7 +386,7 @@ void SetupUnifiedUI() watcher.Added([](const auto&, const auto& info) { if (g_hWndXaml) { - auto idCopy = new std::wstring(info.Id()); + auto idCopy = new std::wstring(info.Id().c_str()); PostMessageW(g_hWnd, WM_APP + 10, reinterpret_cast(idCopy), 0); } }); @@ -388,7 +395,7 @@ void SetupUnifiedUI() winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device) { - picker.SetDisplayStatus(device, _(L"Connecting"), DevicePickerDisplayStatusOptions::ShowProgress | DevicePickerDisplayStatusOptions::ShowDisconnectButton); + if (picker) picker.SetDisplayStatus(device, _(L"Connecting"), DevicePickerDisplayStatusOptions::ShowProgress | DevicePickerDisplayStatusOptions::ShowDisconnectButton); bool success = false; std::wstring errorMessage; @@ -406,7 +413,7 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation devi auto it = g_audioPlaybackConnections.find(std::wstring(sender.DeviceId())); if (it != g_audioPlaybackConnections.end()) { - g_devicePicker.SetDisplayStatus(it->second.first, {}, DevicePickerDisplayStatusOptions::None); + if (g_devicePicker) g_devicePicker.SetDisplayStatus(it->second.first, {}, DevicePickerDisplayStatusOptions::None); g_audioPlaybackConnections.erase(it); } sender.Close(); @@ -463,7 +470,7 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation devi if (success) { - picker.SetDisplayStatus(device, _(L"Connected"), DevicePickerDisplayStatusOptions::ShowDisconnectButton); + if (picker) picker.SetDisplayStatus(device, _(L"Connected"), DevicePickerDisplayStatusOptions::ShowDisconnectButton); } else { @@ -473,7 +480,7 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation devi it->second.second.Close(); g_audioPlaybackConnections.erase(it); } - picker.SetDisplayStatus(device, errorMessage, DevicePickerDisplayStatusOptions::ShowRetryButton); + if (picker) picker.SetDisplayStatus(device, errorMessage, DevicePickerDisplayStatusOptions::ShowRetryButton); } } From adac8642a2ed37b50ecc2c21903454ca75a3cfb3 Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 19:54:40 +0530 Subject: [PATCH 30/70] v1.7.11: Fix missing WinRT UI and Media headers --- pch.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pch.h b/pch.h index 89c916d..3b5a526 100644 --- a/pch.h +++ b/pch.h @@ -50,6 +50,8 @@ #include #include #include +#include +#include #include #include #include From b4c20e4edfa0f48ffc71137f362ead975ac11440 Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 22:47:46 +0530 Subject: [PATCH 31/70] v1.7.12: Fix missing WM_NOTIFYICON wrapper for tray clicks --- AudioPlaybackConnector.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 88f15e8..4951dcf 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -189,6 +189,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) UpdateNotifyIcon(); } break; + case WM_NOTIFYICON: + switch (LOWORD(lParam)) + { case WM_LBUTTONUP: case WM_RBUTTONUP: case NIN_SELECT: @@ -209,6 +212,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_unifiedFlyout.ShowAt(g_xamlCanvas); } break; + } + break; case WM_APP + 10: // Device added { auto idString = reinterpret_cast(wParam); From a15668a37e4e853554a6df7eee2500147f5c85b6 Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 23:01:01 +0530 Subject: [PATCH 32/70] v1.7.13: Fix missing window pos and layout size for unified flyout ShowAt --- AudioPlaybackConnector.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 4951dcf..5bd8334 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -202,13 +202,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (FAILED(hr)) break; auto dpi = GetDpiForWindow(hWnd); - Point point = { - static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi) - }; - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); + + g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_unifiedFlyout.ShowAt(g_xamlCanvas); } break; From 40c0fb3157102434e6deaebfe8aca5f437be3d02 Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 23:21:05 +0530 Subject: [PATCH 33/70] v1.7.14: Roll back to separate Volume Flyout and Context Menu UI while keeping all new features --- AudioPlaybackConnector.cpp | 330 +++++++++++++++++++++++-------------- AudioPlaybackConnector.h | 2 - 2 files changed, 208 insertions(+), 124 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 5bd8334..f078bdf 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -3,7 +3,9 @@ #include LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); -void SetupUnifiedUI(); +void SetupFlyout(); +void SetupVolumeFlyout(); +void SetupMenu(); void UpdateNotifyIcon(); void DisableAbsoluteVolume(); void RevertAbsoluteVolume(); @@ -13,6 +15,7 @@ void SetupEndpointVolume(); void TeardownEndpointVolume(); void SetupSvgIcon(); bool IsRunningAsAdmin(); +void SetupDevicePicker(); winrt::fire_and_forget ConnectDevice(DevicePicker picker, std::wstring_view deviceId); winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device); @@ -132,7 +135,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, LoadSettings(); SetupEndpointVolume(); - SetupUnifiedUI(); + SetupFlyout(); + SetupVolumeFlyout(); + SetupMenu(); + SetupDevicePicker(); SetupSvgIcon(); g_nid.hWnd = g_niid.hWnd = g_hWnd; @@ -193,7 +199,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) switch (LOWORD(lParam)) { case WM_LBUTTONUP: - case WM_RBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { @@ -210,7 +215,30 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_unifiedFlyout.ShowAt(g_xamlCanvas); + g_volumeFlyout.ShowAt(g_xamlCanvas); + } + break; + case WM_RBUTTONUP: + { + g_menuFocusState = FocusState::Pointer; + break; + } + case WM_CONTEXTMENU: + { + if (g_menuFocusState == FocusState::Unfocused) + g_menuFocusState = FocusState::Keyboard; + + auto dpi = GetDpiForWindow(hWnd); + Point point = { + static_cast(GET_X_LPARAM(lParam) * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(GET_Y_LPARAM(lParam) * USER_DEFAULT_SCREEN_DPI / dpi) + }; + + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); + SetForegroundWindow(hWnd); + + g_xamlMenu.ShowAt(g_xamlCanvas, point); } break; } @@ -260,143 +288,201 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) return 0; } -void SetupUnifiedUI() +void SetupFlyout() { - StackPanel root; - root.Width(320); - root.Padding({ 16 }); - root.Spacing(12); - - // --- Header --- - TextBlock header; - header.Text(_(L"Audio Connector")); - header.FontSize(20); - header.FontWeight(winrt::Windows::UI::Text::FontWeights::Bold()); - root.Children().Append(header); + TextBlock textBlock; + textBlock.Text(_(L"All connections will be closed.\nExit anyway?")); + textBlock.Margin({ 0, 0, 0, 12 }); + + static CheckBox checkbox; + checkbox.IsChecked(g_reconnect); + checkbox.Content(winrt::box_value(_(L"Reconnect on next start"))); + + Button button; + button.Content(winrt::box_value(_(L"Exit"))); + button.HorizontalAlignment(HorizontalAlignment::Right); + button.Click([](const auto&, const auto&) { + g_reconnect = checkbox.IsChecked().Value(); + PostMessageW(g_hWnd, WM_CLOSE, 0, 0); + }); - if (!IsRunningAsAdmin()) - { - TextBlock adminWarn; - adminWarn.Text(_(L"⚠ Running without Administrator privileges. System fixes may not apply.")); - adminWarn.FontSize(11); - adminWarn.Foreground(winrt::Windows::UI::Xaml::Media::SolidColorBrush(winrt::Windows::UI::Colors::OrangeRed())); - adminWarn.TextWrapping(TextWrapping::Wrap); - root.Children().Append(adminWarn); - } - - // --- Device Section --- - TextBlock deviceHeader; - deviceHeader.Text(_(L"Available Devices")); - deviceHeader.FontSize(14); - deviceHeader.Opacity(0.7); - root.Children().Append(deviceHeader); - - ListView deviceList; - deviceList.Height(150); - deviceList.ItemsSource(g_devices); - deviceList.SelectionMode(ListViewSelectionMode::None); - deviceList.DisplayMemberPath(L"Name"); - - deviceList.ItemClick([](const auto&, const auto& args) { - auto device = args.ClickedItem().as(); - ConnectDevice(g_devicePicker, device.Id()); // Reuse existing connection logic + StackPanel stackPanel; + stackPanel.Children().Append(textBlock); + stackPanel.Children().Append(checkbox); + stackPanel.Children().Append(button); + + Flyout flyout; + flyout.Content(stackPanel); + flyout.Closed([](const auto&, const auto&) { + ShowWindow(g_hWnd, SW_HIDE); + SaveSettings(); }); - g_deviceListView = deviceList; - root.Children().Append(deviceList); - - // --- Volume Control --- - TextBlock volHeader; - volHeader.Text(_(L"Mobile Volume")); - volHeader.FontSize(14); - volHeader.Opacity(0.7); - root.Children().Append(volHeader); - - Slider volSlider; - volSlider.Minimum(0); - volSlider.Maximum(100); - volSlider.Value(g_volume * 100); - volSlider.ValueChanged([](const auto&, const auto& args) { + + g_xamlFlyout = flyout; +} + +void SetupVolumeFlyout() +{ + TextBlock textBlock; + textBlock.Text(_(L"Mobile Volume")); + textBlock.Margin({ 0, 0, 0, 12 }); + + Slider slider; + slider.Minimum(0); + slider.Maximum(100); + slider.Value(g_volume * 100); + slider.Width(200); + slider.ValueChanged([](const auto&, const auto& args) { g_volume = args.NewValue() / 100.0; UpdateVolume(); }); - root.Children().Append(volSlider); - - // --- Toggles --- - ToggleSwitch lockToggle; - lockToggle.Header(winrt::box_value(_(L"Lock PC Volume Buttons"))); - lockToggle.IsOn(g_volumeLock); - lockToggle.Toggled([](const auto& sender, const auto&) { - g_volumeLock = sender.as().IsOn(); + + StackPanel stackPanel; + stackPanel.Children().Append(textBlock); + stackPanel.Children().Append(slider); + + Flyout flyout; + flyout.ShouldConstrainToRootBounds(false); + flyout.Content(stackPanel); + flyout.Closed([](const auto&, const auto&) { + ShowWindow(g_hWnd, SW_HIDE); + SaveSettings(); + }); + + g_volumeFlyout = flyout; +} + +void SetupMenu() +{ + FontIcon settingsIcon; + settingsIcon.Glyph(L"\xE713"); + + MenuFlyoutItem settingsItem; + settingsItem.Text(_(L"Bluetooth Settings")); + settingsItem.Icon(settingsIcon); + settingsItem.Click([](const auto&, const auto&) { + winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); + }); + + FontIcon connectIcon; + connectIcon.Glyph(L"\xE703"); + + MenuFlyoutItem connectItem; + connectItem.Text(_(L"Connect Device")); + connectItem.Icon(connectIcon); + connectItem.Click([](const auto&, const auto&) { + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) return; + + auto dpi = GetDpiForWindow(g_hWnd); + Point point = { + static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi) + }; + + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_SHOWWINDOW); + g_devicePicker.Show(point); + }); + + ToggleMenuFlyoutItem lockItem; + lockItem.Text(_(L"Lock Phone Volume Buttons")); + lockItem.IsChecked(g_volumeLock); + lockItem.Click([](const auto& sender, const auto&) { + g_volumeLock = sender.as().IsChecked(); if (g_volumeLock && g_endpointVolume) g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); SaveSettings(); }); - root.Children().Append(lockToggle); - ToggleSwitch startupToggle; - startupToggle.Header(winrt::box_value(_(L"Run at Windows Startup"))); - startupToggle.IsOn(g_runAtStartup); - startupToggle.Toggled([](const auto& sender, const auto&) { - g_runAtStartup = sender.as().IsOn(); + ToggleMenuFlyoutItem startupItem; + startupItem.Text(_(L"Run at Windows Startup")); + startupItem.IsChecked(g_runAtStartup); + startupItem.Click([](const auto& sender, const auto&) { + g_runAtStartup = sender.as().IsChecked(); SetRunAtStartup(g_runAtStartup); SaveSettings(); }); - root.Children().Append(startupToggle); - - // --- System Fixes --- - StackPanel fixPanel; - fixPanel.Orientation(Orientation::Horizontal); - fixPanel.Spacing(8); - - Button fixBtn; - fixBtn.Content(winrt::box_value(_(L"Fix Volume Sync"))); - fixBtn.Click([](const auto&, const auto&) { DisableAbsoluteVolume(); }); - fixPanel.Children().Append(fixBtn); - - Button revertBtn; - revertBtn.Content(winrt::box_value(_(L"Revert Fix"))); - revertBtn.Click([](const auto&, const auto&) { RevertAbsoluteVolume(); }); - fixPanel.Children().Append(revertBtn); - - root.Children().Append(fixPanel); - - // --- Instructions Footer --- - TextBlock tipsHeader; - tipsHeader.Text(_(L"Tips & Troubleshooting:")); - tipsHeader.FontSize(12); - tipsHeader.FontWeight(winrt::Windows::UI::Text::FontWeights::SemiBold()); - tipsHeader.Margin({ 0, 8, 0, 0 }); - root.Children().Append(tipsHeader); - - TextBlock tips; - tips.Text(_(L"• If no sound, try disconnecting and reconnecting on the phone.\n• Toggle 'Lock' off and on if volume buttons stop syncing.\n• Always run as Admin for the best experience.\n• System fix requires a REBOOT to work.")); - tips.FontSize(11); - tips.Opacity(0.6); - tips.TextWrapping(TextWrapping::Wrap); - root.Children().Append(tips); - - Button exitBtn; - exitBtn.Content(winrt::box_value(_(L"Exit App"))); - exitBtn.HorizontalAlignment(HorizontalAlignment::Right); - exitBtn.Click([](const auto&, const auto&) { PostMessageW(g_hWnd, WM_CLOSE, 0, 0); }); - root.Children().Append(exitBtn); - Flyout flyout; - flyout.Content(root); - flyout.Closed([](const auto&, const auto&) { ShowWindow(g_hWnd, SW_HIDE); SaveSettings(); }); - g_unifiedFlyout = flyout; + MenuFlyoutItem fixItem; + fixItem.Text(_(L"Fix Volume Sync (Absolute Volume)")); + fixItem.Click([](const auto&, const auto&) { DisableAbsoluteVolume(); }); + + MenuFlyoutItem revertItem; + revertItem.Text(_(L"Revert Volume Fix")); + revertItem.Click([](const auto&, const auto&) { RevertAbsoluteVolume(); }); + + FontIcon closeIcon; + closeIcon.Glyph(L"\xE8BB"); + + MenuFlyoutItem exitItem; + exitItem.Text(_(L"Exit")); + exitItem.Icon(closeIcon); + exitItem.Click([](const auto&, const auto&) { + if (g_audioPlaybackConnections.size() == 0) + { + PostMessageW(g_hWnd, WM_CLOSE, 0, 0); + return; + } + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) return; + auto dpi = GetDpiForWindow(g_hWnd); + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); + g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlFlyout.ShowAt(g_xamlCanvas); + }); - // Start device watching - auto selector = AudioPlaybackConnection::GetDeviceSelector(); - auto watcher = DeviceInformation::CreateWatcher(selector); - watcher.Added([](const auto&, const auto& info) { - if (g_hWndXaml) + MenuFlyout menu; + menu.Items().Append(settingsItem); + menu.Items().Append(connectItem); + menu.Items().Append(MenuFlyoutSeparator()); + menu.Items().Append(lockItem); + menu.Items().Append(startupItem); + menu.Items().Append(fixItem); + menu.Items().Append(revertItem); + menu.Items().Append(MenuFlyoutSeparator()); + menu.Items().Append(exitItem); + + menu.Opened([](const auto& sender, const auto&) { + auto menuItems = sender.as().Items(); + if (menuItems.Size() > 0) { - auto idCopy = new std::wstring(info.Id().c_str()); - PostMessageW(g_hWnd, WM_APP + 10, reinterpret_cast(idCopy), 0); + menuItems.GetAt(menuItems.Size() - 1).Focus(g_menuFocusState); } + g_menuFocusState = FocusState::Unfocused; + }); + + menu.Closed([](const auto&, const auto&) { + ShowWindow(g_hWnd, SW_HIDE); + }); + + g_xamlMenu = menu; +} + +void SetupDevicePicker() +{ + g_devicePicker = DevicePicker(); + winrt::check_hresult(g_devicePicker.as()->Initialize(g_hWnd)); + + g_devicePicker.Filter().SupportedDeviceSelectors().Append(AudioPlaybackConnection::GetDeviceSelector()); + g_devicePicker.DevicePickerDismissed([](const auto&, const auto&) { + SetWindowPos(g_hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_HIDEWINDOW); + }); + g_devicePicker.DeviceSelected([](const auto&, const auto& args) { + ConnectDevice(g_devicePicker, args.SelectedDevice()); + }); + g_devicePicker.DisconnectButtonClicked([](const auto& sender, const auto& args) { + auto device = args.Device(); + auto it = g_audioPlaybackConnections.find(std::wstring(device.Id())); + if (it != g_audioPlaybackConnections.end()) + { + it->second.second.Close(); + g_audioPlaybackConnections.erase(it); + } + sender.SetDisplayStatus(device, {}, DevicePickerDisplayStatusOptions::None); }); - watcher.Start(); } winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device) diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index 641fb8b..5595ee1 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -45,8 +45,6 @@ bool g_runAtStartup = false; float g_lastMasterVolume = 0.5f; bool g_lastMute = false; IAudioEndpointVolume* g_endpointVolume = nullptr; -Flyout g_unifiedFlyout = nullptr; -ListView g_deviceListView = nullptr; winrt::Windows::Foundation::Collections::IObservableVector g_devices = winrt::single_threaded_observable_vector(); // GUID used to tag our own volume changes so the callback ignores them static const GUID g_ourVolumeGuid = { 0x9a4b2d1c, 0x3e5f, 0x4a6b, { 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8, 0xc9 } }; From 9d1aa9276f913ff26c771d62a257d95ea9bb171d Mon Sep 17 00:00:00 2001 From: park-bit Date: Wed, 29 Apr 2026 23:25:42 +0530 Subject: [PATCH 34/70] v1.7.15: Fix build error (duplicate SetupDevicePicker) --- AudioPlaybackConnector.cpp | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index f078bdf..da3775d 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -582,28 +582,6 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, std::wstring_view devi ConnectDevice(picker, device); } -void SetupDevicePicker() -{ - g_devicePicker = DevicePicker(); - winrt::check_hresult(g_devicePicker.as()->Initialize(g_hWnd)); - - g_devicePicker.Filter().SupportedDeviceSelectors().Append(AudioPlaybackConnection::GetDeviceSelector()); - g_devicePicker.DevicePickerDismissed([](const auto&, const auto&) { - SetWindowPos(g_hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_HIDEWINDOW); - }); - g_devicePicker.DeviceSelected([](const auto& sender, const auto& args) { - ConnectDevice(sender, args.SelectedDevice()); - }); - g_devicePicker.DisconnectButtonClicked([](const auto& sender, const auto& args) { - auto device = args.Device(); - auto it = g_audioPlaybackConnections.find(std::wstring(device.Id())); - if (it != g_audioPlaybackConnections.end()) - { - it->second.second.Close(); - g_audioPlaybackConnections.erase(it); - } - sender.SetDisplayStatus(device, {}, DevicePickerDisplayStatusOptions::None); - }); } void SetupSvgIcon() From 3b8a78efa05955d915fc239a76683fe8231f4aa1 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 00:54:28 +0530 Subject: [PATCH 35/70] v1.7.16: Fix syntax error (extra brace) in AudioPlaybackConnector.cpp --- AudioPlaybackConnector.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index da3775d..de288e2 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -582,7 +582,6 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, std::wstring_view devi ConnectDevice(picker, device); } -} void SetupSvgIcon() { From d405c55043b2c6e2737c935a4c50a8d9175e333b Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 01:00:25 +0530 Subject: [PATCH 36/70] v1.7.17: Fix compilation error (Focus requires Control cast) --- AudioPlaybackConnector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index de288e2..583f4e2 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -449,7 +449,7 @@ void SetupMenu() auto menuItems = sender.as().Items(); if (menuItems.Size() > 0) { - menuItems.GetAt(menuItems.Size() - 1).Focus(g_menuFocusState); + menuItems.GetAt(menuItems.Size() - 1).as().Focus(g_menuFocusState); } g_menuFocusState = FocusState::Unfocused; }); From f5644f04fa245ac421fdffa71771dedf746be5ff Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 01:21:15 +0530 Subject: [PATCH 37/70] v1.7.18: Fix DevicePicker.Show() - pass Rect instead of Point --- AudioPlaybackConnector.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 583f4e2..4e8c8f8 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -376,13 +376,16 @@ void SetupMenu() if (FAILED(hr)) return; auto dpi = GetDpiForWindow(g_hWnd); - Point point = { - static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi) + float scale = static_cast(USER_DEFAULT_SCREEN_DPI) / dpi; + Rect rect = { + static_cast(iconRect.left) * scale, + static_cast(iconRect.top) * scale, + static_cast(iconRect.right - iconRect.left) * scale, + static_cast(iconRect.bottom - iconRect.top) * scale }; SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_SHOWWINDOW); - g_devicePicker.Show(point); + g_devicePicker.Show(rect); }); ToggleMenuFlyoutItem lockItem; From 4e6b6ec48a70ec3b9de06c6c06f8065a674ed043 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 01:34:29 +0530 Subject: [PATCH 38/70] v1.7.19: Fix menu/flyout position and DevicePicker anchor to tray icon --- AudioPlaybackConnector.cpp | 64 ++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 4e8c8f8..ba564aa 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -207,13 +207,16 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (FAILED(hr)) break; auto dpi = GetDpiForWindow(hWnd); + float dipW = static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipH = static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); - SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); + // Place host window exactly over the tray icon so XAML coords match screen coords + SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW | SWP_NOACTIVATE); + SetWindowPos(g_hWndXaml, 0, 0, 0, static_cast(dipW), static_cast(dipH), SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Width(dipW); + g_xamlCanvas.Height(dipH); g_volumeFlyout.ShowAt(g_xamlCanvas); } @@ -228,17 +231,32 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (g_menuFocusState == FocusState::Unfocused) g_menuFocusState = FocusState::Keyboard; - auto dpi = GetDpiForWindow(hWnd); - Point point = { - static_cast(GET_X_LPARAM(lParam) * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(GET_Y_LPARAM(lParam) * USER_DEFAULT_SCREEN_DPI / dpi) - }; + // Get the tray icon rect so we can anchor the menu to it + RECT iconRect; + if (FAILED(Shell_NotifyIconGetRect(&g_niid, &iconRect))) + { + // Fall back to cursor position + GetCursorPos(reinterpret_cast(&iconRect)); + iconRect.right = iconRect.left + 1; + iconRect.bottom = iconRect.top + 1; + } - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); + auto dpi = GetDpiForWindow(hWnd); + float dipW = static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipH = static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI) / dpi; + if (dipW < 1.f) dipW = 1.f; + if (dipH < 1.f) dipH = 1.f; + + // Host window must sit at the icon position; XAML coords are relative to it + SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW | SWP_NOACTIVATE); + SetWindowPos(g_hWndXaml, 0, 0, 0, static_cast(dipW), static_cast(dipH), SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlMenu.ShowAt(g_xamlCanvas, point); + g_xamlCanvas.Width(dipW); + g_xamlCanvas.Height(dipH); + + // Show menu at the top-left of the canvas; XAML will place it above/below based on available space + g_xamlMenu.ShowAt(g_xamlCanvas, Point{ 0.f, 0.f }); } break; } @@ -373,18 +391,24 @@ void SetupMenu() connectItem.Click([](const auto&, const auto&) { RECT iconRect; auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) return; + if (FAILED(hr)) + { + // Fall back to cursor position + POINT pt; + GetCursorPos(&pt); + iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; + } - auto dpi = GetDpiForWindow(g_hWnd); - float scale = static_cast(USER_DEFAULT_SCREEN_DPI) / dpi; + // DevicePicker.Show() takes physical pixel coords (RECT in screen space), not DIPs Rect rect = { - static_cast(iconRect.left) * scale, - static_cast(iconRect.top) * scale, - static_cast(iconRect.right - iconRect.left) * scale, - static_cast(iconRect.bottom - iconRect.top) * scale + static_cast(iconRect.left), + static_cast(iconRect.top), + static_cast(iconRect.right - iconRect.left), + static_cast(iconRect.bottom - iconRect.top) }; - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_SHOWWINDOW); + // Make the host window visible so DevicePicker HWND owner is valid + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW | SWP_NOACTIVATE); g_devicePicker.Show(rect); }); From c9526dd3ab563dcca261a01c69b094bd2f2dfa53 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 02:32:48 +0530 Subject: [PATCH 39/70] v1.8.0: Implement mandatory admin run, add Usage Instructions menu, and automate GitHub Releases --- .github/workflows/build.yaml | 11 +++++++++++ AudioPlaybackConnector.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index db5eb86..26d3e2b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,3 +46,14 @@ jobs: name: AudioPlaybackConnector32 path: Release/AudioPlaybackConnector32.exe if-no-files-found: warn + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + x64/Release/AudioPlaybackConnector64.exe + Release/AudioPlaybackConnector32.exe + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index ba564aa..001132a 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -88,6 +88,18 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, g_hInst = hInstance; winrt::init_apartment(); + LoadTranslateData(); + + // Always run as administrator to ensure registry and startup features work + if (!IsRunningAsAdmin()) + { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + if (reinterpret_cast(ShellExecuteW(NULL, L"runas", exePath, lpCmdLine, NULL, SW_SHOWNORMAL)) > 32) + { + return 0; + } + } bool supported = false; try @@ -372,6 +384,20 @@ void SetupVolumeFlyout() void SetupMenu() { + MenuFlyoutItem infoItem; + infoItem.Text(_(L"Usage Instructions")); + FontIcon infoIcon; + infoIcon.Glyph(L"\xE946"); + infoItem.Icon(infoIcon); + infoItem.Click([](const auto&, const auto&) { + TaskDialog(g_hWnd, g_hInst, _(L"Usage Instructions"), _(L"Tips for using AudioPlaybackConnector:"), + _(L"1. Always run as administrator for all features to work.\n" + "2. If no audio, try disconnecting and reconnecting Bluetooth from your phone.\n" + "3. If volume sync is broken, use the 'Fix Volume Sync' option and REBOOT.\n" + "4. Use 'Lock Phone Volume Buttons' to prevent phone buttons from changing PC volume."), + TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + }); + FontIcon settingsIcon; settingsIcon.Glyph(L"\xE713"); @@ -462,6 +488,8 @@ void SetupMenu() }); MenuFlyout menu; + menu.Items().Append(infoItem); + menu.Items().Append(MenuFlyoutSeparator()); menu.Items().Append(settingsItem); menu.Items().Append(connectItem); menu.Items().Append(MenuFlyoutSeparator()); From 3df8dbd7cfec5d33c59c8234a413eaaef45f1e2d Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 02:37:00 +0530 Subject: [PATCH 40/70] v1.8.1: Fix GitHub Action permissions for Releases --- .github/workflows/build.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 26d3e2b..bf9c21e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,6 +4,9 @@ on: push: tags: [ '**' ] +permissions: + contents: write + jobs: build: runs-on: windows-latest From 3eff5c320783548f89f6b321a504503794c0991e Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 02:45:48 +0530 Subject: [PATCH 41/70] v1.8.2: Fix DevicePicker.Show() - use DIPs and ensure host window focus --- AudioPlaybackConnector.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 001132a..b52e63c 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -419,22 +419,24 @@ void SetupMenu() auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); if (FAILED(hr)) { - // Fall back to cursor position POINT pt; GetCursorPos(&pt); iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; } - // DevicePicker.Show() takes physical pixel coords (RECT in screen space), not DIPs + auto dpi = GetDpiForWindow(g_hWnd); + float scale = static_cast(USER_DEFAULT_SCREEN_DPI) / dpi; + + // DevicePicker.Show() takes DIPs, not physical pixels Rect rect = { - static_cast(iconRect.left), - static_cast(iconRect.top), - static_cast(iconRect.right - iconRect.left), - static_cast(iconRect.bottom - iconRect.top) + static_cast(iconRect.left) * scale, + static_cast(iconRect.top) * scale, + static_cast(iconRect.right - iconRect.left) * scale, + static_cast(iconRect.bottom - iconRect.top) * scale }; - // Make the host window visible so DevicePicker HWND owner is valid - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW | SWP_NOACTIVATE); + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW); + SetForegroundWindow(g_hWnd); g_devicePicker.Show(rect); }); From 7c57d4f73e978799246a8104c0ba093c2b343f81 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 20:33:34 +0530 Subject: [PATCH 42/70] v1.8.3: Fix Flyout sizing and DevicePicker coordinates --- AudioPlaybackConnector.cpp | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index b52e63c..472f0ef 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -223,12 +223,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) float dipH = static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI) / dpi; // Place host window exactly over the tray icon so XAML coords match screen coords - SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW | SWP_NOACTIVATE); - SetWindowPos(g_hWndXaml, 0, 0, 0, static_cast(dipW), static_cast(dipH), SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlCanvas.Width(dipW); - g_xamlCanvas.Height(dipH); + g_xamlCanvas.Width(1.f); + g_xamlCanvas.Height(1.f); g_volumeFlyout.ShowAt(g_xamlCanvas); } @@ -260,12 +260,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (dipH < 1.f) dipH = 1.f; // Host window must sit at the icon position; XAML coords are relative to it - SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW | SWP_NOACTIVATE); - SetWindowPos(g_hWndXaml, 0, 0, 0, static_cast(dipW), static_cast(dipH), SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlCanvas.Width(dipW); - g_xamlCanvas.Height(dipH); + g_xamlCanvas.Width(1.f); + g_xamlCanvas.Height(1.f); // Show menu at the top-left of the canvas; XAML will place it above/below based on available space g_xamlMenu.ShowAt(g_xamlCanvas, Point{ 0.f, 0.f }); @@ -342,6 +342,7 @@ void SetupFlyout() stackPanel.Children().Append(button); Flyout flyout; + flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); flyout.Closed([](const auto&, const auto&) { ShowWindow(g_hWnd, SW_HIDE); @@ -427,15 +428,11 @@ void SetupMenu() auto dpi = GetDpiForWindow(g_hWnd); float scale = static_cast(USER_DEFAULT_SCREEN_DPI) / dpi; - // DevicePicker.Show() takes DIPs, not physical pixels - Rect rect = { - static_cast(iconRect.left) * scale, - static_cast(iconRect.top) * scale, - static_cast(iconRect.right - iconRect.left) * scale, - static_cast(iconRect.bottom - iconRect.top) * scale - }; + // DevicePicker.Show() takes DIPs relative to the window client area. + // Since our window is 1x1 at the tray icon, {0,0} corresponds to the tray icon. + Rect rect = { 0.f, 0.f, 1.f, 1.f }; - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, iconRect.right - iconRect.left, iconRect.bottom - iconRect.top, SWP_SHOWWINDOW); + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(g_hWnd); g_devicePicker.Show(rect); }); @@ -483,13 +480,14 @@ void SetupMenu() auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); if (FAILED(hr)) return; auto dpi = GetDpiForWindow(g_hWnd); - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); + g_xamlCanvas.Width(1.f); + g_xamlCanvas.Height(1.f); g_xamlFlyout.ShowAt(g_xamlCanvas); }); MenuFlyout menu; + menu.ShouldConstrainToRootBounds(false); menu.Items().Append(infoItem); menu.Items().Append(MenuFlyoutSeparator()); menu.Items().Append(settingsItem); From 28ea6a1ffecdb0e08aab7cd1f9a9ffb7fdc441d3 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 20:36:47 +0530 Subject: [PATCH 43/70] v1.8.4: Defer DevicePicker.Show() to avoid Focus Loss from MenuFlyout --- AudioPlaybackConnector.cpp | 38 +++++++++++++++++++------------------- AudioPlaybackConnector.h | 1 + 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 472f0ef..0215aee 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -299,6 +299,24 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_lastDevices.clear(); } break; + case WM_SHOW_DEVICE_PICKER: + { + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) + { + POINT pt; + GetCursorPos(&pt); + iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; + } + + Rect rect = { 0.f, 0.f, 1.f, 1.f }; + + SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); + SetForegroundWindow(hWnd); + g_devicePicker.Show(rect); + } + break; case WM_RESTORE_VOLUME: // Fired by the volume callback when a remote (phone) source changed the volume if (g_volumeLock && g_endpointVolume) @@ -416,25 +434,7 @@ void SetupMenu() connectItem.Text(_(L"Connect Device")); connectItem.Icon(connectIcon); connectItem.Click([](const auto&, const auto&) { - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - POINT pt; - GetCursorPos(&pt); - iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; - } - - auto dpi = GetDpiForWindow(g_hWnd); - float scale = static_cast(USER_DEFAULT_SCREEN_DPI) / dpi; - - // DevicePicker.Show() takes DIPs relative to the window client area. - // Since our window is 1x1 at the tray icon, {0,0} corresponds to the tray icon. - Rect rect = { 0.f, 0.f, 1.f, 1.f }; - - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); - SetForegroundWindow(g_hWnd); - g_devicePicker.Show(rect); + PostMessageW(g_hWnd, WM_SHOW_DEVICE_PICKER, 0, 0); }); ToggleMenuFlyoutItem lockItem; diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index 5595ee1..a88c1b9 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -14,6 +14,7 @@ namespace fs = std::filesystem; constexpr UINT WM_NOTIFYICON = WM_APP + 1; constexpr UINT WM_CONNECTDEVICE = WM_APP + 2; constexpr UINT WM_RESTORE_VOLUME = WM_APP + 3; +constexpr UINT WM_SHOW_DEVICE_PICKER = WM_APP + 4; HINSTANCE g_hInst; HWND g_hWnd; From 4437bbada8180a52a9edee3ff9f8f3f5fd22a785 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 20:45:32 +0530 Subject: [PATCH 44/70] v1.8.5: Fix missing Placement mode causing UI failure --- AudioPlaybackConnector.cpp | 9 ++++++--- v1.7.13_AudioPlaybackConnector.cpp | 0 v1.7.14_AudioPlaybackConnector.cpp | Bin 0 -> 62902 bytes 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 v1.7.13_AudioPlaybackConnector.cpp create mode 100644 v1.7.14_AudioPlaybackConnector.cpp diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 0215aee..88c0039 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -224,7 +224,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) // Place host window exactly over the tray icon so XAML coords match screen coords SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 1, 1, SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); g_xamlCanvas.Width(1.f); @@ -261,7 +261,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) // Host window must sit at the icon position; XAML coords are relative to it SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 1, 1, SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); g_xamlCanvas.Width(1.f); @@ -314,7 +314,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_devicePicker.Show(rect); + g_devicePicker.Show(rect, winrt::Windows::UI::Popups::Placement::Above); } break; case WM_RESTORE_VOLUME: @@ -360,6 +360,7 @@ void SetupFlyout() stackPanel.Children().Append(button); Flyout flyout; + flyout.Placement(winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutPlacementMode::Top); flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); flyout.Closed([](const auto&, const auto&) { @@ -391,6 +392,7 @@ void SetupVolumeFlyout() stackPanel.Children().Append(slider); Flyout flyout; + flyout.Placement(winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutPlacementMode::Top); flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); flyout.Closed([](const auto&, const auto&) { @@ -487,6 +489,7 @@ void SetupMenu() }); MenuFlyout menu; + menu.Placement(winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutPlacementMode::Top); menu.ShouldConstrainToRootBounds(false); menu.Items().Append(infoItem); menu.Items().Append(MenuFlyoutSeparator()); diff --git a/v1.7.13_AudioPlaybackConnector.cpp b/v1.7.13_AudioPlaybackConnector.cpp new file mode 100644 index 0000000..e69de29 diff --git a/v1.7.14_AudioPlaybackConnector.cpp b/v1.7.14_AudioPlaybackConnector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..630731f7085541c7d16b4d1f23d806c4d2ea3e59 GIT binary patch literal 62902 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%0D37veoW)tUuzNKZ%emS~aotPmnk??pT8Ct zHoJOPzKo15X)OPYh2?x@SCvHADP+Io*xUJ=t1wMj+xP}-D44V@E}4X zuRf0*>Cf1=yXd|0moW?DU~Cq=p>%8(>AzSmHqLmcZ9OKX!@U6E;Z(a7^40bb9G5og zuCR0}Ul+Q=8?PjdaV}X}?8@))&W3to)OzPlu=6^i6molcj&wX@Uc`U$+IVUcukm>A zP@i6HRjl`yI1kAgBaRwsbu)FRUx@zvey$mPBe-r#cCo|jl82r7nhxLIboZ$qY-^Wa z>N*v#k?F+30C(j3*fTe4Ki*NWDGBPYfD>u3i}qvT$ousU^_g8q#1TFbh8A8E zXBU$Fl+;sk;#R9)LN-kGqiL7?JceMRAb0~EEl|UQ9t$KJXPaHpl9cU-#Rzs zB_i)wpN_0*t}T4oMayJgp;`7DyoaqZx7n(l6kK`NK;-kN;UL<{6&cQbc^f+0=HKe9 z7glL8ecN4sE!Az+tCs!QN4+Xf`J!Ip&pzs_&b!UZkc>`#NP~OvO z{^?>5Grh{o^dx_J9^&f0)v})M&mJN>k~oj1zOn2wJw!50_=MFD8S@Iy^Y^Ii@h(Yh zF!ntCKI}H$q<0W{e=$)i>z`Rex4b|2Q}*}F`OeyJ2`2~oB_HC-Rci0zEw1>?5gnQ0 z5fOww8^-M66++FY(sjIl?B!&%zW8KMPGs(%>N$R{=kR3Deh%%v;HYPNej$EiM|+Lz zu0q!a#xnmt#42%XwtKq_?%I59Jp4gAZKKQ8vI82LP$yga!J__3znl>6cTTmjd+S3J!PlrfqHSttr+4LA fO0IWpoQurd3wf=6-eomhj>F?Im&q;EnDPGu^AP2~ literal 0 HcmV?d00001 From c8c0f15d314a67490a1540ecf2e01505b9a57a69 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 20:51:20 +0530 Subject: [PATCH 45/70] v1.8.6: Implement monitor-relative UI anchoring to fix WinUI popup boundary constraints --- AudioPlaybackConnector.cpp | 86 +++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 88c0039..6c3b191 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -216,21 +216,29 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { RECT iconRect; auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) break; + if (FAILED(hr)) + { + POINT pt; + GetCursorPos(&pt); + iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; + } - auto dpi = GetDpiForWindow(hWnd); - float dipW = static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipH = static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI) / dpi; + HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = { sizeof(mi) }; + GetMonitorInfoW(hMonitor, &mi); - // Place host window exactly over the tray icon so XAML coords match screen coords - SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); - SetWindowPos(g_hWndXaml, 0, 0, 0, 1, 1, SWP_NOZORDER | SWP_SHOWWINDOW); + auto dpi = GetDpiForWindow(hWnd); + + // Anchor host window to the top-left of the monitor so XAML isn't constrained by screen edges + SetWindowPos(hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlCanvas.Width(1.f); - g_xamlCanvas.Height(1.f); + // Calculate tray icon position relative to the monitor, converted to DIPs + float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - g_volumeFlyout.ShowAt(g_xamlCanvas); + g_volumeFlyout.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); } break; case WM_RBUTTONUP: @@ -243,32 +251,28 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (g_menuFocusState == FocusState::Unfocused) g_menuFocusState = FocusState::Keyboard; - // Get the tray icon rect so we can anchor the menu to it RECT iconRect; if (FAILED(Shell_NotifyIconGetRect(&g_niid, &iconRect))) { - // Fall back to cursor position - GetCursorPos(reinterpret_cast(&iconRect)); - iconRect.right = iconRect.left + 1; - iconRect.bottom = iconRect.top + 1; + POINT pt; + GetCursorPos(&pt); + iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; } + HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = { sizeof(mi) }; + GetMonitorInfoW(hMonitor, &mi); + auto dpi = GetDpiForWindow(hWnd); - float dipW = static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipH = static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - if (dipW < 1.f) dipW = 1.f; - if (dipH < 1.f) dipH = 1.f; - - // Host window must sit at the icon position; XAML coords are relative to it - SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); - SetWindowPos(g_hWndXaml, 0, 0, 0, 1, 1, SWP_NOZORDER | SWP_SHOWWINDOW); + + SetWindowPos(hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlCanvas.Width(1.f); - g_xamlCanvas.Height(1.f); + float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - // Show menu at the top-left of the canvas; XAML will place it above/below based on available space - g_xamlMenu.ShowAt(g_xamlCanvas, Point{ 0.f, 0.f }); + g_xamlMenu.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); } break; } @@ -310,10 +314,19 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; } - Rect rect = { 0.f, 0.f, 1.f, 1.f }; + HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = { sizeof(mi) }; + GetMonitorInfoW(hMonitor, &mi); + + auto dpi = GetDpiForWindow(hWnd); - SetWindowPos(hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); + + float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; + + Rect rect = { dipX, dipY, 1.f, 1.f }; g_devicePicker.Show(rect, winrt::Windows::UI::Popups::Placement::Above); } break; @@ -481,11 +494,18 @@ void SetupMenu() RECT iconRect; auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); if (FAILED(hr)) return; + + HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = { sizeof(mi) }; + GetMonitorInfoW(hMonitor, &mi); + auto dpi = GetDpiForWindow(g_hWnd); - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 1, 1, SWP_SHOWWINDOW); - g_xamlCanvas.Width(1.f); - g_xamlCanvas.Height(1.f); - g_xamlFlyout.ShowAt(g_xamlCanvas); + SetWindowPos(g_hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); + + float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; + + g_xamlFlyout.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); }); MenuFlyout menu; From 646c9e6847d24ceffafe6f3b63b31d002f42c2e7 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 21:00:25 +0530 Subject: [PATCH 46/70] v1.8.7: Revert to v1.7.3 behavior for tray icon interaction --- AudioPlaybackConnector.cpp | 25 +++++++++++++------------ AudioPlaybackConnector.h | 2 +- v1.7.3_AudioPlaybackConnector.cpp | Bin 0 -> 56408 bytes 3 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 v1.7.3_AudioPlaybackConnector.cpp diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 6c3b191..51f663f 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -238,7 +238,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - g_volumeFlyout.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); + Rect rect = { dipX, dipY, 1.f, 1.f }; + g_devicePicker.Show(rect, winrt::Windows::UI::Popups::Placement::Above); } break; case WM_RBUTTONUP: @@ -303,7 +304,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_lastDevices.clear(); } break; - case WM_SHOW_DEVICE_PICKER: + case WM_SHOW_VOLUME_FLYOUT: { RECT iconRect; auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); @@ -321,13 +322,13 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) auto dpi = GetDpiForWindow(hWnd); SetWindowPos(hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); SetForegroundWindow(hWnd); float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - Rect rect = { dipX, dipY, 1.f, 1.f }; - g_devicePicker.Show(rect, winrt::Windows::UI::Popups::Placement::Above); + g_volumeFlyout.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); } break; case WM_RESTORE_VOLUME: @@ -442,14 +443,14 @@ void SetupMenu() winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); }); - FontIcon connectIcon; - connectIcon.Glyph(L"\xE703"); + FontIcon volumeIcon; + volumeIcon.Glyph(L"\xE767"); - MenuFlyoutItem connectItem; - connectItem.Text(_(L"Connect Device")); - connectItem.Icon(connectIcon); - connectItem.Click([](const auto&, const auto&) { - PostMessageW(g_hWnd, WM_SHOW_DEVICE_PICKER, 0, 0); + MenuFlyoutItem volumeItem; + volumeItem.Text(_(L"Volume Control")); + volumeItem.Icon(volumeIcon); + volumeItem.Click([](const auto&, const auto&) { + PostMessageW(g_hWnd, WM_SHOW_VOLUME_FLYOUT, 0, 0); }); ToggleMenuFlyoutItem lockItem; @@ -514,7 +515,7 @@ void SetupMenu() menu.Items().Append(infoItem); menu.Items().Append(MenuFlyoutSeparator()); menu.Items().Append(settingsItem); - menu.Items().Append(connectItem); + menu.Items().Append(volumeItem); menu.Items().Append(MenuFlyoutSeparator()); menu.Items().Append(lockItem); menu.Items().Append(startupItem); diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index a88c1b9..b9fbc28 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -14,7 +14,7 @@ namespace fs = std::filesystem; constexpr UINT WM_NOTIFYICON = WM_APP + 1; constexpr UINT WM_CONNECTDEVICE = WM_APP + 2; constexpr UINT WM_RESTORE_VOLUME = WM_APP + 3; -constexpr UINT WM_SHOW_DEVICE_PICKER = WM_APP + 4; +constexpr UINT WM_SHOW_VOLUME_FLYOUT = WM_APP + 4; HINSTANCE g_hInst; HWND g_hWnd; diff --git a/v1.7.3_AudioPlaybackConnector.cpp b/v1.7.3_AudioPlaybackConnector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1cf4adb3be758fc2f0c652c1434a358f489d080a GIT binary patch literal 56408 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 literal 0 HcmV?d00001 From 1a183889edb7f0c34a73ef591983c0cc5390d0f4 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 21:08:24 +0530 Subject: [PATCH 47/70] v1.8.8: Revert DevicePicker host window to fullscreen hidden (v1.7.3 method) --- AudioPlaybackConnector.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 51f663f..35d5354 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -223,22 +223,25 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; } - HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; - GetMonitorInfoW(hMonitor, &mi); + // DevicePicker is a WinRT component that needs the host window to define its bounding box. + // It works perfectly if the host window covers the virtual screen but is HIDDEN. + int vX = GetSystemMetrics(SM_XVIRTUALSCREEN); + int vY = GetSystemMetrics(SM_YVIRTUALSCREEN); + int vW = GetSystemMetrics(SM_CXVIRTUALSCREEN); + int vH = GetSystemMetrics(SM_CYVIRTUALSCREEN); auto dpi = GetDpiForWindow(hWnd); - - // Anchor host window to the top-left of the monitor so XAML isn't constrained by screen edges - SetWindowPos(hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + + SetWindowPos(hWnd, HWND_TOPMOST, vX, vY, vW, vH, SWP_HIDEWINDOW); SetForegroundWindow(hWnd); - // Calculate tray icon position relative to the monitor, converted to DIPs - float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; + // DevicePicker coordinates are relative to the client area of hWnd. + float dipX = static_cast((iconRect.left - vX) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipY = static_cast((iconRect.top - vY) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipW = static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI) / dpi; + float dipH = static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - Rect rect = { dipX, dipY, 1.f, 1.f }; + Rect rect = { dipX, dipY, dipW, dipH }; g_devicePicker.Show(rect, winrt::Windows::UI::Popups::Placement::Above); } break; From 47653dd0f6b525bceb6067fbfb8a8874492a5061 Mon Sep 17 00:00:00 2001 From: park-bit Date: Thu, 30 Apr 2026 21:28:44 +0530 Subject: [PATCH 48/70] v1.8.9: Fix compilation error by including missing Popups.h and removing temp files --- AudioPlaybackConnector.cpp | 1 + v1.7.13_AudioPlaybackConnector.cpp | 0 v1.7.14_AudioPlaybackConnector.cpp | Bin 62902 -> 0 bytes v1.7.3_AudioPlaybackConnector.cpp | Bin 56408 -> 0 bytes 4 files changed, 1 insertion(+) delete mode 100644 v1.7.13_AudioPlaybackConnector.cpp delete mode 100644 v1.7.14_AudioPlaybackConnector.cpp delete mode 100644 v1.7.3_AudioPlaybackConnector.cpp diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 35d5354..5ece8fb 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include "AudioPlaybackConnector.h" #include +#include LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void SetupFlyout(); diff --git a/v1.7.13_AudioPlaybackConnector.cpp b/v1.7.13_AudioPlaybackConnector.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/v1.7.14_AudioPlaybackConnector.cpp b/v1.7.14_AudioPlaybackConnector.cpp deleted file mode 100644 index 630731f7085541c7d16b4d1f23d806c4d2ea3e59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62902 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%0D37veoW)tUuzNKZ%emS~aotPmnk??pT8Ct zHoJOPzKo15X)OPYh2?x@SCvHADP+Io*xUJ=t1wMj+xP}-D44V@E}4X zuRf0*>Cf1=yXd|0moW?DU~Cq=p>%8(>AzSmHqLmcZ9OKX!@U6E;Z(a7^40bb9G5og zuCR0}Ul+Q=8?PjdaV}X}?8@))&W3to)OzPlu=6^i6molcj&wX@Uc`U$+IVUcukm>A zP@i6HRjl`yI1kAgBaRwsbu)FRUx@zvey$mPBe-r#cCo|jl82r7nhxLIboZ$qY-^Wa z>N*v#k?F+30C(j3*fTe4Ki*NWDGBPYfD>u3i}qvT$ousU^_g8q#1TFbh8A8E zXBU$Fl+;sk;#R9)LN-kGqiL7?JceMRAb0~EEl|UQ9t$KJXPaHpl9cU-#Rzs zB_i)wpN_0*t}T4oMayJgp;`7DyoaqZx7n(l6kK`NK;-kN;UL<{6&cQbc^f+0=HKe9 z7glL8ecN4sE!Az+tCs!QN4+Xf`J!Ip&pzs_&b!UZkc>`#NP~OvO z{^?>5Grh{o^dx_J9^&f0)v})M&mJN>k~oj1zOn2wJw!50_=MFD8S@Iy^Y^Ii@h(Yh zF!ntCKI}H$q<0W{e=$)i>z`Rex4b|2Q}*}F`OeyJ2`2~oB_HC-Rci0zEw1>?5gnQ0 z5fOww8^-M66++FY(sjIl?B!&%zW8KMPGs(%>N$R{=kR3Deh%%v;HYPNej$EiM|+Lz zu0q!a#xnmt#42%XwtKq_?%I59Jp4gAZKKQ8vI82LP$yga!J__3znl>6cTTmjd+S3J!PlrfqHSttr+4LA fO0IWpoQurd3wf=6-eomhj>F?Im&q;EnDPGu^AP2~ diff --git a/v1.7.3_AudioPlaybackConnector.cpp b/v1.7.3_AudioPlaybackConnector.cpp deleted file mode 100644 index 1cf4adb3be758fc2f0c652c1434a358f489d080a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56408 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 From 1fd72150213321ab6b2ba33acf00e5ec52612f00 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:11:15 +0530 Subject: [PATCH 49/70] v1.9.0: Fix ShowAt type error - use FlyoutShowOptions instead of Point --- AudioPlaybackConnector.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 5ece8fb..a27e258 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -277,7 +277,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - g_xamlMenu.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); + g_xamlMenu.ShowAt(g_xamlCanvas, [&]{ winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); return opts; }()); } break; } @@ -332,7 +332,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - g_volumeFlyout.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); + g_volumeFlyout.ShowAt(g_xamlCanvas, [&]{ winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); return opts; }()); } break; case WM_RESTORE_VOLUME: @@ -510,7 +510,7 @@ void SetupMenu() float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - g_xamlFlyout.ShowAt(g_xamlCanvas, Point{ dipX, dipY }); + g_xamlFlyout.ShowAt(g_xamlCanvas, [&]{ winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); return opts; }()); }); MenuFlyout menu; From ddfa4acdd79ed4a2ac58148b6ae31c357319a068 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:16:16 +0530 Subject: [PATCH 50/70] v1.9.1: Revert left-click to exact v1.7.3 DevicePicker code --- AudioPlaybackConnector.cpp | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index a27e258..aed9b32 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -215,35 +215,27 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case NIN_SELECT: case NIN_KEYSELECT: { + using namespace winrt::Windows::UI::Popups; + RECT iconRect; auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); if (FAILED(hr)) { - POINT pt; - GetCursorPos(&pt); - iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; + LOG_HR(hr); + break; } - // DevicePicker is a WinRT component that needs the host window to define its bounding box. - // It works perfectly if the host window covers the virtual screen but is HIDDEN. - int vX = GetSystemMetrics(SM_XVIRTUALSCREEN); - int vY = GetSystemMetrics(SM_YVIRTUALSCREEN); - int vW = GetSystemMetrics(SM_CXVIRTUALSCREEN); - int vH = GetSystemMetrics(SM_CYVIRTUALSCREEN); - auto dpi = GetDpiForWindow(hWnd); - - SetWindowPos(hWnd, HWND_TOPMOST, vX, vY, vW, vH, SWP_HIDEWINDOW); + Rect rect = { + static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi) + }; + + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); - - // DevicePicker coordinates are relative to the client area of hWnd. - float dipX = static_cast((iconRect.left - vX) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipY = static_cast((iconRect.top - vY) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipW = static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipH = static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - - Rect rect = { dipX, dipY, dipW, dipH }; - g_devicePicker.Show(rect, winrt::Windows::UI::Popups::Placement::Above); + g_devicePicker.Show(rect, Placement::Above); } break; case WM_RBUTTONUP: From 67066d0127577525a0d7bf24627a0e6213a32cc7 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:22:31 +0530 Subject: [PATCH 51/70] v1.9.2: Use v1.7.3 fullscreen-hidden host window approach for all flyouts --- AudioPlaybackConnector.cpp | 65 ++++---------------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index aed9b32..d45075a 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -248,28 +248,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (g_menuFocusState == FocusState::Unfocused) g_menuFocusState = FocusState::Keyboard; - RECT iconRect; - if (FAILED(Shell_NotifyIconGetRect(&g_niid, &iconRect))) - { - POINT pt; - GetCursorPos(&pt); - iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; - } - - HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; - GetMonitorInfoW(hMonitor, &mi); - - auto dpi = GetDpiForWindow(hWnd); - - SetWindowPos(hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); - - float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - - g_xamlMenu.ShowAt(g_xamlCanvas, [&]{ winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); return opts; }()); + g_xamlMenu.ShowAt(g_xamlCanvas); } break; } @@ -302,29 +283,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case WM_SHOW_VOLUME_FLYOUT: { - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - POINT pt; - GetCursorPos(&pt); - iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; - } - - HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; - GetMonitorInfoW(hMonitor, &mi); - - auto dpi = GetDpiForWindow(hWnd); - - SetWindowPos(hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); - - float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - - g_volumeFlyout.ShowAt(g_xamlCanvas, [&]{ winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); return opts; }()); + g_volumeFlyout.ShowAt(g_xamlCanvas); } break; case WM_RESTORE_VOLUME: @@ -488,21 +449,9 @@ void SetupMenu() PostMessageW(g_hWnd, WM_CLOSE, 0, 0); return; } - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) return; - - HMONITOR hMonitor = MonitorFromPoint(POINT{ iconRect.left, iconRect.top }, MONITOR_DEFAULTTONEAREST); - MONITORINFO mi = { sizeof(mi) }; - GetMonitorInfoW(hMonitor, &mi); - - auto dpi = GetDpiForWindow(g_hWnd); - SetWindowPos(g_hWnd, HWND_TOPMOST, mi.rcMonitor.left, mi.rcMonitor.top, 1, 1, SWP_SHOWWINDOW); - - float dipX = static_cast((iconRect.left - mi.rcMonitor.left) * USER_DEFAULT_SCREEN_DPI) / dpi; - float dipY = static_cast((iconRect.top - mi.rcMonitor.top) * USER_DEFAULT_SCREEN_DPI) / dpi; - - g_xamlFlyout.ShowAt(g_xamlCanvas, [&]{ winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); return opts; }()); + SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); + SetForegroundWindow(g_hWnd); + g_xamlFlyout.ShowAt(g_xamlCanvas); }); MenuFlyout menu; From f4c8aecb07482735b46a0c303976ba5e5af76596 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:33:56 +0530 Subject: [PATCH 52/70] v1.9.3: Exact v1.7.3 tray handlers - cursor pos for menu, NIN_SELECT only for picker --- AudioPlaybackConnector.cpp | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index d45075a..5fcdc2e 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -211,7 +211,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_NOTIFYICON: switch (LOWORD(lParam)) { - case WM_LBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { @@ -248,9 +247,17 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (g_menuFocusState == FocusState::Unfocused) g_menuFocusState = FocusState::Keyboard; - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); + auto dpi = GetDpiForWindow(hWnd); + Point point = { + static_cast(GET_X_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(GET_Y_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi) + }; + + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlMenu.ShowAt(g_xamlCanvas); + + g_xamlMenu.ShowAt(g_xamlCanvas, point); } break; } @@ -283,9 +290,26 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case WM_SHOW_VOLUME_FLYOUT: { - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) + { + POINT pt; + GetCursorPos(&pt); + iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; + } + + auto dpi = GetDpiForWindow(hWnd); + float dipX = static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi); + float dipY = static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi); + + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_volumeFlyout.ShowAt(g_xamlCanvas); + + winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; + opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); + g_volumeFlyout.ShowAt(g_xamlCanvas, opts); } break; case WM_RESTORE_VOLUME: From 6ca2c09686a78e90054e3f58f425263eb09f565b Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:37:59 +0530 Subject: [PATCH 53/70] v1.9.4: Full rollback to v1.7.3 source code --- AudioPlaybackConnector.cpp | 302 +++++++++++-------------------------- AudioPlaybackConnector.h | 7 +- 2 files changed, 91 insertions(+), 218 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 5fcdc2e..b6b1037 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -1,24 +1,18 @@ -#include "pch.h" +#include "pch.h" #include "AudioPlaybackConnector.h" -#include -#include LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void SetupFlyout(); void SetupVolumeFlyout(); void SetupMenu(); -void UpdateNotifyIcon(); -void DisableAbsoluteVolume(); -void RevertAbsoluteVolume(); -void SetRunAtStartup(bool enable); void UpdateVolume(); void SetupEndpointVolume(); void TeardownEndpointVolume(); -void SetupSvgIcon(); -bool IsRunningAsAdmin(); +void DisableAbsoluteVolume(); +winrt::fire_and_forget ConnectDevice(DevicePicker, std::wstring_view); void SetupDevicePicker(); -winrt::fire_and_forget ConnectDevice(DevicePicker picker, std::wstring_view deviceId); -winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device); +void SetupSvgIcon(); +void UpdateNotifyIcon(); // Audio session management globals and helpers static IAudioSessionManager2* g_sessionManager = nullptr; @@ -89,18 +83,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, g_hInst = hInstance; winrt::init_apartment(); - LoadTranslateData(); - - // Always run as administrator to ensure registry and startup features work - if (!IsRunningAsAdmin()) - { - wchar_t exePath[MAX_PATH]; - GetModuleFileNameW(NULL, exePath, MAX_PATH); - if (reinterpret_cast(ShellExecuteW(NULL, L"runas", exePath, lpCmdLine, NULL, SW_SHOWNORMAL)) > 32) - { - return 0; - } - } bool supported = false; try @@ -237,11 +219,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_devicePicker.Show(rect, Placement::Above); } break; - case WM_RBUTTONUP: - { + case WM_RBUTTONUP: // Menu activated by mouse click g_menuFocusState = FocusState::Pointer; break; - } case WM_CONTEXTMENU: { if (g_menuFocusState == FocusState::Unfocused) @@ -254,7 +234,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) }; SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); + SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); g_xamlMenu.ShowAt(g_xamlCanvas, point); @@ -262,22 +242,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; } break; - case WM_APP + 10: // Device added - { - auto idString = reinterpret_cast(wParam); - - // Run async task to get device info and append to list - auto deviceId = *idString; - auto AddDeviceAsync = [](std::wstring id) -> winrt::Windows::Foundation::IAsyncAction - { - auto device = co_await DeviceInformation::CreateFromIdAsync(id); - g_devices.Append(device); - }; - AddDeviceAsync(deviceId); - - delete idString; - } - break; case WM_CONNECTDEVICE: if (g_reconnect) { @@ -288,30 +252,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_lastDevices.clear(); } break; - case WM_SHOW_VOLUME_FLYOUT: - { - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - POINT pt; - GetCursorPos(&pt); - iconRect = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; - } - - auto dpi = GetDpiForWindow(hWnd); - float dipX = static_cast(iconRect.left * USER_DEFAULT_SCREEN_DPI / dpi); - float dipY = static_cast(iconRect.top * USER_DEFAULT_SCREEN_DPI / dpi); - - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); - SetForegroundWindow(hWnd); - - winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutShowOptions opts; - opts.Position(winrt::Windows::Foundation::Point{ dipX, dipY }); - g_volumeFlyout.ShowAt(g_xamlCanvas, opts); - } - break; case WM_RESTORE_VOLUME: // Fired by the volume callback when a remote (phone) source changed the volume if (g_volumeLock && g_endpointVolume) @@ -355,13 +295,8 @@ void SetupFlyout() stackPanel.Children().Append(button); Flyout flyout; - flyout.Placement(winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutPlacementMode::Top); flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); - flyout.Closed([](const auto&, const auto&) { - ShowWindow(g_hWnd, SW_HIDE); - SaveSettings(); - }); g_xamlFlyout = flyout; } @@ -387,7 +322,6 @@ void SetupVolumeFlyout() stackPanel.Children().Append(slider); Flyout flyout; - flyout.Placement(winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutPlacementMode::Top); flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); flyout.Closed([](const auto&, const auto&) { @@ -400,20 +334,7 @@ void SetupVolumeFlyout() void SetupMenu() { - MenuFlyoutItem infoItem; - infoItem.Text(_(L"Usage Instructions")); - FontIcon infoIcon; - infoIcon.Glyph(L"\xE946"); - infoItem.Icon(infoIcon); - infoItem.Click([](const auto&, const auto&) { - TaskDialog(g_hWnd, g_hInst, _(L"Usage Instructions"), _(L"Tips for using AudioPlaybackConnector:"), - _(L"1. Always run as administrator for all features to work.\n" - "2. If no audio, try disconnecting and reconnecting Bluetooth from your phone.\n" - "3. If volume sync is broken, use the 'Fix Volume Sync' option and REBOOT.\n" - "4. Use 'Lock Phone Volume Buttons' to prevent phone buttons from changing PC volume."), - TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); - }); - + // https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font FontIcon settingsIcon; settingsIcon.Glyph(L"\xE713"); @@ -424,6 +345,18 @@ void SetupMenu() winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); }); + // Lock toggle: blocks phone volume buttons from changing PC volume + static ToggleMenuFlyoutItem lockItem; + lockItem.Text(_(L"Lock Phone Volume Buttons")); + lockItem.IsChecked(g_volumeLock); + lockItem.Click([](const auto&, const auto&) { + g_volumeLock = lockItem.IsChecked(); + // When enabling, immediately restore our preferred master volume level + if (g_volumeLock && g_endpointVolume) + g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); + SaveSettings(); + }); + FontIcon volumeIcon; volumeIcon.Glyph(L"\xE767"); @@ -431,35 +364,22 @@ void SetupMenu() volumeItem.Text(_(L"Volume Control")); volumeItem.Icon(volumeIcon); volumeItem.Click([](const auto&, const auto&) { - PostMessageW(g_hWnd, WM_SHOW_VOLUME_FLYOUT, 0, 0); - }); - - ToggleMenuFlyoutItem lockItem; - lockItem.Text(_(L"Lock Phone Volume Buttons")); - lockItem.IsChecked(g_volumeLock); - lockItem.Click([](const auto& sender, const auto&) { - g_volumeLock = sender.as().IsChecked(); - if (g_volumeLock && g_endpointVolume) - g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); - SaveSettings(); - }); + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) + { + LOG_HR(hr); + return; + } - ToggleMenuFlyoutItem startupItem; - startupItem.Text(_(L"Run at Windows Startup")); - startupItem.IsChecked(g_runAtStartup); - startupItem.Click([](const auto& sender, const auto&) { - g_runAtStartup = sender.as().IsChecked(); - SetRunAtStartup(g_runAtStartup); - SaveSettings(); - }); + auto dpi = GetDpiForWindow(g_hWnd); - MenuFlyoutItem fixItem; - fixItem.Text(_(L"Fix Volume Sync (Absolute Volume)")); - fixItem.Click([](const auto&, const auto&) { DisableAbsoluteVolume(); }); + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); + g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); - MenuFlyoutItem revertItem; - revertItem.Text(_(L"Revert Volume Fix")); - revertItem.Click([](const auto&, const auto&) { RevertAbsoluteVolume(); }); + g_volumeFlyout.ShowAt(g_xamlCanvas); + }); FontIcon closeIcon; closeIcon.Glyph(L"\xE8BB"); @@ -473,35 +393,38 @@ void SetupMenu() PostMessageW(g_hWnd, WM_CLOSE, 0, 0); return; } - SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); - SetForegroundWindow(g_hWnd); + + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) + { + LOG_HR(hr); + return; + } + + auto dpi = GetDpiForWindow(g_hWnd); + + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); + g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlFlyout.ShowAt(g_xamlCanvas); }); MenuFlyout menu; - menu.Placement(winrt::Windows::UI::Xaml::Controls::Primitives::FlyoutPlacementMode::Top); - menu.ShouldConstrainToRootBounds(false); - menu.Items().Append(infoItem); - menu.Items().Append(MenuFlyoutSeparator()); menu.Items().Append(settingsItem); - menu.Items().Append(volumeItem); - menu.Items().Append(MenuFlyoutSeparator()); menu.Items().Append(lockItem); - menu.Items().Append(startupItem); - menu.Items().Append(fixItem); - menu.Items().Append(revertItem); - menu.Items().Append(MenuFlyoutSeparator()); + menu.Items().Append(volumeItem); menu.Items().Append(exitItem); - menu.Opened([](const auto& sender, const auto&) { auto menuItems = sender.as().Items(); - if (menuItems.Size() > 0) + auto itemsCount = menuItems.Size(); + if (itemsCount > 0) { - menuItems.GetAt(menuItems.Size() - 1).as().Focus(g_menuFocusState); + menuItems.GetAt(itemsCount - 1).Focus(g_menuFocusState); } g_menuFocusState = FocusState::Unfocused; }); - menu.Closed([](const auto&, const auto&) { ShowWindow(g_hWnd, SW_HIDE); }); @@ -509,33 +432,9 @@ void SetupMenu() g_xamlMenu = menu; } -void SetupDevicePicker() -{ - g_devicePicker = DevicePicker(); - winrt::check_hresult(g_devicePicker.as()->Initialize(g_hWnd)); - - g_devicePicker.Filter().SupportedDeviceSelectors().Append(AudioPlaybackConnection::GetDeviceSelector()); - g_devicePicker.DevicePickerDismissed([](const auto&, const auto&) { - SetWindowPos(g_hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_HIDEWINDOW); - }); - g_devicePicker.DeviceSelected([](const auto&, const auto& args) { - ConnectDevice(g_devicePicker, args.SelectedDevice()); - }); - g_devicePicker.DisconnectButtonClicked([](const auto& sender, const auto& args) { - auto device = args.Device(); - auto it = g_audioPlaybackConnections.find(std::wstring(device.Id())); - if (it != g_audioPlaybackConnections.end()) - { - it->second.second.Close(); - g_audioPlaybackConnections.erase(it); - } - sender.SetDisplayStatus(device, {}, DevicePickerDisplayStatusOptions::None); - }); -} - winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device) { - if (picker) picker.SetDisplayStatus(device, _(L"Connecting"), DevicePickerDisplayStatusOptions::ShowProgress | DevicePickerDisplayStatusOptions::ShowDisconnectButton); + picker.SetDisplayStatus(device, _(L"Connecting"), DevicePickerDisplayStatusOptions::ShowProgress | DevicePickerDisplayStatusOptions::ShowDisconnectButton); bool success = false; std::wstring errorMessage; @@ -553,7 +452,7 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation devi auto it = g_audioPlaybackConnections.find(std::wstring(sender.DeviceId())); if (it != g_audioPlaybackConnections.end()) { - if (g_devicePicker) g_devicePicker.SetDisplayStatus(it->second.first, {}, DevicePickerDisplayStatusOptions::None); + g_devicePicker.SetDisplayStatus(it->second.first, {}, DevicePickerDisplayStatusOptions::None); g_audioPlaybackConnections.erase(it); } sender.Close(); @@ -610,7 +509,7 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation devi if (success) { - if (picker) picker.SetDisplayStatus(device, _(L"Connected"), DevicePickerDisplayStatusOptions::ShowDisconnectButton); + picker.SetDisplayStatus(device, _(L"Connected"), DevicePickerDisplayStatusOptions::ShowDisconnectButton); } else { @@ -620,7 +519,7 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation devi it->second.second.Close(); g_audioPlaybackConnections.erase(it); } - if (picker) picker.SetDisplayStatus(device, errorMessage, DevicePickerDisplayStatusOptions::ShowRetryButton); + picker.SetDisplayStatus(device, errorMessage, DevicePickerDisplayStatusOptions::ShowRetryButton); } } @@ -630,6 +529,29 @@ winrt::fire_and_forget ConnectDevice(DevicePicker picker, std::wstring_view devi ConnectDevice(picker, device); } +void SetupDevicePicker() +{ + g_devicePicker = DevicePicker(); + winrt::check_hresult(g_devicePicker.as()->Initialize(g_hWnd)); + + g_devicePicker.Filter().SupportedDeviceSelectors().Append(AudioPlaybackConnection::GetDeviceSelector()); + g_devicePicker.DevicePickerDismissed([](const auto&, const auto&) { + SetWindowPos(g_hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_HIDEWINDOW); + }); + g_devicePicker.DeviceSelected([](const auto& sender, const auto& args) { + ConnectDevice(sender, args.SelectedDevice()); + }); + g_devicePicker.DisconnectButtonClicked([](const auto& sender, const auto& args) { + auto device = args.Device(); + auto it = g_audioPlaybackConnections.find(std::wstring(device.Id())); + if (it != g_audioPlaybackConnections.end()) + { + it->second.second.Close(); + g_audioPlaybackConnections.erase(it); + } + sender.SetDisplayStatus(device, {}, DevicePickerDisplayStatusOptions::None); + }); +} void SetupSvgIcon() { @@ -902,7 +824,7 @@ void UpdateVolume() ApplyVolumeToOurSessions(g_sessionManager); } -bool IsRunningAsAdmin() +static bool IsRunningAsAdmin() { BOOL isAdmin = FALSE; HANDLE token = NULL; @@ -919,11 +841,16 @@ bool IsRunningAsAdmin() void DisableAbsoluteVolume() { + // If not admin, relaunch with UAC elevation if (!IsRunningAsAdmin()) { wchar_t exePath[MAX_PATH]; GetModuleFileNameW(NULL, exePath, MAX_PATH); - ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); + HINSTANCE result = ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); + if (reinterpret_cast(result) <= 32) + { + TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to apply the system fix.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); + } return; } @@ -952,61 +879,10 @@ void DisableAbsoluteVolume() if (success) { - TaskDialog(g_hWnd, NULL, _(L"System Fix Applied"), _(L"Registry paths for Absolute Volume have been updated.\n\nYou MUST REBOOT your laptop now for this to take effect."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); - } -} - -void RevertAbsoluteVolume() -{ - if (!IsRunningAsAdmin()) - { - TaskDialog(g_hWnd, NULL, _(L"Admin Required"), _(L"Please run the app as Administrator to revert registry changes."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); - return; - } - - const wchar_t* paths[] = { - L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", - L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT", - L"SYSTEM\\CurrentControlSet\\Services\\HidBth\\Parameters", - L"SYSTEM\\CurrentControlSet\\Services\\BthAvrcpTg\\Parameters", - L"SOFTWARE\\Microsoft\\Bluetooth\\Audio\\AVRCP\\CT" - }; - - bool success = false; - for (auto path : paths) - { - HKEY hKey; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, path, 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) - { - DWORD val0 = 0; - RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); - RegSetValueExW(hKey, L"EnableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); - RegCloseKey(hKey); - success = true; - } - } - - if (success) - { - TaskDialog(g_hWnd, NULL, _(L"Fix Reverted"), _(L"Absolute Volume sync has been restored to default.\n\nYou MUST REBOOT for this to take effect."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + TaskDialog(g_hWnd, NULL, _(L"System Fix Applied DEFINITIVELY"), _(L"All known registry paths for Absolute Volume have been updated.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nIf volume buttons still sync after reboot, it means your Bluetooth driver is ignoring system settings."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); } -} - -void SetRunAtStartup(bool enable) -{ - HKEY hKey; - if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) + else { - if (enable) - { - wchar_t exePath[MAX_PATH]; - GetModuleFileNameW(NULL, exePath, MAX_PATH); - RegSetValueExW(hKey, L"AudioPlaybackConnector", 0, REG_SZ, (const BYTE*)exePath, static_cast((wcslen(exePath) + 1) * sizeof(wchar_t))); - } - else - { - RegDeleteValueW(hKey, L"AudioPlaybackConnector"); - } - RegCloseKey(hKey); + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); } } diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index b9fbc28..6b0d6c7 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "resource.h" @@ -14,7 +14,6 @@ namespace fs = std::filesystem; constexpr UINT WM_NOTIFYICON = WM_APP + 1; constexpr UINT WM_CONNECTDEVICE = WM_APP + 2; constexpr UINT WM_RESTORE_VOLUME = WM_APP + 3; -constexpr UINT WM_SHOW_VOLUME_FLYOUT = WM_APP + 4; HINSTANCE g_hInst; HWND g_hWnd; @@ -40,13 +39,11 @@ NOTIFYICONIDENTIFIER g_niid = { UINT WM_TASKBAR_CREATED = 0; bool g_reconnect = false; std::vector g_lastDevices; -double g_volume = 0.1; +double g_volume = 0.2; bool g_volumeLock = true; -bool g_runAtStartup = false; float g_lastMasterVolume = 0.5f; bool g_lastMute = false; IAudioEndpointVolume* g_endpointVolume = nullptr; -winrt::Windows::Foundation::Collections::IObservableVector g_devices = winrt::single_threaded_observable_vector(); // GUID used to tag our own volume changes so the callback ignores them static const GUID g_ourVolumeGuid = { 0x9a4b2d1c, 0x3e5f, 0x4a6b, { 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8, 0xc9 } }; From c58b35f072301fe7338f69b6272c15d14f55f5ad Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:48:54 +0530 Subject: [PATCH 54/70] v1.9.5: Add WM_LBUTTONUP fallback for left-click with debounce --- AudioPlaybackConnector.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index b6b1037..f90c578 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -1,4 +1,4 @@ -#include "pch.h" +#include "pch.h" #include "AudioPlaybackConnector.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); @@ -193,9 +193,16 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_NOTIFYICON: switch (LOWORD(lParam)) { + case WM_LBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { + // Debounce: WM_LBUTTONUP and NIN_SELECT can both fire for one click + static DWORD s_lastPickerTick = 0; + DWORD now = GetTickCount(); + if (now - s_lastPickerTick < 500) break; + s_lastPickerTick = now; + using namespace winrt::Windows::UI::Popups; RECT iconRect; From a482731be27c9591d3109df17ec63b73d930ea43 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:53:31 +0530 Subject: [PATCH 55/70] v1.9.6: Show window for DevicePicker + try-catch to surface errors --- AudioPlaybackConnector.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index f90c578..a37fd59 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -221,9 +221,18 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi) }; - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); + // Show the window (transparent, so invisible) - DevicePicker needs a visible parent window + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_devicePicker.Show(rect, Placement::Above); + try + { + g_devicePicker.Show(rect, Placement::Above); + } + catch (winrt::hresult_error const& ex) + { + TaskDialog(hWnd, g_hInst, L"Error", L"DevicePicker.Show failed", ex.message().c_str(), TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); + } + ShowWindow(hWnd, SW_HIDE); } break; case WM_RBUTTONUP: // Menu activated by mouse click From 455e5406539e45cce121620590d548d3e345d8bf Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 13:56:26 +0530 Subject: [PATCH 56/70] v1.9.7: Rollback SettingsUtil.hpp to v1.7.3 to fix g_runAtStartup mismatch --- SettingsUtil.hpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/SettingsUtil.hpp b/SettingsUtil.hpp index 67785fc..e2c8e0c 100644 --- a/SettingsUtil.hpp +++ b/SettingsUtil.hpp @@ -1,4 +1,4 @@ -#pragma once +#pragma once constexpr auto CONFIG_NAME = L"AudioPlaybackConnector.json"; constexpr auto BUFFER_SIZE = 4096; @@ -9,7 +9,6 @@ void DefaultSettings() g_lastDevices.clear(); g_volume = 0.1; g_volumeLock = true; - g_runAtStartup = false; } void LoadSettings() @@ -44,10 +43,6 @@ void LoadSettings() { g_volumeLock = jsonObj.Lookup(L"volumeLock").GetBoolean(); } - if (jsonObj.HasKey(L"runAtStartup")) - { - g_runAtStartup = jsonObj.Lookup(L"runAtStartup").GetBoolean(); - } auto lastDevices = jsonObj.Lookup(L"lastDevices").GetArray(); g_lastDevices.reserve(lastDevices.Size()); @@ -68,7 +63,6 @@ void SaveSettings() jsonObj.Insert(L"reconnect", JsonValue::CreateBooleanValue(g_reconnect)); jsonObj.Insert(L"volume", JsonValue::CreateNumberValue(g_volume)); jsonObj.Insert(L"volumeLock", JsonValue::CreateBooleanValue(g_volumeLock)); - jsonObj.Insert(L"runAtStartup", JsonValue::CreateBooleanValue(g_runAtStartup)); JsonArray lastDevices; for (const auto& i : g_audioPlaybackConnections) From 88b23b4837535d2bd2757b5f6bce4ed97b3f0f47 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 14:01:36 +0530 Subject: [PATCH 57/70] v1.9.8: Fix silent break on Shell_NotifyIconGetRect failure - use cursor fallback --- AudioPlaybackConnector.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index a37fd59..0956c9c 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -206,11 +206,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) using namespace winrt::Windows::UI::Popups; RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) + if (FAILED(Shell_NotifyIconGetRect(&g_niid, &iconRect))) { - LOG_HR(hr); - break; + // Fallback: use cursor position if icon rect unavailable + POINT pt; + GetCursorPos(&pt); + iconRect = { pt.x - 8, pt.y - 8, pt.x + 8, pt.y + 8 }; } auto dpi = GetDpiForWindow(hWnd); From 94c80b0381d449cf71c978542fe85b0fa6b8c889 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 14:12:35 +0530 Subject: [PATCH 58/70] v1.9.9-debug: Add tray notification code debug dialog --- AudioPlaybackConnector.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 0956c9c..18dd2a7 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -191,6 +191,13 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_NOTIFYICON: + { + // DEBUG: show notification code - REMOVE AFTER TESTING + wchar_t dbg[128]; + swprintf_s(dbg, L"WM_NOTIFYICON\nLOWORD(lParam)=0x%04X\nNIN_SELECT=0x%04X\nWM_LBUTTONUP=0x%04X", + LOWORD(lParam), NIN_SELECT, WM_LBUTTONUP); + MessageBoxW(nullptr, dbg, L"Tray Debug", MB_OK | MB_SYSTEMMODAL); + } switch (LOWORD(lParam)) { case WM_LBUTTONUP: From 13fa491265d1a40b222e480535ae18391a39d6f3 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 14:21:49 +0530 Subject: [PATCH 59/70] v2.0.0: Remove debug dialog, revert SWP_HIDEWINDOW, catch all exceptions --- AudioPlaybackConnector.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 18dd2a7..893914f 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -191,13 +191,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_NOTIFYICON: - { - // DEBUG: show notification code - REMOVE AFTER TESTING - wchar_t dbg[128]; - swprintf_s(dbg, L"WM_NOTIFYICON\nLOWORD(lParam)=0x%04X\nNIN_SELECT=0x%04X\nWM_LBUTTONUP=0x%04X", - LOWORD(lParam), NIN_SELECT, WM_LBUTTONUP); - MessageBoxW(nullptr, dbg, L"Tray Debug", MB_OK | MB_SYSTEMMODAL); - } switch (LOWORD(lParam)) { case WM_LBUTTONUP: @@ -230,7 +223,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) }; // Show the window (transparent, so invisible) - DevicePicker needs a visible parent window - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_SHOWWINDOW); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); try { @@ -240,6 +233,10 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { TaskDialog(hWnd, g_hInst, L"Error", L"DevicePicker.Show failed", ex.message().c_str(), TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); } + catch (...) + { + TaskDialog(hWnd, g_hInst, L"Error", L"DevicePicker.Show threw unknown exception", nullptr, TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); + } ShowWindow(hWnd, SW_HIDE); } break; From dacc6462c6dd451c62a8ec965c16d99d0e016d42 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 14:30:07 +0530 Subject: [PATCH 60/70] v2.0.0: Stable Rollback + Robust Click Handling (v1.7.3 based) --- AudioPlaybackConnector.cpp | 227 +++++++++++-------------------------- 1 file changed, 68 insertions(+), 159 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 893914f..33ae395 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -115,10 +115,11 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, RegisterClassExW(&wcex); - // When parent window size is 0x0 or invisible, the dpi scale of menu is incorrect. Here we set window size to 1x1 and use WS_EX_LAYERED to make window looks like invisible. - g_hWnd = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOPMOST, L"AudioPlaybackConnector", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr); + // Using 1x1 SHOWN transparent window - most stable for hosting WinRT Flyouts/Pickers + g_hWnd = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOPMOST, L"AudioPlaybackConnector", nullptr, WS_POPUP, 0, 0, 1, 1, nullptr, nullptr, hInstance, nullptr); FAIL_FAST_LAST_ERROR_IF_NULL(g_hWnd); FAIL_FAST_IF_WIN32_BOOL_FALSE(SetLayeredWindowAttributes(g_hWnd, 0, 0, LWA_ALPHA)); + ShowWindow(g_hWnd, SW_SHOW); DesktopWindowXamlSource desktopSource; auto desktopSourceNative2 = desktopSource.as(); @@ -191,24 +192,23 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_NOTIFYICON: - switch (LOWORD(lParam)) + { + UINT uMsg = LOWORD(lParam); + switch (uMsg) { case WM_LBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { - // Debounce: WM_LBUTTONUP and NIN_SELECT can both fire for one click - static DWORD s_lastPickerTick = 0; - DWORD now = GetTickCount(); - if (now - s_lastPickerTick < 500) break; - s_lastPickerTick = now; + static DWORD s_lastTick = 0; + if (GetTickCount() - s_lastTick < 500) break; + s_lastTick = GetTickCount(); using namespace winrt::Windows::UI::Popups; RECT iconRect; if (FAILED(Shell_NotifyIconGetRect(&g_niid, &iconRect))) { - // Fallback: use cursor position if icon rect unavailable POINT pt; GetCursorPos(&pt); iconRect = { pt.x - 8, pt.y - 8, pt.x + 8, pt.y + 8 }; @@ -222,47 +222,43 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi) }; - // Show the window (transparent, so invisible) - DevicePicker needs a visible parent window - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); - try - { + try { g_devicePicker.Show(rect, Placement::Above); + } catch (...) { + LOG_CAUGHT_EXCEPTION(); } - catch (winrt::hresult_error const& ex) - { - TaskDialog(hWnd, g_hInst, L"Error", L"DevicePicker.Show failed", ex.message().c_str(), TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); - } - catch (...) - { - TaskDialog(hWnd, g_hInst, L"Error", L"DevicePicker.Show threw unknown exception", nullptr, TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); - } - ShowWindow(hWnd, SW_HIDE); } break; - case WM_RBUTTONUP: // Menu activated by mouse click - g_menuFocusState = FocusState::Pointer; - break; + case WM_RBUTTONUP: case WM_CONTEXTMENU: { - if (g_menuFocusState == FocusState::Unfocused) - g_menuFocusState = FocusState::Keyboard; + static DWORD s_lastTick = 0; + if (GetTickCount() - s_lastTick < 500) break; + s_lastTick = GetTickCount(); + + POINT pt; + if (uMsg == WM_CONTEXTMENU && LOWORD(lParam) == WM_CONTEXTMENU) { + // VERSION_4 sends coordinates in wParam for WM_CONTEXTMENU + pt.x = GET_X_LPARAM(wParam); + pt.y = GET_Y_LPARAM(wParam); + } else { + GetCursorPos(&pt); + } auto dpi = GetDpiForWindow(hWnd); Point point = { - static_cast(GET_X_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(GET_Y_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi) + static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) }; - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); - SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlMenu.ShowAt(g_xamlCanvas, point); } break; } - break; + } + break; case WM_CONNECTDEVICE: if (g_reconnect) { @@ -346,7 +342,6 @@ void SetupVolumeFlyout() flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); flyout.Closed([](const auto&, const auto&) { - ShowWindow(g_hWnd, SW_HIDE); SaveSettings(); }); @@ -385,21 +380,14 @@ void SetupMenu() volumeItem.Text(_(L"Volume Control")); volumeItem.Icon(volumeIcon); volumeItem.Click([](const auto&, const auto&) { - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - LOG_HR(hr); - return; - } - + POINT pt; + GetCursorPos(&pt); auto dpi = GetDpiForWindow(g_hWnd); - - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); - - g_volumeFlyout.ShowAt(g_xamlCanvas); + Point point = { + static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) + }; + g_volumeFlyout.ShowAt(g_xamlCanvas, point); }); FontIcon closeIcon; @@ -415,21 +403,15 @@ void SetupMenu() return; } - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - LOG_HR(hr); - return; - } - + POINT pt; + GetCursorPos(&pt); auto dpi = GetDpiForWindow(g_hWnd); + Point point = { + static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) + }; - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); - - g_xamlFlyout.ShowAt(g_xamlCanvas); + g_xamlFlyout.ShowAt(g_xamlCanvas, point); }); MenuFlyout menu; @@ -442,12 +424,8 @@ void SetupMenu() auto itemsCount = menuItems.Size(); if (itemsCount > 0) { - menuItems.GetAt(itemsCount - 1).Focus(g_menuFocusState); + menuItems.GetAt(itemsCount - 1).Focus(FocusState::Pointer); } - g_menuFocusState = FocusState::Unfocused; - }); - menu.Closed([](const auto&, const auto&) { - ShowWindow(g_hWnd, SW_HIDE); }); g_xamlMenu = menu; @@ -556,9 +534,6 @@ void SetupDevicePicker() winrt::check_hresult(g_devicePicker.as()->Initialize(g_hWnd)); g_devicePicker.Filter().SupportedDeviceSelectors().Append(AudioPlaybackConnection::GetDeviceSelector()); - g_devicePicker.DevicePickerDismissed([](const auto&, const auto&) { - SetWindowPos(g_hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_HIDEWINDOW); - }); g_devicePicker.DeviceSelected([](const auto& sender, const auto& args) { ConnectDevice(sender, args.SelectedDevice()); }); @@ -601,16 +576,10 @@ void UpdateNotifyIcon() LOG_IF_WIN32_ERROR(RegGetValueW(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &cbValue)); g_nid.hIcon = value != 0 ? g_hIconLight : g_hIconDark; - if (!Shell_NotifyIconW(NIM_MODIFY, &g_nid)) + Shell_NotifyIconW(NIM_DELETE, &g_nid); + if (Shell_NotifyIconW(NIM_ADD, &g_nid)) { - if (Shell_NotifyIconW(NIM_ADD, &g_nid)) - { - FAIL_FAST_IF_WIN32_BOOL_FALSE(Shell_NotifyIconW(NIM_SETVERSION, &g_nid)); - } - else - { - LOG_LAST_ERROR(); - } + Shell_NotifyIconW(NIM_SETVERSION, &g_nid); } } @@ -784,13 +753,13 @@ void SetupEndpointVolume() winrt::check_hresult(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device)); enumerator->Release(); - // Register AVRCP guardian (blocks phone buttons from changing master volume) + // Register AVRCP guardian (blocks phone buttons) IAudioEndpointVolume* epVol = nullptr; winrt::check_hresult(device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (void**)&epVol)); g_endpointVolume = epVol; - // Initialize our Authority levels from the current system state + // Initialize our Authority levels float currentVol = 0.5f; BOOL currentMute = FALSE; if (SUCCEEDED(g_endpointVolume->GetMasterVolumeLevelScalar(¤tVol))) g_lastMasterVolume = currentVol; @@ -799,15 +768,14 @@ void SetupEndpointVolume() g_volumeCallback = new VolumeCallback(); g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback); - // Register session notifier so we catch AudioPlaybackConnection sessions the moment they start + // Register session notifier IAudioSessionManager2* mgr = nullptr; if (SUCCEEDED(device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC_SERVER, NULL, (void**)&mgr))) { - g_sessionManager = mgr; // keep alive for UpdateVolume + g_sessionManager = mgr; g_sessionNotifier = new SessionNotifier(); mgr->RegisterSessionNotification(g_sessionNotifier); - // Apply to any sessions already running ApplyVolumeToOurSessions(mgr); } device->Release(); @@ -820,90 +788,31 @@ void SetupEndpointVolume() void TeardownEndpointVolume() { - if (g_sessionManager && g_sessionNotifier) - { - g_sessionManager->UnregisterSessionNotification(g_sessionNotifier); - g_sessionNotifier->Release(); - g_sessionNotifier = nullptr; - g_sessionManager->Release(); - g_sessionManager = nullptr; - } - if (g_endpointVolume && g_volumeCallback) + if (g_endpointVolume) { - g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); - g_volumeCallback->Release(); - g_volumeCallback = nullptr; + if (g_volumeCallback) + { + g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); + g_volumeCallback->Release(); + g_volumeCallback = nullptr; + } g_endpointVolume->Release(); g_endpointVolume = nullptr; } -} - -void UpdateVolume() -{ - // Set volume on our process's sessions (the AudioPlaybackConnection audio) if (g_sessionManager) - ApplyVolumeToOurSessions(g_sessionManager); -} - -static bool IsRunningAsAdmin() -{ - BOOL isAdmin = FALSE; - HANDLE token = NULL; - if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) - { - TOKEN_ELEVATION elevation = {}; - DWORD cbSize = sizeof(elevation); - if (GetTokenInformation(token, TokenElevation, &elevation, cbSize, &cbSize)) - isAdmin = elevation.TokenIsElevated; - CloseHandle(token); - } - return isAdmin != FALSE; -} - -void DisableAbsoluteVolume() -{ - // If not admin, relaunch with UAC elevation - if (!IsRunningAsAdmin()) - { - wchar_t exePath[MAX_PATH]; - GetModuleFileNameW(NULL, exePath, MAX_PATH); - HINSTANCE result = ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); - if (reinterpret_cast(result) <= 32) - { - TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to apply the system fix.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); - } - return; - } - - const wchar_t* paths[] = { - L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", - L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT", - L"SYSTEM\\CurrentControlSet\\Services\\HidBth\\Parameters", - L"SYSTEM\\CurrentControlSet\\Services\\BthAvrcpTg\\Parameters", - L"SOFTWARE\\Microsoft\\Bluetooth\\Audio\\AVRCP\\CT" - }; - - bool success = false; - for (auto path : paths) { - HKEY hKey; - if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL) == ERROR_SUCCESS) + if (g_sessionNotifier) { - DWORD val1 = 1; - DWORD val0 = 0; - RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val1, sizeof(val1)); - RegSetValueExW(hKey, L"EnableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); - RegCloseKey(hKey); - success = true; + g_sessionManager->UnregisterSessionNotification(g_sessionNotifier); + g_sessionNotifier->Release(); + g_sessionNotifier = nullptr; } + g_sessionManager->Release(); + g_sessionManager = nullptr; } +} - if (success) - { - TaskDialog(g_hWnd, NULL, _(L"System Fix Applied DEFINITIVELY"), _(L"All known registry paths for Absolute Volume have been updated.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nIf volume buttons still sync after reboot, it means your Bluetooth driver is ignoring system settings."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); - } - else - { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); - } +void UpdateVolume() +{ + if (g_sessionManager) ApplyVolumeToOurSessions(g_sessionManager); } From 8b18a1e69971df1257da35b95cb9b0e0e768b3ff Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 14:38:39 +0530 Subject: [PATCH 61/70] v2.0.2: Fix Flyout.ShowAt build error --- AudioPlaybackConnector.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 33ae395..3ebedeb 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -135,7 +135,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, SetupVolumeFlyout(); SetupMenu(); SetupDevicePicker(); - SetupSvgIcon(); g_nid.hWnd = g_niid.hWnd = g_hWnd; wcscpy_s(g_nid.szTip, _(L"AudioPlaybackConnector")); @@ -387,7 +386,10 @@ void SetupMenu() static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) }; - g_volumeFlyout.ShowAt(g_xamlCanvas, point); + using namespace winrt::Windows::UI::Xaml::Controls::Primitives; + FlyoutShowOptions options; + options.Position(point); + g_volumeFlyout.ShowAt(g_xamlCanvas, options); }); FontIcon closeIcon; @@ -411,7 +413,10 @@ void SetupMenu() static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) }; - g_xamlFlyout.ShowAt(g_xamlCanvas, point); + using namespace winrt::Windows::UI::Xaml::Controls::Primitives; + FlyoutShowOptions options; + options.Position(point); + g_xamlFlyout.ShowAt(g_xamlCanvas, options); }); MenuFlyout menu; From 3bcac6d323eac5d7a211dd6277f68b42d62a9bf9 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 14:45:36 +0530 Subject: [PATCH 62/70] v2.0.3: VERBATIM ROLLBACK TO v1.7.3-volume-fix --- AudioPlaybackConnector.cpp | 231 ++++++++++++++++++++++++------------- AudioPlaybackConnector.h | 2 +- SettingsUtil.hpp | 2 +- pch.h | 2 - 4 files changed, 150 insertions(+), 87 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 3ebedeb..5c1ee7f 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -115,11 +115,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, RegisterClassExW(&wcex); - // Using 1x1 SHOWN transparent window - most stable for hosting WinRT Flyouts/Pickers - g_hWnd = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOPMOST, L"AudioPlaybackConnector", nullptr, WS_POPUP, 0, 0, 1, 1, nullptr, nullptr, hInstance, nullptr); + // When parent window size is 0x0 or invisible, the dpi scale of menu is incorrect. Here we set window size to 1x1 and use WS_EX_LAYERED to make window looks like invisible. + g_hWnd = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOPMOST, L"AudioPlaybackConnector", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr); FAIL_FAST_LAST_ERROR_IF_NULL(g_hWnd); FAIL_FAST_IF_WIN32_BOOL_FALSE(SetLayeredWindowAttributes(g_hWnd, 0, 0, LWA_ALPHA)); - ShowWindow(g_hWnd, SW_SHOW); DesktopWindowXamlSource desktopSource; auto desktopSourceNative2 = desktopSource.as(); @@ -135,6 +134,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, SetupVolumeFlyout(); SetupMenu(); SetupDevicePicker(); + SetupSvgIcon(); g_nid.hWnd = g_niid.hWnd = g_hWnd; wcscpy_s(g_nid.szTip, _(L"AudioPlaybackConnector")); @@ -191,26 +191,19 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_NOTIFYICON: - { - UINT uMsg = LOWORD(lParam); - switch (uMsg) + switch (LOWORD(lParam)) { - case WM_LBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { - static DWORD s_lastTick = 0; - if (GetTickCount() - s_lastTick < 500) break; - s_lastTick = GetTickCount(); - using namespace winrt::Windows::UI::Popups; RECT iconRect; - if (FAILED(Shell_NotifyIconGetRect(&g_niid, &iconRect))) + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) { - POINT pt; - GetCursorPos(&pt); - iconRect = { pt.x - 8, pt.y - 8, pt.x + 8, pt.y + 8 }; + LOG_HR(hr); + break; } auto dpi = GetDpiForWindow(hWnd); @@ -221,43 +214,34 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi) }; + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); - try { - g_devicePicker.Show(rect, Placement::Above); - } catch (...) { - LOG_CAUGHT_EXCEPTION(); - } + g_devicePicker.Show(rect, Placement::Above); } break; - case WM_RBUTTONUP: + case WM_RBUTTONUP: // Menu activated by mouse click + g_menuFocusState = FocusState::Pointer; + break; case WM_CONTEXTMENU: { - static DWORD s_lastTick = 0; - if (GetTickCount() - s_lastTick < 500) break; - s_lastTick = GetTickCount(); - - POINT pt; - if (uMsg == WM_CONTEXTMENU && LOWORD(lParam) == WM_CONTEXTMENU) { - // VERSION_4 sends coordinates in wParam for WM_CONTEXTMENU - pt.x = GET_X_LPARAM(wParam); - pt.y = GET_Y_LPARAM(wParam); - } else { - GetCursorPos(&pt); - } + if (g_menuFocusState == FocusState::Unfocused) + g_menuFocusState = FocusState::Keyboard; auto dpi = GetDpiForWindow(hWnd); Point point = { - static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) + static_cast(GET_X_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(GET_Y_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi) }; + SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); + SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); + g_xamlMenu.ShowAt(g_xamlCanvas, point); } break; } - } - break; + break; case WM_CONNECTDEVICE: if (g_reconnect) { @@ -341,6 +325,7 @@ void SetupVolumeFlyout() flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); flyout.Closed([](const auto&, const auto&) { + ShowWindow(g_hWnd, SW_HIDE); SaveSettings(); }); @@ -379,17 +364,21 @@ void SetupMenu() volumeItem.Text(_(L"Volume Control")); volumeItem.Icon(volumeIcon); volumeItem.Click([](const auto&, const auto&) { - POINT pt; - GetCursorPos(&pt); + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) + { + LOG_HR(hr); + return; + } + auto dpi = GetDpiForWindow(g_hWnd); - Point point = { - static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) - }; - using namespace winrt::Windows::UI::Xaml::Controls::Primitives; - FlyoutShowOptions options; - options.Position(point); - g_volumeFlyout.ShowAt(g_xamlCanvas, options); + + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); + g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + + g_volumeFlyout.ShowAt(g_xamlCanvas); }); FontIcon closeIcon; @@ -405,18 +394,21 @@ void SetupMenu() return; } - POINT pt; - GetCursorPos(&pt); + RECT iconRect; + auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); + if (FAILED(hr)) + { + LOG_HR(hr); + return; + } + auto dpi = GetDpiForWindow(g_hWnd); - Point point = { - static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) - }; - - using namespace winrt::Windows::UI::Xaml::Controls::Primitives; - FlyoutShowOptions options; - options.Position(point); - g_xamlFlyout.ShowAt(g_xamlCanvas, options); + + SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); + g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); + g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + + g_xamlFlyout.ShowAt(g_xamlCanvas); }); MenuFlyout menu; @@ -429,8 +421,12 @@ void SetupMenu() auto itemsCount = menuItems.Size(); if (itemsCount > 0) { - menuItems.GetAt(itemsCount - 1).Focus(FocusState::Pointer); + menuItems.GetAt(itemsCount - 1).Focus(g_menuFocusState); } + g_menuFocusState = FocusState::Unfocused; + }); + menu.Closed([](const auto&, const auto&) { + ShowWindow(g_hWnd, SW_HIDE); }); g_xamlMenu = menu; @@ -539,6 +535,9 @@ void SetupDevicePicker() winrt::check_hresult(g_devicePicker.as()->Initialize(g_hWnd)); g_devicePicker.Filter().SupportedDeviceSelectors().Append(AudioPlaybackConnection::GetDeviceSelector()); + g_devicePicker.DevicePickerDismissed([](const auto&, const auto&) { + SetWindowPos(g_hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_HIDEWINDOW); + }); g_devicePicker.DeviceSelected([](const auto& sender, const auto& args) { ConnectDevice(sender, args.SelectedDevice()); }); @@ -581,10 +580,16 @@ void UpdateNotifyIcon() LOG_IF_WIN32_ERROR(RegGetValueW(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &cbValue)); g_nid.hIcon = value != 0 ? g_hIconLight : g_hIconDark; - Shell_NotifyIconW(NIM_DELETE, &g_nid); - if (Shell_NotifyIconW(NIM_ADD, &g_nid)) + if (!Shell_NotifyIconW(NIM_MODIFY, &g_nid)) { - Shell_NotifyIconW(NIM_SETVERSION, &g_nid); + if (Shell_NotifyIconW(NIM_ADD, &g_nid)) + { + FAIL_FAST_IF_WIN32_BOOL_FALSE(Shell_NotifyIconW(NIM_SETVERSION, &g_nid)); + } + else + { + LOG_LAST_ERROR(); + } } } @@ -758,13 +763,13 @@ void SetupEndpointVolume() winrt::check_hresult(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device)); enumerator->Release(); - // Register AVRCP guardian (blocks phone buttons) + // Register AVRCP guardian (blocks phone buttons from changing master volume) IAudioEndpointVolume* epVol = nullptr; winrt::check_hresult(device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (void**)&epVol)); g_endpointVolume = epVol; - // Initialize our Authority levels + // Initialize our Authority levels from the current system state float currentVol = 0.5f; BOOL currentMute = FALSE; if (SUCCEEDED(g_endpointVolume->GetMasterVolumeLevelScalar(¤tVol))) g_lastMasterVolume = currentVol; @@ -773,14 +778,15 @@ void SetupEndpointVolume() g_volumeCallback = new VolumeCallback(); g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback); - // Register session notifier + // Register session notifier so we catch AudioPlaybackConnection sessions the moment they start IAudioSessionManager2* mgr = nullptr; if (SUCCEEDED(device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC_SERVER, NULL, (void**)&mgr))) { - g_sessionManager = mgr; + g_sessionManager = mgr; // keep alive for UpdateVolume g_sessionNotifier = new SessionNotifier(); mgr->RegisterSessionNotification(g_sessionNotifier); + // Apply to any sessions already running ApplyVolumeToOurSessions(mgr); } device->Release(); @@ -793,31 +799,90 @@ void SetupEndpointVolume() void TeardownEndpointVolume() { - if (g_endpointVolume) + if (g_sessionManager && g_sessionNotifier) { - if (g_volumeCallback) - { - g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); - g_volumeCallback->Release(); - g_volumeCallback = nullptr; - } + g_sessionManager->UnregisterSessionNotification(g_sessionNotifier); + g_sessionNotifier->Release(); + g_sessionNotifier = nullptr; + g_sessionManager->Release(); + g_sessionManager = nullptr; + } + if (g_endpointVolume && g_volumeCallback) + { + g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); + g_volumeCallback->Release(); + g_volumeCallback = nullptr; g_endpointVolume->Release(); g_endpointVolume = nullptr; } +} + +void UpdateVolume() +{ + // Set volume on our process's sessions (the AudioPlaybackConnection audio) if (g_sessionManager) + ApplyVolumeToOurSessions(g_sessionManager); +} + +static bool IsRunningAsAdmin() +{ + BOOL isAdmin = FALSE; + HANDLE token = NULL; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { - if (g_sessionNotifier) - { - g_sessionManager->UnregisterSessionNotification(g_sessionNotifier); - g_sessionNotifier->Release(); - g_sessionNotifier = nullptr; - } - g_sessionManager->Release(); - g_sessionManager = nullptr; + TOKEN_ELEVATION elevation = {}; + DWORD cbSize = sizeof(elevation); + if (GetTokenInformation(token, TokenElevation, &elevation, cbSize, &cbSize)) + isAdmin = elevation.TokenIsElevated; + CloseHandle(token); } + return isAdmin != FALSE; } -void UpdateVolume() +void DisableAbsoluteVolume() { - if (g_sessionManager) ApplyVolumeToOurSessions(g_sessionManager); + // If not admin, relaunch with UAC elevation + if (!IsRunningAsAdmin()) + { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + HINSTANCE result = ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); + if (reinterpret_cast(result) <= 32) + { + TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to apply the system fix.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); + } + return; + } + + const wchar_t* paths[] = { + L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", + L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT", + L"SYSTEM\\CurrentControlSet\\Services\\HidBth\\Parameters", + L"SYSTEM\\CurrentControlSet\\Services\\BthAvrcpTg\\Parameters", + L"SOFTWARE\\Microsoft\\Bluetooth\\Audio\\AVRCP\\CT" + }; + + bool success = false; + for (auto path : paths) + { + HKEY hKey; + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL) == ERROR_SUCCESS) + { + DWORD val1 = 1; + DWORD val0 = 0; + RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val1, sizeof(val1)); + RegSetValueExW(hKey, L"EnableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); + RegCloseKey(hKey); + success = true; + } + } + + if (success) + { + TaskDialog(g_hWnd, NULL, _(L"System Fix Applied DEFINITIVELY"), _(L"All known registry paths for Absolute Volume have been updated.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nIf volume buttons still sync after reboot, it means your Bluetooth driver is ignoring system settings."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + } + else + { + TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); + } } diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index 6b0d6c7..ef65c31 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "resource.h" diff --git a/SettingsUtil.hpp b/SettingsUtil.hpp index e2c8e0c..45ce027 100644 --- a/SettingsUtil.hpp +++ b/SettingsUtil.hpp @@ -1,4 +1,4 @@ -#pragma once +#pragma once constexpr auto CONFIG_NAME = L"AudioPlaybackConnector.json"; constexpr auto BUFFER_SIZE = 4096; diff --git a/pch.h b/pch.h index 3b5a526..89c916d 100644 --- a/pch.h +++ b/pch.h @@ -50,8 +50,6 @@ #include #include #include -#include -#include #include #include #include From 10fc64de8e5e1e0efaa4f0f54e0975bdbdbd0953 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 16:26:05 +0530 Subject: [PATCH 63/70] v2.0.4: Fix tray interaction deadlock (force clean state) --- AudioPlaybackConnector.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 5c1ee7f..7b96c97 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -193,6 +193,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_NOTIFYICON: switch (LOWORD(lParam)) { + case WM_LBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { @@ -202,8 +203,10 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); if (FAILED(hr)) { - LOG_HR(hr); - break; + // Fallback to cursor position if rect fails + POINT pt; + GetCursorPos(&pt); + iconRect = { pt.x - 8, pt.y - 8, pt.x + 8, pt.y + 8 }; } auto dpi = GetDpiForWindow(hWnd); @@ -580,16 +583,10 @@ void UpdateNotifyIcon() LOG_IF_WIN32_ERROR(RegGetValueW(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &cbValue)); g_nid.hIcon = value != 0 ? g_hIconLight : g_hIconDark; - if (!Shell_NotifyIconW(NIM_MODIFY, &g_nid)) + Shell_NotifyIconW(NIM_DELETE, &g_nid); + if (Shell_NotifyIconW(NIM_ADD, &g_nid)) { - if (Shell_NotifyIconW(NIM_ADD, &g_nid)) - { - FAIL_FAST_IF_WIN32_BOOL_FALSE(Shell_NotifyIconW(NIM_SETVERSION, &g_nid)); - } - else - { - LOG_LAST_ERROR(); - } + Shell_NotifyIconW(NIM_SETVERSION, &g_nid); } } From 26fd7facb1fb5f9117fc2412b5c8cda8e61a9f2b Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 16:34:53 +0530 Subject: [PATCH 64/70] v2.0.5: Restore menu features + Fix Admin startup prompt --- AudioPlaybackConnector.cpp | 587 +++++++++++--------------------- AudioPlaybackConnector.manifest | 16 +- 2 files changed, 204 insertions(+), 399 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 7b96c97..e4ed824 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -9,6 +9,9 @@ void UpdateVolume(); void SetupEndpointVolume(); void TeardownEndpointVolume(); void DisableAbsoluteVolume(); +void RevertAbsoluteVolume(); +void SetRunAtStartup(bool enable); +bool IsRunningAsAdmin(); winrt::fire_and_forget ConnectDevice(DevicePicker, std::wstring_view); void SetupDevicePicker(); void SetupSvgIcon(); @@ -59,25 +62,33 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(nCmdShow); - // If relaunched as admin to apply the Absolute Volume fix, do it and exit - if (lpCmdLine && wcsstr(lpCmdLine, L"--fix-absolute-volume") != nullptr) + // If relaunched as admin to apply/revert the Absolute Volume fix + if (lpCmdLine) { - HKEY hKey; - LONG openResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, KEY_SET_VALUE, &hKey); - if (openResult != ERROR_SUCCESS) - openResult = RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL); - if (openResult == ERROR_SUCCESS) - { - DWORD value = 1; - RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)); - RegCloseKey(hKey); - TaskDialog(nullptr, nullptr, L"Success", L"Absolute Volume disabled.\n\nReboot your PC for the change to take effect.\nAfter rebooting, your phone volume buttons will only control phone volume.", nullptr, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, nullptr); - } - else + bool fix = wcsstr(lpCmdLine, L"--fix-absolute-volume") != nullptr; + bool revert = wcsstr(lpCmdLine, L"--revert-absolute-volume") != nullptr; + + if (fix || revert) { - TaskDialog(nullptr, nullptr, L"Error", L"Failed to write registry key.", nullptr, TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); + const wchar_t* path = L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT"; + HKEY hKey; + LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, path, 0, KEY_SET_VALUE, &hKey); + if (result != ERROR_SUCCESS) + result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL); + + if (result == ERROR_SUCCESS) + { + DWORD value = fix ? 1 : 0; + RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&value, sizeof(value)); + RegCloseKey(hKey); + TaskDialog(nullptr, nullptr, L"Success", fix ? L"Absolute Volume disabled.\n\nREBOOT your PC for changes to take effect." : L"Absolute Volume restored.\n\nREBOOT your PC for changes to take effect.", nullptr, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, nullptr); + } + else + { + TaskDialog(nullptr, nullptr, L"Error", L"Failed to write registry key. Run as Administrator.", nullptr, TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); + } + return 0; } - return 0; } g_hInst = hInstance; @@ -115,10 +126,11 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, RegisterClassExW(&wcex); - // When parent window size is 0x0 or invisible, the dpi scale of menu is incorrect. Here we set window size to 1x1 and use WS_EX_LAYERED to make window looks like invisible. - g_hWnd = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOPMOST, L"AudioPlaybackConnector", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr); + // Using 1x1 SHOWN transparent window - most stable for hosting WinRT Flyouts/Pickers + g_hWnd = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOPMOST, L"AudioPlaybackConnector", nullptr, WS_POPUP, 0, 0, 1, 1, nullptr, nullptr, hInstance, nullptr); FAIL_FAST_LAST_ERROR_IF_NULL(g_hWnd); FAIL_FAST_IF_WIN32_BOOL_FALSE(SetLayeredWindowAttributes(g_hWnd, 0, 0, LWA_ALPHA)); + ShowWindow(g_hWnd, SW_SHOW); DesktopWindowXamlSource desktopSource; auto desktopSourceNative2 = desktopSource.as(); @@ -191,19 +203,23 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_NOTIFYICON: - switch (LOWORD(lParam)) + { + UINT uMsg = LOWORD(lParam); + switch (uMsg) { case WM_LBUTTONUP: case NIN_SELECT: case NIN_KEYSELECT: { + static DWORD s_lastTick = 0; + if (GetTickCount() - s_lastTick < 500) break; + s_lastTick = GetTickCount(); + using namespace winrt::Windows::UI::Popups; RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) + if (FAILED(Shell_NotifyIconGetRect(&g_niid, &iconRect))) { - // Fallback to cursor position if rect fails POINT pt; GetCursorPos(&pt); iconRect = { pt.x - 8, pt.y - 8, pt.x + 8, pt.y + 8 }; @@ -217,34 +233,42 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi) }; - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); - g_devicePicker.Show(rect, Placement::Above); + try { + g_devicePicker.Show(rect, Placement::Above); + } catch (...) { + LOG_CAUGHT_EXCEPTION(); + } } break; - case WM_RBUTTONUP: // Menu activated by mouse click - g_menuFocusState = FocusState::Pointer; - break; + case WM_RBUTTONUP: case WM_CONTEXTMENU: { - if (g_menuFocusState == FocusState::Unfocused) - g_menuFocusState = FocusState::Keyboard; + static DWORD s_lastTick = 0; + if (GetTickCount() - s_lastTick < 500) break; + s_lastTick = GetTickCount(); + + POINT pt; + if (uMsg == WM_CONTEXTMENU && LOWORD(lParam) == WM_CONTEXTMENU) { + pt.x = GET_X_LPARAM(wParam); + pt.y = GET_Y_LPARAM(wParam); + } else { + GetCursorPos(&pt); + } auto dpi = GetDpiForWindow(hWnd); Point point = { - static_cast(GET_X_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi), - static_cast(GET_Y_LPARAM(wParam) * USER_DEFAULT_SCREEN_DPI / dpi) + static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), + static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) }; - SetWindowPos(g_hWndXaml, 0, 0, 0, 0, 0, SWP_NOZORDER | SWP_SHOWWINDOW); - SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 1, 1, SWP_SHOWWINDOW); SetForegroundWindow(hWnd); - g_xamlMenu.ShowAt(g_xamlCanvas, point); } break; } - break; + } + break; case WM_CONNECTDEVICE: if (g_reconnect) { @@ -256,7 +280,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_RESTORE_VOLUME: - // Fired by the volume callback when a remote (phone) source changed the volume if (g_volumeLock && g_endpointVolume) { g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); @@ -328,7 +351,6 @@ void SetupVolumeFlyout() flyout.ShouldConstrainToRootBounds(false); flyout.Content(stackPanel); flyout.Closed([](const auto&, const auto&) { - ShowWindow(g_hWnd, SW_HIDE); SaveSettings(); }); @@ -337,7 +359,6 @@ void SetupVolumeFlyout() void SetupMenu() { - // https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font FontIcon settingsIcon; settingsIcon.Glyph(L"\xE713"); @@ -348,13 +369,11 @@ void SetupMenu() winrt::Windows::System::Launcher::LaunchUriAsync(Uri(L"ms-settings:bluetooth")); }); - // Lock toggle: blocks phone volume buttons from changing PC volume static ToggleMenuFlyoutItem lockItem; lockItem.Text(_(L"Lock Phone Volume Buttons")); lockItem.IsChecked(g_volumeLock); lockItem.Click([](const auto&, const auto&) { g_volumeLock = lockItem.IsChecked(); - // When enabling, immediately restore our preferred master volume level if (g_volumeLock && g_endpointVolume) g_endpointVolume->SetMasterVolumeLevelScalar(g_lastMasterVolume, &g_ourVolumeGuid); SaveSettings(); @@ -362,168 +381,159 @@ void SetupMenu() FontIcon volumeIcon; volumeIcon.Glyph(L"\xE767"); - MenuFlyoutItem volumeItem; volumeItem.Text(_(L"Volume Control")); volumeItem.Icon(volumeIcon); volumeItem.Click([](const auto&, const auto&) { - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - LOG_HR(hr); - return; - } - + POINT pt; GetCursorPos(&pt); auto dpi = GetDpiForWindow(g_hWnd); + Point point = { static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) }; + using namespace winrt::Windows::UI::Xaml::Controls::Primitives; + FlyoutShowOptions options; options.Position(point); + g_volumeFlyout.ShowAt(g_xamlCanvas, options); + }); - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); + static ToggleMenuFlyoutItem startupItem; + startupItem.Text(_(L"Run at Startup")); + startupItem.IsChecked(g_runAtStartup); + startupItem.Click([](const auto&, const auto&) { + g_runAtStartup = startupItem.IsChecked(); + SetRunAtStartup(g_runAtStartup); + SaveSettings(); + }); - g_volumeFlyout.ShowAt(g_xamlCanvas); + MenuFlyoutItem helpItem; + helpItem.Text(_(L"Instructions & Tips")); + helpItem.Click([](const auto&, const auto&) { + TaskDialog(g_hWnd, NULL, L"Instructions", L"ΓÇó Left-Click tray icon to Connect Phone.\nΓÇó Right-Click for Settings & Volume.\nΓÇó Use 'Lock' if phone buttons change PC volume.\nΓÇó 'Fix Volume Sync' requires Admin + Reboot.", L"If sound is missing, disconnect and reconnect on the phone.", TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); }); - FontIcon closeIcon; - closeIcon.Glyph(L"\xE8BB"); + MenuFlyoutSubItem fixMenu; + fixMenu.Text(_(L"System Fixes (Admin)")); + + MenuFlyoutItem fixItem; + fixItem.Text(_(L"Apply Volume Sync Fix")); + fixItem.Click([](const auto&, const auto&) { DisableAbsoluteVolume(); }); + + MenuFlyoutItem revertItem; + revertItem.Text(_(L"Revert Volume Fix")); + revertItem.Click([](const auto&, const auto&) { RevertAbsoluteVolume(); }); + + fixMenu.Items().Append(fixItem); + fixMenu.Items().Append(revertItem); MenuFlyoutItem exitItem; exitItem.Text(_(L"Exit")); - exitItem.Icon(closeIcon); + exitItem.Icon(FontIcon{ .Glyph = L"\xE8BB" }); exitItem.Click([](const auto&, const auto&) { - if (g_audioPlaybackConnections.size() == 0) - { - PostMessageW(g_hWnd, WM_CLOSE, 0, 0); - return; - } - - RECT iconRect; - auto hr = Shell_NotifyIconGetRect(&g_niid, &iconRect); - if (FAILED(hr)) - { - LOG_HR(hr); - return; - } - + if (g_audioPlaybackConnections.size() == 0) { PostMessageW(g_hWnd, WM_CLOSE, 0, 0); return; } + POINT pt; GetCursorPos(&pt); auto dpi = GetDpiForWindow(g_hWnd); - - SetWindowPos(g_hWnd, HWND_TOPMOST, iconRect.left, iconRect.top, 0, 0, SWP_HIDEWINDOW); - g_xamlCanvas.Width(static_cast((iconRect.right - iconRect.left) * USER_DEFAULT_SCREEN_DPI / dpi)); - g_xamlCanvas.Height(static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi)); - - g_xamlFlyout.ShowAt(g_xamlCanvas); + Point point = { static_cast(pt.x * USER_DEFAULT_SCREEN_DPI / dpi), static_cast(pt.y * USER_DEFAULT_SCREEN_DPI / dpi) }; + using namespace winrt::Windows::UI::Xaml::Controls::Primitives; + FlyoutShowOptions options; options.Position(point); + g_xamlFlyout.ShowAt(g_xamlCanvas, options); }); MenuFlyout menu; menu.Items().Append(settingsItem); menu.Items().Append(lockItem); menu.Items().Append(volumeItem); + menu.Items().Append(startupItem); + menu.Items().Append(MenuFlyoutSeparator{}); + menu.Items().Append(helpItem); + menu.Items().Append(fixMenu); + menu.Items().Append(MenuFlyoutSeparator{}); menu.Items().Append(exitItem); + menu.Opened([](const auto& sender, const auto&) { auto menuItems = sender.as().Items(); - auto itemsCount = menuItems.Size(); - if (itemsCount > 0) - { - menuItems.GetAt(itemsCount - 1).Focus(g_menuFocusState); - } - g_menuFocusState = FocusState::Unfocused; - }); - menu.Closed([](const auto&, const auto&) { - ShowWindow(g_hWnd, SW_HIDE); + if (menuItems.Size() > 0) menuItems.GetAt(menuItems.Size() - 1).Focus(FocusState::Pointer); }); g_xamlMenu = menu; } -winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device) +void SetRunAtStartup(bool enable) { - picker.SetDisplayStatus(device, _(L"Connecting"), DevicePickerDisplayStatusOptions::ShowProgress | DevicePickerDisplayStatusOptions::ShowDisconnectButton); + HKEY hKey; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) + { + if (enable) + { + wchar_t path[MAX_PATH]; + GetModuleFileNameW(NULL, path, MAX_PATH); + RegSetValueExW(hKey, L"AudioPlaybackConnector", 0, REG_SZ, (const BYTE*)path, (wcslen(path) + 1) * sizeof(wchar_t)); + } + else + { + RegDeleteValueW(hKey, L"AudioPlaybackConnector"); + } + RegCloseKey(hKey); + } +} - bool success = false; - std::wstring errorMessage; +bool IsRunningAsAdmin() +{ + BOOL isAdmin = FALSE; + HANDLE token = NULL; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + { + TOKEN_ELEVATION elevation = {}; + DWORD cbSize = sizeof(elevation); + if (GetTokenInformation(token, TokenElevation, &elevation, cbSize, &cbSize)) + isAdmin = elevation.TokenIsElevated; + CloseHandle(token); + } + return isAdmin != FALSE; +} +void DisableAbsoluteVolume() +{ + if (!IsRunningAsAdmin()) + { + wchar_t path[MAX_PATH]; GetModuleFileNameW(NULL, path, MAX_PATH); + ShellExecuteW(NULL, L"runas", path, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); + return; + } + // Logic handled in wWinMain for --fix-absolute-volume +} + +void RevertAbsoluteVolume() +{ + if (!IsRunningAsAdmin()) + { + wchar_t path[MAX_PATH]; GetModuleFileNameW(NULL, path, MAX_PATH); + ShellExecuteW(NULL, L"runas", path, L"--revert-absolute-volume", NULL, SW_SHOWNORMAL); + return; + } + // Logic handled in wWinMain for --revert-absolute-volume +} + +winrt::fire_and_forget ConnectDevice(DevicePicker picker, DeviceInformation device) +{ + picker.SetDisplayStatus(device, _(L"Connecting"), DevicePickerDisplayStatusOptions::ShowProgress | DevicePickerDisplayStatusOptions::ShowDisconnectButton); try { auto connection = AudioPlaybackConnection::TryCreateFromId(device.Id()); if (connection) { g_audioPlaybackConnections.emplace(device.Id(), std::pair(device, connection)); - connection.StateChanged([](const auto& sender, const auto&) { if (sender.State() == AudioPlaybackConnectionState::Closed) { auto it = g_audioPlaybackConnections.find(std::wstring(sender.DeviceId())); - if (it != g_audioPlaybackConnections.end()) - { - g_devicePicker.SetDisplayStatus(it->second.first, {}, DevicePickerDisplayStatusOptions::None); - g_audioPlaybackConnections.erase(it); - } + if (it != g_audioPlaybackConnections.end()) { g_devicePicker.SetDisplayStatus(it->second.first, {}, DevicePickerDisplayStatusOptions::None); g_audioPlaybackConnections.erase(it); } sender.Close(); } }); - co_await connection.StartAsync(); auto result = co_await connection.OpenAsync(); - - switch (result.Status()) - { - case AudioPlaybackConnectionOpenResultStatus::Success: - success = true; - break; - case AudioPlaybackConnectionOpenResultStatus::RequestTimedOut: - success = false; - errorMessage = _(L"The request timed out"); - break; - case AudioPlaybackConnectionOpenResultStatus::DeniedBySystem: - success = false; - errorMessage = _(L"The operation was denied by the system"); - break; - case AudioPlaybackConnectionOpenResultStatus::UnknownFailure: - success = false; - winrt::throw_hresult(result.ExtendedError()); - break; - } - } - else - { - success = false; - errorMessage = _(L"Unknown error"); - } - } - catch (winrt::hresult_error const& ex) - { - success = false; - errorMessage.resize(64); - while (1) - { - auto result = swprintf(errorMessage.data(), errorMessage.size(), L"%s (0x%08X)", ex.message().c_str(), static_cast(ex.code())); - if (result < 0) - { - errorMessage.resize(errorMessage.size() * 2); - } - else - { - errorMessage.resize(result); - break; - } - } - LOG_CAUGHT_EXCEPTION(); - } - - if (success) - { - picker.SetDisplayStatus(device, _(L"Connected"), DevicePickerDisplayStatusOptions::ShowDisconnectButton); - } - else - { - auto it = g_audioPlaybackConnections.find(std::wstring(device.Id())); - if (it != g_audioPlaybackConnections.end()) - { - it->second.second.Close(); - g_audioPlaybackConnections.erase(it); + if (result.Status() == AudioPlaybackConnectionOpenResultStatus::Success) picker.SetDisplayStatus(device, _(L"Connected"), DevicePickerDisplayStatusOptions::ShowDisconnectButton); + else picker.SetDisplayStatus(device, _(L"Failed"), DevicePickerDisplayStatusOptions::ShowRetryButton); } - picker.SetDisplayStatus(device, errorMessage, DevicePickerDisplayStatusOptions::ShowRetryButton); } + catch (...) { LOG_CAUGHT_EXCEPTION(); } } winrt::fire_and_forget ConnectDevice(DevicePicker picker, std::wstring_view deviceId) @@ -536,22 +546,12 @@ void SetupDevicePicker() { g_devicePicker = DevicePicker(); winrt::check_hresult(g_devicePicker.as()->Initialize(g_hWnd)); - g_devicePicker.Filter().SupportedDeviceSelectors().Append(AudioPlaybackConnection::GetDeviceSelector()); - g_devicePicker.DevicePickerDismissed([](const auto&, const auto&) { - SetWindowPos(g_hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_HIDEWINDOW); - }); - g_devicePicker.DeviceSelected([](const auto& sender, const auto& args) { - ConnectDevice(sender, args.SelectedDevice()); - }); + g_devicePicker.DeviceSelected([](const auto& sender, const auto& args) { ConnectDevice(sender, args.SelectedDevice()); }); g_devicePicker.DisconnectButtonClicked([](const auto& sender, const auto& args) { auto device = args.Device(); auto it = g_audioPlaybackConnections.find(std::wstring(device.Id())); - if (it != g_audioPlaybackConnections.end()) - { - it->second.second.Close(); - g_audioPlaybackConnections.erase(it); - } + if (it != g_audioPlaybackConnections.end()) { it->second.second.Close(); g_audioPlaybackConnections.erase(it); } sender.SetDisplayStatus(device, {}, DevicePickerDisplayStatusOptions::None); }); } @@ -559,20 +559,11 @@ void SetupDevicePicker() void SetupSvgIcon() { auto hRes = FindResourceW(g_hInst, MAKEINTRESOURCEW(1), L"SVG"); - FAIL_FAST_LAST_ERROR_IF_NULL(hRes); - auto size = SizeofResource(g_hInst, hRes); - FAIL_FAST_LAST_ERROR_IF(size == 0); - auto hResData = LoadResource(g_hInst, hRes); - FAIL_FAST_LAST_ERROR_IF_NULL(hResData); - auto svgData = reinterpret_cast(LockResource(hResData)); - FAIL_FAST_IF_NULL_ALLOC(svgData); - const std::string_view svg(svgData, size); const int width = GetSystemMetrics(SM_CXSMICON), height = GetSystemMetrics(SM_CYSMICON); - g_hIconLight = SvgTohIcon(svg, width, height, { 0, 0, 0, 1 }); g_hIconDark = SvgTohIcon(svg, width, height, { 1, 1, 1, 1 }); } @@ -580,43 +571,27 @@ void SetupSvgIcon() void UpdateNotifyIcon() { DWORD value = 0, cbValue = sizeof(value); - LOG_IF_WIN32_ERROR(RegGetValueW(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &cbValue)); + RegGetValueW(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &cbValue); g_nid.hIcon = value != 0 ? g_hIconLight : g_hIconDark; - Shell_NotifyIconW(NIM_DELETE, &g_nid); - if (Shell_NotifyIconW(NIM_ADD, &g_nid)) - { - Shell_NotifyIconW(NIM_SETVERSION, &g_nid); - } + if (Shell_NotifyIconW(NIM_ADD, &g_nid)) Shell_NotifyIconW(NIM_SETVERSION, &g_nid); } -// Applies g_volume to every active audio session belonging to our process. -// AudioPlaybackConnection audio appears as a session in our PID when the phone streams. static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) { IAudioSessionEnumerator* sessionEnum = nullptr; if (FAILED(mgr->GetSessionEnumerator(&sessionEnum))) return; - - int count = 0; - sessionEnum->GetCount(&count); - + int count = 0; sessionEnum->GetCount(&count); for (int i = 0; i < count; ++i) { - IAudioSessionControl* ctrl = nullptr; - if (FAILED(sessionEnum->GetSession(i, &ctrl))) continue; - + IAudioSessionControl* ctrl = nullptr; if (FAILED(sessionEnum->GetSession(i, &ctrl))) continue; IAudioSessionControl2* ctrl2 = nullptr; if (SUCCEEDED(ctrl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2))) { if (IsBluetoothSession(ctrl2, ctrl)) { ISimpleAudioVolume* vol = nullptr; - if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) - { - // Use a 0.7x scale to keep mobile audio in a comfortable range. - vol->SetMasterVolume(static_cast(g_volume * 0.7), nullptr); - vol->Release(); - } + if (SUCCEEDED(ctrl->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { vol->SetMasterVolume(static_cast(g_volume * 0.7), nullptr); vol->Release(); } } ctrl2->Release(); } @@ -625,75 +600,23 @@ static void ApplyVolumeToOurSessions(IAudioSessionManager2* mgr) sessionEnum->Release(); } -// Intercepts master volume changes; blocks AVRCP (phone buttons) from altering PC volume. class VolumeCallback : public IAudioEndpointVolumeCallback { public: ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_ref); } - ULONG STDMETHODCALLTYPE Release() override - { - auto r = InterlockedDecrement(&m_ref); - if (r == 0) delete this; - return r; - } + ULONG STDMETHODCALLTYPE Release() override { auto r = InterlockedDecrement(&m_ref); if (r == 0) delete this; return r; } HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override { - if (riid == __uuidof(IUnknown) || riid == __uuidof(IAudioEndpointVolumeCallback)) - { - *ppv = static_cast(this); - AddRef(); - return S_OK; - } - *ppv = nullptr; - return E_NOINTERFACE; + if (riid == __uuidof(IUnknown) || riid == __uuidof(IAudioEndpointVolumeCallback)) { *ppv = static_cast(this); AddRef(); return S_OK; } + *ppv = nullptr; return E_NOINTERFACE; } HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) override { - // 1. Allow our own changes - if (IsEqualGUID(pNotify->guidEventContext, g_ourVolumeGuid)) - return S_OK; - - bool isRemote = false; - - // Check if it's a system key press or mouse move - if (IsEqualGUID(pNotify->guidEventContext, GUID_NULL)) - { - LASTINPUTINFO lii = { sizeof(lii) }; - lii.cbSize = sizeof(lii); - if (GetLastInputInfo(&lii)) - { - DWORD idleTime = GetTickCount() - lii.dwTime; - // If the user hasn't touched the PC in the last 1.5 seconds, - // this volume change is almost certainly from the phone (AVRCP). - if (idleTime > 1500) - { - isRemote = true; - } - } - } - else - { - // Any other GUID (phone app or other remote source) - isRemote = true; - } - - if (isRemote) - { - // Remote change detected (Phone buttons) - if (g_volumeLock && g_hWnd) - { - // Sync the phone's requested level to our app's slider (g_volume) - g_volume = pNotify->fMasterVolume; - // Post message to restore master volume to our Authority level and re-apply g_volume to the session - PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); - } - } - else - { - // Local authority: update the last known good level set by the user (laptop keys) - g_lastMasterVolume = pNotify->fMasterVolume; - g_lastMute = pNotify->bMuted; - } + if (IsEqualGUID(pNotify->guidEventContext, g_ourVolumeGuid)) return S_OK; + bool isRemote = !IsEqualGUID(pNotify->guidEventContext, GUID_NULL); + if (!isRemote) { LASTINPUTINFO lii = { sizeof(lii) }; if (GetLastInputInfo(&lii) && (GetTickCount() - lii.dwTime) > 1500) isRemote = true; } + if (isRemote && g_volumeLock && g_hWnd) { g_volume = pNotify->fMasterVolume; PostMessageW(g_hWnd, WM_RESTORE_VOLUME, 0, 0); } + else { g_lastMasterVolume = pNotify->fMasterVolume; g_lastMute = pNotify->bMuted; } return S_OK; } private: @@ -701,28 +624,15 @@ class VolumeCallback : public IAudioEndpointVolumeCallback }; static VolumeCallback* g_volumeCallback = nullptr; -// Called by Windows when a new audio session is created. -// We use this to immediately apply our volume when AudioPlaybackConnection starts streaming. class SessionNotifier : public IAudioSessionNotification { public: ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_ref); } - ULONG STDMETHODCALLTYPE Release() override - { - auto r = InterlockedDecrement(&m_ref); - if (r == 0) delete this; - return r; - } + ULONG STDMETHODCALLTYPE Release() override { auto r = InterlockedDecrement(&m_ref); if (r == 0) delete this; return r; } HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override { - if (riid == __uuidof(IUnknown) || riid == __uuidof(IAudioSessionNotification)) - { - *ppv = static_cast(this); - AddRef(); - return S_OK; - } - *ppv = nullptr; - return E_NOINTERFACE; + if (riid == __uuidof(IUnknown) || riid == __uuidof(IAudioSessionNotification)) { *ppv = static_cast(this); AddRef(); return S_OK; } + *ppv = nullptr; return E_NOINTERFACE; } HRESULT STDMETHODCALLTYPE OnSessionCreated(IAudioSessionControl* pNewSession) override { @@ -732,11 +642,7 @@ class SessionNotifier : public IAudioSessionNotification if (IsBluetoothSession(ctrl2, pNewSession)) { ISimpleAudioVolume* vol = nullptr; - if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) - { - vol->SetMasterVolume(static_cast(g_volume * 0.7), nullptr); - vol->Release(); - } + if (SUCCEEDED(pNewSession->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&vol))) { vol->SetMasterVolume(static_cast(g_volume * 0.7), nullptr); vol->Release(); } } ctrl2->Release(); } @@ -745,7 +651,6 @@ class SessionNotifier : public IAudioSessionNotification private: long m_ref = 1; }; - static SessionNotifier* g_sessionNotifier = nullptr; void SetupEndpointVolume() @@ -753,133 +658,29 @@ void SetupEndpointVolume() try { IMMDeviceEnumerator* enumerator = nullptr; - winrt::check_hresult(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, - CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)&enumerator)); - - IMMDevice* device = nullptr; - winrt::check_hresult(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device)); - enumerator->Release(); - - // Register AVRCP guardian (blocks phone buttons from changing master volume) - IAudioEndpointVolume* epVol = nullptr; - winrt::check_hresult(device->Activate(__uuidof(IAudioEndpointVolume), - CLSCTX_INPROC_SERVER, NULL, (void**)&epVol)); + CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)&enumerator); + IMMDevice* device = nullptr; enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); enumerator->Release(); + IAudioEndpointVolume* epVol = nullptr; device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (void**)&epVol); g_endpointVolume = epVol; - - // Initialize our Authority levels from the current system state - float currentVol = 0.5f; - BOOL currentMute = FALSE; + float currentVol = 0.5f; BOOL currentMute = FALSE; if (SUCCEEDED(g_endpointVolume->GetMasterVolumeLevelScalar(¤tVol))) g_lastMasterVolume = currentVol; if (SUCCEEDED(g_endpointVolume->GetMute(¤tMute))) g_lastMute = (currentMute != FALSE); - - g_volumeCallback = new VolumeCallback(); - g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback); - - // Register session notifier so we catch AudioPlaybackConnection sessions the moment they start + g_volumeCallback = new VolumeCallback(); g_endpointVolume->RegisterControlChangeNotify(g_volumeCallback); IAudioSessionManager2* mgr = nullptr; - if (SUCCEEDED(device->Activate(__uuidof(IAudioSessionManager2), - CLSCTX_INPROC_SERVER, NULL, (void**)&mgr))) + if (SUCCEEDED(device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC_SERVER, NULL, (void**)&mgr))) { - g_sessionManager = mgr; // keep alive for UpdateVolume - g_sessionNotifier = new SessionNotifier(); - mgr->RegisterSessionNotification(g_sessionNotifier); - // Apply to any sessions already running + g_sessionManager = mgr; g_sessionNotifier = new SessionNotifier(); mgr->RegisterSessionNotification(g_sessionNotifier); ApplyVolumeToOurSessions(mgr); } device->Release(); } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - } + catch (...) {} } void TeardownEndpointVolume() { - if (g_sessionManager && g_sessionNotifier) - { - g_sessionManager->UnregisterSessionNotification(g_sessionNotifier); - g_sessionNotifier->Release(); - g_sessionNotifier = nullptr; - g_sessionManager->Release(); - g_sessionManager = nullptr; - } - if (g_endpointVolume && g_volumeCallback) - { - g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); - g_volumeCallback->Release(); - g_volumeCallback = nullptr; - g_endpointVolume->Release(); - g_endpointVolume = nullptr; - } -} - -void UpdateVolume() -{ - // Set volume on our process's sessions (the AudioPlaybackConnection audio) - if (g_sessionManager) - ApplyVolumeToOurSessions(g_sessionManager); -} - -static bool IsRunningAsAdmin() -{ - BOOL isAdmin = FALSE; - HANDLE token = NULL; - if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) - { - TOKEN_ELEVATION elevation = {}; - DWORD cbSize = sizeof(elevation); - if (GetTokenInformation(token, TokenElevation, &elevation, cbSize, &cbSize)) - isAdmin = elevation.TokenIsElevated; - CloseHandle(token); - } - return isAdmin != FALSE; + if (g_endpointVolume) { if (g_volumeCallback) { g_endpointVolume->UnregisterControlChangeNotify(g_volumeCallback); g_volumeCallback->Release(); } g_endpointVolume->Release(); } + if (g_sessionManager) { if (g_sessionNotifier) { g_sessionManager->UnregisterSessionNotification(g_sessionNotifier); g_sessionNotifier->Release(); } g_sessionManager->Release(); } } -void DisableAbsoluteVolume() -{ - // If not admin, relaunch with UAC elevation - if (!IsRunningAsAdmin()) - { - wchar_t exePath[MAX_PATH]; - GetModuleFileNameW(NULL, exePath, MAX_PATH); - HINSTANCE result = ShellExecuteW(g_hWnd, L"runas", exePath, L"--fix-absolute-volume", NULL, SW_SHOWNORMAL); - if (reinterpret_cast(result) <= 32) - { - TaskDialog(g_hWnd, NULL, _(L"Cancelled"), _(L"Administrator privileges are required to apply the system fix.\nPlease try again and click Yes on the UAC prompt."), NULL, TDCBF_OK_BUTTON, TD_WARNING_ICON, NULL); - } - return; - } - - const wchar_t* paths[] = { - L"SYSTEM\\CurrentControlSet\\Control\\Bluetooth\\Audio\\AVRCP\\CT", - L"SYSTEM\\ControlSet001\\Control\\Bluetooth\\Audio\\AVRCP\\CT", - L"SYSTEM\\CurrentControlSet\\Services\\HidBth\\Parameters", - L"SYSTEM\\CurrentControlSet\\Services\\BthAvrcpTg\\Parameters", - L"SOFTWARE\\Microsoft\\Bluetooth\\Audio\\AVRCP\\CT" - }; - - bool success = false; - for (auto path : paths) - { - HKEY hKey; - if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL) == ERROR_SUCCESS) - { - DWORD val1 = 1; - DWORD val0 = 0; - RegSetValueExW(hKey, L"DisableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val1, sizeof(val1)); - RegSetValueExW(hKey, L"EnableAbsoluteVolume", 0, REG_DWORD, (const BYTE*)&val0, sizeof(val0)); - RegCloseKey(hKey); - success = true; - } - } - - if (success) - { - TaskDialog(g_hWnd, NULL, _(L"System Fix Applied DEFINITIVELY"), _(L"All known registry paths for Absolute Volume have been updated.\n\nCRITICAL: You MUST REBOOT your laptop now for this to take effect.\n\nIf volume buttons still sync after reboot, it means your Bluetooth driver is ignoring system settings."), NULL, TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); - } - else - { - TaskDialog(g_hWnd, NULL, _(L"Error"), _(L"Failed to write registry values."), NULL, TDCBF_OK_BUTTON, TD_ERROR_ICON, NULL); - } -} +void UpdateVolume() { if (g_sessionManager) ApplyVolumeToOurSessions(g_sessionManager); } diff --git a/AudioPlaybackConnector.manifest b/AudioPlaybackConnector.manifest index 8bac84b..bec3439 100644 --- a/AudioPlaybackConnector.manifest +++ b/AudioPlaybackConnector.manifest @@ -1,12 +1,12 @@ - AudioPlaybackConnector + AudioPlaybackConnector - Connect mobile audio to PC + + + + + + + - - - From f725ed755e17a0ed14bb950b77a125514368ccc7 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 16:38:01 +0530 Subject: [PATCH 65/70] v2.0.6: Fix build errors (g_runAtStartup + FontIcon) --- AudioPlaybackConnector.cpp | 6 ++++-- AudioPlaybackConnector.h | 1 + SettingsUtil.hpp | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index e4ed824..1fbc509 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -424,7 +424,9 @@ void SetupMenu() MenuFlyoutItem exitItem; exitItem.Text(_(L"Exit")); - exitItem.Icon(FontIcon{ .Glyph = L"\xE8BB" }); + FontIcon exitIcon; + exitIcon.Glyph(L"\xE8BB"); + exitItem.Icon(exitIcon); exitItem.Click([](const auto&, const auto&) { if (g_audioPlaybackConnections.size() == 0) { PostMessageW(g_hWnd, WM_CLOSE, 0, 0); return; } POINT pt; GetCursorPos(&pt); @@ -463,7 +465,7 @@ void SetRunAtStartup(bool enable) { wchar_t path[MAX_PATH]; GetModuleFileNameW(NULL, path, MAX_PATH); - RegSetValueExW(hKey, L"AudioPlaybackConnector", 0, REG_SZ, (const BYTE*)path, (wcslen(path) + 1) * sizeof(wchar_t)); + RegSetValueExW(hKey, L"AudioPlaybackConnector", 0, REG_SZ, (const BYTE*)path, static_cast((wcslen(path) + 1) * sizeof(wchar_t))); } else { diff --git a/AudioPlaybackConnector.h b/AudioPlaybackConnector.h index ef65c31..9658db7 100644 --- a/AudioPlaybackConnector.h +++ b/AudioPlaybackConnector.h @@ -38,6 +38,7 @@ NOTIFYICONIDENTIFIER g_niid = { }; UINT WM_TASKBAR_CREATED = 0; bool g_reconnect = false; +bool g_runAtStartup = false; std::vector g_lastDevices; double g_volume = 0.2; bool g_volumeLock = true; diff --git a/SettingsUtil.hpp b/SettingsUtil.hpp index 45ce027..67785fc 100644 --- a/SettingsUtil.hpp +++ b/SettingsUtil.hpp @@ -9,6 +9,7 @@ void DefaultSettings() g_lastDevices.clear(); g_volume = 0.1; g_volumeLock = true; + g_runAtStartup = false; } void LoadSettings() @@ -43,6 +44,10 @@ void LoadSettings() { g_volumeLock = jsonObj.Lookup(L"volumeLock").GetBoolean(); } + if (jsonObj.HasKey(L"runAtStartup")) + { + g_runAtStartup = jsonObj.Lookup(L"runAtStartup").GetBoolean(); + } auto lastDevices = jsonObj.Lookup(L"lastDevices").GetArray(); g_lastDevices.reserve(lastDevices.Size()); @@ -63,6 +68,7 @@ void SaveSettings() jsonObj.Insert(L"reconnect", JsonValue::CreateBooleanValue(g_reconnect)); jsonObj.Insert(L"volume", JsonValue::CreateNumberValue(g_volume)); jsonObj.Insert(L"volumeLock", JsonValue::CreateBooleanValue(g_volumeLock)); + jsonObj.Insert(L"runAtStartup", JsonValue::CreateBooleanValue(g_runAtStartup)); JsonArray lastDevices; for (const auto& i : g_audioPlaybackConnections) From 6f6a4a3ab80a5a412633fc795bda9b5eeffacfa8 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 16:43:49 +0530 Subject: [PATCH 66/70] v2.0.7: Restore critical window state logic for DevicePicker --- AudioPlaybackConnector.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 1fbc509..2031909 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -233,6 +233,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) static_cast((iconRect.bottom - iconRect.top) * USER_DEFAULT_SCREEN_DPI / dpi) }; + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW); SetForegroundWindow(hWnd); try { g_devicePicker.Show(rect, Placement::Above); From 145f1091037355b249b434fddada2779343d4937 Mon Sep 17 00:00:00 2001 From: park-bit Date: Fri, 1 May 2026 16:55:59 +0530 Subject: [PATCH 67/70] v2.0.9: Fix instructions text encoding + add explorer tip --- AudioPlaybackConnector.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AudioPlaybackConnector.cpp b/AudioPlaybackConnector.cpp index 2031909..e177fef 100644 --- a/AudioPlaybackConnector.cpp +++ b/AudioPlaybackConnector.cpp @@ -406,7 +406,15 @@ void SetupMenu() MenuFlyoutItem helpItem; helpItem.Text(_(L"Instructions & Tips")); helpItem.Click([](const auto&, const auto&) { - TaskDialog(g_hWnd, NULL, L"Instructions", L"ΓÇó Left-Click tray icon to Connect Phone.\nΓÇó Right-Click for Settings & Volume.\nΓÇó Use 'Lock' if phone buttons change PC volume.\nΓÇó 'Fix Volume Sync' requires Admin + Reboot.", L"If sound is missing, disconnect and reconnect on the phone.", TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); + TaskDialog(g_hWnd, NULL, L"Instructions", + L"- Left-Click tray icon to Connect Phone.\n" + L"- Right-Click for Settings & Volume.\n" + L"- Use 'Lock' if phone buttons change PC volume.\n" + L"- 'Fix Volume Sync' requires Admin + Reboot.", + L"Tips:\n" + L"1. If sound is missing, disconnect and reconnect on the phone.\n" + L"2. If clicks aren't working, restart 'Windows Explorer' in Task Manager.", + TDCBF_OK_BUTTON, TD_INFORMATION_ICON, NULL); }); MenuFlyoutSubItem fixMenu; From ae011f468c0c55f812f5d1ddab0101f943e3ca80 Mon Sep 17 00:00:00 2001 From: park-bit Date: Sat, 2 May 2026 18:26:56 +0530 Subject: [PATCH 68/70] Docs: Update feature list in README --- README.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f1f818f..436e81d 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,28 @@ -# AudioPlaybackConnector (Fork with Volume Fix) +# AudioPlaybackConnector (Enhanced Fork) **English** | [简体中文](https://github.com/ysc3839/AudioPlaybackConnector/blob/master/README.zh_CN.md) Bluetooth audio playback (A2DP Sink) connector for Windows 10 2004+. -### Added Features (Volume Patch): -* **Mobile Volume Control:** Adjust the incoming Bluetooth audio volume independently from your system volume. -* **Decouple Phone Volume:** Option to stop your phone's volume buttons from changing your PC's master volume (fixes "Absolute Volume" sync issues). -* **Low Default Volume:** Starts at 20% to prevent sudden loud noises. +### ✨ New Features in this Fork: +* **Mobile Volume Control:** Adjust the incoming Bluetooth audio volume independently from your system volume via a dedicated slider. +* **Lock Phone Volume Buttons:** Block your phone's volume buttons from altering your PC's master volume (fixes "Absolute Volume" sync issues). +* **Run at Startup:** Seamlessly start the app with Windows (runs as a normal user, no admin prompt required). +* **Persistent Connections:** Automatically reconnect to your last used device on startup. +* **System Fixes:** Built-in menu to apply/revert the registry fix for Absolute Volume sync. +* **Tray Integration:** Robust left-click to connect and right-click for full settings. + +> **Note:** This fork has been updated and polished using **vibecode** to ensure a stable, feature-rich experience. # Preview ![Preview](https://cdn.jsdelivr.net/gh/ysc3839/AudioPlaybackConnector@master/AudioPlaybackConnector.gif) # Usage * Download and run AudioPlaybackConnector from [releases](https://github.com/park-bit/AudioPlaybackConnectorFork/releases). -* Add a bluetooth device in system bluetooth settings. You can right click AudioPlaybackConnector icon in notification area and select "Bluetooth Settings". -* **Volume Fix:** If your phone buttons are changing your PC volume, right-click the tray icon and select **"Decouple Phone Volume (Fix Sync)"**, then **REBOOT** your computer. -* Click AudioPlaybackConnector icon and select the device you want to connect. -* Enjoy! +* Add a bluetooth device in system bluetooth settings. You can right click the tray icon and select "Bluetooth Settings". +* **Left-Click** the icon to quickly connect or disconnect a device. +* **Right-Click** the icon to access Volume Control, Startup settings, and Advanced Fixes. +* **Absolute Volume Fix:** If your phone buttons are changing your PC volume, use the "System Fixes" menu, then **REBOOT** your computer. + +# Credits +Original project by [ysc3839](https://github.com/ysc3839/AudioPlaybackConnector). +Enhanced and maintained by [park-bit](https://github.com/park-bit). From fe6aba8b3b8d8c5806aed1ba51a398c2f9bdb5cc Mon Sep 17 00:00:00 2001 From: park-bit Date: Sat, 2 May 2026 18:27:06 +0530 Subject: [PATCH 69/70] Docs: Clean up README for PR --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 436e81d..d7722bd 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ Bluetooth audio playback (A2DP Sink) connector for Windows 10 2004+. * **System Fixes:** Built-in menu to apply/revert the registry fix for Absolute Volume sync. * **Tray Integration:** Robust left-click to connect and right-click for full settings. -> **Note:** This fork has been updated and polished using **vibecode** to ensure a stable, feature-rich experience. - # Preview ![Preview](https://cdn.jsdelivr.net/gh/ysc3839/AudioPlaybackConnector@master/AudioPlaybackConnector.gif) From e3a0eada6fd3729e72f3bc5eb55e7e6b6fd7ff7f Mon Sep 17 00:00:00 2001 From: park-bit Date: Sat, 2 May 2026 18:33:35 +0530 Subject: [PATCH 70/70] CI: Revert workflow changes for PR compliance --- .github/workflows/build.yaml | 114 ++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bf9c21e..7ed0a86 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,59 +4,113 @@ on: push: tags: [ '**' ] -permissions: - contents: write - jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v2 with: submodules: true - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v2 - - uses: nuget/setup-nuget@v2 + uses: microsoft/setup-msbuild@v1.0.0 + - uses: nuget/setup-nuget@v1 with: nuget-version: latest - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v1 with: - python-version: '3.10' - - name: Setup translations - run: | - git config --global url."https://github.com/".insteadOf git://github.com/ + python-version: 3.7 + - run: | cd translate pip install -r requirements.txt ./gen_rc.sh shell: bash - - name: NuGet restore - run: nuget restore AudioPlaybackConnector.sln - - name: Build x64 - run: msbuild AudioPlaybackConnector.sln -p:Configuration=Release -p:Platform=x64 -v:minimal - shell: powershell - - name: Build x86 - run: msbuild AudioPlaybackConnector.sln -p:Configuration=Release -p:Platform=x86 -v:minimal + - run: nuget restore AudioPlaybackConnector.sln + - run: | + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x64" } + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=x86" } + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM64" } + Start-Job -Init ([ScriptBlock]::Create("Set-Location '$pwd'")) -Script { + msbuild AudioPlaybackConnector.sln "-p:Configuration=Release;Platform=ARM" } + Get-Job | Wait-Job | Receive-Job shell: powershell - - name: Upload x64 - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v2 with: name: AudioPlaybackConnector64 path: x64/Release/AudioPlaybackConnector64.exe - if-no-files-found: error - - name: Upload x86 - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v2 + with: + name: AudioPlaybackConnector64 + path: x64/Release/AudioPlaybackConnector64.pdb + - uses: actions/upload-artifact@v2 with: name: AudioPlaybackConnector32 path: Release/AudioPlaybackConnector32.exe - if-no-files-found: warn + - uses: actions/upload-artifact@v2 + with: + name: AudioPlaybackConnector32 + path: Release/AudioPlaybackConnector32.pdb + - uses: actions/upload-artifact@v2 + with: + name: AudioPlaybackConnectorARM64 + path: ARM64/Release/AudioPlaybackConnectorARM64.exe + - uses: actions/upload-artifact@v2 + with: + name: AudioPlaybackConnectorARM64 + path: ARM64/Release/AudioPlaybackConnectorARM64.pdb + - uses: actions/upload-artifact@v2 + with: + name: AudioPlaybackConnectorARM + path: ARM/Release/AudioPlaybackConnectorARM.exe + - uses: actions/upload-artifact@v2 + with: + name: AudioPlaybackConnectorARM + path: ARM/Release/AudioPlaybackConnectorARM.pdb - name: Create Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - files: | - x64/Release/AudioPlaybackConnector64.exe - Release/AudioPlaybackConnector32.exe - draft: false + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: true prerelease: false + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: x64/Release/AudioPlaybackConnector64.exe + asset_name: AudioPlaybackConnector64.exe + asset_content_type: application/octet-stream + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: Release/AudioPlaybackConnector32.exe + asset_name: AudioPlaybackConnector32.exe + asset_content_type: application/octet-stream + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ARM64/Release/AudioPlaybackConnectorARM64.exe + asset_name: AudioPlaybackConnectorARM64.exe + asset_content_type: application/octet-stream + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ARM/Release/AudioPlaybackConnectorARM.exe + asset_name: AudioPlaybackConnectorARM.exe + asset_content_type: application/octet-stream