Mute HyperX QuadCast S at logon and sync Windows mute to it

Sets the mic's own hardware mute (USB Audio SET_CUR mute on feature
unit 6, via IDeviceTopology/IAudioMute) when it appears, and follows its
05 10 xx HID mute reports to keep the Windows endpoint mute in step.
Includes build/install/uninstall scripts and a read-only HID listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-11-01 23:10:45 +01:00
commit 3128ccd207
7 files changed
+614

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
bin/
*.pcap
*.pcapng
+87
View File
@@ -0,0 +1,87 @@
# quadcast-mute-sync
Makes a **HyperX QuadCast S** start **muted on the mic itself** (light off) every time you log in to Windows, and keeps the **Windows microphone mute in step** with the mic's tap-to-mute button.
HyperX NGENUITY has no "start muted" option, and the QuadCast S always powers up live. This small background app fixes both.
## What it does
| When | Mic (hardware) | Windows mic mute |
|---|---|---|
| You log in / the mic is plugged in | muted | on |
| You tap the mic to unmute | live | off |
| You tap the mic to mute | muted | on |
The sync is one-way, from the mic to Windows. Changing mute in Windows doesn't change the mic.
## Install
Requirements: Windows 10/11, a QuadCast S. No admin rights, no extra runtimes (it builds with the C# compiler that ships with Windows' .NET Framework 4).
```powershell
powershell -ExecutionPolicy Bypass -File .\install.ps1
```
This builds `bin\QuadCastMuteSync.exe`, copies it to `%USERPROFILE%\QuadCastMuteSync`, registers a per-user logon task named **QuadCastMuteSync**, and starts it right away. Pass `-InstallDir <folder>` to install somewhere else.
Uninstall:
```powershell
powershell -ExecutionPolicy Bypass -File .\uninstall.ps1
```
## Command line
```
QuadCastMuteSync.exe run # background mode (what the logon task runs)
QuadCastMuteSync.exe mute # mute mic + Windows now
QuadCastMuteSync.exe unmute # unmute mic + Windows now
QuadCastMuteSync.exe status # show Windows mute and the KS mute nodes
```
`run` writes a short log to `log.txt` next to the exe. It waits for the mic if it isn't connected yet, and mutes it again whenever it is reconnected, since the mic powers up live.
## How it works
Found by capturing the NGENUITY software with USBPcap + Wireshark while toggling mute.
The QuadCast S shows up as two USB devices:
| Device | Role |
|---|---|
| `03F0:0294` | USB Audio device (the microphone) |
| `03F0:028C` | Controller: RGB, plus a vendor HID collection that reports mute |
**Muting the mic.** NGENUITY sends a standard USB Audio Class request to the audio device:
```
bmRequestType 0x21 bRequest 0x01 (SET_CUR)
wValue 0x0100 (MUTE_CONTROL, channel 0) wIndex 0x0600 (feature unit 6, interface 0)
data 01 = mute, 00 = unmute
```
The mic's firmware treats this like a tap, so the light changes too. Windows' `usbaudio` driver exposes that feature unit as a KS *Mute* node (`.../00020006`), so the app sets it through Core Audio `IDeviceTopology` → `IAudioMute`. No driver, WinUSB or admin rights needed. The Windows endpoint mute (`IAudioEndpointVolume`) is a separate software mute, which is why Windows mute and the mic button normally don't follow each other.
**Reading the mic's state.** Every mute change, from a tap or from software, makes the controller send an 8-byte input report on its vendor collection (usage page `0xFFFF`):
```
05 10 01 ... mic is live
05 10 00 ... mic is muted
```
The app blocks on reads from that collection and sets the Windows endpoint mute to match.
## Limitations
- Only tested on the QuadCast S (`03F0:0294` / `03F0:028C`). The QuadCast 2 uses a different protocol.
- The KS node id `/00020006` is what Windows assigned on the test machine. If `status` lists different ids for you, pass the right suffix: `QuadCastMuteSync.exe run /000200xx`.
- If the mic shows up under a name without "QuadCast", the audio endpoint won't be found.
- After sleep/resume the mic usually keeps its state. If it comes back live, it's because the device re-enumerated, and the app mutes it again.
## Tools
`tools/hidlisten.ps1` is a read-only listener that prints every input report from HyperX (`VID 03F0`) HID interfaces. It's useful for checking the mute reports on another unit or model.
```powershell
powershell -ExecutionPolicy Bypass -File .\tools\hidlisten.ps1 -Seconds 30
```
+9
View File
@@ -0,0 +1,9 @@
# Builds bin\QuadCastMuteSync.exe with the C# compiler that ships with Windows (.NET Framework 4).
$ErrorActionPreference = 'Stop'
$csc = Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
if (-not (Test-Path $csc)) { throw "csc.exe not found at $csc" }
$bin = Join-Path $PSScriptRoot 'bin'
New-Item -ItemType Directory -Force $bin | Out-Null
& $csc /nologo /target:winexe /optimize+ /platform:anycpu "/out:$bin\QuadCastMuteSync.exe" "$PSScriptRoot\src\QuadCastMuteSync.cs"
if ($LASTEXITCODE -ne 0) { throw "build failed" }
Write-Host "Built $bin\QuadCastMuteSync.exe"
+26
View File
@@ -0,0 +1,26 @@
# Builds QuadCastMuteSync, copies it to the install folder and runs it at every logon.
# No admin rights needed: the task runs as the current user.
# The default folder is outside AppData on purpose: when run from an MSIX-packaged app
# (such as some terminals), writes to AppData are redirected and Task Scheduler can't see them.
param([string]$InstallDir = (Join-Path $env:USERPROFILE 'QuadCastMuteSync'))
$ErrorActionPreference = 'Stop'
$TaskName = 'QuadCastMuteSync'
$dest = $InstallDir
$exe = Join-Path $dest 'QuadCastMuteSync.exe'
& (Join-Path $PSScriptRoot 'build.ps1')
Get-Process QuadCastMuteSync -ErrorAction SilentlyContinue | Stop-Process -Force
New-Item -ItemType Directory -Force $dest | Out-Null
Copy-Item (Join-Path $PSScriptRoot 'bin\QuadCastMuteSync.exe') $exe -Force
$action = New-ScheduledTaskAction -Execute $exe -Argument 'run'
$trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew `
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings `
-Description 'Mutes the HyperX QuadCast S at logon and keeps Windows mic mute in sync with it.' -Force | Out-Null
Start-ScheduledTask -TaskName $TaskName
Write-Host "Installed to $dest and registered logon task '$TaskName' (started now)."
+387
View File
@@ -0,0 +1,387 @@
// QuadCastMuteSync - mutes the HyperX QuadCast S itself at logon and keeps the
// Windows microphone mute in step with the mic's own tap-to-mute state.
//
// How it works (found by sniffing the HyperX software with USBPcap):
// * Hardware mute is the standard USB Audio Class request SET_CUR / MUTE_CONTROL
// on feature unit 6 of the audio device (03F0:0294). Windows exposes it as a
// KS "Mute" node, reachable through IDeviceTopology -> IAudioMute.
// * Every mute change (tap or software) is reported by the mic's controller
// (03F0:028C, vendor HID collection, page 0xFFFF) as input report 05 10 xx,
// where xx = 01 live, 00 muted.
//
// Built for .NET Framework 4 (csc.exe that ships with Windows), so C# 5 only.
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
namespace QuadCastMuteSync
{
static class Program
{
// KS node id suffix of the feature-unit mute the HyperX software uses.
const string DefaultNode = "/00020006";
const string EndpointMatch = "QuadCast";
static string logPath;
[DllImport("kernel32.dll")] static extern bool AttachConsole(int pid);
[MTAThread]
static int Main(string[] args)
{
string cmd = args.Length > 0 ? args[0].ToLowerInvariant() : "run";
string node = args.Length > 1 ? args[1] : DefaultNode;
logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt");
if (cmd != "run") AttachConsole(-1); // winexe: print to the calling console
switch (cmd)
{
case "run": return Run(node);
case "status": Console.WriteLine(); Console.WriteLine(Audio.Status()); return 0;
case "mute":
case "unmute":
bool m = cmd == "mute";
bool ok = Audio.SetHardwareMute(m, node);
if (ok) Audio.SetWindowsMute(m);
Console.WriteLine(); Console.WriteLine(ok ? cmd + "d (mic and Windows)" : "QuadCast mute control not found");
return ok ? 0 : 1;
default:
Console.WriteLine(); Console.WriteLine("usage: QuadCastMuteSync.exe [run|status|mute|unmute] [nodeSuffix]");
return 2;
}
}
static void Log(string s)
{
try { File.AppendAllText(logPath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ") + s + Environment.NewLine); } catch { }
}
static int Run(string node)
{
bool created;
var single = new Mutex(true, @"Local\QuadCastMuteSync", out created);
if (!created) return 0;
try { File.WriteAllText(logPath, ""); } catch { }
Log("started");
bool waitingLogged = false;
while (true)
{
Hid.Device hid = Hid.FindStateCollection();
if (hid == null || !Audio.EndpointPresent(EndpointMatch))
{
if (!waitingLogged) { Log("waiting for QuadCast S"); waitingLogged = true; }
Thread.Sleep(2000);
continue;
}
waitingLogged = false;
Thread.Sleep(1500); // let the audio stack finish setting up the endpoint
// The mic always powers up live, so mute it whenever it (re)appears.
bool ok = false;
for (int i = 0; i < 5 && !ok; i++) { ok = Audio.SetHardwareMute(true, node); if (!ok) Thread.Sleep(1000); }
Audio.SetWindowsMute(true);
Log("QuadCast S found: hardware mute " + (ok ? "set" : "FAILED") + ", Windows mute on");
try
{
using (var stream = hid.OpenRead())
{
var buf = new byte[hid.InputLength];
while (true)
{
int n = stream.Read(buf, 0, buf.Length);
if (n == 0) throw new IOException("read returned 0");
if (n >= 3 && buf[0] == 0x05 && buf[1] == 0x10)
{
bool muted = buf[2] == 0x00;
Audio.SetWindowsMute(muted);
Log("mic " + (muted ? "muted" : "live") + " -> Windows mute " + (muted ? "on" : "off"));
}
}
}
}
catch (Exception e) { Log("lost QuadCast S (" + e.Message + ")"); }
Thread.Sleep(2000);
}
}
}
// ---------------------------------------------------------------- HID
static class Hid
{
public class Device
{
public string Path;
public int InputLength;
public FileStream OpenRead()
{
var h = CreateFile(Path, GENERIC_READ, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
if (h.IsInvalid) throw new IOException("open failed, error " + Marshal.GetLastWin32Error());
return new FileStream(h, FileAccess.Read, InputLength, false);
}
}
const uint GENERIC_READ = 0x80000000;
[StructLayout(LayoutKind.Sequential)] struct SP_DID { public int cbSize; public Guid g; public int flags; public IntPtr r; }
[StructLayout(LayoutKind.Sequential)]
struct HIDP_CAPS
{
public ushort Usage, UsagePage, InputLen, OutputLen, FeatureLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 27)] public ushort[] Rest;
}
[DllImport("hid.dll")] static extern void HidD_GetHidGuid(out Guid g);
[DllImport("hid.dll")] static extern bool HidD_GetPreparsedData(SafeFileHandle h, out IntPtr p);
[DllImport("hid.dll")] static extern bool HidD_FreePreparsedData(IntPtr p);
[DllImport("hid.dll")] static extern int HidP_GetCaps(IntPtr p, out HIDP_CAPS c);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] static extern IntPtr SetupDiGetClassDevs(ref Guid g, string e, IntPtr w, int f);
[DllImport("setupapi.dll")] static extern bool SetupDiDestroyDeviceInfoList(IntPtr s);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] static extern bool SetupDiEnumDeviceInterfaces(IntPtr s, IntPtr d, ref Guid g, int i, ref SP_DID data);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr s, ref SP_DID data, IntPtr detail, int size, out int req, IntPtr di);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern SafeFileHandle CreateFile(string n, uint a, uint sh, IntPtr sec, uint disp, uint flags, IntPtr t);
// The QuadCast S controller (03F0:028C) collection that sends the 05 10 xx mute reports.
public static Device FindStateCollection()
{
Guid g; HidD_GetHidGuid(out g);
IntPtr set = SetupDiGetClassDevs(ref g, null, IntPtr.Zero, 0x12); // PRESENT | DEVICEINTERFACE
try
{
for (int i = 0; ; i++)
{
var d = new SP_DID(); d.cbSize = Marshal.SizeOf(d);
if (!SetupDiEnumDeviceInterfaces(set, IntPtr.Zero, ref g, i, ref d)) return null;
int req; SetupDiGetDeviceInterfaceDetail(set, ref d, IntPtr.Zero, 0, out req, IntPtr.Zero);
IntPtr buf = Marshal.AllocHGlobal(req);
string path = null;
try
{
Marshal.WriteInt32(buf, IntPtr.Size == 8 ? 8 : 6);
if (SetupDiGetDeviceInterfaceDetail(set, ref d, buf, req, out req, IntPtr.Zero))
path = Marshal.PtrToStringUni(new IntPtr(buf.ToInt64() + 4));
}
finally { Marshal.FreeHGlobal(buf); }
if (path == null || path.ToLowerInvariant().IndexOf("vid_03f0&pid_028c") < 0) continue;
using (var h = CreateFile(path, 0, 3, IntPtr.Zero, 3, 0, IntPtr.Zero))
{
if (h.IsInvalid) continue;
IntPtr pp; HIDP_CAPS c;
if (!HidD_GetPreparsedData(h, out pp)) continue;
HidP_GetCaps(pp, out c); HidD_FreePreparsedData(pp);
if (c.UsagePage == 0xFFFF && c.InputLen == 8)
return new Device { Path = path, InputLength = c.InputLen };
}
}
}
finally { SetupDiDestroyDeviceInfoList(set); }
}
}
// ---------------------------------------------------------------- Core Audio
static class Audio
{
public static bool EndpointPresent(string match) { return FindEndpoint(match) != null; }
static IMMDevice FindEndpoint(string match)
{
var en = (IMMDeviceEnumerator)new MMDeviceEnumeratorCo();
IMMDeviceCollection col;
if (en.EnumAudioEndpoints(1, 1, out col) != 0) return null; // eCapture, DEVICE_STATE_ACTIVE
int n; col.GetCount(out n);
for (int i = 0; i < n; i++)
{
IMMDevice d; col.Item(i, out d);
if (Name(d).IndexOf(match, StringComparison.OrdinalIgnoreCase) >= 0) return d;
}
return null;
}
static string Name(IMMDevice d)
{
IPropertyStore s;
if (d.OpenPropertyStore(0, out s) != 0) return "";
var k = new PROPERTYKEY { fmtid = new Guid("A45C254E-DF1C-4EFD-8020-67D146A850E0"), pid = 14 }; // PKEY_Device_FriendlyName
PROPVARIANT v; s.GetValue(ref k, out v);
return v.vt == 31 ? Marshal.PtrToStringUni(v.p) : "";
}
static IAudioEndpointVolume EndpointVolume()
{
var d = FindEndpoint("QuadCast");
if (d == null) return null;
Guid iid = typeof(IAudioEndpointVolume).GUID; object o;
return d.Activate(ref iid, 23, IntPtr.Zero, out o) == 0 ? (IAudioEndpointVolume)o : null;
}
public static void SetWindowsMute(bool mute)
{
var v = EndpointVolume();
if (v == null) return;
int cur; v.GetMute(out cur);
if ((cur != 0) != mute) v.SetMute(mute ? 1 : 0, IntPtr.Zero); // only on change, avoids feedback loops
}
// Every KS mute node on the QuadCast capture path, keyed by global id.
static List<KeyValuePair<string, IAudioMute>> MuteNodes()
{
var result = new List<KeyValuePair<string, IAudioMute>>();
var d = FindEndpoint("QuadCast");
if (d == null) return result;
Guid iidTopo = typeof(IDeviceTopology).GUID; object o;
if (d.Activate(ref iidTopo, 23, IntPtr.Zero, out o) != 0) return result;
IConnector c, other;
if (((IDeviceTopology)o).GetConnector(0, out c) != 0 || c.GetConnectedTo(out other) != 0) return result;
var queue = new Queue<IPart>(); var seen = new HashSet<string>();
queue.Enqueue((IPart)other);
while (queue.Count > 0)
{
IPart p = queue.Dequeue(); string gid; p.GetGlobalId(out gid);
if (!seen.Add(gid)) continue;
Guid iidMute = typeof(IAudioMute).GUID; object m;
if (p.Activate(23, ref iidMute, out m) == 0 && m != null)
result.Add(new KeyValuePair<string, IAudioMute>(gid, (IAudioMute)m));
IPartsList l; int k;
if (p.EnumPartsIncoming(out l) == 0 && l != null) { l.GetCount(out k); for (int j = 0; j < k; j++) { IPart q; l.GetPart(j, out q); queue.Enqueue(q); } }
if (p.EnumPartsOutgoing(out l) == 0 && l != null) { l.GetCount(out k); for (int j = 0; j < k; j++) { IPart q; l.GetPart(j, out q); queue.Enqueue(q); } }
}
return result;
}
public static bool SetHardwareMute(bool mute, string nodeSuffix)
{
foreach (var n in MuteNodes())
if (n.Key.EndsWith(nodeSuffix, StringComparison.OrdinalIgnoreCase))
return n.Value.SetMute(mute ? 1 : 0, IntPtr.Zero) == 0;
return false;
}
public static string Status()
{
var sb = new StringBuilder();
var v = EndpointVolume();
if (v == null) return "QuadCast capture endpoint not found";
int wm; v.GetMute(out wm);
sb.AppendLine("Windows mute: " + (wm != 0 ? "on" : "off"));
foreach (var n in MuteNodes())
{
int m; n.Value.GetMute(out m);
sb.AppendLine("KS mute node " + n.Key.Substring(n.Key.LastIndexOf('/')) + ": " + (m != 0 ? "muted" : "live"));
}
sb.Append("(KS nodes show the last mute set by software; taps on the mic are not reflected there)");
return sb.ToString();
}
}
// ---------------------------------------------------------------- COM interop
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorCo { }
[ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator
{
[PreserveSig] int EnumAudioEndpoints(int dataFlow, int stateMask, out IMMDeviceCollection devices);
}
[ComImport, Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceCollection
{
[PreserveSig] int GetCount(out int count);
[PreserveSig] int Item(int i, out IMMDevice device);
}
[ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice
{
[PreserveSig] int Activate(ref Guid iid, int ctx, IntPtr p, [MarshalAs(UnmanagedType.IUnknown)] out object o);
[PreserveSig] int OpenPropertyStore(int access, out IPropertyStore store);
}
[ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPropertyStore
{
[PreserveSig] int GetCount(out int c);
[PreserveSig] int GetAt(int i, out PROPERTYKEY k);
[PreserveSig] int GetValue(ref PROPERTYKEY k, out PROPVARIANT v);
}
[StructLayout(LayoutKind.Sequential)] struct PROPERTYKEY { public Guid fmtid; public int pid; }
[StructLayout(LayoutKind.Sequential)] struct PROPVARIANT { public short vt; short r1, r2, r3; public IntPtr p; IntPtr p2; }
[ComImport, Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume
{
// Vtable order matters; only SetMute/GetMute are called.
[PreserveSig] int RegisterControlChangeNotify(IntPtr n);
[PreserveSig] int UnregisterControlChangeNotify(IntPtr n);
[PreserveSig] int GetChannelCount(out int c);
[PreserveSig] int SetMasterVolumeLevel(float db, IntPtr ctx);
[PreserveSig] int SetMasterVolumeLevelScalar(float level, IntPtr ctx);
[PreserveSig] int GetMasterVolumeLevel(out float db);
[PreserveSig] int GetMasterVolumeLevelScalar(out float level);
[PreserveSig] int SetChannelVolumeLevel(int ch, float db, IntPtr ctx);
[PreserveSig] int SetChannelVolumeLevelScalar(int ch, float level, IntPtr ctx);
[PreserveSig] int GetChannelVolumeLevel(int ch, out float db);
[PreserveSig] int GetChannelVolumeLevelScalar(int ch, out float level);
[PreserveSig] int SetMute(int mute, IntPtr ctx);
[PreserveSig] int GetMute(out int mute);
}
[ComImport, Guid("2A07407E-6497-4A18-9787-32F79BD0D98F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IDeviceTopology
{
[PreserveSig] int GetConnectorCount(out int c);
[PreserveSig] int GetConnector(int i, out IConnector c);
}
[ComImport, Guid("9C2C4058-23F5-41DE-877A-DF3AF236A09E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IConnector
{
[PreserveSig] int GetType(out int t);
[PreserveSig] int GetDataFlow(out int f);
[PreserveSig] int ConnectTo(IConnector c);
[PreserveSig] int Disconnect();
[PreserveSig] int IsConnected(out int b);
[PreserveSig] int GetConnectedTo(out IConnector c);
}
[ComImport, Guid("AE2DE0E4-5BCA-4F2D-AA46-5D13F8FDB3A9"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPart
{
[PreserveSig] int GetName([MarshalAs(UnmanagedType.LPWStr)] out string n);
[PreserveSig] int GetLocalId(out int id);
[PreserveSig] int GetGlobalId([MarshalAs(UnmanagedType.LPWStr)] out string id);
[PreserveSig] int GetPartType(out int t);
[PreserveSig] int GetSubType(out Guid g);
[PreserveSig] int GetControlInterfaceCount(out int c);
[PreserveSig] int GetControlInterface(int i, out IntPtr c);
[PreserveSig] int EnumPartsIncoming(out IPartsList l);
[PreserveSig] int EnumPartsOutgoing(out IPartsList l);
[PreserveSig] int GetTopologyObject(out IntPtr t);
[PreserveSig] int Activate(int ctx, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object o);
}
[ComImport, Guid("6DAA848C-5EB0-45CC-AEA5-998A2CDA1FFB"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPartsList
{
[PreserveSig] int GetCount(out int c);
[PreserveSig] int GetPart(int i, out IPart p);
}
[ComImport, Guid("DF45AEEA-B74A-4B6B-AFAD-2366B6AA012E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioMute
{
[PreserveSig] int SetMute(int mute, IntPtr ctx);
[PreserveSig] int GetMute(out int mute);
}
}
+94
View File
@@ -0,0 +1,94 @@
param([int]$Seconds = 10)
# Read-only listener for HyperX HID interfaces: never writes output or feature reports.
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
public static class HidListen {
[DllImport("hid.dll")] static extern void HidD_GetHidGuid(out Guid g);
[DllImport("hid.dll")] static extern bool HidD_GetPreparsedData(SafeFileHandle h, out IntPtr p);
[DllImport("hid.dll")] static extern bool HidD_FreePreparsedData(IntPtr p);
[DllImport("hid.dll")] static extern int HidP_GetCaps(IntPtr p, out HIDP_CAPS c);
[DllImport("hid.dll")] static extern bool HidD_GetInputReport(SafeFileHandle h, byte[] b, int len);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] static extern IntPtr SetupDiGetClassDevs(ref Guid g, string e, IntPtr w, int f);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] static extern bool SetupDiEnumDeviceInterfaces(IntPtr s, IntPtr d, ref Guid g, int i, ref SP_DID data);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr s, ref SP_DID data, IntPtr detail, int size, out int req, IntPtr di);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern SafeFileHandle CreateFile(string n, uint a, uint sh, IntPtr sec, uint disp, uint flags, IntPtr t);
[StructLayout(LayoutKind.Sequential)] struct SP_DID { public int cbSize; public Guid g; public int flags; public IntPtr r; }
[StructLayout(LayoutKind.Sequential)] public struct HIDP_CAPS {
public ushort Usage, UsagePage, InputLen, OutputLen, FeatureLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] public ushort[] Reserved;
public ushort a, b, c, d, e, f, g, h, i, j;
}
static object lk = new object();
static void Log(string s) { lock (lk) { Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " " + s); } }
static string Hex(byte[] b, int n) { var sb = new StringBuilder(); for (int i = 0; i < n; i++) sb.Append(b[i].ToString("X2")).Append(' '); return sb.ToString().TrimEnd(); }
static List<string> Paths(string filter) {
var res = new List<string>(); Guid g; HidD_GetHidGuid(out g);
IntPtr set = SetupDiGetClassDevs(ref g, null, IntPtr.Zero, 0x12);
for (int i = 0; ; i++) {
var d = new SP_DID(); d.cbSize = Marshal.SizeOf(d);
if (!SetupDiEnumDeviceInterfaces(set, IntPtr.Zero, ref g, i, ref d)) break;
int req; SetupDiGetDeviceInterfaceDetail(set, ref d, IntPtr.Zero, 0, out req, IntPtr.Zero);
IntPtr buf = Marshal.AllocHGlobal(req);
Marshal.WriteInt32(buf, IntPtr.Size == 8 ? 8 : 6);
if (SetupDiGetDeviceInterfaceDetail(set, ref d, buf, req, out req, IntPtr.Zero)) {
string p = Marshal.PtrToStringUni(new IntPtr(buf.ToInt64() + 4));
if (p.ToLower().Contains(filter)) res.Add(p);
}
Marshal.FreeHGlobal(buf);
}
return res;
}
public static void Run(int seconds) {
DateTime end = DateTime.Now.AddSeconds(seconds);
var threads = new List<Thread>();
foreach (var p in Paths("vid_03f0")) {
// Access 0 (query only) first to read caps, then try read access.
var h = CreateFile(p, 0x80000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
bool canRead = !h.IsInvalid;
if (!canRead) h = CreateFile(p, 0, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
if (h.IsInvalid) { Log("OPEN FAIL " + p + " err=" + Marshal.GetLastWin32Error()); continue; }
IntPtr pp; HIDP_CAPS c = new HIDP_CAPS();
if (HidD_GetPreparsedData(h, out pp)) { HidP_GetCaps(pp, out c); HidD_FreePreparsedData(pp); }
string name = p.Substring(p.IndexOf("vid_"), 40);
Log(string.Format("DEV {0} page=0x{1:X4} usage=0x{2:X4} in={3} out={4} feat={5} read={6}", name, c.UsagePage, c.Usage, c.InputLen, c.OutputLen, c.FeatureLen, canRead));
if (!canRead || c.InputLen == 0) continue;
var hh = h; var cc = c; var nm = name;
// Thread 1: interrupt input reports.
var t = new Thread(() => {
var fs = new FileStream(hh, FileAccess.Read, cc.InputLen, false);
var b = new byte[cc.InputLen];
while (DateTime.Now < end) {
try { int n = fs.Read(b, 0, b.Length); if (n > 0) Log("IN " + nm + " : " + Hex(b, n)); }
catch (Exception ex) { Log("READ ERR " + nm + " " + ex.Message); break; }
}
});
t.IsBackground = true; t.Start(); threads.Add(t);
// Thread 2: poll GET_REPORT(Input) on the control pipe, log only changes.
var h2 = CreateFile(p, 0x80000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
var t2 = new Thread(() => {
string last = null;
while (DateTime.Now < end) {
var b = new byte[cc.InputLen];
if (HidD_GetInputReport(h2, b, b.Length)) { string s = Hex(b, b.Length); if (s != last) { Log("GET " + nm + " : " + s); last = s; } }
Thread.Sleep(100);
}
});
t2.IsBackground = true; t2.Start(); threads.Add(t2);
}
while (DateTime.Now < end) Thread.Sleep(200);
Log("DONE");
}
}
'@
[HidListen]::Run($Seconds)
+8
View File
@@ -0,0 +1,8 @@
# Stops QuadCastMuteSync, removes its logon task and its install folder.
param([string]$InstallDir = (Join-Path $env:USERPROFILE 'QuadCastMuteSync'))
$TaskName = 'QuadCastMuteSync'
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
Get-Process QuadCastMuteSync -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Milliseconds 500
Remove-Item $InstallDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Removed task '$TaskName' and $InstallDir."