feat(windows): system-wide push-to-talk via Raw Input

PTT was focus-scoped (WinForms KeyDown/KeyUp) so it died the moment the
window lost focus. Add an optional system-wide path using the Raw Input
API (RegisterRawInputDevices + WM_INPUT with RIDEV_INPUTSINK) instead of
a WH_KEYBOARD_LL low-level hook -- the latter is the keylogger pattern AV
heuristics flag, which is worse for our unsigned MinGW binary. Raw Input
involves no DLL injection or global hook and passes keys through.

- New VoiceCat.App/Native/RawInput.cs: P/Invoke + structs; register the
  keyboard sink, decode WM_INPUT to vkey/up-down, GetAsyncKeyState helper.
- MainForm overrides OnHandleCreated/OnHandleDestroyed/WndProc to manage
  the sink and route WM_INPUT to PTT; gates the focus-scoped KeyDown/KeyUp
  off when system-wide is on; makes the Deactivate force-release
  conditional; adds a GetAsyncKeyState watchdog on the pump timer so a
  missed key-up (RDP/lock-screen) cannot leave PTT stuck.
- VoiceSettings.SystemWidePtt (default ON) + system-wide checkbox in the
  Audio settings PTT section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 13:14:01 +02:00
parent 65736df464
commit 2baefddbe4
5 changed files with 310 additions and 18 deletions

View File

@@ -34,6 +34,13 @@ public sealed class VoiceSettings
/// <summary>Push-to-talk key, stored as the integer value of System.Windows.Forms.Keys.</summary>
public int PttKey { get; set; } = (int)Keys.F8;
/// <summary>Make push-to-talk work system-wide — i.e. while another app is focused. When on,
/// MainForm registers a background Raw Input keyboard device (WM_INPUT + RIDEV_INPUTSINK) so the
/// PTT key is observed even when VoiceCat is not in the foreground; when off, PTT is focus-scoped
/// (only fires while the VoiceCat window has focus). Raw Input is used instead of a low-level
/// keyboard hook to avoid antivirus keylogger heuristics.</summary>
public bool SystemWidePtt { get; set; } = true;
/// <summary>Saved device ID from the last session; null means use the system default.</summary>
public string? InputDeviceId { get; set; } = null;