@ -994,7 +994,8 @@ private func checkStreamPlaybackCompletedIfNeeded() {
hasNotifiedPlaybackStartedForStream = false
hasNotifiedPlaybackStartedForStream = false
// o s _ l o g ( " 调 用 链 : 流 式 播 放 完 成 s e s s i o n = % { p u b l i c } @ " , l o g : l o g , t y p e : . i n f o , s e s s i o n i d )
// o s _ l o g ( " 调 用 链 : 流 式 播 放 完 成 s e s s i o n = % { p u b l i c } @ " , l o g : l o g , t y p e : . i n f o , s e s s i o n i d )
notifyEvent ( eventType : . playbackCompleted )
notifyEvent ( eventType : . playbackCompleted )
deactivateAudioSessionAfterTTSIfIdle ( )
// 段 完 成 : 防 抖 后 再 释 放 s e s s i o n , 避 免 段 间 隙 被 外 部 A p p 抢 回
scheduleDebouncedDeactivate ( )
}
}
}
}
@ -1269,7 +1270,8 @@ public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully fl
notifyEvent ( eventType : . playbackCompleted )
notifyEvent ( eventType : . playbackCompleted )
// 如 果 还 有 剩 余 , 继 续 播 放
// 如 果 还 有 剩 余 , 继 续 播 放
startPlaybackIfNeeded ( )
startPlaybackIfNeeded ( )
deactivateAudioSessionAfterTTSIfIdle ( )
// 段 完 成 : 防 抖 后 再 释 放 s e s s i o n , 避 免 段 间 隙 被 外 部 A p p 抢 回
scheduleDebouncedDeactivate ( )
}
}
/* *
/* *
@ -1418,11 +1420,118 @@ private var shouldDeactivateAudioSessionWhenIdle = false
// / 外 部 调 用 方 ( 如 A g e n t S e r v i c e I m p l ) 在 本 轮 唤 醒 / 识 别 开 始 时 设 置 :
// / 外 部 调 用 方 ( 如 A g e n t S e r v i c e I m p l ) 在 本 轮 唤 醒 / 识 别 开 始 时 设 置 :
// / - t r u e 表 示 唤 醒 前 有 外 部 或 内 部 音 乐 在 放 , T T S 空 闲 释 放 s e s s i o n 时 应 带 上 . n o t i f y O t h e r s O n D e a c t i v a t i o n 让 被 打 断 的 A p p 恢 复 播 放
// / - t r u e 表 示 唤 醒 前 有 外 部 或 内 部 音 乐 在 放 , T T S 空 闲 释 放 s e s s i o n 时 应 带 上 . n o t i f y O t h e r s O n D e a c t i v a t i o n 让 被 打 断 的 A p p 恢 复 播 放
// / - 成 功 d e a c t i v a t e 后 由 本 类 自 动 清 零
// / - 成 功 d e a c t i v a t e 后 由 本 类 自 动 清 零
public var notifyOthersOnNextDeactivate : Bool = false
public var notifyOthersOnNextDeactivate : Bool = false {
didSet {
// 新 一 轮 对 话 要 求 通 知 外 部 恢 复 时 , 重 置 重 试 预 算 , 避 免 上 一 轮 耗 尽 导 致 本 轮 永 不 重 试
if notifyOthersOnNextDeactivate && ! oldValue {
deactivateRetryCount = 0
}
}
}
// 新 增 : 路 由 变 化 的 去 抖 与 配 置 重 入 保 护 标 记
// 新 增 : 路 由 变 化 的 去 抖 与 配 置 重 入 保 护 标 记
private var routeChangeDebounceWorkItem : DispatchWorkItem ?
private var routeChangeDebounceWorkItem : DispatchWorkItem ?
private var isApplyingAudioSessionConfig = false
private var isApplyingAudioSessionConfig = false
// / d e a c t i v a t e 重 试 调 度 : p l a y e r N o d e 残 留 i s P l a y i n g / s e t A c t i v e ( f a l s e ) 抛 I s B u s y 时 ,
// / 用 退 避 t i m e r 兜 底 再 试 , 避 免 外 部 音 乐 因 一 次 失 败 永 远 无 法 恢 复 。
private let deactivateRetryDelays : [ TimeInterval ] = [ 0.3 , 0.8 , 1.5 ]
private var deactivateRetryCount : Int = 0
private var pendingDeactivateRetryWorkItem : DispatchWorkItem ?
// / 由 业 务 层 ( A g e n t S e r v i c e I m p l ) 注 入 : 返 回 t r u e 表 示 A I / A S R 仍 在 处 理 本 轮 对 话 ,
// / 即 使 T T S 段 已 播 完 也 不 应 d e a c t i v a t e — — 否 则 外 部 A p p 会 在 A I 流 式 段 间 被 反 复
// / " 打 断 - 恢 复 " , 多 轮 折 腾 后 再 也 不 自 动 恢 复 。
public var externalBusyProvider : ( ( ) -> Bool ) ?
// / 由 业 务 层 注 入 , 用 于 把 T T S 音 频 会 话 相 关 的 关 键 日 志 转 发 到 a p p 侧 日 志 通 道 ( 可 导 出 ) 。
// / 不 注 入 时 仅 写 O S L o g 。
public var exportLog : ( ( String , String ) -> Void ) ?
// / 我 们 自 己 追 踪 的 s e s s i o n 激 活 状 态 — — s e t A c t i v e ( t r u e ) 成 功 置 t r u e , s e t A c t i v e ( f a l s e ) 成 功 置 f a l s e 。
// / 用 于 a p p l y A u d i o S e s s i o n F o r T T S 的 快 速 路 径 : 已 激 活 且 c a t e g o r y / m o d e 匹 配 时 不 重 复 s e t A c t i v e ( t r u e ) ,
// / 避 免 P C M c h u n k 级 的 重 入 疯 狂 触 发 系 统 调 用 ( 实 测 单 轮 A I 回 复 可 调 2 5 0 + 次 ) 。
private var internalSessionActive : Bool = false
// / 统 一 打 印 : O S L o g + a p p 导 出 通 道 双 写 。
// / t a g 用 固 定 前 缀 ` [ T T S / A u d i o ] ` 方 便 在 导 出 日 志 里 过 滤 。
private func dlog ( _ level : String = " INFO " , _ message : String ) {
let line = " [TTS/Audio] " + message
switch level {
case " ERROR " :
os_log ( " %{public}@ " , log : self . log , type : . error , line )
case " WARNING " :
os_log ( " %{public}@ " , log : self . log , type : . default , line )
case " DEBUG " :
os_log ( " %{public}@ " , log : self . log , type : . debug , line )
default :
os_log ( " %{public}@ " , log : self . log , type : . info , line )
}
exportLog ? ( level , line )
}
// / A z u r e 流 式 T T S 按 标 点 切 段 , 每 段 都 会 发 p l a y b a c k C o m p l e t e d 。
// / 若 每 段 都 立 即 d e a c t i v a t e ( n o t i f y O t h e r s ) , 段 间 隙 外 部 A p p 会 短 暂 抢 回 播 放
// / — — 体 感 就 是 " A I 说 到 一 半 外 部 音 乐 冒 出 来 " 。
// / 用 1 s d e b o u n c e : p l a y b a c k C o m p l e t e d → 延 迟 释 放 ; 期 间 新 段 开 始 则 取 消 p e n d i n g 。
private let deactivateDebounceDelay : TimeInterval = 1.0
private var pendingDeactivateWorkItem : DispatchWorkItem ?
// / 段 间 防 抖 d e a c t i v a t e : 延 迟 1 s 后 调 用 d e a c t i v a t e A u d i o S e s s i o n A f t e r T T S I f I d l e 。
// / 新 段 开 始 播 ( a p p l y A u d i o S e s s i o n F o r T T S ) 会 取 消 p e n d i n g 。
// / 注 意 : 业 务 层 ( A g e n t S e r v i c e I m p l ) 可 能 在 A I 终 态 时 调 用 本 方 法 , 而 此 时 T T S 从 未
// / 实 际 a p p l y 过 s e s s i o n ( 例 如 A I 无 文 本 产 出 ) , s h o u l d D e a c t i v a t e A u d i o S e s s i o n W h e n I d l e 还 是 f a l s e 。
// / 为 了 让 A S R 激 活 但 未 释 放 的 s e s s i o n 也 能 被 释 放 以 通 知 外 部 A p p , 这 里 显 式 打 开 该 标 志 。
public func scheduleDebouncedDeactivate ( ) {
dlog ( " INFO " , " 防抖 deactivate 排队 delay= \( String ( format : " %.2f " , deactivateDebounceDelay ) ) s notifyOthers= \( notifyOthersOnNextDeactivate ) externalBusy= \( externalBusyProvider ? ( ) ? ? false ) " )
shouldDeactivateAudioSessionWhenIdle = true
cancelPendingDebouncedDeactivate ( )
let work = DispatchWorkItem { [ weak self ] in
guard let self = self else { return }
self . pendingDeactivateWorkItem = nil
self . dlog ( " INFO " , " 防抖 deactivate 触发执行 " )
self . deactivateAudioSessionAfterTTSIfIdle ( )
}
pendingDeactivateWorkItem = work
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + deactivateDebounceDelay , execute : work )
}
// / 取 消 等 待 中 的 防 抖 d e a c t i v a t e 。
public func cancelPendingDebouncedDeactivate ( ) {
if pendingDeactivateWorkItem != nil {
dlog ( " INFO " , " 取消 pending 防抖 deactivate " )
}
pendingDeactivateWorkItem ? . cancel ( )
pendingDeactivateWorkItem = nil
}
// / 调 度 下 一 次 d e a c t i v a t e 重 试 ; 若 已 到 上 限 或 已 在 排 队 则 不 重 复 调 度 。
private func scheduleDeactivateRetry ( reason : String ) {
if pendingDeactivateRetryWorkItem != nil { return }
if deactivateRetryCount >= deactivateRetryDelays . count {
dlog ( " WARNING " , " deactivate 重试已达上限( \( deactivateRetryDelays . count ) ) reason= \( reason ) " )
return
}
let delay = deactivateRetryDelays [ deactivateRetryCount ]
deactivateRetryCount += 1
dlog ( " INFO " , " 安排 deactivate 重试 # \( deactivateRetryCount ) delay= \( String ( format : " %.2f " , delay ) ) s reason= \( reason ) " )
let work = DispatchWorkItem { [ weak self ] in
guard let self = self else { return }
self . pendingDeactivateRetryWorkItem = nil
self . deactivateAudioSessionAfterTTSIfIdle ( )
}
pendingDeactivateRetryWorkItem = work
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + delay , execute : work )
}
// / 取 消 等 待 中 的 d e a c t i v a t e 重 试 ( 新 T T S 段 开 始 时 调 用 ) 。
private func cancelPendingDeactivateRetry ( ) {
if pendingDeactivateRetryWorkItem != nil {
dlog ( " INFO " , " 取消 pending deactivate 重试 " )
}
pendingDeactivateRetryWorkItem ? . cancel ( )
pendingDeactivateRetryWorkItem = nil
}
private func isTtsBusy ( ) -> Bool {
private func isTtsBusy ( ) -> Bool {
if speaking { return true }
if speaking { return true }
if isPlaying { return true }
if isPlaying { return true }
@ -1461,7 +1570,22 @@ private func ensureAudioSessionQueueSpecificKeySet() {
*/
*/
private func applyAudioSessionForTTS ( ) -> Bool {
private func applyAudioSessionForTTS ( ) -> Bool {
var success = true
var success = true
// o s _ l o g ( " l i w e i - - - - - - - - - - - - - T T S a p p l y A u d i o S e s s i o n F o r T T S c u r r e n t R e c o g n i t i o n M o d e : % { p u b l i c } @ " , l o g : s e l f . l o g , t y p e : . i n f o , c u r r e n t R e c o g n i t i o n M o d e )
// 新 段 即 将 开 始 播 : 取 消 段 间 隙 p e n d i n g 的 d e a c t i v a t e + r e t r y , 避 免 中 途 释 放 s e s s i o n
cancelPendingDebouncedDeactivate ( )
cancelPendingDeactivateRetry ( )
// 快 速 路 径 : s e s s i o n 已 激 活 且 c a t e g o r y / m o d e 匹 配 当 前 模 式 预 期 时 直 接 返 回 ,
// 避 免 P C M c h u n k 级 调 用 触 发 2 5 0 + 次 s e t A c t i v e ( t r u e ) 对 外 部 A p p 的 反 复 干 扰
let sessionSnapshot = AVAudioSession . sharedInstance ( )
let isBlePlaybackMode = ( currentRecognitionMode = = " push_to_talk "
|| currentRecognitionMode = = " normal "
|| currentRecognitionMode = = " ble_wakeup " )
if internalSessionActive && ! audioInterrupted && isBlePlaybackMode
&& sessionSnapshot . category = = . playback
&& sessionSnapshot . mode = = . spokenAudio {
return true
}
dlog ( " INFO " , " applyAudioSessionForTTS 入口 mode= \( currentRecognitionMode ) otherAudioPlaying= \( sessionSnapshot . isOtherAudioPlaying ) category= \( sessionSnapshot . category . rawValue ) sessionMode= \( sessionSnapshot . mode . rawValue ) internalActive= \( internalSessionActive ) " )
ensureAudioSessionQueueSpecificKeySet ( )
ensureAudioSessionQueueSpecificKeySet ( )
let applyBody = {
let applyBody = {
if self . isApplyingAudioSessionConfig { return }
if self . isApplyingAudioSessionConfig { return }
@ -1536,6 +1660,7 @@ private func applyAudioSessionForTTS() -> Bool {
_ = try ? audioSession . setPreferredIOBufferDuration ( 0.02 )
_ = try ? audioSession . setPreferredIOBufferDuration ( 0.02 )
}
}
try audioSession . setActive ( true )
try audioSession . setActive ( true )
self . internalSessionActive = true
if self . currentRecognitionMode = = " phone_call " , ! hasHeadphones {
if self . currentRecognitionMode = = " phone_call " , ! hasHeadphones {
_ = try ? audioSession . overrideOutputAudioPort ( . speaker )
_ = try ? audioSession . overrideOutputAudioPort ( . speaker )
}
}
@ -1627,40 +1752,101 @@ private func isTelephonyActive() -> Bool {
*/
*/
private func deactivateAudioSessionAfterTTSIfIdle ( ) {
private func deactivateAudioSessionAfterTTSIfIdle ( ) {
ensureAudioSessionQueueSpecificKeySet ( )
ensureAudioSessionQueueSpecificKeySet ( )
dlog ( " INFO " , " deactivateAudioSessionAfterTTSIfIdle 入口 " )
let work = { [ weak self ] in
let work = { [ weak self ] in
guard let self = self else { return }
guard let self = self else { return }
if self . micCapture ? . isCapturing = = true { return }
// 注 : 这 里 刻 意 不 再 用 e x t e r n a l B u s y s k i p 。
if self . speaking { return }
// 之 前 试 过 " A I 还 在 s t r e a m i n g 时 段 间 s k i p " — — 会 导 致 整 轮 对 话 只 在 最 终 发 一 次
if self . isPlaying { return }
// n o t i f y O t h e r s O n D e a c t i v a t i o n , 有 些 外 部 A p p ( 尤 其 第 三 方 音 乐 ) 一 次 信 号 来 不
if ! self . playbackQueue . isEmpty { return }
// 及 / 会 被 吃 掉 , 表 现 为 " 完 全 不 恢 复 " 。
if self . pendingTextCount > 0 { return }
// 放 行 后 段 间 会 反 复 d e a c t i v a t e ( n o t i f y O t h e r s ) , 外 部 A p p 能 持 续 收 到 e n d e d ,
if ! self . pendingTasks . isEmpty { return }
// 虽 然 有 段 间 短 暂 冒 音 的 副 作 用 , 但 能 稳 定 恢 复 。
if self . activeStreamSynthesisCount > 0 { return }
if self . externalBusyProvider ? ( ) = = true {
if ! self . pcmPendingData . isEmpty { return }
self . dlog ( " INFO " , " external busy (AI/ASR 仍忙)——仍允许 deactivate,外部 App 稳定恢复优先 " )
if self . scheduledBufferCount > 0 { return }
}
if self . micCapture ? . isCapturing = = true {
self . dlog ( " INFO " , " deactivate skip: mic capturing,排重试 " )
self . scheduleDeactivateRetry ( reason : " micCapturingOuter " )
return
}
if self . speaking {
self . dlog ( " INFO " , " deactivate skip: speaking=true " )
return
}
if self . isPlaying {
self . dlog ( " INFO " , " deactivate skip: isPlaying=true " )
return
}
if ! self . playbackQueue . isEmpty {
self . dlog ( " INFO " , " deactivate skip: playbackQueue= \( self . playbackQueue . count ) " )
return
}
if self . pendingTextCount > 0 {
self . dlog ( " INFO " , " deactivate skip: pendingTextCount= \( self . pendingTextCount ) " )
return
}
if ! self . pendingTasks . isEmpty {
self . dlog ( " INFO " , " deactivate skip: pendingTasks= \( self . pendingTasks . count ) " )
return
}
if self . activeStreamSynthesisCount > 0 {
self . dlog ( " INFO " , " deactivate skip: activeStreamSynthesisCount= \( self . activeStreamSynthesisCount ) " )
return
}
if ! self . pcmPendingData . isEmpty {
self . dlog ( " INFO " , " deactivate skip: pcmPendingData= \( self . pcmPendingData . count ) " )
return
}
if self . scheduledBufferCount > 0 {
self . dlog ( " INFO " , " deactivate skip: scheduledBufferCount= \( self . scheduledBufferCount ) " )
return
}
dlog ( " INFO " , " deactivate 进入 playbackQueue:准备 stop engine/playerNode " )
self . audioPlaybackQueue . async { [ weak self ] in
self . audioPlaybackQueue . async { [ weak self ] in
guard let self = self else { return }
guard let self = self else { return }
if self . playerNode ? . isPlaying = = true { return }
let wasPlaying = self . playerNode ? . isPlaying = = true
// A : p l a y e r N o d e 若 仍 标 记 i s P l a y i n g ( b u f f e r 播 完 后 f l a g 未 翻 转 也 会 落 在 这 ) ,
// 不 再 直 接 r e t u r n 放 任 s e s s i o n 挂 住 , 而 是 主 动 s t o p 再 继 续 。
self . playerNode ? . stop ( )
self . playerNode ? . stop ( )
self . audioEngine ? . stop ( )
self . audioEngine ? . stop ( )
self . audioEngine = nil
self . audioEngine = nil
self . playerNode = nil
self . playerNode = nil
self . pcmFormat = nil
self . pcmFormat = nil
if self . shouldDeactivateAudioSessionWhenIdle {
self . dlog ( " INFO " , " deactivate 清理完成 wasPlayerPlaying= \( wasPlaying ) shouldDeactivate= \( self . shouldDeactivateAudioSessionWhenIdle ) notifyOthers= \( self . notifyOthersOnNextDeactivate ) " )
guard self . shouldDeactivateAudioSessionWhenIdle else {
self . dlog ( " INFO " , " deactivate 跳过:shouldDeactivateAudioSessionWhenIdle=false " )
return
}
// 麦 克 风 仍 在 采 集 时 , s e s s i o n 还 被 A S R 持 有 , d e a c t i v a t e 必 失 败
// 麦 克 风 仍 在 采 集 时 , s e s s i o n 还 被 A S R 持 有 , d e a c t i v a t e 必 失 败
// 保 留 标 记 等 下 次 i d l e 再 试 , 避 免 标 记 被 清 空 后 永 远 不 再 触 发
// 保 留 标 记 等 下 次 i d l e 再 试 , 避 免 标 记 被 清 空 后 永 远 不 再 触 发
if self . micCapture ? . isCapturing = = true { return }
if self . micCapture ? . isCapturing = = true {
self . dlog ( " INFO " , " deactivate 跳过:mic 仍在采集,排重试 " )
self . scheduleDeactivateRetry ( reason : " micCapturing " )
return
}
let opts : AVAudioSession . SetActiveOptions =
let session = AVAudioSession . sharedInstance ( )
self . notifyOthersOnNextDeactivate ? . notifyOthersOnDeactivation : [ ]
let otherAudioPlaying = session . isOtherAudioPlaying
// 总 是 带 上 . n o t i f y O t h e r s O n D e a c t i v a t i o n :
// - 这 是 系 统 h i n t , 只 对 本 应 用 实 际 暂 停 过 的 外 部 A p p 生 效 , 未 被 暂 停 的 A p p 不 受 影 响
// - 之 前 依 赖 n o t i f y O t h e r s O n N e x t D e a c t i v a t e 标 志 , 但 多 轮 对 话 里 标 志 会 被
// " s e t A c t i v e 成 功 后 清 零 + 下 轮 s t a r t R e c o g n i t i o n 读 到 i s O t h e r A u d i o P l a y i n g = f a l s e 覆 盖 "
// 组 合 攻 击 擦 掉 , 导 致 外 部 音 乐 恢 复 不 了
let opts : AVAudioSession . SetActiveOptions = . notifyOthersOnDeactivation
self . dlog ( " INFO " , " 准备 setActive(false) flagHint= \( self . notifyOthersOnNextDeactivate ) otherAudioPlaying= \( otherAudioPlaying ) category= \( session . category . rawValue ) mode= \( session . mode . rawValue ) " )
do {
do {
try AVAudioSession . sharedInstance ( ) . setActive ( false , options : opts )
try session . setActive ( false , options : opts )
self . dlog ( " INFO " , " ✅ setActive(false) 成功(已带 notifyOthers) " )
self . shouldDeactivateAudioSessionWhenIdle = false
self . shouldDeactivateAudioSessionWhenIdle = false
self . notifyOthersOnNextDeactivate = false
self . notifyOthersOnNextDeactivate = false
self . internalSessionActive = false
self . deactivateRetryCount = 0
} catch {
} catch {
// 失 败 保 留 标 记 , 下 次 i d l e 再 尝 试 ( 常 见 原 因 : 其 他 A u d i o U n i t 仍 在 运 行 )
let nsError = error as NSError
os_log ( " TTS deactivate 失败,保留标记待下次重试: %{public}@ " , log : self . log , type : . info , error . localizedDescription )
// B : 失 败 保 留 标 记 , 并 排 一 次 退 避 重 试 ( 0 . 3 → 0 . 8 → 1 . 5 s ) 。
}
// 常 见 原 因 : 其 他 A u d i o U n i t 仍 在 收 尾 ( I s B u s y ) 。
self . dlog ( " ERROR " , " ❌ setActive(false) 失败 domain= \( nsError . domain ) code= \( nsError . code ) msg= \( error . localizedDescription ) ,安排退避重试 " )
self . scheduleDeactivateRetry ( reason : " setActiveFailed " )
}
}
}
}
}
}
@ -1685,6 +1871,8 @@ private func deactivateAudioSessionAfterTTSIfIdle() {
switch type {
switch type {
case . began :
case . began :
audioInterrupted = true
audioInterrupted = true
// 系 统 已 挂 起 我 们 的 s e s s i o n , 内 部 激 活 状 态 置 f a l s e , 让 e n d e d 后 的 重 配 能 走 完 整 a p p l y 流 程
internalSessionActive = false
_ = stop ( )
_ = stop ( )
os_log ( " 音频中断开始(可能为通话/Siri),停止TTS " , log : log , type : . info )
os_log ( " 音频中断开始(可能为通话/Siri),停止TTS " , log : log , type : . info )
case . ended :
case . ended :