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 System.ComponentModel.IContainer? components = null;
|
||||||
|
|
||||||
private Label lblPttKey = null!;
|
private Label lblPttKey = null!;
|
||||||
private ComboBox cmbPttKey = null!;
|
private TextBox txtPttKey = null!;
|
||||||
private Label lblVoice = null!;
|
private Label lblVoice = null!;
|
||||||
private ComboBox cmbVoice = null!;
|
private ComboBox cmbVoice = null!;
|
||||||
private Button btnTestVoice = null!;
|
private Button btnTestVoice = null!;
|
||||||
@@ -45,7 +45,7 @@ partial class MainForm
|
|||||||
components = new System.ComponentModel.Container();
|
components = new System.ComponentModel.Container();
|
||||||
|
|
||||||
lblPttKey = new Label();
|
lblPttKey = new Label();
|
||||||
cmbPttKey = new ComboBox();
|
txtPttKey = new TextBox();
|
||||||
lblVoice = new Label();
|
lblVoice = new Label();
|
||||||
cmbVoice = new ComboBox();
|
cmbVoice = new ComboBox();
|
||||||
btnTestVoice = new Button();
|
btnTestVoice = new Button();
|
||||||
@@ -80,10 +80,11 @@ partial class MainForm
|
|||||||
lblPttKey.Size = new Size(60, 23);
|
lblPttKey.Size = new Size(60, 23);
|
||||||
lblPttKey.TextAlign = ContentAlignment.MiddleLeft;
|
lblPttKey.TextAlign = ContentAlignment.MiddleLeft;
|
||||||
|
|
||||||
// cmbPttKey
|
// txtPttKey
|
||||||
cmbPttKey.Location = new Point(75, 12);
|
txtPttKey.Location = new Point(75, 12);
|
||||||
cmbPttKey.Size = new Size(80, 23);
|
txtPttKey.Size = new Size(80, 23);
|
||||||
cmbPttKey.DropDownStyle = ComboBoxStyle.DropDownList;
|
txtPttKey.ReadOnly = true;
|
||||||
|
txtPttKey.TextAlign = HorizontalAlignment.Center;
|
||||||
|
|
||||||
// lblVoice
|
// lblVoice
|
||||||
lblVoice.Text = "Voice:";
|
lblVoice.Text = "Voice:";
|
||||||
@@ -249,7 +250,7 @@ partial class MainForm
|
|||||||
AutoScaleMode = AutoScaleMode.Font;
|
AutoScaleMode = AutoScaleMode.Font;
|
||||||
ClientSize = new Size(824, 509);
|
ClientSize = new Size(824, 509);
|
||||||
Controls.Add(lblPttKey);
|
Controls.Add(lblPttKey);
|
||||||
Controls.Add(cmbPttKey);
|
Controls.Add(txtPttKey);
|
||||||
Controls.Add(lblVoice);
|
Controls.Add(lblVoice);
|
||||||
Controls.Add(cmbVoice);
|
Controls.Add(cmbVoice);
|
||||||
Controls.Add(btnTestVoice);
|
Controls.Add(btnTestVoice);
|
||||||
|
|||||||
+84
-14
@@ -31,11 +31,11 @@ internal sealed partial class MainForm : Form
|
|||||||
|
|
||||||
private async void OnLoad(object? sender, EventArgs e)
|
private async void OnLoad(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
PopulatePttKeys();
|
|
||||||
PopulateVoices();
|
PopulateVoices();
|
||||||
PopulateOutputDevices();
|
PopulateOutputDevices();
|
||||||
|
|
||||||
cmbPttKey.SelectedItem = Keys.F8;
|
_pttKey = Keys.F8;
|
||||||
|
txtPttKey.Text = KeyToDisplayString(_pttKey);
|
||||||
if (cmbVoice.Items.Count > 0)
|
if (cmbVoice.Items.Count > 0)
|
||||||
cmbVoice.SelectedIndex = 0;
|
cmbVoice.SelectedIndex = 0;
|
||||||
|
|
||||||
@@ -45,7 +45,8 @@ internal sealed partial class MainForm : Form
|
|||||||
btnTestVoice.Click += OnTestVoice;
|
btnTestVoice.Click += OnTestVoice;
|
||||||
btnManageVoices.Click += OnManageVoices;
|
btnManageVoices.Click += OnManageVoices;
|
||||||
btnClearLog.Click += (_, _) => txtLog.Clear();
|
btnClearLog.Click += (_, _) => txtLog.Clear();
|
||||||
cmbPttKey.SelectedIndexChanged += OnPttKeyChanged;
|
txtPttKey.Enter += OnPttKeyFocus;
|
||||||
|
txtPttKey.KeyDown += OnPttKeyDown;
|
||||||
cmbOutput.SelectedIndexChanged += OnOutputChanged;
|
cmbOutput.SelectedIndexChanged += OnOutputChanged;
|
||||||
cmbVoice.SelectedIndexChanged += OnVoiceChanged;
|
cmbVoice.SelectedIndexChanged += OnVoiceChanged;
|
||||||
|
|
||||||
@@ -61,14 +62,88 @@ internal sealed partial class MainForm : Form
|
|||||||
await InitializeEngineAsync();
|
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)
|
||||||
|
{
|
||||||
|
if (!_capturingPttKey) return;
|
||||||
|
|
||||||
|
if (key == Keys.Escape)
|
||||||
{
|
{
|
||||||
Keys.F1, Keys.F2, Keys.F3, Keys.F4, Keys.F5, Keys.F6,
|
CancelCapture();
|
||||||
Keys.F7, Keys.F8, Keys.F9, Keys.F10, Keys.F11, Keys.F12,
|
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()
|
private void PopulateVoices()
|
||||||
@@ -160,7 +235,7 @@ internal sealed partial class MainForm : Form
|
|||||||
private void SetupHotkey()
|
private void SetupHotkey()
|
||||||
{
|
{
|
||||||
_pttHotkey?.Dispose();
|
_pttHotkey?.Dispose();
|
||||||
_pttHotkey = new PttHotkey { Key = (Keys)(cmbPttKey.SelectedItem ?? Keys.F8) };
|
_pttHotkey = new PttHotkey { Key = _pttKey };
|
||||||
_pttHotkey.Pressed += OnPttPressed;
|
_pttHotkey.Pressed += OnPttPressed;
|
||||||
_pttHotkey.Released += OnPttReleased;
|
_pttHotkey.Released += OnPttReleased;
|
||||||
_pttHotkey.Install();
|
_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)
|
private void OnOutputChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_orchestrator is not null)
|
if (_orchestrator is not null)
|
||||||
|
|||||||
+109
-17
@@ -6,46 +6,81 @@ namespace Robovoice.App;
|
|||||||
internal sealed class PttHotkey : IDisposable
|
internal sealed class PttHotkey : IDisposable
|
||||||
{
|
{
|
||||||
private const int WH_KEYBOARD_LL = 13;
|
private const int WH_KEYBOARD_LL = 13;
|
||||||
|
private const int WH_MOUSE_LL = 14;
|
||||||
private const int WM_KEYDOWN = 0x0100;
|
private const int WM_KEYDOWN = 0x0100;
|
||||||
private const int WM_KEYUP = 0x0101;
|
private const int WM_KEYUP = 0x0101;
|
||||||
private const int WM_SYSKEYDOWN = 0x0104;
|
private const int WM_SYSKEYDOWN = 0x0104;
|
||||||
private const int WM_SYSKEYUP = 0x0105;
|
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 readonly LowLevelKeyboardProc _kbProc;
|
||||||
private IntPtr _hook = IntPtr.Zero;
|
private readonly LowLevelMouseProc _msProc;
|
||||||
|
private IntPtr _kbHook = IntPtr.Zero;
|
||||||
|
private IntPtr _msHook = IntPtr.Zero;
|
||||||
private bool _isDown;
|
private bool _isDown;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public Keys Key { get; set; } = Keys.F8;
|
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? Pressed;
|
||||||
public event EventHandler? Released;
|
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()
|
public PttHotkey()
|
||||||
{
|
{
|
||||||
_proc = HookCallback;
|
_kbProc = KeyboardCallback;
|
||||||
|
_msProc = MouseCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Install()
|
public void Install()
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
if (_hook != IntPtr.Zero) return;
|
if (_kbHook != IntPtr.Zero || _msHook != IntPtr.Zero) return;
|
||||||
|
|
||||||
IntPtr hModule = GetModuleHandle(null);
|
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()
|
public void Uninstall()
|
||||||
{
|
{
|
||||||
if (_hook != IntPtr.Zero)
|
if (_kbHook != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
UnhookWindowsHookEx(_hook);
|
UnhookWindowsHookEx(_kbHook);
|
||||||
_hook = IntPtr.Zero;
|
_kbHook = IntPtr.Zero;
|
||||||
_isDown = false;
|
|
||||||
}
|
}
|
||||||
|
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)
|
if (nCode >= 0)
|
||||||
{
|
{
|
||||||
@@ -55,19 +90,72 @@ internal sealed class PttHotkey : IDisposable
|
|||||||
bool isDown = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN;
|
bool isDown = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN;
|
||||||
bool isUp = wParam == WM_KEYUP || wParam == WM_SYSKEYUP;
|
bool isUp = wParam == WM_KEYUP || wParam == WM_SYSKEYUP;
|
||||||
|
|
||||||
if (key == Key && isDown && !_isDown)
|
if (CaptureKeyPressed is not null && isDown)
|
||||||
{
|
{
|
||||||
_isDown = true;
|
CaptureKeyPressed.Invoke(key);
|
||||||
Pressed?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
}
|
||||||
else if (key == Key && isUp && _isDown)
|
else if (key == Key)
|
||||||
{
|
{
|
||||||
_isDown = false;
|
if (isDown && !_isDown)
|
||||||
Released?.Invoke(this, EventArgs.Empty);
|
{
|
||||||
|
_isDown = true;
|
||||||
|
Pressed?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
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()
|
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 LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
|
||||||
|
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
|
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)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||||||
|
|||||||
Reference in New Issue
Block a user