Files
voice-cat/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
Talon 2e0e0caccb feat(clients): wire RNNoise mic noise reduction into Windows, macOS, and iOS
Expose the existing send-side vc_set_input_noise_reduction C ABI (MIC-only,
mono, LOCAL — denoises captured mic PCM before input gain and VAD/PTT gate)
as a persisted global toggle in each client's audio settings, applied live
and re-applied on Join Voice. Mirrors the existing mic-gain wiring pattern.

- Shared Swift (VoiceCatCore): add setInputNoiseReduction(_:) wrapper
- Windows: P/Invoke + SetInputNoiseReduction wrapper, MicNoiseReduction in
  VoiceSettings, new checkbox in AudioSettingsForm (layout shifted +28px),
  apply on Join Voice; also fix stale 'planned - currently passthrough'
  label on the receive-side per-user NR checkbox (RNNoise now backs it)
- macOS: inputNoiseReduction state + UserDefaults in MainWindowController,
  NR checkbox + nrChanged action in SettingsWindowController
- iOS: inputNoiseReduction in VoiceState + setter + restore in SessionState,
  NR Toggle in SettingsView Voice section

Aux/screen are out of scope by design (core's NR guards kind == MIC). Apple
builds require a rebuilt VoiceCatCore.xcframework with VOICECAT_HAS_NS.
2026-06-23 14:11:18 +02:00

512 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.ComponentModel;
using VoiceCat.App.Audio;
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;
// 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<bool>? _applyAuxEnabled;
private readonly Action<string?>? _applyAuxDevice;
private readonly Action<float>? _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 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 _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<bool>? applyAuxEnabled = null, Action<string?>? applyAuxDevice = null,
Action<float>? 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;
_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, 583);
// ── 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 1100.";
_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 0300 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;
// ── 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, 376),
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, 406),
AutoSize = true,
};
_cboAuxDevice = new ComboBox
{
Location = new Point(12, 426),
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, 424),
Size = new Size(80, 26),
};
_btnAuxRefresh.Click += (_, _) => LoadAuxDevices();
_lblAuxGain = new Label
{
Text = "Aux vo&lume:",
Location = new Point(12, 462),
AutoSize = true,
};
_trkAuxGain = new TrackBar
{
Location = new Point(12, 482),
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 0300 percent.";
_trkAuxGain.Scroll += TrkAuxGain_Scroll;
// ── OK / Cancel ───────────────────────────────────────────────────────
var btnOk = new Button
{
Text = "&OK",
DialogResult = DialogResult.OK,
Location = new Point(228, 544),
Size = new Size(80, 27),
};
var btnCancel = new Button
{
Text = "&Cancel",
DialogResult = DialogResult.Cancel,
Location = new Point(316, 544),
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,
_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<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);
}
// ── 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<InputDeviceInfo>(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 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.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);
}
// 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);
}