Skip to content

Repository files navigation

HyperX Audio Guard

A portable Windows tray app (WPF, .NET 10) that keeps the default audio output on a device that actually plays sound. It exists because the HyperX driver (NGENUITY / HyperX Virtual Audio Device) parks the output on its own virtual endpoints, so audio disappears while the headset is off, and because the same headset sometimes fails to come back from hibernation at all. Interface in English, Ukrainian and Russian.

Main window

The problem

NGENUITY registers a set of virtual output devices:

NGENUITY - Chat (HyperX Virtual Audio Device)
NGENUITY - Stream Mix (HyperX Virtual Audio Device)
NGENUITY - 8/12 Channel Spatial (HyperX Virtual Audio Device)
...

They stay alive whether or not the headset is attached. Once HyperX Cloud III goes NotPresent, Windows picks the next "available" device — and that turns out to be an NGENUITY endpoint routing audio into a headset that isn't there. A default device formally exists; sound does not.

To inspect the current state: HyperXAudioGuard.exe --list.

Wireless models are harder. The dongle stays an active audio device even with the headset powered off, so Windows events say nothing and Core Audio cannot tell whether anyone is listening. For those models the app queries the dongle directly over HID — the protocol was documented by platorp/HyperX-Cloud-III-3-S-Audio-Switcher: usage page 0x1C0, report id 0x0C, request 0C 02 03 01 00 02 …, answer in byte [6] (2 — headset on, 0 — off). Polled every 3 seconds.

The dongle is matched by vendor id 0x03F0 (HP) and 0x0951 (Kingston, pre-2021 models), with no model hardcoded — but the usage page alone is not enough. A wired Cloud III exposes the same collection with zero-length reports: nothing to write, nothing to read. So the interface is only accepted when its InputReportByteLength and OutputReportByteLength are large enough for the exchange; otherwise the app concludes there is no dongle and relies on Windows events, which is sufficient for wired models — unplugging the cable makes the device physically disappear.

To see what your machine exposes: HyperXAudioGuard.exe --hid.

How it works

The app subscribes to IMMNotificationClient (Core Audio events: endpoint added, removed, state changed) and, when a dongle is present, polls it as well. Every change re-evaluates one rule:

  1. Take the active devices, drop the blocked ones (! lines) and — if the dongle reports the headset is off — every HyperX device too.
  2. Find the first match against the priority list in config.json.
  3. Set it as the default (Console, Multimedia and Communications roles) if the current device is unusable, missing, or ranks lower.

A manual choice outside the rules (say, a Bluetooth speaker that isn't listed) is left alone — otherwise the app would fight the person using it.

Recording devices go through the same rule, with their own priority and block lists under [input]. That is not a bonus feature but the other half of the same problem: a headset arrives with a microphone, and virtual drivers hijack that just as eagerly — on this machine the default microphone was NGENUITY's virtual one until the app moved it back. Bluetooth is worse. Every wireless headset shows up twice: as … Stereo (A2DP) and as Hands-Free AG Audio (HFP) — mono, 16 kHz, the sound of a phone call. Windows switches the output to that mono channel the moment its Hands-Free microphone becomes the default input, which is why !Hands-Free is blocked on both sides out of the box.

Input devices

Changing the default endpoint goes through the undocumented IPolicyConfig COM interface. Windows exposes no public API for this; every known tool takes the same route (see below).

After hibernation

A second failure mode, and a nastier one. On waking, the headset's USB device sometimes does not return at all: the PnP node is still there, the audio endpoints are gone, replugging the cable changes nothing, and Windows parks the default output on an NGENUITY endpoint that plays into nowhere. Only a reboot used to fix it — and the app was usually not even running by then, because a logon task does not fire when a machine resumes.

The wake is noticed. SystemEvents.PowerModeChanged records whether a headset was present before sleep, so one that was simply left at home is never "repaired". On resume the dongle handle is dropped — it does not survive sleep — and the rule is recalculated after 5 seconds and then every 10, four times over, because USB devices come back slowly and Core Audio may send no event at all.

The microphone counts too. A headset is one USB node — USB\VID_03F0&PID_089D&MI_00 carries both the playback and the recording endpoint — and the pair usually disappears together. Usually: the microphone can also vanish on its own while the headphones keep working, and that used to slip past unnoticed, because the app declared the headset "back" as soon as the playback endpoint was there. Now it waits for both. Whether the headset even has a microphone is not configured anywhere: Windows keeps the endpoint in its list long after the device is gone, so the app asks it. One disabled by hand in mmsys.cpl does not count — otherwise the repair would chase a microphone its owner switched off deliberately.

There is no separate driver to restart for a microphone, and no separate command for it: reinstalling that node brings back both halves — as long as the node is the right one, see below.

A headset still missing after that is reinstalled. The app takes the PnP nodes whose description carries HyperX — the headset itself and the NGENUITY virtual audio driver — and restarts them through pnputil /restart-device. If that is not enough it restarts AudioEndpointBuilder, which rebuilds the entire endpoint list; that is the part a reboot was doing.

The whole USB device, not one interface. Only the audio interface of the headset is called HyperX (USB\VID_03F0&PID_089D&MI_00); the HID interface next to it (…&MI_03, plus its collections) is named after nothing in particular — and that is the one NGENUITY talks to. Restarting the audio interface alone therefore brought the sound back while NGENUITY went on insisting no headset was connected, and the microphone came back only after a few more rounds or a reboot. So the app climbs from the matched node to its composite parent (USB\VID_03F0&PID_089D\…) and restarts that instead: every interface of the device is re-enumerated at once, which is exactly what pulling the cable out and back in does.

NGENUITY holds that virtual driver open, so the automatic repair takes it out of the way first — killed the way Task Manager kills it — and starts it again when the drivers are back. It has no "start minimised" switch of its own, but closing its window leaves the process running in the tray, so that is what the app does with it. If NGENUITY was not running, both steps are skipped.

On demand the choice is yours. Restore sound, in the window and in the tray menu, opens two actions: restart the audio drivers alone, or restart NGENUITY along with them. While NGENUITY is not running there is nothing to choose — the second action would do exactly what the first one does — so it is not offered, and the button restarts the drivers on the first click. From the console the same pair is --recover and --recover ngenuity.

Restore sound

And the ports are kept awake. "Allow the computer to turn off this device to save power" is checked by default on USB hubs, and it is the likeliest reason the device does not survive hibernation in the first place. The Keep the ports awake toggle unchecks it on every USB node Windows offers the setting for — no guessing which port the headset is in, and no re-pinning when it moves. Only nodes where the box was still checked are taken, and their names go into config.json; turning the toggle off restores exactly those and nothing else.

Reinstalling a device and restarting a service both need administrator rights, and the app never asks for them on its own. Enable Run at logon once from an elevated instance and the task keeps those rights; otherwise recovery writes a line into the log saying it cannot proceed.

Install

A ready-to-run build sits in releaseHyperXAudioGuard.exe, a single file with the runtime inside (~72 MB). Nothing to install: download and run, from a flash drive if you like.

The app is portable. Everything it creates lives next to the executable and nowhere else:

File Contents
config.json everything the app remembers: rules, settings, language, USB nodes
log.txt switch log, trimmed to the last two days on every start

Uninstalling means deleting the folder. Nothing is written to the registry or the user profile. Two toggles are the exception, because they change Windows itself: Run at logon creates a scheduler task, since there is no other way to have autostart, and Keep the ports awake unchecks a power setting on the USB nodes. Clearing each toggle undoes its own change, so clear them before deleting the folder — config.json is the only record of which nodes were touched.

On read-only media (a disc, a write-protected stick) the app falls back to defaults: rules won't persist, but audio switching still works.

Build it yourself:

dotnet build -c Release                  # for development
.\tools\publish.ps1                      # self-contained exe into release\ (~72 MB)
.\tools\publish.ps1 -FrameworkDependent  # same thing at ~300 KB, needs .NET Desktop Runtime 10

The sources sit in seven places, one namespace for all of them:

Folder Contents
root Program.cs — the entry point, and nothing else
Audio/ the point of the app: the watcher loop, the rules, the volume
App/ its own housekeeping: the settings file, the journal, the build version
Interop/ the bindings: Core Audio COM interfaces, HID and SetupAPI imports
Platform/ what those make the system do: PnP restart, scheduler task, USB power
Ui/ the window and the tray icon
Lang/ language: one dictionary per language, and the code that switches them

The window

No system frame: the title bar is drawn by the app and doubles as its header, with its own settings, minimise, maximise and close buttons. Dragging, resizing and snapping stay native (WindowChrome), and a maximised window does not cover the taskbar.

Tray icon: red when watching, grey when paused. Double-click opens the window. Both the close button and minimise send it back to the tray — the app never sits on the taskbar; exit is in the tray menu.

Transitions are animated: window entry, hover on buttons and rows, the toggle knob, rows sliding in when the list refreshes. Opening the log is deliberately not animated — the window has to grow by exactly the height of the log panel, and while that height was easing, the device list jumped up and back down.

Inside:

  • Default device — what is playing now, the dongle state (whether one was found and what it reports about the headset), and the Watching/Paused badge. The badge is the pause switch: click it and the app stops choosing devices until you click it again.

  • Output / Input — two tabs over one list: each direction has its own rules, and the buttons below act on whichever is open. Every endpoint, active ones first. On the left a badge: priority number, or a red for blocked; hovering it names the rule that put it there — worth knowing, because a rule is a fragment of a name and can cover several devices. Next to the state, a chip with the level the device is set to on a switch — a dash while nothing is pinned. Clicking it opens a slider: dragging it turns the volume of that device right away, and on closing the level is written to config.json; Do not pin drops the rule and leaves the volume alone. Peeking at the chip and closing it again changes nothing. On the right the state with a coloured dot. The current device is highlighted and labelled.

  • Buttons — Prioritise / Block / Clear rule / Up / Down. They act on the selected row, write config.json immediately and re-apply the rule; Clear rule asks first when the rule it is about to drop covers other devices too. Restore sound, apart at the right end of the row, acts on the system instead of on a device.

  • Settings — the sliders button in the title bar. Everything you set once and forget is there: run at logon, USB port power, a balloon when the device changes, and hotkeys — the last one off until you ask for it, because the combinations are registered system-wide. The balloon is silent and goes out through Shell_NotifyIcon directly: WinForms can only show one with a sound, and while a previous balloon is still up the shell drops the next one without a word — which looked exactly like notifications arriving once and never again. Below them, apart, Check for updates: not a setting but an action, and the only time the app touches the network. It asks GitHub for the latest release and answers in place — a newer version, the current one, or an honest "could not check" when the answer never came. A newer version also leaves a link in the footer, so the news survives the popup closing; clicking it opens the releases page. Nothing is downloaded or installed: the app is a single portable exe, and replacing it is your call.

    Settings

  • Log — an expander with the switch history; on open it pulls the tail of log.txt, so events from before the window was opened are visible too. The file is trimmed on every start: entries older than two days go, and no more than 500 lines are kept. The point of it is “what happened to the sound last night”, not a month of history.

  • Footer — authorship, the version of the running build, the RU / UA / EN switch, and — if the check is on and a newer release exists — a link to it.

Log expanded

The icons (app.ico, app-paused.ico) are generated by tools/make-icons.ps1 in seven sizes from 16 to 256. Re-run it after changing the drawing.

Configuration

Normally edited in the window. Everything the app remembers lives in one config.json next to the executable, created on first run:

{
  "language": "en",
  "notify": true,
  "hotkeys": false,
  "pauseHotkey": "Ctrl+Alt+P",
  "recoverHotkey": "Ctrl+Alt+R",
  "output": {
    "priority": [ "HyperX", "Speakers (Realtek(R) Audio)" ],
    "blocked": [ "NGENUITY", "HyperX Virtual Audio Device", "Hands-Free" ]
  },
  "input": {
    "priority": [ "HyperX", "Microphone (Realtek(R) Audio)" ],
    "blocked": [ "Hands-Free", "NGENUITY" ]
  },
  "volume": [ { "match": "HyperX", "percent": 25 } ],
  "usbPower": false,
  "usbPowerHeld": []
}

The first priority is just HyperX, so any HyperX headset matches; the driver's virtual devices carrying the same word are cut out by blocked, which outranks priorities. The second entry is the fallback — on first run it is filled with whatever is default at that moment, unless a virtual driver has already hijacked it, in which case the first real device is taken instead. input is the same rule for microphones. Names come from --list output, which prints both directions.

A missing output or input is filled with the defaults on the next run. An empty one, on the other hand, is left empty — that is a decision, not an omission, and that direction is then never touched.

volume is the level to set a device to at the moment the app switches to it — the first entry whose match is part of the device name wins. The level chip in the list writes here, and a rule added by hand can cover several devices at once, which the chip then says. Windows already remembers a volume per endpoint, so the list is only for keeping a device at a fixed level; while it is empty the app never touches the volume, and the level is not re-applied afterwards, so turning the knob yourself is not fought. usbPowerHeld is written by the app itself: the USB nodes it took the power-saving checkbox off, and the only way back.

The hotkeys can only be changed here. Modifiers are Ctrl, Alt, Shift and Win in any combination plus one key; the names are WPF's (P, F9, Oem3, …). If the system says the combination is taken, the log says so too, and the other one still works.

Hand edits are read as written: comments and a trailing comma are tolerated, the app rewrites the file only when a setting changes. Versions up to 1.0.1 kept all of this in five text files (devices.txt, settings.txt, volume.txt, language.txt, usbpower.txt) — on the first run the app moves them into config.json, deletes them and says so in the log.

Autostart

The Run at logon toggle creates one task, HyperXAudioGuard, that starts HyperXAudioGuard.exe --tray on two triggers: logon, and the Power-Troubleshooter wake event. A logon trigger stays silent when a machine resumes, and if the app did not survive the sleep nothing else would bring it back; a live instance turns the duplicate launch away through its mutex.

The task is described by XML rather than by schtasks switches, because the defaults that command applies are fatal here: a task is not started at all while the machine runs on battery, is killed the moment the cable is pulled, and is stopped after three days. On a laptop that means the app quietly never runs. All three are off in the XML, and a task left behind by an earlier version is rewritten silently at the next start, keeping the rights it had.

The task is visible and editable in taskschd.msc; clearing the toggle deletes it. Re-enabling rewrites it with the current executable path, which is handy after moving the folder — and with the rights of the instance that created it, which is what gives recovery its administrator rights. From the console:

HyperXAudioGuard.exe --autostart        # is the task there or not
HyperXAudioGuard.exe --autostart on     # create it
HyperXAudioGuard.exe --autostart off    # remove it

Languages

Strings live in Lang/Lang.en.xaml, Lang/Lang.uk.xaml and Lang/Lang.ru.xamlResourceDictionary files swapped wholesale at runtime. The language is switched in the window footer and remembered in config.json next to the executable; English is the default. The console modes and the messages in the log follow the same setting — no part of the program is fixed to one language. Audio device names are not translated — Windows supplies them.

To add a language: drop a Lang.<code>.xaml beside the others with the same keys and add the code to Localization.Codes.

Command line

Argument What it does
(none) open the window, keep running in the tray
--tray start in the tray without a window — used by autostart
--once apply the rule once and exit (console)
--list list every output device, its state and the current default
--test self-check: switch to another device and switch back
--hid HyperX HID interfaces and the raw dongle status reply
--recover [ngenuity] repair the audio devices; ngenuity restarts NGENUITY as well
--autostart [on|off] query, create or remove the scheduler task

Doing it without the app

If the NGENUITY virtual endpoints aren't needed, disabling them makes the problem go away with no software at all: Win+Rmmsys.cpl → right-click each NGENUITY - ...Disable. The app is for when NGENUITY is actually used (equaliser, mixer, spatial) or when the driver re-enables those endpoints after an update.

Similar projects

All of them change the default device through the same IPolicyConfig; none addresses the virtual-endpoint problem specifically, but they are useful references:

License

Apache License 2.0 — see LICENSE and NOTICE.

Copyright 2026 Dykalo Pavlo. Forks and derivative works must keep the contents of NOTICE and mark the files they changed (sections 4b and 4d of the license).

The dongle polling protocol was reverse engineered by platorp/HyperX-Cloud-III-3-S-Audio-Switcher (MIT); none of its code is included here — the implementation is written from scratch in C# on top of the Win32 HID API.

HyperX, NGENUITY and Cloud are trademarks of HP Inc. This project is independent, not affiliated with or supported by HP; the names identify the hardware and drivers the software interoperates with.

Author

Developed by Dykalo Pavlo, 2026.

About

Portable Windows tray app that keeps audio on a device that actually plays sound — stops NGENUITY's virtual outputs from hijacking the default endpoint when your HyperX headset is unplugged.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages