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:
325
clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
Normal file
325
clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
Normal file
@@ -0,0 +1,325 @@
|
||||
using System.ComponentModel;
|
||||
using VoiceCat.App.Models;
|
||||
using VoiceCat.Interop;
|
||||
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<DeviceInfo>(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);
|
||||
}
|
||||
Reference in New Issue
Block a user