feat(windows): move audio settings to dedicated dialog, fix device ComboBox accessibility

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 12:15:18 +02:00
parent 95f1fb70b0
commit a48b47d4ca
4 changed files with 356 additions and 298 deletions

View File

@@ -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();
}
/// <summary>
/// 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 (<see cref="BtnMicToggle_Click"/>).
/// Setting the radio fires CheckedChanged, which only adjusts visibility while the mic is idle.
/// </summary>
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;
}