feat(audio,ios): AVAudioSession PlayAndRecord+voiceChat in AppDelegate

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).
This commit is contained in:
EdisonJwa
2026-05-16 13:50:43 +08:00
parent 383b707e7c
commit f3320715ea
2 changed files with 46 additions and 2 deletions
@@ -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)
}