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:
2026-08-10 12:07:34 +00:00
parent f7ea07b78e
commit 9b0dc11ee6
3 changed files with 201 additions and 38 deletions
+8 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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);