diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 5137faee5..9c46e528e 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -55,6 +55,11 @@ class AzureAsrService extends GetxService implements AsrService { // 当前音频源类型 AudioSourceType _audioSourceType = AudioSourceType.microphone; + // 防抖时间戳与间隔(实例级) + DateTime? _lastStartAt; + DateTime? _lastStopAt; + static const Duration _debounce = Duration(milliseconds: 600); + AzureAsrService() { _loadConfig(); } @@ -172,7 +177,21 @@ class AzureAsrService extends GetxService implements AsrService { } @override + + /// 开始连续语音识别(带防抖) + /// + /// 行为说明: + /// - 在 600ms 的冷却时间内再次调用会被防抖拦截并返回 false; + /// - 首次或冷却期外调用会继续启动连续识别; + /// - 若当前已处于连续识别中,会先调用停止后再启动。 Future startContinuousRecognition(bool audioSourceType) async { + // 防抖判断:短时间内重复调用直接拦截 + final now = DateTime.now(); + if (_lastStartAt != null && now.difference(_lastStartAt!) < _debounce) { + return false; + } + _lastStartAt = now; + if (!_isInitialized) { await initialize(); } @@ -201,7 +220,21 @@ class AzureAsrService extends GetxService implements AsrService { } @override + + /// 停止连续语音识别(带防抖) + /// + /// 行为说明: + /// - 在 600ms 的冷却时间内再次调用会被防抖拦截并返回 false; + /// - 若当前不在识别中,直接返回 true; + /// - 调用成功后立即将内部状态置为 false(同时事件通道回调仍会再次校正)。 Future stopContinuousRecognition() async { + // 防抖判断:短时间内重复调用直接拦截 + final now = DateTime.now(); + if (_lastStopAt != null && now.difference(_lastStopAt!) < _debounce) { + return false; + } + _lastStopAt = now; + if (!_isInitialized || !_isContinuousRecognitionActive) { return true; } @@ -209,6 +242,9 @@ class AzureAsrService extends GetxService implements AsrService { try { final bool result = await _channel.invokeMethod('stopContinuousRecognition'); + if (result) { + _isContinuousRecognitionActive = false; + } return result; } catch (e) { Logger.error('停止连续语音识别失败: ${e.toString()}'); diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 631f1c78d..111add8e4 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -37,7 +37,7 @@ enum VoiceInputMode { phoneCall, // 打电话模式 } -class AgentController extends GetxController { +class AgentController extends GetxController with WidgetsBindingObserver { static AgentController get to => Get.find(); //单例访问模式 // 聊天消息列表 @@ -171,6 +171,9 @@ class AgentController extends GetxController { startVoiceInput(); //开启语音输入 + // 监听应用生命周期:当应用切到后台/失焦时兜底结束按住说话,防止 onPointerUp 未触发 + WidgetsBinding.instance.addObserver(this); + // _qqmusicManager.syncCurrentPlayInfo(); } @@ -217,6 +220,10 @@ class AgentController extends GetxController { _amapeventSubscription?.cancel(); _bleEventSubscription?.cancel(); Intro.of(Get.context!).dispose(); + + // 取消应用生命周期监听 + WidgetsBinding.instance.removeObserver(this); + super.onClose(); } @@ -2042,6 +2049,26 @@ class AgentController extends GetxController { } } + /// 应用生命周期变化回调 + /// 目的: + /// - 当应用进入后台、失去焦点或被系统分离(inactive/paused/detached)时, + /// 兜底结束按住说话(调用 endPushToTalk),避免因 onPointerUp 未触发而卡在“按下”状态。 + /// 参数: + /// - state: 当前应用生命周期状态 + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.inactive || + state == AppLifecycleState.paused || + state == AppLifecycleState.detached) { + if (isPushToTalkActive.value) { + // 使用 microtask 避免阻塞当前生命周期回调 + Future.microtask(() { + endPushToTalk(); + }); + } + } + } + // ========== 媒体会话回调处理方法 ========== /// 处理来自系统通知栏的播放操作 diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 326001c0a..fb258a9d8 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -31,7 +31,7 @@ enum AudioOutputDevice { speaker, // 强制扬声器 } -class TranslationController extends GetxController { +class TranslationController extends GetxController with WidgetsBindingObserver { static const String TAG = 'TranslationController'; // 服务 @@ -268,6 +268,9 @@ class TranslationController extends GetxController { // 初始化历史管理器 _historyManager.initialize(); + + // 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker + WidgetsBinding.instance.addObserver(this); } /// 预先检查权限 @@ -460,9 +463,29 @@ class TranslationController extends GetxController { _timerManager.dispose(); _historyManager.dispose(); + // 取消应用生命周期监听 + WidgetsBinding.instance.removeObserver(this); + super.onClose(); } + /// 应用生命周期变化回调 + /// 当应用进入后台、失去焦点或分离(inactive/paused/detached)时, + /// 兜底将 activeSpeaker 重置为 0,等价于“松手”,避免 onPointerUp 未触发导致的卡住问题。 + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.inactive || + state == AppLifecycleState.paused || + state == AppLifecycleState.detached) { + if (activeSpeaker.value != 0) { + // 使用 microtask 避免阻塞当前生命周期回调 + Future.microtask(() { + setActiveSpeaker(0); + }); + } + } + } + // 切换TTS功能 void toggleTts() => isTtsEnabled.toggle(); diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index b82ae3667..6796213ba 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -608,23 +608,13 @@ class TranslationView extends GetView { return; } - _holdTimer = Timer(const Duration(milliseconds: 200), () { - if (activeSpeaker == 0) { - controller.setActiveSpeaker(speakerId); - } - }); + controller.setActiveSpeaker(speakerId); }, onPointerUp: (_) { - _holdTimer?.cancel(); - if (isActive) { - controller.setActiveSpeaker(0); - } + controller.setActiveSpeaker(0); }, onPointerCancel: (_) { - _holdTimer?.cancel(); - if (isActive) { - controller.setActiveSpeaker(0); - } + controller.setActiveSpeaker(0); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index c2aa49785..afa0077b0 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -393,13 +393,12 @@ class AgentServiceImpl: NSObject { stopRecognition() } - // 检查当前音乐播放状态并记录 - let currentPlayState = QPlayAutoManager.shared.currentPlayState - wasMusicPlayingBeforeRecognition = (currentPlayState == .playing) + + wasMusicPlayingBeforeRecognition = MusicService.shared.isPlaying // 如果音乐正在播放,则暂停 - if wasMusicPlayingBeforeRecognition { - // QPlayAutoManager.shared.pause() + if MusicService.shared.isPlaying { + MusicService.shared.pause() } // 设置当前识别模式 @@ -469,6 +468,7 @@ class AgentServiceImpl: NSObject { if wasMusicPlayingBeforeRecognition { // QPlayAutoManager.shared.play() wasMusicPlayingBeforeRecognition = false + MusicService.shared.resume() } BleService.shared.closeCodec() diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift index dd6e17009..30dfd7464 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift @@ -271,7 +271,7 @@ public class AzureTtsHelper: NSObject, ITtsService { if !micCapture.isCapturing{ // 停止时设置音频会话为非活跃状态 - try AVAudioSession.sharedInstance().setActive(false) + // try AVAudioSession.sharedInstance().setActive(false) } print("音频会话已设置为非活跃状态") @@ -526,7 +526,7 @@ public class AzureTtsHelper: NSObject, ITtsService { // 检查是否为最后一个文本播放完毕 if self.pendingTextCount == 0 && self.pendingTasks.isEmpty && !micCapture.isCapturing{ do { - try AVAudioSession.sharedInstance().setActive(false) + // try AVAudioSession.sharedInstance().setActive(false) print("最后一个文本播放完毕,音频会话已设置为非活跃状态") } catch { print("设置音频会话为非活跃状态失败: \(error.localizedDescription)") @@ -682,5 +682,4 @@ public func resetTextCount() { } - - + \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift index b888aa715..f85a7e906 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift @@ -220,7 +220,7 @@ public class MicrophoneCapture: NSObject { throw NSError(domain: "未知权限状态", code: 1) } } - + /** * 检查音频格式是否符合Microsoft音频堆栈要求 * 如果符合则无需转换,直接使用原始格式 @@ -256,9 +256,15 @@ public class MicrophoneCapture: NSObject { // 获取实际硬件采样率 sampleRate = audioSession.sampleRate + // 安全地停止音频引擎 + if let engine = audioEngine { + engine.stop() + } - // 移除之前的音频处理块,避免重复添加 - audioInputNode.removeTap(onBus: 0) + // 安全地移除音频处理块 + if let inputNode = audioInputNode { + inputNode.removeTap(onBus: 0) + } // 设置音频格式 let inputFormat = audioInputNode.inputFormat(forBus: 0) @@ -447,7 +453,7 @@ public class MicrophoneCapture: NSObject { } try? audioSession.overrideOutputAudioPort(.none) try? audioSession.setActive(true) - } + } /** * 重置音频捕获状态 diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index c506c98aa..320702e8b 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -206,7 +206,7 @@ public class MusicService: NSObject { } } - func pause() { + public func pause() { player?.pause() isPlaying = false updatePlaybackState() @@ -215,7 +215,7 @@ public class MusicService: NSObject { print("已暂停") } - func resume() { + public func resume() { player?.play() isPlaying = true updatePlaybackState()