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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)%"
|
||||
|
||||
Reference in New Issue
Block a user