Files
Robovoice/Robovoice.App/PttHotkey.cs
T
mute 9b0dc11ee6 v0.3: PTT hotkey capture, mouse support, model params
- Replace PTT key dropdown with live key-capture textbox
- Use low-level keyboard hook for capture (distinguishes Left/Right Ctrl)
- Support mouse buttons as PTT (Left/Right/Middle/X1/X2 via WH_MOUSE_LL)
- Filter mouse clicks from capture, allow Left/Right Ctrl
- Add Noise/Speed/NoiseW sliders with live labels, reinit on MouseUp
- Stream audio via BufferedWaveProvider (no temp WAV files)
- Add text input field with Speak button
- Fix sentence terminator, UTF-8 marshaling, voice catalogue JSON
2026-08-10 12:07:34 +00:00

187 lines
6.1 KiB
C#

using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace Robovoice.App;
internal sealed class PttHotkey : IDisposable
{
private const int WH_KEYBOARD_LL = 13;
private const int WH_MOUSE_LL = 14;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private const int WM_SYSKEYDOWN = 0x0104;
private const int WM_SYSKEYUP = 0x0105;
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_LBUTTONUP = 0x0202;
private const int WM_RBUTTONDOWN = 0x0204;
private const int WM_RBUTTONUP = 0x0205;
private const int WM_MBUTTONDOWN = 0x0207;
private const int WM_MBUTTONUP = 0x0208;
private const int WM_XBUTTONDOWN = 0x020B;
private const int WM_XBUTTONUP = 0x020C;
private readonly LowLevelKeyboardProc _kbProc;
private readonly LowLevelMouseProc _msProc;
private IntPtr _kbHook = IntPtr.Zero;
private IntPtr _msHook = IntPtr.Zero;
private bool _isDown;
private bool _disposed;
public Keys Key { get; set; } = Keys.F8;
public static bool IsMouseButton(Keys key) =>
key is Keys.LButton or Keys.RButton or Keys.MButton or Keys.XButton1 or Keys.XButton2;
public event EventHandler? Pressed;
public event EventHandler? Released;
public event Action<Keys>? CaptureKeyPressed;
public void InstallCaptureHook()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_kbHook != IntPtr.Zero) return;
IntPtr hModule = GetModuleHandle(null);
_kbHook = SetWindowsHookEx(WH_KEYBOARD_LL, _kbProc, hModule, 0);
}
public PttHotkey()
{
_kbProc = KeyboardCallback;
_msProc = MouseCallback;
}
public void Install()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_kbHook != IntPtr.Zero || _msHook != IntPtr.Zero) return;
IntPtr hModule = GetModuleHandle(null);
if (IsMouseButton(Key))
_msHook = SetWindowsHookEx(WH_MOUSE_LL, _msProc, hModule, 0);
else
_kbHook = SetWindowsHookEx(WH_KEYBOARD_LL, _kbProc, hModule, 0);
}
public void Uninstall()
{
if (_kbHook != IntPtr.Zero)
{
UnhookWindowsHookEx(_kbHook);
_kbHook = IntPtr.Zero;
}
if (_msHook != IntPtr.Zero)
{
UnhookWindowsHookEx(_msHook);
_msHook = IntPtr.Zero;
}
_isDown = false;
}
private IntPtr KeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
int vkCode = Marshal.ReadInt32(lParam);
Keys key = (Keys)vkCode;
bool isDown = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN;
bool isUp = wParam == WM_KEYUP || wParam == WM_SYSKEYUP;
if (CaptureKeyPressed is not null && isDown)
{
CaptureKeyPressed.Invoke(key);
}
else if (key == Key)
{
if (isDown && !_isDown)
{
_isDown = true;
Pressed?.Invoke(this, EventArgs.Empty);
}
else if (isUp && _isDown)
{
_isDown = false;
Released?.Invoke(this, EventArgs.Empty);
}
}
}
return CallNextHookEx(_kbHook, nCode, wParam, lParam);
}
private IntPtr MouseCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
Keys? button = null;
switch ((int)wParam)
{
case WM_LBUTTONDOWN: button = Keys.LButton; break;
case WM_LBUTTONUP: button = Keys.LButton; break;
case WM_RBUTTONDOWN: button = Keys.RButton; break;
case WM_RBUTTONUP: button = Keys.RButton; break;
case WM_MBUTTONDOWN: button = Keys.MButton; break;
case WM_MBUTTONUP: button = Keys.MButton; break;
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
{
int xButton = Marshal.ReadInt32(lParam + 8) >> 16;
button = xButton == 1 ? Keys.XButton1 : Keys.XButton2;
break;
}
}
if (button is Keys btn && btn == Key)
{
bool isDown = wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN
|| wParam == WM_MBUTTONDOWN || wParam == WM_XBUTTONDOWN;
bool isUp = wParam == WM_LBUTTONUP || wParam == WM_RBUTTONUP
|| wParam == WM_MBUTTONUP || wParam == WM_XBUTTONUP;
if (isDown && !_isDown)
{
_isDown = true;
Pressed?.Invoke(this, EventArgs.Empty);
}
else if (isUp && _isDown)
{
_isDown = false;
Released?.Invoke(this, EventArgs.Empty);
}
}
}
return CallNextHookEx(_msHook, nCode, wParam, lParam);
}
public void Dispose()
{
if (_disposed) return;
Uninstall();
_disposed = true;
}
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr GetModuleHandle(string? lpModuleName);
}