using System.ComponentModel; using VoiceCat.App.Audio; 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; // Aux-stream live-apply callbacks, supplied by MainForm (which owns the aux capture). Null // when not connected — the controls still edit settings, just without live effect. private readonly Action? _applyAuxEnabled; private readonly Action? _applyAuxDevice; private readonly Action? _applyAuxGain; // 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 bool _origMicNoiseReduction; private readonly bool _origStereoMic; private readonly Keys _origPttKey; private readonly bool _origAuxEnabled; private readonly string? _origAuxDeviceId; private readonly int _origAuxGain; 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 readonly CheckBox _chkNoiseReduction; private readonly CheckBox _chkStereoMic; private readonly CheckBox _chkAux; private readonly Label _lblAuxDevice; private readonly ComboBox _cboAuxDevice; private readonly Button _btnAuxRefresh; private readonly Label _lblAuxGain; private readonly TrackBar _trkAuxGain; private Keys _pttKey; public AudioSettingsForm(VoiceCatClient client, VoiceSettings settings, uint micStreamId, Action? applyAuxEnabled = null, Action? applyAuxDevice = null, Action? applyAuxGain = null) { _client = client; _settings = settings; _micStreamId = micStreamId; _applyAuxEnabled = applyAuxEnabled; _applyAuxDevice = applyAuxDevice; _applyAuxGain = applyAuxGain; _pttKey = (Keys)settings.PttKey; _origDeviceId = settings.InputDeviceId; _origMode = (VcInputMode)settings.InputMode; _origVadSlider = settings.VadThresholdSlider; _origMicGain = settings.MicGain; _origMicNoiseReduction = settings.MicNoiseReduction; _origStereoMic = settings.StereoMic; _origPttKey = _pttKey; _origAuxEnabled = settings.AuxEnabled; _origAuxDeviceId = settings.AuxDeviceId; _origAuxGain = settings.AuxGain; Text = "Audio settings"; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; StartPosition = FormStartPosition.CenterParent; AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(420, 611); // ── 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; // ── Mic noise reduction ─────────────────────────────────────────────── // Send-side RNNoise denoise of the mic stream. MIC-only (the core's NR runs before // the input gain and VAD/PTT gate; aux/screen are excluded). One pass for all listeners. _chkNoiseReduction = new CheckBox { Text = "Noise &reduction (RNNoise)", Location = new Point(12, 340), AutoSize = true, Checked = settings.MicNoiseReduction, AccessibleName = "Microphone noise reduction", AccessibleDescription = "RNNoise denoising of your microphone. Cleans your signal for everyone listening.", }; _chkNoiseReduction.CheckedChanged += ChkNoiseReduction_CheckedChanged; // ── Stereo microphone ───────────────────────────────────────────────── // Opens the mic capture device in stereo (interleaved L/R) instead of mono. Real stereo // only reaches the wire on a stereo channel; the core folds a stereo mic to mono on a mono // channel. Toggling while connected restarts the capture device (vc_audio_restart) so the // new channel count takes effect immediately. _chkStereoMic = new CheckBox { Text = "&Stereo microphone", Location = new Point(12, 364), AutoSize = true, Checked = settings.StereoMic, AccessibleName = "Stereo microphone", AccessibleDescription = "Capture your microphone in stereo. Only transmitted in stereo on a stereo channel.", }; _chkStereoMic.CheckedChanged += ChkStereoMic_CheckedChanged; // ── Aux input stream ────────────────────────────────────────────────── // A second outgoing stream from another hardware input device (e.g. line-in / aux), // captured client-side and fed to the core. Device + volume only — aux is always-on // (the core never gates AUX_DEVICE on VAD/PTT). _chkAux = new CheckBox { Text = "&Aux stream (second input device)", Location = new Point(12, 404), AutoSize = true, Checked = settings.AuxEnabled, AccessibleName = "Enable aux input stream", AccessibleDescription = "Transmit a second hardware input device alongside your microphone.", }; _chkAux.CheckedChanged += ChkAux_CheckedChanged; _lblAuxDevice = new Label { Text = "Aux d&evice:", Location = new Point(12, 434), AutoSize = true, }; _cboAuxDevice = new ComboBox { Location = new Point(12, 454), Width = 300, DropDownStyle = ComboBoxStyle.DropDownList, DisplayMember = "Name", ValueMember = "Id", AccessibleName = "Aux input device", AccessibleDescription = "Select the second audio input device to transmit.", }; _cboAuxDevice.SelectedIndexChanged += CboAuxDevice_SelectedIndexChanged; _btnAuxRefresh = new Button { Text = "Re&fresh", Location = new Point(320, 452), Size = new Size(80, 26), }; _btnAuxRefresh.Click += (_, _) => LoadAuxDevices(); _lblAuxGain = new Label { Text = "Aux vo&lume:", Location = new Point(12, 490), AutoSize = true, }; _trkAuxGain = new TrackBar { Location = new Point(12, 510), Size = new Size(200, 45), Minimum = 0, Maximum = 300, TickFrequency = 25, SmallChange = 5, LargeChange = 25, Value = Math.Clamp(settings.AuxGain, 0, 300), }; _trkAuxGain.AccessibleName = "Aux volume"; _trkAuxGain.AccessibleDescription = "Volume of the aux input stream. 100 is unity gain; range 0–300 percent."; _trkAuxGain.Scroll += TrkAuxGain_Scroll; // ── OK / Cancel ─────────────────────────────────────────────────────── var btnOk = new Button { Text = "&OK", DialogResult = DialogResult.OK, Location = new Point(228, 572), Size = new Size(80, 27), }; var btnCancel = new Button { Text = "&Cancel", DialogResult = DialogResult.Cancel, Location = new Point(316, 572), 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, _chkNoiseReduction, _chkStereoMic, _chkAux, _lblAuxDevice, _cboAuxDevice, _btnAuxRefresh, _lblAuxGain, _trkAuxGain, 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(); LoadAuxDevices(); UpdateAuxControlsEnabled(); } 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); } // ── Aux input stream ────────────────────────────────────────────────────── private void LoadAuxDevices() { string? currentId = (_cboAuxDevice.SelectedItem as InputDeviceInfo)?.Id ?? _settings.AuxDeviceId; var devices = InputDeviceEnumerator.List().ToList(); // Set the data source AND restore the selection while unsubscribed so neither the initial // populate nor a Refresh fires a spurious device-change (which would restart the capture). _cboAuxDevice.SelectedIndexChanged -= CboAuxDevice_SelectedIndexChanged; _cboAuxDevice.DataSource = new BindingList(devices); int idx = -1; if (currentId is not null) idx = devices.FindIndex(d => d.Id == currentId); if (idx < 0) idx = devices.FindIndex(d => d.IsDefault); _cboAuxDevice.SelectedIndex = idx >= 0 ? idx : (devices.Count > 0 ? 0 : -1); _cboAuxDevice.SelectedIndexChanged += CboAuxDevice_SelectedIndexChanged; // Persist the resolved selection (null = default) so Join Voice opens the same device. if (_cboAuxDevice.SelectedItem is InputDeviceInfo dev) _settings.AuxDeviceId = dev.IsDefault ? null : dev.Id; } private void ChkAux_CheckedChanged(object? sender, EventArgs e) { _settings.AuxEnabled = _chkAux.Checked; UpdateAuxControlsEnabled(); _applyAuxEnabled?.Invoke(_chkAux.Checked); } private void CboAuxDevice_SelectedIndexChanged(object? sender, EventArgs e) { if (_cboAuxDevice.SelectedItem is not InputDeviceInfo dev) return; _settings.AuxDeviceId = dev.IsDefault ? null : dev.Id; if (_chkAux.Checked) _applyAuxDevice?.Invoke(dev.IsDefault ? null : dev.Id); } private void TrkAuxGain_Scroll(object? sender, EventArgs e) { _settings.AuxGain = _trkAuxGain.Value; if (_chkAux.Checked) _applyAuxGain?.Invoke(_trkAuxGain.Value / 100f); } private void UpdateAuxControlsEnabled() { bool on = _chkAux.Checked; _lblAuxDevice.Enabled = on; _cboAuxDevice.Enabled = on; _btnAuxRefresh.Enabled = on; _lblAuxGain.Enabled = on; _trkAuxGain.Enabled = on; } 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 ChkNoiseReduction_CheckedChanged(object? sender, EventArgs e) { _settings.MicNoiseReduction = _chkNoiseReduction.Checked; if (_micStreamId != 0) _client.SetInputNoiseReduction(_chkNoiseReduction.Checked); } private void ChkStereoMic_CheckedChanged(object? sender, EventArgs e) { _settings.StereoMic = _chkStereoMic.Checked; // Channel count only takes effect when the capture device (re)starts, so restart it live. if (_micStreamId != 0) { _client.SetCaptureChannels(_micStreamId, _chkStereoMic.Checked ? 2u : 1u); _client.AudioRestart(); } } 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.MicNoiseReduction = _origMicNoiseReduction; _settings.StereoMic = _origStereoMic; _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); _client.SetInputNoiseReduction(_origMicNoiseReduction); // Restore capture channel count; restart the device only if it actually changed. if (_origStereoMic != _chkStereoMic.Checked) { _client.SetCaptureChannels(_micStreamId, _origStereoMic ? 2u : 1u); _client.AudioRestart(); } } // Aux: restore originals to settings and re-apply live (order: device + gain first so a // re-enable starts with the right configuration). _settings.AuxEnabled = _origAuxEnabled; _settings.AuxDeviceId = _origAuxDeviceId; _settings.AuxGain = _origAuxGain; _applyAuxDevice?.Invoke(_origAuxDeviceId); _applyAuxGain?.Invoke(_origAuxGain / 100f); _applyAuxEnabled?.Invoke(_origAuxEnabled); } 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); }