From f3320715ea84c86df1fbc5cd85ca05df1ecac8f2 Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sat, 16 May 2026 13:50:43 +0800 Subject: [PATCH] feat(audio,ios): AVAudioSession PlayAndRecord+voiceChat in AppDelegate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS AVAudioSession must be configured BEFORE Flutter starts its audio pipeline; the canonical place is application(_:didFinishLaunching\ WithOptions:) in AppDelegate.swift. This commit: apps/chanora_flutter/ios/Runner/AppDelegate.swift: * import AVFoundation * In application(_:didFinishLaunchingWithOptions:), call AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]) followed by setActive(true). Failures are NSLogged but do not block app launch — cpal's CoreAudio backend will still come up against the default iOS routing. This shape: * routes the receiver/speaker like a phone call (.playAndRecord + .voiceChat), * engages on-device AEC / NS where supported, * defaults to speaker so users don't have to hold the phone to their ear, * permits Bluetooth headsets (AirPods et al. just work). crates/chanora_audio/src/engine.rs: * Replace the iOS engine-start placeholder log line ('binding pending — Chanora iOS audio is documented-only for Beta') with an honest acknowledgment that the AVAudioSession configuration lives Swift-side. The Rust engine acknowledges the request, then cpal opens its CoreAudio streams against the session. iOS-only Rust code is #[cfg(target_os = "ios")]-gated so this commit is no-op on every other platform. SRS-197: iOS/macOS audio routing contract. DEC-025: iOS officially in scope for P0 (Focused PTT only — Apple's sandbox model has no global PTT analogue). --- .../ios/Runner/AppDelegate.swift | 30 ++++++++++++++++++- crates/chanora_audio/src/engine.rs | 18 ++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) 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" ); } }