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.
This commit is contained in:
2026-06-23 14:11:18 +02:00
parent bad9c7533a
commit 2e0e0caccb
11 changed files with 110 additions and 12 deletions

View File

@@ -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)%"