import AVFoundation import VoiceCatCore @MainActor final class AudioSessionManager { static let shared = AudioSessionManager() weak var client: VoiceCatClient? func configure() { // Load stored audio routing preferences and apply them before any audio session // activation. IOSAudioRouter drives all iOS audio route selection via AVAudioSession; // miniaudio (the core) does NOT touch AVAudioSession on iOS. IOSAudioRouter.shared.loadStoredPreferences() IOSAudioRouter.shared.applyConfiguration() IOSAudioRouter.shared.refreshRoutes() NotificationCenter.default.addObserver( self, selector: #selector(handleInterruption), name: AVAudioSession.interruptionNotification, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: nil) } func activateForStreaming() throws { // Re-apply the routing configuration before activating, in case the user changed // settings since the last apply. The core (miniaudio) will open whatever route // AVAudioSession has established. IOSAudioRouter.shared.applyConfiguration() try AVAudioSession.sharedInstance().setActive(true, options: []) } func deactivateAfterStreaming() { try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } @objc private func handleInterruption(_ notification: Notification) { guard let info = notification.userInfo, let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } switch type { case .began: client?.audioSuspend() case .ended: let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0 let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) if options.contains(.shouldResume) { try? AVAudioSession.sharedInstance().setActive(true) client?.audioResume() } @unknown default: break } } @objc private func handleRouteChange(_ notification: Notification) { // Refresh the router's published state so the Settings UI updates, and re-apply // the stored preferences (the new route may need the preferred input re-set). IOSAudioRouter.shared.refreshRoutes() IOSAudioRouter.shared.applyConfiguration() NotificationCenter.default.post(name: .voiceCatDeviceListChanged, object: nil) } } extension Notification.Name { static let voiceCatDeviceListChanged = Notification.Name("cat.voice.deviceListChanged") }