import AVFoundation import Foundation #if os(iOS) import UIKit #endif /// Where the system's microphone mode is read from, and how its picker is /// opened. A protocol only so the tests can stand in for AVCaptureDevice, /// whose mode is a class property that a test cannot set: Apple lets the /// person choose the mode and lets an app do no more than look at it and /// open the picker. public protocol MicrophoneModeSource: AnyObject { /// What the person chose in Control Center. var preferred: AVCaptureDevice.MicrophoneMode { get } /// What the microphone is actually doing, which can differ when the /// route cannot honour the choice. var active: AVCaptureDevice.MicrophoneMode { get } /// Called whenever either may have changed, from any thread. var changed: (@Sendable () -> Void)? { get set } func showPicker() } /// The iPhone's microphone mode — Standard, Voice Isolation or Wide /// Spectrum — shown in words, with the way to Apple's picker beside it. /// /// Voice Isolation is the best noise suppression the phone has: wind, /// typing and a room's echo, taken out before WebRTC ever sees the audio. /// It is also not ours to switch on. There is no setter; the mode belongs /// to the person and lives in Control Center, and the most an app can do /// is say which one is in force and open the picker at the right page. /// /// It applies to Calls because the iOS WebRTC audio device runs on /// Apple's voice-processing I/O unit, under a `.videoChat` session, and /// the mic modes are voice-processing modes. The Mac app is deliberately /// left out: WebRTC's macOS audio device does not use voice processing, /// so the mode would not reach the call, and the Mac audio is the last /// place a change nobody needed should be made. @MainActor public final class MicrophoneModes: ObservableObject { @Published public private(set) var preferred: AVCaptureDevice.MicrophoneMode @Published public private(set) var active: AVCaptureDevice.MicrophoneMode private let source: MicrophoneModeSource public init(source: MicrophoneModeSource) { self.source = source preferred = source.preferred active = source.active source.changed = { [weak self] in Task { @MainActor in self?.refresh() } } } #if os(iOS) public convenience init() { self.init(source: SystemMicrophoneModes()) } #endif public func refresh() { preferred = source.preferred active = source.active } public func showPicker() { source.showPicker() } /// The mode in words. When the route has overridden the choice both are /// named, because "Voice Isolation" on its own would be a claim about /// audio the call is not getting. public var summary: String { MicrophoneModes.summary(preferred: preferred, active: active) } public nonisolated static func summary( preferred: AVCaptureDevice.MicrophoneMode, active: AVCaptureDevice.MicrophoneMode ) -> String { if preferred == active { return name(active) } return "\(name(active)) — \(name(preferred)) is chosen, but this microphone cannot use it." } /// Apple's own names for the modes, so the words here match the ones in /// Control Center. public nonisolated static func name(_ mode: AVCaptureDevice.MicrophoneMode) -> String { switch mode { case .standard: return "Standard" case .voiceIsolation: return "Voice Isolation" case .wideSpectrum: return "Wide Spectrum" @unknown default: return "Unknown" } } } #if os(iOS) /// The real thing: AVCaptureDevice's class properties, key-value observed. /// /// They are class properties, so the observation is registered on the /// class object itself, which Swift's key-path `observe` cannot express. /// Becoming active is watched as well: the picker is Control Center, so /// the change happens while Calls is in the background, and returning is /// the moment the panel most needs to be right. final class SystemMicrophoneModes: NSObject, MicrophoneModeSource { private static let keys = ["preferredMicrophoneMode", "activeMicrophoneMode"] var changed: (@Sendable () -> Void)? private var returning: NSObjectProtocol? override init() { super.init() for key in Self.keys { (AVCaptureDevice.self as AnyObject).addObserver(self, forKeyPath: key, options: [.new], context: nil) } returning = NotificationCenter.default.addObserver( forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main ) { [weak self] _ in self?.changed?() } } deinit { for key in Self.keys { (AVCaptureDevice.self as AnyObject).removeObserver(self, forKeyPath: key) } if let returning { NotificationCenter.default.removeObserver(returning) } } var preferred: AVCaptureDevice.MicrophoneMode { AVCaptureDevice.preferredMicrophoneMode } var active: AVCaptureDevice.MicrophoneMode { AVCaptureDevice.activeMicrophoneMode } func showPicker() { AVCaptureDevice.showSystemUserInterface(.microphoneModes) } override func observeValue( forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer? ) { changed?() } } #endif