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:
commit
3128ccd207
7 files changed
+614
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
bin/
|
||||||
|
*.pcap
|
||||||
|
*.pcapng
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
@@ -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)."
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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."
|
||||||
Reference in new issue
Block a user