From a48b47d4ca958dfb9817854034d20b6e17f56e79 Mon Sep 17 00:00:00 2001 From: Talon Date: Tue, 23 Jun 2026 12:15:18 +0200 Subject: [PATCH] feat(windows): move audio settings to dedicated dialog, fix device ComboBox accessibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio input settings (device picker, VAD/PTT mode, VAD sensitivity, mic gain, PTT key) move from the always-visible bottom panel into Settings > Audio..., matching the macOS/iOS pattern. The device ComboBox now uses DataSource + DisplayMember="Name" instead of Items.Add() with no DisplayMember — this fixes both the display bug (was showing the full DeviceInfo record ToString()) and the NVDA silence on dropdown open (DataSource binding exposes proper MSAA text per item). Changes apply live for immediate feedback; Cancel reverts. VoiceSettings gains InputDeviceId to persist the chosen device across sessions. Co-Authored-By: Claude Sonnet 4.6 --- .../VoiceCat.App/Forms/AudioSettingsForm.cs | 325 ++++++++++++++++++ .../VoiceCat.App/Forms/MainForm.Designer.cs | 141 +------- .../windows/VoiceCat.App/Forms/MainForm.cs | 185 ++-------- .../VoiceCat.App/Models/VoiceSettings.cs | 3 + 4 files changed, 356 insertions(+), 298 deletions(-) create mode 100644 clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs 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(