diff --git a/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs b/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
new file mode 100644
index 0000000..35ca910
--- /dev/null
+++ b/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
@@ -0,0 +1,325 @@
+using System.ComponentModel;
+using VoiceCat.App.Models;
+using VoiceCat.Interop;
+
+namespace VoiceCat.App.Forms;
+
+///
+/// Edits audio input settings: device selection, transmission mode (VAD/PTT/always-on),
+/// VAD sensitivity, mic gain, and PTT key. Changes are applied live to the client for
+/// immediate feedback; Cancel reverts them.
+///
+public sealed class AudioSettingsForm : Form
+{
+ private readonly VoiceCatClient _client;
+ private readonly VoiceSettings _settings;
+ private readonly uint _micStreamId;
+
+ // Snapshot of original values so Cancel can restore them
+ private readonly string? _origDeviceId;
+ private readonly VcInputMode _origMode;
+ private readonly int _origVadSlider;
+ private readonly int _origMicGain;
+ private readonly Keys _origPttKey;
+
+ private readonly ComboBox _cboDevice;
+ private readonly Button _btnRefresh;
+ private readonly RadioButton _radioVad;
+ private readonly RadioButton _radioPtt;
+ private readonly RadioButton _radioAlwaysOn;
+ private readonly Label _lblPttKey;
+ private readonly Button _btnChangePtt;
+ private readonly Label _lblSensitivity;
+ private readonly TrackBar _trkVad;
+ private readonly TrackBar _trkGain;
+
+ private Keys _pttKey;
+
+ public AudioSettingsForm(VoiceCatClient client, VoiceSettings settings, uint micStreamId)
+ {
+ _client = client;
+ _settings = settings;
+ _micStreamId = micStreamId;
+ _pttKey = (Keys)settings.PttKey;
+
+ _origDeviceId = settings.InputDeviceId;
+ _origMode = (VcInputMode)settings.InputMode;
+ _origVadSlider = settings.VadThresholdSlider;
+ _origMicGain = settings.MicGain;
+ _origPttKey = _pttKey;
+
+ Text = "Audio settings";
+ FormBorderStyle = FormBorderStyle.FixedDialog;
+ MaximizeBox = false;
+ MinimizeBox = false;
+ StartPosition = FormStartPosition.CenterParent;
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(420, 370);
+
+ // ── Device row ────────────────────────────────────────────────────────
+ var lblDevice = new Label
+ {
+ Text = "Input &device:",
+ Location = new Point(12, 16),
+ AutoSize = true,
+ };
+
+ _cboDevice = new ComboBox
+ {
+ Location = new Point(12, 36),
+ Width = 300,
+ DropDownStyle = ComboBoxStyle.DropDownList,
+ DisplayMember = "Name",
+ ValueMember = "Id",
+ AccessibleName = "Input device",
+ AccessibleDescription = "Select which microphone or audio input device to use.",
+ };
+ _cboDevice.SelectedIndexChanged += CboDevice_SelectedIndexChanged;
+
+ _btnRefresh = new Button
+ {
+ Text = "&Refresh",
+ Location = new Point(320, 34),
+ Size = new Size(80, 26),
+ };
+ _btnRefresh.Click += (_, _) => LoadDevices();
+
+ // ── Transmission mode ─────────────────────────────────────────────────
+ var lblMode = new Label
+ {
+ Text = "Transmission mode:",
+ Location = new Point(12, 76),
+ AutoSize = true,
+ };
+
+ _radioVad = new RadioButton
+ {
+ Text = "&Voice activation",
+ Location = new Point(20, 96),
+ AutoSize = true,
+ };
+ _radioVad.CheckedChanged += RadioMode_CheckedChanged;
+
+ _lblSensitivity = new Label
+ {
+ Text = "Sensitivity:",
+ Location = new Point(36, 122),
+ AutoSize = true,
+ };
+ _trkVad = new TrackBar
+ {
+ Location = new Point(36, 140),
+ Size = new Size(200, 45),
+ Minimum = 1,
+ Maximum = 100,
+ TickFrequency = 10,
+ SmallChange = 1,
+ LargeChange = 10,
+ Value = Math.Clamp(settings.VadThresholdSlider, 1, 100),
+ };
+ _trkVad.AccessibleName = "VAD sensitivity";
+ _trkVad.AccessibleDescription =
+ "Voice detection sensitivity. Higher = more sensitive. Range 1–100.";
+ _trkVad.Scroll += TrkVad_Scroll;
+
+ _radioPtt = new RadioButton
+ {
+ Text = "&Push to talk",
+ Location = new Point(20, 192),
+ AutoSize = true,
+ };
+ _radioPtt.CheckedChanged += RadioMode_CheckedChanged;
+
+ _lblPttKey = new Label
+ {
+ Location = new Point(140, 194),
+ AutoSize = true,
+ };
+
+ _btnChangePtt = new Button
+ {
+ Text = "Change key...",
+ Location = new Point(200, 189),
+ Size = new Size(105, 26),
+ };
+ _btnChangePtt.Click += BtnChangePtt_Click;
+
+ _radioAlwaysOn = new RadioButton
+ {
+ Text = "A&lways on",
+ Location = new Point(20, 224),
+ AutoSize = true,
+ };
+ _radioAlwaysOn.CheckedChanged += RadioMode_CheckedChanged;
+
+ // ── Mic gain ──────────────────────────────────────────────────────────
+ var lblGain = new Label
+ {
+ Text = "Microphone &volume:",
+ Location = new Point(12, 268),
+ AutoSize = true,
+ };
+ _trkGain = new TrackBar
+ {
+ Location = new Point(12, 288),
+ Size = new Size(200, 45),
+ Minimum = 0,
+ Maximum = 300,
+ TickFrequency = 25,
+ SmallChange = 5,
+ LargeChange = 25,
+ Value = Math.Clamp(settings.MicGain, 0, 300),
+ };
+ _trkGain.AccessibleName = "Microphone volume";
+ _trkGain.AccessibleDescription =
+ "Boost a quiet microphone. 100 is unity gain; range 0–300 percent.";
+ _trkGain.Scroll += TrkGain_Scroll;
+
+ // ── OK / Cancel ───────────────────────────────────────────────────────
+ var btnOk = new Button
+ {
+ Text = "&OK",
+ DialogResult = DialogResult.OK,
+ Location = new Point(228, 334),
+ Size = new Size(80, 27),
+ };
+ var btnCancel = new Button
+ {
+ Text = "&Cancel",
+ DialogResult = DialogResult.Cancel,
+ Location = new Point(316, 334),
+ Size = new Size(80, 27),
+ };
+
+ AcceptButton = btnOk;
+ CancelButton = btnCancel;
+
+ btnOk.Click += (_, _) =>
+ {
+ _settings.Save();
+ };
+ FormClosing += AudioSettingsForm_FormClosing;
+
+ Controls.AddRange([
+ lblDevice, _cboDevice, _btnRefresh,
+ lblMode, _radioVad, _lblSensitivity, _trkVad,
+ _radioPtt, _lblPttKey, _btnChangePtt, _radioAlwaysOn,
+ lblGain, _trkGain,
+ btnOk, btnCancel,
+ ]);
+
+ // Apply saved mode (fires RadioMode_CheckedChanged which sets visibility)
+ switch ((VcInputMode)settings.InputMode)
+ {
+ case VcInputMode.PushToTalk: _radioPtt.Checked = true; break;
+ case VcInputMode.AlwaysOn: _radioAlwaysOn.Checked = true; break;
+ default: _radioVad.Checked = true; break;
+ }
+
+ LoadDevices();
+ }
+
+ private void LoadDevices()
+ {
+ string? currentId = (_cboDevice.SelectedItem as DeviceInfo)?.Id ?? _settings.InputDeviceId;
+
+ var devices = _client.ListDevices(VcDeviceKind.Input);
+ _cboDevice.SelectedIndexChanged -= CboDevice_SelectedIndexChanged;
+ _cboDevice.DataSource = new BindingList(devices);
+ _cboDevice.SelectedIndexChanged += CboDevice_SelectedIndexChanged;
+
+ // Try to restore previous selection
+ int idx = -1;
+ if (currentId is not null)
+ idx = devices.FindIndex(d => d.Id == currentId);
+ if (idx < 0)
+ idx = devices.FindIndex(d => d.IsDefault);
+ _cboDevice.SelectedIndex = idx >= 0 ? idx : (devices.Count > 0 ? 0 : -1);
+ }
+
+ private void CboDevice_SelectedIndexChanged(object? sender, EventArgs e)
+ {
+ if (_cboDevice.SelectedItem is not DeviceInfo dev) return;
+ _settings.InputDeviceId = dev.IsDefault ? null : dev.Id;
+ if (_micStreamId != 0)
+ _client.SetInputDevice(_micStreamId, dev.IsDefault ? null : dev.Id);
+ }
+
+ private void RadioMode_CheckedChanged(object? sender, EventArgs e)
+ {
+ var mode = CurrentMode();
+ bool isVad = mode == VcInputMode.VoiceActivation;
+ bool isPtt = mode == VcInputMode.PushToTalk;
+
+ _lblSensitivity.Visible = isVad;
+ _trkVad.Visible = isVad;
+ _lblPttKey.Visible = isPtt;
+ _btnChangePtt.Visible = isPtt;
+ UpdatePttKeyLabel();
+
+ _settings.InputMode = (int)mode;
+ if (_micStreamId != 0)
+ {
+ _client.SetInputMode(mode);
+ if (isVad) _client.SetVadThreshold(VadThresholdFromSlider());
+ if (isPtt) _client.SetPushToTalk(false);
+ }
+ }
+
+ private void TrkVad_Scroll(object? sender, EventArgs e)
+ {
+ _settings.VadThresholdSlider = _trkVad.Value;
+ if (_micStreamId != 0 && CurrentMode() == VcInputMode.VoiceActivation)
+ _client.SetVadThreshold(VadThresholdFromSlider());
+ }
+
+ private void TrkGain_Scroll(object? sender, EventArgs e)
+ {
+ _settings.MicGain = _trkGain.Value;
+ if (_micStreamId != 0)
+ _client.SetInputGain(_trkGain.Value / 100f);
+ }
+
+ private void BtnChangePtt_Click(object? sender, EventArgs e)
+ {
+ using var dlg = new PttKeyCaptureDialog(_pttKey);
+ if (dlg.ShowDialog(this) == DialogResult.OK)
+ {
+ _pttKey = dlg.CapturedKey;
+ _settings.PttKey = (int)_pttKey;
+ UpdatePttKeyLabel();
+ }
+ }
+
+ private void AudioSettingsForm_FormClosing(object? sender, FormClosingEventArgs e)
+ {
+ if (DialogResult == DialogResult.OK) return;
+
+ // Cancel: restore originals to settings and live client
+ _settings.InputDeviceId = _origDeviceId;
+ _settings.InputMode = (int)_origMode;
+ _settings.VadThresholdSlider = _origVadSlider;
+ _settings.MicGain = _origMicGain;
+ _settings.PttKey = (int)_origPttKey;
+
+ if (_micStreamId != 0)
+ {
+ _client.SetInputDevice(_micStreamId, _origDeviceId);
+ _client.SetInputMode(_origMode);
+ if (_origMode == VcInputMode.VoiceActivation)
+ _client.SetVadThreshold(0.1f * (1f - (_origVadSlider - 1f) / 99f));
+ _client.SetInputGain(_origMicGain / 100f);
+ }
+ }
+
+ private void UpdatePttKeyLabel() =>
+ _lblPttKey.Text = $"({_pttKey})";
+
+ private VcInputMode CurrentMode() =>
+ _radioPtt.Checked ? VcInputMode.PushToTalk :
+ _radioAlwaysOn.Checked ? VcInputMode.AlwaysOn :
+ VcInputMode.VoiceActivation;
+
+ private float VadThresholdFromSlider() =>
+ 0.1f * (1f - (_trkVad.Value - 1f) / 99f);
+}
diff --git a/clients/windows/VoiceCat.App/Forms/MainForm.Designer.cs b/clients/windows/VoiceCat.App/Forms/MainForm.Designer.cs
index 1edb4dd..6773a2a 100644
--- a/clients/windows/VoiceCat.App/Forms/MainForm.Designer.cs
+++ b/clients/windows/VoiceCat.App/Forms/MainForm.Designer.cs
@@ -36,23 +36,10 @@ partial class MainForm
// Voice control panel (docked Bottom)
private Panel pnlVoice = null!;
private FlowLayoutPanel flpVoiceTop = null!;
- private FlowLayoutPanel flpVoiceBottom = null!;
private CheckBox chkMute = null!;
private CheckBox chkDeafen = null!;
- private RadioButton radioVad = null!;
- private RadioButton radioPtt = null!;
- private RadioButton radioAlwaysOn = null!;
- private Label lblPttKey = null!;
- private Button btnChangePtt = null!;
- private Label lblInputDevice = null!;
- private ComboBox cboInputDevice = null!;
- private Button btnRefreshDevices = null!;
private Label lblLevel = null!;
private ProgressBar pbLevel = null!;
- private Label lblVadThreshold = null!;
- private TrackBar trkVadThreshold = null!;
- private Label lblMicGain = null!;
- private TrackBar trkMicGain = null!;
protected override void Dispose(bool disposing)
{
@@ -81,23 +68,10 @@ partial class MainForm
trkOutputVolume = new TrackBar();
pnlVoice = new Panel();
flpVoiceTop = new FlowLayoutPanel();
- flpVoiceBottom = new FlowLayoutPanel();
chkMute = new CheckBox();
chkDeafen = new CheckBox();
- radioVad = new RadioButton();
- radioPtt = new RadioButton();
- radioAlwaysOn = new RadioButton();
- lblPttKey = new Label();
- btnChangePtt = new Button();
- lblInputDevice = new Label();
- cboInputDevice = new ComboBox();
- btnRefreshDevices = new Button();
lblLevel = new Label();
pbLevel = new ProgressBar();
- lblVadThreshold = new Label();
- trkVadThreshold = new TrackBar();
- lblMicGain = new Label();
- trkMicGain = new TrackBar();
menuStrip = new MenuStrip();
toolStrip = new ToolStrip();
tsbJoinVoice = new ToolStripButton();
@@ -245,71 +219,15 @@ partial class MainForm
chkDeafen.Margin = new Padding(0, 4, 12, 0);
chkDeafen.TabIndex = 2;
- var lblMode = new Label { Text = "Mode:", AutoSize = true, Margin = new Padding(0, 5, 4, 0) };
-
- radioVad.Text = "&Voice activation";
- radioVad.AutoSize = true;
- radioVad.Checked = true;
- radioVad.Enabled = false;
- radioVad.Margin = new Padding(0, 4, 6, 0);
- radioVad.TabIndex = 3;
-
- radioPtt.Text = "&Push to talk";
- radioPtt.AutoSize = true;
- radioPtt.Enabled = false;
- radioPtt.Margin = new Padding(0, 4, 4, 0);
- radioPtt.TabIndex = 4;
-
- radioAlwaysOn.Text = "A&lways on";
- radioAlwaysOn.AutoSize = true;
- radioAlwaysOn.Enabled = false;
- radioAlwaysOn.Margin = new Padding(0, 4, 12, 0);
- radioAlwaysOn.TabIndex = 5;
-
- lblPttKey.Text = "(F8)";
- lblPttKey.AutoSize = true;
- lblPttKey.Margin = new Padding(2, 5, 4, 0);
- lblPttKey.Visible = false;
-
- btnChangePtt.Text = "Change key...";
- btnChangePtt.AutoSize = true;
- btnChangePtt.Margin = new Padding(0, 2, 0, 0);
- btnChangePtt.Visible = false;
- btnChangePtt.TabIndex = 6;
-
- flpVoiceTop.Dock = DockStyle.Top;
- flpVoiceTop.Height = 34;
+ flpVoiceTop.Dock = DockStyle.Fill;
flpVoiceTop.AutoSize = false;
flpVoiceTop.Padding = new Padding(4, 2, 4, 0);
flpVoiceTop.Controls.Add(chkMute);
flpVoiceTop.Controls.Add(chkDeafen);
- flpVoiceTop.Controls.Add(lblMode);
- flpVoiceTop.Controls.Add(radioVad);
- flpVoiceTop.Controls.Add(radioPtt);
- flpVoiceTop.Controls.Add(radioAlwaysOn);
- flpVoiceTop.Controls.Add(lblPttKey);
- flpVoiceTop.Controls.Add(btnChangePtt);
-
- // Bottom row: device picker + level meter
- lblInputDevice.Text = "Input:";
- lblInputDevice.AutoSize = true;
- lblInputDevice.Margin = new Padding(0, 5, 4, 0);
-
- cboInputDevice.AccessibleName = "Input device";
- cboInputDevice.AccessibleDescription = "Select which microphone or audio device to use.";
- cboInputDevice.DropDownStyle = ComboBoxStyle.DropDownList;
- cboInputDevice.Width = 200;
- cboInputDevice.Margin = new Padding(0, 2, 4, 0);
- cboInputDevice.TabIndex = 7;
-
- btnRefreshDevices.Text = "Re&fresh";
- btnRefreshDevices.AutoSize = true;
- btnRefreshDevices.Margin = new Padding(0, 2, 12, 0);
- btnRefreshDevices.TabIndex = 8;
lblLevel.Text = "Level:";
lblLevel.AutoSize = true;
- lblLevel.Margin = new Padding(0, 5, 4, 0);
+ lblLevel.Margin = new Padding(12, 5, 4, 0);
pbLevel.AccessibleName = "Microphone level";
pbLevel.AccessibleDescription = "Current input level from the microphone.";
@@ -320,61 +238,14 @@ partial class MainForm
pbLevel.Style = ProgressBarStyle.Continuous;
pbLevel.TabStop = false;
- lblVadThreshold.Text = "Sensitivity:";
- lblVadThreshold.AutoSize = true;
- lblVadThreshold.Margin = new Padding(12, 5, 4, 0);
- lblVadThreshold.Visible = true;
-
- trkVadThreshold.AccessibleName = "VAD sensitivity";
- trkVadThreshold.AccessibleDescription =
- "Voice detection sensitivity. Higher = more sensitive (triggers on quieter sounds). " +
- "Range 1–100; default 25.";
- trkVadThreshold.Minimum = 1;
- trkVadThreshold.Maximum = 100;
- trkVadThreshold.Value = 76;
- trkVadThreshold.TickFrequency = 10;
- trkVadThreshold.SmallChange = 1;
- trkVadThreshold.LargeChange = 10;
- trkVadThreshold.Width = 120;
- trkVadThreshold.Margin = new Padding(0, 2, 0, 0);
- trkVadThreshold.TabIndex = 9;
- trkVadThreshold.Visible = true;
-
- lblMicGain.Text = "Mic volume:";
- lblMicGain.AutoSize = true;
- lblMicGain.Margin = new Padding(12, 5, 4, 0);
-
- trkMicGain.AccessibleName = "Microphone volume";
- trkMicGain.AccessibleDescription =
- "Boost a quiet microphone. 100 is unity; range 0–300 percent.";
- trkMicGain.Minimum = 0;
- trkMicGain.Maximum = 300;
- trkMicGain.Value = 100;
- trkMicGain.TickFrequency = 25;
- trkMicGain.SmallChange = 5;
- trkMicGain.LargeChange = 25;
- trkMicGain.Width = 120;
- trkMicGain.Margin = new Padding(0, 2, 0, 0);
- trkMicGain.TabIndex = 10;
-
- flpVoiceBottom.Dock = DockStyle.Fill;
- flpVoiceBottom.Padding = new Padding(4, 0, 4, 2);
- flpVoiceBottom.Controls.Add(lblInputDevice);
- flpVoiceBottom.Controls.Add(cboInputDevice);
- flpVoiceBottom.Controls.Add(btnRefreshDevices);
- flpVoiceBottom.Controls.Add(lblLevel);
- flpVoiceBottom.Controls.Add(pbLevel);
- flpVoiceBottom.Controls.Add(lblVadThreshold);
- flpVoiceBottom.Controls.Add(trkVadThreshold);
- flpVoiceBottom.Controls.Add(lblMicGain);
- flpVoiceBottom.Controls.Add(trkMicGain);
+ flpVoiceTop.Controls.Add(lblLevel);
+ flpVoiceTop.Controls.Add(pbLevel);
pnlVoice.Dock = DockStyle.Bottom;
- pnlVoice.Height = 68;
+ pnlVoice.Height = 38;
pnlVoice.BorderStyle = BorderStyle.FixedSingle;
pnlVoice.Padding = new Padding(0);
- pnlVoice.Controls.Add(flpVoiceBottom); // Fill — added first
- pnlVoice.Controls.Add(flpVoiceTop); // Top — added last
+ pnlVoice.Controls.Add(flpVoiceTop);
// ── Toolbar ───────────────────────────────────────────────────────────
tsbJoinVoice.Text = "Join Voice";
diff --git a/clients/windows/VoiceCat.App/Forms/MainForm.cs b/clients/windows/VoiceCat.App/Forms/MainForm.cs
index 00d2883..e0e5c70 100644
--- a/clients/windows/VoiceCat.App/Forms/MainForm.cs
+++ b/clients/windows/VoiceCat.App/Forms/MainForm.cs
@@ -83,14 +83,6 @@ public partial class MainForm : Form
// Voice controls
chkMute.CheckedChanged += (_, _) => ApplySelfMute();
chkDeafen.CheckedChanged += (_, _) => ApplySelfMute();
- radioVad.CheckedChanged += RadioVad_CheckedChanged;
- radioPtt.CheckedChanged += RadioPtt_CheckedChanged;
- radioAlwaysOn.CheckedChanged += RadioAlwaysOn_CheckedChanged;
- trkVadThreshold.Scroll += TrkVadThreshold_Scroll;
- trkMicGain.Scroll += TrkMicGain_Scroll;
- btnChangePtt.Click += BtnChangePtt_Click;
- btnRefreshDevices.Click += (_, _) => LoadInputDevices();
- cboInputDevice.SelectedIndexChanged += CboInputDevice_SelectedIndexChanged;
// PTT and global hotkeys (focus-scoped — work only while this form has focus)
KeyDown += MainForm_KeyDown;
@@ -105,29 +97,9 @@ public partial class MainForm : Form
BootstrapFromServer();
}
- ///
- /// Restore the saved transmission mode / VAD sensitivity / mic gain / PTT key into the UI so a
- /// relaunch keeps the user's input settings instead of resetting to the designer defaults. The
- /// values are pushed into the core when the mic stream starts ().
- /// Setting the radio fires CheckedChanged, which only adjusts visibility while the mic is idle.
- ///
- private void ApplyPersistedVoiceSettings()
- {
+ private void ApplyPersistedVoiceSettings() =>
_pttKey = (Keys)_voiceSettings.PttKey;
- trkVadThreshold.Value = Math.Clamp(_voiceSettings.VadThresholdSlider,
- trkVadThreshold.Minimum, trkVadThreshold.Maximum);
- trkMicGain.Value = Math.Clamp(_voiceSettings.MicGain,
- trkMicGain.Minimum, trkMicGain.Maximum);
-
- switch ((VcInputMode)_voiceSettings.InputMode)
- {
- case VcInputMode.PushToTalk: radioPtt.Checked = true; break;
- case VcInputMode.AlwaysOn: radioAlwaysOn.Checked = true; break;
- default: radioVad.Checked = true; break;
- }
- }
-
// ── Startup ──────────────────────────────────────────────────────────────
private void BootstrapFromServer()
@@ -157,7 +129,6 @@ public partial class MainForm : Form
base.OnLoad(e);
splitMain.SplitterDistance = Math.Min(220, splitMain.Width - 304);
splitLeft.SplitterDistance = Math.Min(260, splitLeft.Height - 84);
- LoadInputDevices();
}
// ── Menu / context menu builders ─────────────────────────────────────────
@@ -195,6 +166,14 @@ public partial class MainForm : Form
// Settings menu — always visible
var settingsMenu = new ToolStripMenuItem("&Settings");
+ var miAudio = new ToolStripMenuItem("&Audio...");
+ miAudio.Click += (_, _) =>
+ {
+ using var dlg = new AudioSettingsForm(_client, _voiceSettings, _micStreamId);
+ dlg.ShowDialog(this);
+ _pttKey = (Keys)_voiceSettings.PttKey;
+ };
+ settingsMenu.DropDownItems.Add(miAudio);
var miNotifications = new ToolStripMenuItem("&Notifications...");
miNotifications.Click += (_, _) =>
{
@@ -642,38 +621,6 @@ public partial class MainForm : Form
lblStatus.Text = $"Connected as {_nickname}{suffix} — {chanName} ({count} user{(count == 1 ? "" : "s")})";
}
- // ── Device management ─────────────────────────────────────────────────────
-
- private void LoadInputDevices()
- {
- var devices = _client.ListDevices(VcDeviceKind.Input);
- DeviceInfo? prevDevice = cboInputDevice.SelectedItem as DeviceInfo;
-
- cboInputDevice.Items.Clear();
- foreach (var d in devices) cboInputDevice.Items.Add(d);
-
- if (prevDevice is not null)
- {
- for (int i = 0; i < cboInputDevice.Items.Count; i++)
- {
- if (cboInputDevice.Items[i] is DeviceInfo d && d.Id == prevDevice.Id)
- {
- cboInputDevice.SelectedIndex = i;
- return;
- }
- }
- }
- for (int i = 0; i < cboInputDevice.Items.Count; i++)
- {
- if (cboInputDevice.Items[i] is DeviceInfo d && d.IsDefault)
- {
- cboInputDevice.SelectedIndex = i;
- return;
- }
- }
- if (cboInputDevice.Items.Count > 0) cboInputDevice.SelectedIndex = 0;
- }
-
// ── Voice controls ────────────────────────────────────────────────────────
private void BtnMicToggle_Click(object? sender, EventArgs e)
@@ -684,11 +631,13 @@ public partial class MainForm : Form
if (result == VcResult.Ok)
{
_micStreamId = streamId;
- if (cboInputDevice.SelectedItem is DeviceInfo { IsDefault: false } dev)
- _client.SetInputDevice(streamId, dev.Id);
- _client.SetInputMode(CurrentInputMode());
- if (radioVad.Checked) _client.SetVadThreshold(VadThresholdFromSlider());
- _client.SetInputGain(trkMicGain.Value / 100f);
+ var mode = (VcInputMode)_voiceSettings.InputMode;
+ if (_voiceSettings.InputDeviceId is string devId)
+ _client.SetInputDevice(streamId, devId);
+ _client.SetInputMode(mode);
+ if (mode == VcInputMode.VoiceActivation)
+ _client.SetVadThreshold(VadThresholdFromSettings());
+ _client.SetInputGain(_voiceSettings.MicGain / 100f);
SetVoiceJoinedState(true);
AddActivity("Joined voice — microphone active");
_feedback.PlaySound(SoundEvent.VoiceOn);
@@ -716,9 +665,6 @@ public partial class MainForm : Form
_miJoinVoice.Text = joined ? "Leave &Voice" : "&Join Voice";
chkMute.Enabled = joined;
chkDeafen.Enabled = joined;
- radioVad.Enabled = joined;
- radioPtt.Enabled = joined;
- radioAlwaysOn.Enabled = joined;
}
private void ApplySelfMute() =>
@@ -800,104 +746,17 @@ public partial class MainForm : Form
private void TrkOutputVolume_Scroll(object? sender, EventArgs e) =>
_client.SetOutputVolume(trkOutputVolume.Value / 100f);
- private void RadioVad_CheckedChanged(object? sender, EventArgs e)
- {
- if (!radioVad.Checked) return;
- lblPttKey.Visible = false;
- btnChangePtt.Visible = false;
- lblVadThreshold.Visible = true;
- trkVadThreshold.Visible = true;
- SaveInputMode(VcInputMode.VoiceActivation);
- if (_micStreamId != 0)
- {
- _client.SetInputMode(VcInputMode.VoiceActivation);
- _client.SetVadThreshold(VadThresholdFromSlider());
- }
- }
-
- private void RadioPtt_CheckedChanged(object? sender, EventArgs e)
- {
- if (!radioPtt.Checked) return;
- lblPttKey.Text = $"({_pttKey})";
- lblPttKey.Visible = true;
- btnChangePtt.Visible = true;
- lblVadThreshold.Visible = false;
- trkVadThreshold.Visible = false;
- SaveInputMode(VcInputMode.PushToTalk);
- if (_micStreamId != 0)
- {
- _client.SetInputMode(VcInputMode.PushToTalk);
- _client.SetPushToTalk(false);
- }
- }
-
- private void RadioAlwaysOn_CheckedChanged(object? sender, EventArgs e)
- {
- if (!radioAlwaysOn.Checked) return;
- lblPttKey.Visible = false;
- btnChangePtt.Visible = false;
- lblVadThreshold.Visible = false;
- trkVadThreshold.Visible = false;
- SaveInputMode(VcInputMode.AlwaysOn);
- if (_micStreamId != 0) _client.SetInputMode(VcInputMode.AlwaysOn);
- }
-
- private void TrkVadThreshold_Scroll(object? sender, EventArgs e)
- {
- _voiceSettings.VadThresholdSlider = trkVadThreshold.Value;
- _voiceSettings.Save();
- if (_micStreamId != 0 && radioVad.Checked)
- _client.SetVadThreshold(VadThresholdFromSlider());
- }
-
- private void TrkMicGain_Scroll(object? sender, EventArgs e)
- {
- _voiceSettings.MicGain = trkMicGain.Value;
- _voiceSettings.Save();
- if (_micStreamId != 0) _client.SetInputGain(trkMicGain.Value / 100f);
- }
-
- private void SaveInputMode(VcInputMode mode)
- {
- _voiceSettings.InputMode = (int)mode;
- _voiceSettings.Save();
- }
-
- private float VadThresholdFromSlider() =>
- 0.1f * (1f - (trkVadThreshold.Value - 1f) / 99f);
-
- private VcInputMode CurrentInputMode() =>
- radioPtt.Checked ? VcInputMode.PushToTalk :
- radioAlwaysOn.Checked ? VcInputMode.AlwaysOn :
- VcInputMode.VoiceActivation;
-
- private void BtnChangePtt_Click(object? sender, EventArgs e)
- {
- using var dlg = new PttKeyCaptureDialog(_pttKey);
- if (dlg.ShowDialog(this) == DialogResult.OK)
- {
- _pttKey = dlg.CapturedKey;
- lblPttKey.Text = $"({_pttKey})";
- _voiceSettings.PttKey = (int)_pttKey;
- _voiceSettings.Save();
- }
- }
-
- private void CboInputDevice_SelectedIndexChanged(object? sender, EventArgs e)
- {
- if (_micStreamId == 0) return;
- string? deviceId = (cboInputDevice.SelectedItem as DeviceInfo)?.Id;
- _client.SetInputDevice(_micStreamId, deviceId);
- }
+ private float VadThresholdFromSettings() =>
+ 0.1f * (1f - (_voiceSettings.VadThresholdSlider - 1f) / 99f);
// ── PTT key handling (focus-scoped) ───────────────────────────────────────
private void MainForm_KeyDown(object? sender, KeyEventArgs e)
{
- if (!radioPtt.Checked || e.KeyCode != _pttKey || _micStreamId == 0) return;
+ if ((VcInputMode)_voiceSettings.InputMode != VcInputMode.PushToTalk) return;
+ if (e.KeyCode != _pttKey || _micStreamId == 0) return;
if (ActiveControl is TextBox or RichTextBox) return;
_client.SetPushToTalk(true);
- lblPttKey.Text = $"({_pttKey} ▶)";
if (!_pttEngaged) // first key-down only, not auto-repeat
{
_pttEngaged = true;
@@ -935,10 +794,10 @@ public partial class MainForm : Form
private void MainForm_KeyUp(object? sender, KeyEventArgs e)
{
- if (!radioPtt.Checked || e.KeyCode != _pttKey || _micStreamId == 0) return;
+ if ((VcInputMode)_voiceSettings.InputMode != VcInputMode.PushToTalk) return;
+ if (e.KeyCode != _pttKey || _micStreamId == 0) return;
_client.SetPushToTalk(false);
_pttEngaged = false;
- lblPttKey.Text = $"({_pttKey})";
e.Handled = true;
}
diff --git a/clients/windows/VoiceCat.App/Models/VoiceSettings.cs b/clients/windows/VoiceCat.App/Models/VoiceSettings.cs
index 6ff639d..e95a0e8 100644
--- a/clients/windows/VoiceCat.App/Models/VoiceSettings.cs
+++ b/clients/windows/VoiceCat.App/Models/VoiceSettings.cs
@@ -24,6 +24,9 @@ public sealed class VoiceSettings
/// Push-to-talk key, stored as the integer value of System.Windows.Forms.Keys.
public int PttKey { get; set; } = (int)Keys.F8;
+ /// Saved device ID from the last session; null means use the system default.
+ public string? InputDeviceId { get; set; } = null;
+
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
private static string AppDataDir => Path.Combine(