diff --git a/apps/chanora_flutter/ios/Runner/AppDelegate.swift b/apps/chanora_flutter/ios/Runner/AppDelegate.swift index c30b367..08764f5 100644 --- a/apps/chanora_flutter/ios/Runner/AppDelegate.swift +++ b/apps/chanora_flutter/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ -import Flutter import UIKit +import Flutter +import AVFoundation @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { @@ -7,6 +8,33 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + // Configure the iOS AVAudioSession for voice chat BEFORE Flutter + // starts the audio engine. The category + mode combination tells + // iOS to: + // * route via the receiver/speaker like a phone call + // (`.playAndRecord` + `.voiceChat`) + // * engage hardware AEC / NS where the device supports it + // * default the speaker output (so the user doesn't have to + // hold the phone to their ear) + // * permit Bluetooth headsets (so AirPods et al. just work) + // + // SRS-197 covers the iOS audio routing contract; this is the + // matching iOS-side implementation. Failures are logged but do + // not block app launch — the audio engine will still come up, + // just at the iOS default playback route. + do { + let session = AVAudioSession.sharedInstance() + try session.setCategory( + .playAndRecord, + mode: .voiceChat, + options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP] + ) + try session.setActive(true, options: []) + NSLog("chanora_flutter: AVAudioSession configured (playAndRecord/voiceChat)") + } catch { + NSLog("chanora_flutter: AVAudioSession setup failed: \(error)") + } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index 22e87ff..de046b9 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -261,9 +261,25 @@ impl AudioEngine { #[cfg(target_os = "ios")] { if cfg.mobile_voice_preset { + // iOS AVAudioSession configuration is performed + // Swift-side in `apps/chanora_flutter/ios/Runner/ + // AppDelegate.swift::application(_:didFinishLaunching\ + // WithOptions:)` BEFORE Flutter starts its audio + // pipeline. The category/mode set there + // (`.playAndRecord` + `.voiceChat`, + // `defaultToSpeaker | allowBluetooth | + // allowBluetoothA2DP`) is the recommended iOS shape + // for voice clients and engages on-device AEC / NS + // routing where supported. cpal's CoreAudio + // backend then opens its streams against that + // session and inherits the routing. Logging here + // just records that the engine-start path + // acknowledges the request; the actual session + // mutation lives in Swift because it must happen + // before Dart loads. info!( target: "chanora_audio", - "ios: voice-chat session mode requested (binding pending — Chanora iOS audio is documented-only for Beta)" + "ios: voice-chat session mode requested — AVAudioSession configured in AppDelegate" ); } }