diff --git a/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift b/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift
index 74e7a30..ddbaecb 100644
--- a/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift
+++ b/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift
@@ -417,6 +417,15 @@ public final class VoiceCatClient {
VoiceCatResult(vc_set_input_gain(handle, gain < 0 ? 0 : gain))
}
+ /// Send-side microphone noise suppression (RNNoise). Denoises captured MIC PCM before the
+ /// input gain and VAD/PTT gate, so everyone hears the cleaned signal (one pass for all
+ /// listeners). MIC stream only, mono only; always LOCAL — no protocol traffic. Independent
+ /// of the per-listener receive-side NR in `setRemoteStream` (docs/voice.md §10).
+ @discardableResult
+ public func setInputNoiseReduction(_ enable: Bool) -> VoiceCatResult {
+ VoiceCatResult(vc_set_input_noise_reduction(handle, enable ? 1 : 0))
+ }
+
// MARK: - AVAudioSession interruption hooks (iOS)
/// Pause miniaudio device I/O. Call when AVAudioSession interruption begins.
diff --git a/clients/apple/iOS/VoiceCatiOS/SessionState.swift b/clients/apple/iOS/VoiceCatiOS/SessionState.swift
index 3ac8c92..798ff78 100644
--- a/clients/apple/iOS/VoiceCatiOS/SessionState.swift
+++ b/clients/apple/iOS/VoiceCatiOS/SessionState.swift
@@ -27,6 +27,7 @@ struct VoiceState {
var inputMode: VoiceCatInputMode = .voiceActivation
var vadThreshold: Float = 0.025
var inputGain: Float = 1.0
+ var inputNoiseReduction: Bool = false
var level: Float = 0.0
var currentDeviceId: String?
var localStreamId: UInt32 = 0
@@ -353,12 +354,19 @@ final class SessionState {
UserDefaults.standard.set(gain, forKey: DefaultsKey.inputGain)
}
+ func setInputNoiseReduction(_ on: Bool) {
+ client.setInputNoiseReduction(on)
+ voiceState.inputNoiseReduction = on
+ UserDefaults.standard.set(on, forKey: DefaultsKey.inputNoiseReduction)
+ }
+
// MARK: - Persisted input settings
private enum DefaultsKey {
static let inputMode = "voice.inputMode"
static let vadThreshold = "voice.vadThreshold"
static let inputGain = "voice.inputGain"
+ static let inputNoiseReduction = "voice.inputNoiseReduction"
}
/// Restore the saved input mode / VAD threshold / mic gain and push them into the core so a
@@ -375,9 +383,13 @@ final class SessionState {
if d.object(forKey: DefaultsKey.inputGain) != nil {
voiceState.inputGain = d.float(forKey: DefaultsKey.inputGain)
}
+ if d.object(forKey: DefaultsKey.inputNoiseReduction) != nil {
+ voiceState.inputNoiseReduction = d.bool(forKey: DefaultsKey.inputNoiseReduction)
+ }
client.setInputMode(voiceState.inputMode)
client.setVadThreshold(voiceState.vadThreshold)
client.setInputGain(voiceState.inputGain)
+ client.setInputNoiseReduction(voiceState.inputNoiseReduction)
}
private var pttEngaged = false
diff --git a/clients/apple/iOS/VoiceCatiOS/Views/SettingsView.swift b/clients/apple/iOS/VoiceCatiOS/Views/SettingsView.swift
index d4001db..d680d8a 100644
--- a/clients/apple/iOS/VoiceCatiOS/Views/SettingsView.swift
+++ b/clients/apple/iOS/VoiceCatiOS/Views/SettingsView.swift
@@ -251,6 +251,13 @@ struct SettingsView: View {
.accessibilityLabel("Microphone volume")
.accessibilityValue("\(Int((session.voiceState.inputGain * 100).rounded())) percent")
}
+
+ Toggle("Noise Reduction (RNNoise)", isOn: Binding(
+ get: { session.voiceState.inputNoiseReduction },
+ set: { session.setInputNoiseReduction($0) }
+ ))
+ .accessibilityLabel("Microphone noise reduction")
+ .accessibilityHint("Denoises your microphone signal for everyone listening.")
}
// MARK: - Notifications
diff --git a/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift b/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift
index 240d9f4..87a0eab 100644
--- a/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift
+++ b/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift
@@ -61,6 +61,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
static let inputMode = "voice.inputMode"
static let vadThreshold = "voice.vadThreshold"
static let inputGain = "voice.inputGain"
+ static let inputNoiseReduction = "voice.inputNoiseReduction"
static let pttKeyCode = "voice.pttKeyCode"
static let auxEnabled = "voice.auxEnabled"
static let auxDeviceUID = "voice.auxDeviceUID"
@@ -76,6 +77,9 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
internal var inputGain: Float = 1.0 {
didSet { UserDefaults.standard.set(inputGain, forKey: AudioDefaults.inputGain) }
}
+ internal var inputNoiseReduction: Bool = false {
+ didSet { UserDefaults.standard.set(inputNoiseReduction, forKey: AudioDefaults.inputNoiseReduction) }
+ }
internal var selectedInputDeviceId: String?
// Aux outgoing stream: a second hardware input device the client captures itself and feeds to
@@ -156,6 +160,9 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
if d.object(forKey: AudioDefaults.inputGain) != nil {
inputGain = d.float(forKey: AudioDefaults.inputGain)
}
+ if d.object(forKey: AudioDefaults.inputNoiseReduction) != nil {
+ inputNoiseReduction = d.bool(forKey: AudioDefaults.inputNoiseReduction)
+ }
if d.object(forKey: AudioDefaults.pttKeyCode) != nil {
pttKeyCode = UInt16(d.integer(forKey: AudioDefaults.pttKeyCode))
}
@@ -779,6 +786,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
client.setVadThreshold(vadThresholdValue)
}
client.setInputGain(inputGain)
+ client.setInputNoiseReduction(inputNoiseReduction)
setVoiceJoinedState(true)
addActivity("Joined voice — microphone active")
EventFeedback.shared.play(.voiceOn)
diff --git a/clients/apple/macOS/VoiceCatMac/Windows/SettingsWindowController.swift b/clients/apple/macOS/VoiceCatMac/Windows/SettingsWindowController.swift
index 12ca6c9..a962033 100644
--- a/clients/apple/macOS/VoiceCatMac/Windows/SettingsWindowController.swift
+++ b/clients/apple/macOS/VoiceCatMac/Windows/SettingsWindowController.swift
@@ -58,6 +58,11 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
}()
private let inputGainValueLabel = NSTextField(labelWithString: "100%")
+ // Send-side mic noise reduction (RNNoise). MIC-only. Persisted via
+ // MainWindowController.inputNoiseReduction.
+ private let nrCheckbox = NSButton(checkboxWithTitle: "Noise reduction (RNNoise)",
+ target: nil, action: nil)
+
// Aux input stream: a second outgoing stream from another hardware input device (e.g. line-in
// / aux), captured client-side. Device + volume only — aux is always-on. Persisted via
// MainWindowController.auxEnabled / auxDeviceUID / auxGain.
@@ -168,6 +173,11 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
inputGainSlider.setAccessibilityHelp("Boost a quiet microphone. 100% is unity.")
inputGainValueLabel.setAccessibilityLabel("Microphone volume value")
+ nrCheckbox.target = self
+ nrCheckbox.action = #selector(nrChanged)
+ nrCheckbox.setAccessibilityLabel("Microphone noise reduction")
+ nrCheckbox.setAccessibilityHelp("RNNoise denoising of your microphone. Cleans your signal for everyone.")
+
let inputModeRow = NSStackView(views: [inputModeLabel, inputModeControl])
inputModeRow.orientation = .horizontal
inputModeRow.spacing = 8
@@ -249,7 +259,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
volumeRow.orientation = .horizontal
volumeRow.spacing = 8
- let stack = NSStackView(views: [inputModeRow, vadRow, inputGainRow, pttRow, deviceRow,
+ let stack = NSStackView(views: [inputModeRow, vadRow, inputGainRow, nrCheckbox, pttRow, deviceRow,
levelRow, auxHeader, auxCheckbox, auxDeviceRow, auxGainRow,
notificationsHeader, soundsCheckbox, volumeRow,
speechCheckbox, selfTalkCheckbox, pttSoundCheckbox])
@@ -319,6 +329,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
inputGainSlider.doubleValue = Double(mc.inputGain * 100)
updateInputGainLabel()
+ nrCheckbox.state = mc.inputNoiseReduction ? .on : .off
auxCheckbox.state = mc.auxEnabled ? .on : .off
auxGainSlider.doubleValue = Double(mc.auxGain * 100)
@@ -371,6 +382,14 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
}
}
+ @objc private func nrChanged() {
+ let on = nrCheckbox.state == .on
+ mainController?.inputNoiseReduction = on
+ if let mc = mainController, mc.micStreamId != 0 {
+ client.setInputNoiseReduction(on)
+ }
+ }
+
private func updateInputGainLabel() {
let pct = Int(inputGainSlider.doubleValue.rounded())
inputGainValueLabel.stringValue = "\(pct)%"
diff --git a/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs b/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
index 3a85e01..691d098 100644
--- a/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
+++ b/clients/windows/VoiceCat.App/Forms/AudioSettingsForm.cs
@@ -27,6 +27,7 @@ public sealed class AudioSettingsForm : Form
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;
@@ -42,6 +43,7 @@ public sealed class AudioSettingsForm : Form
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;
@@ -67,6 +69,7 @@ public sealed class AudioSettingsForm : Form
_origMode = (VcInputMode)settings.InputMode;
_origVadSlider = settings.VadThresholdSlider;
_origMicGain = settings.MicGain;
+ _origMicNoiseReduction = settings.MicNoiseReduction;
_origPttKey = _pttKey;
_origAuxEnabled = settings.AuxEnabled;
_origAuxDeviceId = settings.AuxDeviceId;
@@ -78,7 +81,7 @@ public sealed class AudioSettingsForm : Form
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
AutoScaleMode = AutoScaleMode.Font;
- ClientSize = new Size(420, 555);
+ ClientSize = new Size(420, 583);
// ── Device row ────────────────────────────────────────────────────────
var lblDevice = new Label
@@ -199,6 +202,21 @@ public sealed class AudioSettingsForm : Form
"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;
+
// ── 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
@@ -206,7 +224,7 @@ public sealed class AudioSettingsForm : Form
_chkAux = new CheckBox
{
Text = "&Aux stream (second input device)",
- Location = new Point(12, 348),
+ Location = new Point(12, 376),
AutoSize = true,
Checked = settings.AuxEnabled,
AccessibleName = "Enable aux input stream",
@@ -218,12 +236,12 @@ public sealed class AudioSettingsForm : Form
_lblAuxDevice = new Label
{
Text = "Aux d&evice:",
- Location = new Point(12, 378),
+ Location = new Point(12, 406),
AutoSize = true,
};
_cboAuxDevice = new ComboBox
{
- Location = new Point(12, 398),
+ Location = new Point(12, 426),
Width = 300,
DropDownStyle = ComboBoxStyle.DropDownList,
DisplayMember = "Name",
@@ -236,7 +254,7 @@ public sealed class AudioSettingsForm : Form
_btnAuxRefresh = new Button
{
Text = "Re&fresh",
- Location = new Point(320, 396),
+ Location = new Point(320, 424),
Size = new Size(80, 26),
};
_btnAuxRefresh.Click += (_, _) => LoadAuxDevices();
@@ -244,12 +262,12 @@ public sealed class AudioSettingsForm : Form
_lblAuxGain = new Label
{
Text = "Aux vo&lume:",
- Location = new Point(12, 434),
+ Location = new Point(12, 462),
AutoSize = true,
};
_trkAuxGain = new TrackBar
{
- Location = new Point(12, 454),
+ Location = new Point(12, 482),
Size = new Size(200, 45),
Minimum = 0,
Maximum = 300,
@@ -268,14 +286,14 @@ public sealed class AudioSettingsForm : Form
{
Text = "&OK",
DialogResult = DialogResult.OK,
- Location = new Point(228, 516),
+ Location = new Point(228, 544),
Size = new Size(80, 27),
};
var btnCancel = new Button
{
Text = "&Cancel",
DialogResult = DialogResult.Cancel,
- Location = new Point(316, 516),
+ Location = new Point(316, 544),
Size = new Size(80, 27),
};
@@ -292,7 +310,7 @@ public sealed class AudioSettingsForm : Form
lblDevice, _cboDevice, _btnRefresh,
lblMode, _radioVad, _lblSensitivity, _trkVad,
_radioPtt, _lblPttKey, _btnChangePtt, _radioAlwaysOn,
- lblGain, _trkGain,
+ lblGain, _trkGain, _chkNoiseReduction,
_chkAux, _lblAuxDevice, _cboAuxDevice, _btnAuxRefresh, _lblAuxGain, _trkAuxGain,
btnOk, btnCancel,
]);
@@ -430,6 +448,13 @@ public sealed class AudioSettingsForm : Form
_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);
@@ -450,6 +475,7 @@ public sealed class AudioSettingsForm : Form
_settings.InputMode = (int)_origMode;
_settings.VadThresholdSlider = _origVadSlider;
_settings.MicGain = _origMicGain;
+ _settings.MicNoiseReduction = _origMicNoiseReduction;
_settings.PttKey = (int)_origPttKey;
if (_micStreamId != 0)
@@ -459,6 +485,7 @@ public sealed class AudioSettingsForm : Form
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
diff --git a/clients/windows/VoiceCat.App/Forms/MainForm.cs b/clients/windows/VoiceCat.App/Forms/MainForm.cs
index 1e81e74..4c3de16 100644
--- a/clients/windows/VoiceCat.App/Forms/MainForm.cs
+++ b/clients/windows/VoiceCat.App/Forms/MainForm.cs
@@ -650,6 +650,7 @@ public partial class MainForm : Form
if (mode == VcInputMode.VoiceActivation)
_client.SetVadThreshold(VadThresholdFromSettings());
_client.SetInputGain(_voiceSettings.MicGain / 100f);
+ _client.SetInputNoiseReduction(_voiceSettings.MicNoiseReduction);
SetVoiceJoinedState(true);
AddActivity("Joined voice — microphone active");
_feedback.PlaySound(SoundEvent.VoiceOn);
diff --git a/clients/windows/VoiceCat.App/Forms/PerUserTuningDialog.cs b/clients/windows/VoiceCat.App/Forms/PerUserTuningDialog.cs
index d1b78a1..b7de3c3 100644
--- a/clients/windows/VoiceCat.App/Forms/PerUserTuningDialog.cs
+++ b/clients/windows/VoiceCat.App/Forms/PerUserTuningDialog.cs
@@ -150,7 +150,7 @@ public sealed class PerUserTuningDialog : Form
var chkNr = new CheckBox
{
- Text = "&Noise reduction (planned — currently passthrough)",
+ Text = "&Noise reduction",
AutoSize = true,
Location = new Point(96, y + 48),
Checked = nr0,
diff --git a/clients/windows/VoiceCat.App/Models/VoiceSettings.cs b/clients/windows/VoiceCat.App/Models/VoiceSettings.cs
index 9b09795..7dbdc94 100644
--- a/clients/windows/VoiceCat.App/Models/VoiceSettings.cs
+++ b/clients/windows/VoiceCat.App/Models/VoiceSettings.cs
@@ -21,6 +21,11 @@ public sealed class VoiceSettings
/// Microphone input gain slider position, 0–300 percent (100 = unity).
public int MicGain { get; set; } = 100;
+ /// Send-side mic noise reduction (RNNoise) toggle. MIC stream only, mono only;
+ /// denoises captured mic PCM before input gain and VAD/PTT gate so everyone hears the
+ /// cleaned signal. Independent of the per-listener receive-side NR.
+ public bool MicNoiseReduction { get; set; } = false;
+
/// Push-to-talk key, stored as the integer value of System.Windows.Forms.Keys.
public int PttKey { get; set; } = (int)Keys.F8;
diff --git a/clients/windows/VoiceCat.Interop/NativeMethods.cs b/clients/windows/VoiceCat.Interop/NativeMethods.cs
index cc54aaf..35f776b 100644
--- a/clients/windows/VoiceCat.Interop/NativeMethods.cs
+++ b/clients/windows/VoiceCat.Interop/NativeMethods.cs
@@ -83,6 +83,9 @@ internal static partial class NativeMethods
[LibraryImport(LibName)]
internal static partial VcResult vc_set_input_gain(nint c, float gain);
+ [LibraryImport(LibName)]
+ internal static partial VcResult vc_set_input_noise_reduction(nint c, int enable);
+
[LibraryImport(LibName)]
internal static partial VcResult vc_set_remote_stream(nint c, uint userId, uint streamId,
float gain, int muted, int noiseReduction);
diff --git a/clients/windows/VoiceCat.Interop/VoiceCatClient.cs b/clients/windows/VoiceCat.Interop/VoiceCatClient.cs
index bee65d7..501a14e 100644
--- a/clients/windows/VoiceCat.Interop/VoiceCatClient.cs
+++ b/clients/windows/VoiceCat.Interop/VoiceCatClient.cs
@@ -232,6 +232,13 @@ public sealed class VoiceCatClient : IDisposable
public VcResult SetInputGain(float gain) =>
NativeMethods.vc_set_input_gain(_handle.DangerousGetHandle(), gain < 0f ? 0f : gain);
+ /// Send-side microphone noise suppression (RNNoise). Denoises captured MIC PCM before
+ /// the input gain and VAD/PTT gate, so everyone hears the cleaned signal. MIC stream only,
+ /// mono only; always LOCAL — no protocol traffic. Independent of per-listener receive-side
+ /// NR in .
+ public VcResult SetInputNoiseReduction(bool enable) =>
+ NativeMethods.vc_set_input_noise_reduction(_handle.DangerousGetHandle(), enable ? 1 : 0);
+
public VcResult SetRemoteStream(uint userId, uint streamId, float gain, bool muted, bool noiseReduction) =>
NativeMethods.vc_set_remote_stream(_handle.DangerousGetHandle(), userId, streamId, gain,
muted ? 1 : 0, noiseReduction ? 1 : 0);