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

+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)