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:
@@ -1,5 +1,6 @@
|
|||||||
import Flutter
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Flutter
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
@@ -7,6 +8,33 @@ import UIKit
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> 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)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,9 +261,25 @@ impl AudioEngine {
|
|||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
{
|
{
|
||||||
if cfg.mobile_voice_preset {
|
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!(
|
info!(
|
||||||
target: "chanora_audio",
|
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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user