Browse Source

fix: 修复语音识别与TTS播放的音频会话冲突

修复iOS端音频会话冲突问题,避免语音识别时TTS仍在播放导致无声或异常。
- 注释掉AgentServiceImpl中初始化时设置识别模式和音频会话的代码,避免干扰
- 调整translation_controller中ASR服务启动逻辑,同时传译模式使用phone_call,面对面模式使用push_to_talk
- 修复AzureTtsHelper中播放状态管理,增加播放失败处理和状态重置
- 重新启用音频路由变化处理,当检测到音频会话切换到录音时立即停止TTS播放
- 添加调试日志便于问题追踪
newdev_shunjiawei
liwei1dao 9 months ago
parent
commit
3d280a834c
  1. 8
      lib/modules/translation/controllers/translation_controller.dart
  2. 4
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  3. 87
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  4. 9
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

8
lib/modules/translation/controllers/translation_controller.dart

@ -957,10 +957,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
return; return;
} }
if (currentMode.value == 'simultaneous') { if (currentMode.value == 'simultaneous') {
await _startAsrService('push_to_talk'); Logger.info('liwei-------- 开始语音识别,模式:同声传译,ASR phone_call');
} else if (currentMode.value == 'faceToFace') {
await _startAsrService('phone_call'); await _startAsrService('phone_call');
} else if (currentMode.value == 'faceToFace') {
Logger.info('iwei-------- 开始语音识别,模式:面对面,ASR push_to_talk');
await _startAsrService('push_to_talk');
} else { } else {
Logger.info(
'iwei-------- 开始语音识别,模式:${currentMode.value},ASR 模式:normal');
await _startAsrService('normal'); await _startAsrService('normal');
} }
await _finalizeRecognitionStart(); await _finalizeRecognitionStart();

4
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -313,8 +313,8 @@ class AgentServiceImpl: NSObject {
LocationService.shared.getCurrentLocation() { location in LocationService.shared.getCurrentLocation() { location in
// self.locationInfo = location // self.locationInfo = location
} }
currentRecognitionMode = "normal" // currentRecognitionMode = "normal"
checkAdnSetAudioSession() // checkAdnSetAudioSession()
return isInitialized return isInitialized
} }

87
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift

@ -549,7 +549,7 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate {
} }
speaking = true speaking = true
// lastSpokenText = text // lastSpokenText = text
// os_log("调用链: performSynthesis 准备合成 session=%{public}@ pushStream=%{public}@ suppress=%{public}@ textLen=%{public}d", log: log, type: .info, sessionid, isUsingPushStreamCapture.description, suppressStreamPlayback.description, text.count) os_log("liwei----------- 调用链: performSynthesis 准备合成 session=%{public}@ pushStream=%{public}@ suppress=%{public}@ textLen=%{public}d", log: log, type: .info, sessionid, isUsingPushStreamCapture.description, suppressStreamPlayback.description, text.count)
if !applyAudioSessionForTTS() { if !applyAudioSessionForTTS() {
speaking = false speaking = false
notifyEvent(eventType: .error, params: [ notifyEvent(eventType: .error, params: [
@ -1088,6 +1088,10 @@ private func checkStreamPlaybackCompletedIfNeeded() {
* - Throws: 无 * - Throws: 无
*/ */
private func startPlaybackIfNeeded() { private func startPlaybackIfNeeded() {
if isPlaying, audioPlayer?.isPlaying != true {
isPlaying = false
audioPlayer = nil
}
guard !isPlaying, !playbackQueue.isEmpty else { return } guard !isPlaying, !playbackQueue.isEmpty else { return }
let next = playbackQueue.first! let next = playbackQueue.first!
let playableData = ensureWavDataForPlayback(next) let playableData = ensureWavDataForPlayback(next)
@ -1106,15 +1110,20 @@ private func checkStreamPlaybackCompletedIfNeeded() {
} }
audioPlayer?.delegate = self audioPlayer?.delegate = self
audioPlayer?.prepareToPlay() audioPlayer?.prepareToPlay()
isPlaying = true let started = audioPlayer?.play() == true
// os_log("调用链: 开始播放 队首长度=%{public}dB session=%{public}@", log: log, type: .info, next.count, sessionid) if started {
notifyEvent(eventType: .playbackStarted) isPlaying = true
audioPlayer?.play() // os_log("调用链: 开始播放 队首长度=%{public}dB session=%{public}@", log: log, type: .info, next.count, sessionid)
notifyEvent(eventType: .playbackStarted)
} else {
throw NSError(domain: "AzureTtsHelper", code: -1, userInfo: [NSLocalizedDescriptionKey: "AVAudioPlayer.play() 返回 false"])
}
} catch { } catch {
os_log("播放初始化失败: %{public}@", log: log, type: .error, error.localizedDescription) os_log("播放初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
// 异常情况下移除该条并尝试播放下一条 // 异常情况下移除该条并尝试播放下一条
playbackQueue.removeFirst() playbackQueue.removeFirst()
isPlaying = false isPlaying = false
audioPlayer = nil
startPlaybackIfNeeded() startPlaybackIfNeeded()
} }
} }
@ -1415,6 +1424,7 @@ private func ensureAudioSessionQueueSpecificKeySet() {
*/ */
private func applyAudioSessionForTTS() -> Bool { private func applyAudioSessionForTTS() -> Bool {
var success = true var success = true
os_log("liwei-------------TTS applyAudioSessionForTTS currentRecognitionMode:%{public}@", log: self.log, type: .info, currentRecognitionMode)
ensureAudioSessionQueueSpecificKeySet() ensureAudioSessionQueueSpecificKeySet()
let applyBody = { let applyBody = {
if self.isApplyingAudioSessionConfig { return } if self.isApplyingAudioSessionConfig { return }
@ -1648,55 +1658,24 @@ private func deactivateAudioSessionAfterTTSIfIdle() {
* - 配置过程中(isApplyingAudioSessionConfig = true)直接忽略 * - 配置过程中(isApplyingAudioSessionConfig = true)直接忽略
*/ */
@objc private func handleRouteChange(_ notification: Notification) { @objc private func handleRouteChange(_ notification: Notification) {
// guard let info = notification.userInfo, guard let info = notification.userInfo,
// let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt, let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
// let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
// if !isTtsBusy() { return } if reason != .categoryChange { return }
// os_log("音频路由变化: %{public}@", log: log, type: .info, String(describing: reason))
let session = AVAudioSession.sharedInstance()
// // 配置中触发的路由变化,直接忽略,避免重入导致死循环 if session.category != .record { return }
// if isApplyingAudioSessionConfig {
// os_log("配置进行中,忽略本次路由变化", log: log, type: .debug) let isActivelyPlayingNow =
// return speaking ||
// } isPlaying ||
(audioPlayer?.isPlaying == true) ||
// // 忽略会造成循环的原因:categoryChange(你的日志 reason=3),以及 routeConfigurationChange 等 (playerNode?.isPlaying == true)
// switch reason { if !isActivelyPlayingNow { return }
// case .categoryChange, .routeConfigurationChange, .wakeFromSleep, .noSuitableRouteForCategory:
// os_log("忽略路由变化原因: %{public}@", log: log, type: .debug, String(describing: reason)) _ = stop()
// return os_log("检测到音频会话切到录音,停止TTS以避免冲突", log: log, type: .info)
// default:
// break
// }
// // 通话期间避免重配,避免与电话路由冲突
// if isTelephonyActive() {
// os_log("通话期间检测到路由变化,跳过重配", log: log, type: .info)
// return
// }
// let isActivelyPlayingNow =
// speaking ||
// isPlaying ||
// (audioPlayer?.isPlaying == true) ||
// (playerNode?.isPlaying == true)
// if isActivelyPlayingNow {
// routeChangeDebounceWorkItem?.cancel()
// _ = stop()
// os_log("路由变化时正在播报,已停止以避免音源冲突", log: log, type: .info)
// return
// }
// 去抖处理:300ms 内只执行最后一次重配
// 忽略 categoryChange 等
// 去抖:在 audioSessionQueue 上延迟调用
// routeChangeDebounceWorkItem?.cancel()
// let work = DispatchWorkItem { [weak self] in
// self?.safeConfigureAudioSessionForTTS()
// }
// routeChangeDebounceWorkItem = work
// audioSessionQueue.asyncAfter(deadline: .now() + 0.3, execute: work)
} }
/** /**

9
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -135,7 +135,7 @@ public class SimpleAudioReceiver: NSObject {
_isWriting = true _isWriting = true
print("音源设置---------- startAudioRecord=mod:\(mod), audioSourceType:\(audioSourceType), isHeadphones:\(isHeadphones)") print("liwei---------- startAudioRecord=mod:\(mod), audioSourceType:\(audioSourceType), isHeadphones:\(isHeadphones)")
do { do {
try configureAudioSession() try configureAudioSession()
@ -284,11 +284,12 @@ public class SimpleAudioReceiver: NSObject {
/** /**
* 开启音频写入线程 * 开启音频写入线程
* 修复:使用userInitiated QoS避免优先级反转 * 参数:无
* 返回值:无
* 异常:无
*/ */
public func startWriteThread() { public func startWriteThread() {
// 使用userInitiated QoS匹配音频录制线程的优先级 writeThread = DispatchQueue(label: "audio.stream.writer", qos: .default)
writeThread = DispatchQueue(label: "audio.stream.writer", qos: .userInitiated)
writeThread?.async { [weak self] in writeThread?.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }

Loading…
Cancel
Save