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

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

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