Skip to content

AudioSwap hotfix 2.3.0#4698

Merged
m417z merged 74 commits into
ramensoftware:mainfrom
BlackPaw21:main
Jul 8, 2026
Merged

AudioSwap hotfix 2.3.0#4698
m417z merged 74 commits into
ramensoftware:mainfrom
BlackPaw21:main

Conversation

@BlackPaw21

Copy link
Copy Markdown
Contributor

Changelog

If this pull request updates an existing mod, describe the changes below:

added Persistent Mute option
general patches
added screenshots to readme

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

Adds a network toggle feature to the system tray, allowing users to enable or disable network adapters with a single click. The implementation includes error logging, UAC handling, and dynamic icon updates based on network status.
Added a readme section for the Network Toggle mod, detailing its features and functionality.
using original code snippet from:
https://github.com/ramensoftware/windhawk/wiki/Mods-as-tools:-Running-mods-in-a-dedicated-process

Icon does not disappear on explorer.exe restart

Removed -lwtsapi32 and -lwininet 

Removed <wininet.h>, <wtsapi32.h>, and all #pragma directives.

Removed the unused RunPowerShellCommandAsSystem function.

Removed SetThreadPriority; the tray UI thread now runs at standard priority as requested.

overall stability and maintenance upgrade
Updated usage instructions and added known issues section.
This mod adds a tray icon for toggling between two audio outputs, allowing users to switch devices quickly.
Updated version to 1.1.0 and improved device selection handling.
now devices can be automatically detected and selected via Right Click on the icon.
Updated device selection persistence to use Windhawk storage instead of registry. Simplified loading and saving of device selections.
Updated version to 1.1.1 and added new features including opening WindHawk.exe from the mod itself, new icons in the settings.
created MicSwap mod that allows toggling between two audio input devices from the system tray.
new features and functions added.
scroll capability
over 2 devices can be selected.
bug fixes and improvements.
added a red dot over the icon when muted.
persistent across restarts.
UNICODE-dependencies converted to explicit W/INFOW;
-DUNICODE -D_UNICODE removed
Updated version to 1.5.0 and added custom icon support.
Added WM_RELOAD_ICONS message to reload tray icons and updated icon handling in WhTool_ModSettingsChanged function.
BlackPaw21 added 19 commits May 30, 2026 22:04
### v2.2.0
- New: Right-click → **Sound Settings...** opens Windows Sound dialog directly on the Playback tab.
- New: Middle-click tray icon → compact volume slider popup with a custom dark design. Drag to adjust output volume live; click outside or press Escape to close.
- New: Scroll wheel over the tray icon (Click to Swap mode) adjusts output volume, matching native Windows sound icon behaviour.
### v2.0.0
- **Complete rebuild.** Mod renamed to Net-Toggle.
- New: **DNS Monitoring** — The icon monitors your connection and changes color if your internet drops out (configurable in Mod Settings).
- New: **WiFi-style tray icon** with 5 color states (Red / Yellow / Blue / Green / Grey).
- New: Middle-click → **Full Network Reset** (flush DNS, release/renew, winsock + IP reset).
- New: Right-click context menu — toggle network, open Network Settings, or exit.
- New: Donate button on the mod page.
- Improved: Tray icon is now independent from the Windhawk app and no longer groups with it in the taskbar.
- Improved: Tray icon persists reliably across Explorer restarts.
- Improved: Icon stays in sync even if you disable your adapter directly in Windows Settings.
- Fixed: Occasional hangs during network toggles or timeouts.
- Fixed: Tray icon could show the wrong state if a toggle failed.
fix: production-readiness audit fixes (v2.0.0)

  - Offload DNS ping to worker thread (prevents UI thread block)
  - CoInitializeEx(COINIT_APARTMENTTHREADED) + HRESULT check in all 3 threads
  - Atomic g_networkIsUp read via InterlockedOr in ProcessTrayClick
  - Remove dead WM_UPDATE_DNS_STATE define and handler
  - lstrcpyW → wcscpy_s for AUMID string copy
  - GetModuleHandle → GetModuleHandleW
  - Update stale log message in ModInit
Updated network reset functionality, improved context menu options and general patches.
fixes implemented (hopefully) and optional changes discarded due to functionality issues

Replaced `powershell.exe` spawn in `CheckActualNetworkState` with native `GetAdaptersAddresses` API calls to
 eliminate startup blocking and latency.
Updated compiler options to include WIN32_LEAN_AND_MEAN for better compatibility.
-DWIN32_LEAN_AND_MEAN removed.
-D flag broke the automatic PR validation
added screenshots
added  -DWIN32_LEAN_AND_MEAN
Fixed CheckActualNetworkState
Raised debounce from 2s → 10s — Prevents the user from firing a second toggle before the first PowerShell command has had time to fully apply its changes
  - Add secondary (fallback) DNS server support
  - New default check: real DNS query (fixes false "unreachable" on networks that block TCP/53) — fixes ramensoftware#4288
  - New per-server check methods: UDP DNS / TCP 53 / DoT 853 / DoH 443
  - New Orange icon state: primary down, fallback still up
  - Tooltip shows per-server status
  - Minor reliability fixes (DNS state reads, shutdown handle cleanup)
A lightweight tray icon that shows a mini task manager popup with live CPU, GPU
and RAM usage, plus the single top-consuming process for each.
1. Removed persistent registry writes: Replaced  RegCreateKeyExW  /  RegGetValueW  with Windhawk's native  Wh_SetIntValue  and  Wh_GetIntValue  to store the refresh interval cleanly.
  2. Fixed double-buffer for top CPU detection: Introduced  g_curProcs  alongside  g_prevProcs  to ensure that we don't read and write to the same buffer in a single pass, which caused
  the top process to intermittently drop out.
  3. Optimized GPU Process Lookup: Removed the per-tick  CreateToolhelp32Snapshot  call for the GPU process name and reused the name that was already collected in the process loop.
  4. Clamped GPU to 100%: Added limits to prevent  grandTotal  and  topVal  from displaying over 100% since PDH sums across all engines.
  5. Debounced UI Popup Toggling: Added a 200ms debounce to the tray icon left-click to prevent the popup from immediately reopening when clicking the tray icon to close it.
  6. Removed  g_statsLock : Removed the  CRITICAL_SECTION  because all reads and writes to those stats run exclusively on the tray thread's message loop, making locking unnecessary.
  7. README Screenshot: Added a screenshot to the README section.
Updated version to 2.2.1, added persistent mute feature and general performance patches.
@m417z

m417z commented Jul 6, 2026

Copy link
Copy Markdown
Member

Submission review

Note: This review was done by Claude, and then refined manually. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.

Note: I haven't verified the claims manually. If it's a mistake and the code is correct as is, let me know.


The new Persistent Mute feature has a device-tracking bug that leaves a device stuck muted.

Persistent mute never re-tracks the muted device after a swap, and never unmutes the previous one. When persistent mute is on and you cycle to a new device, CycleAudioDevice skips RestoreMute() (leaving the old device muted) and then mutes the new default with ApplyMute(nullptr, TRUE) — but it never updates g_mutedDeviceId, the persisted MutedDeviceId, or unmutes the old device:

// CycleAudioDevice — lines ~2022-2027
if (keepMute) {
    ApplyMute(nullptr, TRUE);   // mutes the new default...
    HWND hw = g_trayHwnd;       // ...but g_mutedDeviceId still points to the OLD device
    if (hw) PostMessageW(hw, WM_UPDATE_TRAY_STATE, 0, 0);
}

The same omission is in the WM_REBIND_VOLUME_CALLBACK branch (lines ~2674-2678). Because g_mutedDeviceId is now stale, everything that relies on it targets the wrong device:

  • Manual unmute unmutes the wrong device. After a swap, ToggleMuteCurrentDevice/RestoreMute call ApplyMute(g_mutedDeviceId=oldDevice, FALSE), unmuting the old device while the current device stays muted. The user clicks unmute and nothing happens to the audible device.
  • Reversibility is broken. On settings change / disable / uninit, RestoreMuteExternal() (line ~2876) restores the stale old ID, so the mod leaves the current device muted after it's turned off. Every intermediate device cycled through while muted is also left muted with no tracking.

Fix: when persistent mute re-applies the mute, release the previously muted device, mute + track the new default (you already know its ID — localIds[validSlot]), and persist it:

if (keepMute) {
    WCHAR prevId[512];
    { EnterCriticalSection(&g_stateLock); lstrcpynW(prevId, g_mutedDeviceId, 512); LeaveCriticalSection(&g_stateLock); }
    if (prevId[0] && wcscmp(prevId, localIds[validSlot]) != 0)
        ApplyMute(prevId, FALSE);                 // release the device we no longer point at
    ApplyMute(localIds[validSlot], TRUE);         // mute the new default
    EnterCriticalSection(&g_stateLock);
    lstrcpynW(g_mutedDeviceId, localIds[validSlot], 512);
    LeaveCriticalSection(&g_stateLock);
    Wh_SetStringValue(L"MutedDeviceId", localIds[validSlot]);
    HWND hw = g_trayHwnd;
    if (hw) PostMessageW(hw, WM_UPDATE_TRAY_STATE, 0, 0);
}

The WM_REBIND_VOLUME_CALLBACK path should retrack the same way (fetch the new default's ID and store it into g_mutedDeviceId / MutedDeviceId).

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • g_persistentMute locking is inconsistent with its stated invariant. It's declared inside the // protected by g_stateLock block (line 225), but it's written without the lock in the WM_RELOAD_ALL handler (line ~2685, tray thread) and WhTool_ModInit (line ~2853), and read without the lock at line ~2675 — while CycleAudioDevice reads it under the lock (line 1969). It's a plain bool, so this is benign in practice, but either take g_stateLock on those accesses or move it out of the guarded block (e.g. make it a volatile LONG / std::atomic<bool>) so the invariant comment stays honest.

Functionality notes

Non-critical observations about the feature behavior itself.

  • WM_REFRESH_DEVICE_LIST can revert an unsaved slot selection. In the slot-combo repopulation loop (lines ~1092-1102) newSel is first set from the preserved current selection (findNewIdx(savedSlotSel[i])) but then unconditionally overridden by if (s->slots[i].id == activeDevices[j].id) newSel = idx;, which snaps the combo back to the last saved device ID. So if the user changes a slot dropdown without saving and a device is hot-plugged while the dashboard is open, their in-progress change is silently reverted. The priority combos (lines ~1104-1113) only use savedPrioSel and don't have this override, so the two panels behave differently. If preserving the live selection is intended, drop the s->slots[i].id override (or only apply it when savedSlotSel[i] was invalid).

@BlackPaw21

BlackPaw21 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@m417z
working on a patch, is there a specific prompt you use to get the report on the Windhawk mods the creators make? i would like to run it myself and spare both of us some work. thank you :)

@m417z

m417z commented Jul 6, 2026

Copy link
Copy Markdown
Member

It's a skill that I maintain, created from past manual reviews and Windhawk knowledge base (API, wiki, existing mods, etc.), running in an environment with the ability to query some of these knowledge base sources. Currently I don't plan on publishing it.

@BlackPaw21

Copy link
Copy Markdown
Contributor Author

i see, that is really cool man keep it up.
need to make something similar on my own that is really useful.

@BlackPaw21 BlackPaw21 changed the title AudioSwap hotfix 2.2.1 AudioSwap hotfix 2.3.0 Jul 7, 2026
Updated version to 2.3.0 with new features and fixes.
used new skills i made based on the entire Windhawk repo and wiki.
@BlackPaw21

Copy link
Copy Markdown
Contributor Author

took some time to craft the skill but it sure was worth it. hopefully that is good and doesn't need more tweaking.

@m417z

m417z commented Jul 7, 2026

Copy link
Copy Markdown
Member

A small COM usage issue. I haven't reviewed the other notes.

Submission review

Note: This review was done by Claude, and then refined manually. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


  • CoUninitialize guard should be SUCCEEDED(hrCo), not hrCo == S_OK. CoInitialize/CoInitializeEx returns S_FALSE when the thread was already COM‑initialized, and that call still increments the refcount and must be balanced with CoUninitialize. The new pattern if (hrCo == S_OK) CoUninitialize(); skips the S_FALSE case. This is reachable: GetCurrentVolumePct (line 1953) and SetCurrentDeviceVolume (line 1981) run on the tray thread, which already holds a COM apartment from TrayThreadProc, so their CoInitializeEx returns S_FALSE and the refcount grows by one on every middle‑click / popup volume change. It's harmless in practice (torn down when the thread exits) but incorrect — use if (SUCCEEDED(hrCo)) CoUninitialize(); (that still correctly skips RPC_E_CHANGED_MODE, where init genuinely failed).
Optional improvements

Minor polish — none of this affects users in normal operation, so it's your call.

  • A g_stateLock critical section was removed in ToggleMuteCurrentDevice (the per‑device mute branch, around line 1529). Previously the write to g_mutedDeviceId / g_isMutedByUs was under the lock; now it isn't:

    if (ApplyMute(pId, TRUE)) {
        lstrcpynW(g_mutedDeviceId, pId, 512);   // <-- no lock
        g_isMutedByUs = true;
        Wh_SetStringValue(L"MutedDeviceId", pId);
    }

    These fields are read under the lock by RestoreMuteExternal (main thread, on settings change / uninit), so this is a data race — narrow (only when a mute click coincides with a settings change or unload), but re-adding the two lines is free:

    if (ApplyMute(pId, TRUE)) {
        EnterCriticalSection(&g_stateLock);
        lstrcpynW(g_mutedDeviceId, pId, 512);
        g_isMutedByUs = true;
        LeaveCriticalSection(&g_stateLock);
        Wh_SetStringValue(L"MutedDeviceId", pId);
    }
  • #include <propvarutil.h> appears unused — no InitPropVariantFrom* / PropVariantTo* helpers are called (the AUMID PROPVARIANT is filled by hand, and PropVariantInit/PropVariantClear come from propidl.h). Safe to drop. (propsys.h for IPropertyStore is a reasonable addition to keep.)

Functionality notes

Non-critical observations about the Persistent Mute feature and the live dashboard refresh — feature behavior, no integration impact.

  • Persistent Mute mutes/unmutes every active render endpoint and doesn't remember prior state. ApplyMuteAll sets the OS‑level per‑device mute on all render devices, and un‑toggling calls ApplyMuteAll(FALSE) on all of them. So if a user had independently muted an unrelated device (e.g. via Windows), enabling+disabling Persistent Mute will silently unmute that device against their intent. Since cycling only ever touches the configured swap slots, scoping the mute/unmute to the configured slot device IDs (and/or remembering each device's prior mute state and restoring it) would be more contained and avoid clobbering unrelated devices.

  • Turning Persistent Mute off while it is active can leave devices stuck muted. In the all‑mute path g_mutedDeviceId is left empty and MutedDeviceId is persisted as "all". If the user then unchecks Persistent Mute and saves, WM_RELOAD_ALL sets g_persistentMute = FALSE but doesn't unmute anything; the next mute toggle takes the per‑device branch and ApplyMute(nullptr, FALSE) only unmutes the current default device — the others remain muted with g_isMutedByUs now cleared, so the mod no longer tracks them. Consider unmuting‑all when the flag transitions off (e.g. in WM_RELOAD_ALL, if the previous state was muted‑all and persistentMute is now 0, call ApplyMuteAll(FALSE)).

  • A device plugged in while persistent‑muted is not muted. ApplyMuteAll is a one‑time snapshot, so a device added afterward plays sound even though the tray shows muted. OnDeviceAdded already fires — muting the new endpoint there (when g_isMutedByUs && g_persistentMute) would keep the state consistent.

  • Live dashboard refresh (WM_REFRESH_DEVICE_LIST) has a couple of rough edges when a device event arrives while the dashboard is open:

    • An unsaved, in‑progress combo selection snaps back to the saved value: the repopulate loop overrides newSel whenever s->slots[i].id (the persisted value) matches a device, discarding what the user just picked but hasn't saved.
    • Offline priority entries are dropped. The "(offline)" label appended in WM_CREATE isn't re-added on refresh, and its old index maps to -1, so the combo reverts to "None"; saving after that loses the remembered offline priority device.

    Neither is critical, but they can surprise a user configuring the dashboard while hot‑plugging devices.

persistent mute device-tracking fix and fixed various crash issues.
@m417z m417z merged commit e350c1e into ramensoftware:main Jul 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants