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
This commit is contained in:
Generated
+8
-7
@@ -6,7 +6,7 @@ partial class MainForm
|
||||
private System.ComponentModel.IContainer? components = null;
|
||||
|
||||
private Label lblPttKey = null!;
|
||||
private ComboBox cmbPttKey = null!;
|
||||
private TextBox txtPttKey = null!;
|
||||
private Label lblVoice = null!;
|
||||
private ComboBox cmbVoice = null!;
|
||||
private Button btnTestVoice = null!;
|
||||
@@ -45,7 +45,7 @@ partial class MainForm
|
||||
components = new System.ComponentModel.Container();
|
||||
|
||||
lblPttKey = new Label();
|
||||
cmbPttKey = new ComboBox();
|
||||
txtPttKey = new TextBox();
|
||||
lblVoice = new Label();
|
||||
cmbVoice = new ComboBox();
|
||||
btnTestVoice = new Button();
|
||||
@@ -80,10 +80,11 @@ partial class MainForm
|
||||
lblPttKey.Size = new Size(60, 23);
|
||||
lblPttKey.TextAlign = ContentAlignment.MiddleLeft;
|
||||
|
||||
// cmbPttKey
|
||||
cmbPttKey.Location = new Point(75, 12);
|
||||
cmbPttKey.Size = new Size(80, 23);
|
||||
cmbPttKey.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
// txtPttKey
|
||||
txtPttKey.Location = new Point(75, 12);
|
||||
txtPttKey.Size = new Size(80, 23);
|
||||
txtPttKey.ReadOnly = true;
|
||||
txtPttKey.TextAlign = HorizontalAlignment.Center;
|
||||
|
||||
// lblVoice
|
||||
lblVoice.Text = "Voice:";
|
||||
@@ -249,7 +250,7 @@ partial class MainForm
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(824, 509);
|
||||
Controls.Add(lblPttKey);
|
||||
Controls.Add(cmbPttKey);
|
||||
Controls.Add(txtPttKey);
|
||||
Controls.Add(lblVoice);
|
||||
Controls.Add(cmbVoice);
|
||||
Controls.Add(btnTestVoice);
|
||||
|
||||
+84
-14
@@ -31,11 +31,11 @@ internal sealed partial class MainForm : Form
|
||||
|
||||
private async void OnLoad(object? sender, EventArgs e)
|
||||
{
|
||||
PopulatePttKeys();
|
||||
PopulateVoices();
|
||||
PopulateOutputDevices();
|
||||
|
||||
cmbPttKey.SelectedItem = Keys.F8;
|
||||
_pttKey = Keys.F8;
|
||||
txtPttKey.Text = KeyToDisplayString(_pttKey);
|
||||
if (cmbVoice.Items.Count > 0)
|
||||
cmbVoice.SelectedIndex = 0;
|
||||
|
||||
@@ -45,7 +45,8 @@ internal sealed partial class MainForm : Form
|
||||
btnTestVoice.Click += OnTestVoice;
|
||||
btnManageVoices.Click += OnManageVoices;
|
||||
btnClearLog.Click += (_, _) => txtLog.Clear();
|
||||
cmbPttKey.SelectedIndexChanged += OnPttKeyChanged;
|
||||
txtPttKey.Enter += OnPttKeyFocus;
|
||||
txtPttKey.KeyDown += OnPttKeyDown;
|
||||
cmbOutput.SelectedIndexChanged += OnOutputChanged;
|
||||
cmbVoice.SelectedIndexChanged += OnVoiceChanged;
|
||||
|
||||
@@ -61,14 +62,88 @@ internal sealed partial class MainForm : Form
|
||||
await InitializeEngineAsync();
|
||||
}
|
||||
|
||||
private void PopulatePttKeys()
|
||||
private Keys _pttKey = Keys.F8;
|
||||
private bool _capturingPttKey;
|
||||
private PttHotkey? _captureHook;
|
||||
|
||||
private void OnPttKeyFocus(object? sender, EventArgs e)
|
||||
{
|
||||
var keys = new[]
|
||||
_capturingPttKey = true;
|
||||
txtPttKey.Text = "Press a key...";
|
||||
txtPttKey.BackColor = Color.LightYellow;
|
||||
|
||||
_captureHook?.Dispose();
|
||||
_captureHook = new PttHotkey();
|
||||
_captureHook.CaptureKeyPressed += OnCaptureKey;
|
||||
_captureHook.InstallCaptureHook();
|
||||
}
|
||||
|
||||
private void OnCaptureKey(Keys key)
|
||||
{
|
||||
Keys.F1, Keys.F2, Keys.F3, Keys.F4, Keys.F5, Keys.F6,
|
||||
Keys.F7, Keys.F8, Keys.F9, Keys.F10, Keys.F11, Keys.F12,
|
||||
if (!_capturingPttKey) return;
|
||||
|
||||
if (key == Keys.Escape)
|
||||
{
|
||||
CancelCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key is Keys.ShiftKey or Keys.Menu or Keys.LWin or Keys.RWin)
|
||||
return;
|
||||
|
||||
_pttKey = key;
|
||||
_capturingPttKey = false;
|
||||
txtPttKey.Text = KeyToDisplayString(_pttKey);
|
||||
txtPttKey.BackColor = SystemColors.Window;
|
||||
_captureHook?.Dispose();
|
||||
_captureHook = null;
|
||||
SetupHotkey();
|
||||
}
|
||||
|
||||
private void CancelCapture()
|
||||
{
|
||||
_capturingPttKey = false;
|
||||
txtPttKey.Text = KeyToDisplayString(_pttKey);
|
||||
txtPttKey.BackColor = SystemColors.Window;
|
||||
_captureHook?.Dispose();
|
||||
_captureHook = null;
|
||||
}
|
||||
|
||||
private void OnPttKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (_capturingPttKey && e.KeyCode == Keys.Escape)
|
||||
{
|
||||
CancelCapture();
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string KeyToDisplayString(Keys key)
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
>= Keys.F1 and <= Keys.F12 => key.ToString(),
|
||||
Keys.RControlKey => "Right Ctrl",
|
||||
Keys.LControlKey => "Left Ctrl",
|
||||
Keys.Space => "Space",
|
||||
Keys.LButton => "Mouse Left",
|
||||
Keys.RButton => "Mouse Right",
|
||||
Keys.MButton => "Mouse Middle",
|
||||
Keys.XButton1 => "Mouse X1",
|
||||
Keys.XButton2 => "Mouse X2",
|
||||
Keys.Oemtilde => "`",
|
||||
Keys.CapsLock => "CapsLock",
|
||||
Keys.NumLock => "NumLock",
|
||||
Keys.Scroll => "ScrollLock",
|
||||
Keys.Pause => "Pause",
|
||||
Keys.Insert => "Insert",
|
||||
Keys.Delete => "Delete",
|
||||
Keys.Home => "Home",
|
||||
Keys.End => "End",
|
||||
Keys.PageUp => "PageUp",
|
||||
Keys.PageDown => "PageDown",
|
||||
_ => key.ToString(),
|
||||
};
|
||||
cmbPttKey.Items.AddRange(keys.Cast<object>().ToArray());
|
||||
}
|
||||
|
||||
private void PopulateVoices()
|
||||
@@ -160,7 +235,7 @@ internal sealed partial class MainForm : Form
|
||||
private void SetupHotkey()
|
||||
{
|
||||
_pttHotkey?.Dispose();
|
||||
_pttHotkey = new PttHotkey { Key = (Keys)(cmbPttKey.SelectedItem ?? Keys.F8) };
|
||||
_pttHotkey = new PttHotkey { Key = _pttKey };
|
||||
_pttHotkey.Pressed += OnPttPressed;
|
||||
_pttHotkey.Released += OnPttReleased;
|
||||
_pttHotkey.Install();
|
||||
@@ -299,11 +374,6 @@ internal sealed partial class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPttKeyChanged(object? sender, EventArgs e)
|
||||
{
|
||||
SetupHotkey();
|
||||
}
|
||||
|
||||
private void OnOutputChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_orchestrator is not null)
|
||||
|
||||
+105
-13
@@ -6,46 +6,81 @@ 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 _proc;
|
||||
private IntPtr _hook = IntPtr.Zero;
|
||||
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()
|
||||
{
|
||||
_proc = HookCallback;
|
||||
_kbProc = KeyboardCallback;
|
||||
_msProc = MouseCallback;
|
||||
}
|
||||
|
||||
public void Install()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_hook != IntPtr.Zero) return;
|
||||
if (_kbHook != IntPtr.Zero || _msHook != IntPtr.Zero) return;
|
||||
|
||||
IntPtr hModule = GetModuleHandle(null);
|
||||
_hook = SetWindowsHookEx(WH_KEYBOARD_LL, _proc, hModule, 0);
|
||||
|
||||
if (IsMouseButton(Key))
|
||||
_msHook = SetWindowsHookEx(WH_MOUSE_LL, _msProc, hModule, 0);
|
||||
else
|
||||
_kbHook = SetWindowsHookEx(WH_KEYBOARD_LL, _kbProc, hModule, 0);
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
if (_hook != IntPtr.Zero)
|
||||
if (_kbHook != IntPtr.Zero)
|
||||
{
|
||||
UnhookWindowsHookEx(_hook);
|
||||
_hook = IntPtr.Zero;
|
||||
UnhookWindowsHookEx(_kbHook);
|
||||
_kbHook = IntPtr.Zero;
|
||||
}
|
||||
if (_msHook != IntPtr.Zero)
|
||||
{
|
||||
UnhookWindowsHookEx(_msHook);
|
||||
_msHook = IntPtr.Zero;
|
||||
}
|
||||
_isDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
|
||||
private IntPtr KeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
if (nCode >= 0)
|
||||
{
|
||||
@@ -55,19 +90,72 @@ internal sealed class PttHotkey : IDisposable
|
||||
bool isDown = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN;
|
||||
bool isUp = wParam == WM_KEYUP || wParam == WM_SYSKEYUP;
|
||||
|
||||
if (key == Key && isDown && !_isDown)
|
||||
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 (key == Key && isUp && _isDown)
|
||||
else if (isUp && _isDown)
|
||||
{
|
||||
_isDown = false;
|
||||
Released?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CallNextHookEx(_hook, nCode, wParam, lParam);
|
||||
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()
|
||||
@@ -78,10 +166,14 @@ internal sealed class PttHotkey : IDisposable
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user