Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
fd45ef0f55
  1. 36
      lib/data/services/speech_impl/azure_asr_service.dart
  2. 29
      lib/modules/agent/controllers/agent_controller.dart
  3. 25
      lib/modules/translation/controllers/translation_controller.dart
  4. 16
      lib/modules/translation/views/translation_view.dart
  5. 10
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  6. 63
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  7. 7
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  8. 14
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift
  9. 1
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  10. 4
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

36
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<bool> 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<bool> 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()}');

29
lib/modules/agent/controllers/agent_controller.dart

@ -39,7 +39,7 @@ enum VoiceInputMode {
phoneCall, // 打电话模式
}
class AgentController extends GetxController {
class AgentController extends GetxController with WidgetsBindingObserver {
static AgentController get to => Get.find(); //单例访问模式
// 聊天消息列表
@ -173,6 +173,9 @@ class AgentController extends GetxController {
startVoiceInput(); //开启语音输入
// 监听应用生命周期:当应用切到后台/失焦时兜底结束按住说话,防止 onPointerUp 未触发
WidgetsBinding.instance.addObserver(this);
// _qqmusicManager.syncCurrentPlayInfo();
}
@ -219,6 +222,10 @@ class AgentController extends GetxController {
_amapeventSubscription?.cancel();
_bleEventSubscription?.cancel();
Intro.of(Get.context!).dispose();
// 取消应用生命周期监听
WidgetsBinding.instance.removeObserver(this);
super.onClose();
}
@ -2065,6 +2072,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();
});
}
}
}
// ========== 媒体会话回调处理方法 ==========
/// 处理来自系统通知栏的播放操作

25
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();

16
lib/modules/translation/views/translation_view.dart

@ -608,23 +608,13 @@ class TranslationView extends GetView<TranslationController> {
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),

10
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()

63
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -41,6 +41,11 @@ public class AzureAsrHelper: NSObject {
private var lastAudioStartTime: Int64 = 0
private let audioOperationDebounceInterval: Int64 = 500 // 500毫秒防抖间隔
private var audioStartWorkItem: DispatchWorkItem?
// 串行队列和状态标记,用于顺序控制音频启动/停止,避免竞态
private let audioOpQueue = DispatchQueue(label: "com.azure.speech.audio.operations")
private var isAudioStarting = false
private var isAudioStarted = false
private var pendingStopRequest = false
// 状态管理
private var _isContinuousRecognitionActive = false
@ -269,9 +274,27 @@ public class AzureAsrHelper: NSObject {
os_log("开始异步启动音频录制", log: self.log, type: .info)
// 在后台队列执行耗时的音频启动操作
DispatchQueue.global(qos: .userInitiated).async {
// 使用串行队列,保证与 stop 顺序执行,避免竞态
self.audioOpQueue.async { [weak self] in
guard let self = self else {
DispatchQueue.main.async { completion(false) }
return
}
// 准备启动
self.isAudioStarting = true
self.pendingStopRequest = false // 启动前清空旧的停止请求
let success = self.performAudioStart(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback)
self.isAudioStarting = false
self.isAudioStarted = success
// 如果启动过程中有 stop 请求,立即顺序停止,确保麦克风不会被重新“开启”
if self.pendingStopRequest {
self.pendingStopRequest = false
_ = self.performAudioStop()
self.isAudioStarted = false
os_log("启动完成后检测到待停止请求,已立即停止音频", log: self.log, type: .info)
}
// 回到主队列执行完成回调
DispatchQueue.main.async {
@ -318,9 +341,18 @@ public class AzureAsrHelper: NSObject {
private func stopAudioRecordAsync(completion: @escaping (Bool) -> Void) {
os_log("开始立即停止音频录制", log: log, type: .info)
// 在后台队列执行耗时的音频停止操作
DispatchQueue.global(qos: .userInitiated).async {
// 在串行队列顺序执行停止,避免与启动并发导致的竞态
audioOpQueue.async { [weak self] in
guard let self = self else {
DispatchQueue.main.async { completion(false) }
return
}
// 若启动过程中收到停止请求,记录标记,启动完成后会立即停止
if self.isAudioStarting {
self.pendingStopRequest = true
}
let success = self.performAudioStop()
self.isAudioStarted = false
// 回到主队列执行完成回调
DispatchQueue.main.async {
@ -333,7 +365,7 @@ public class AzureAsrHelper: NSObject {
* 执行音频停止操作
*/
private func performAudioStop() -> Bool {
do {
do {
audioStream?.stopMicrophoneCapture()
os_log("音频录制停止成功", log: log, type: .info)
return true
@ -433,16 +465,10 @@ public class AzureAsrHelper: NSObject {
os_log("语音服务未初始化", log: log, type: .error)
return false
}
guard let recognizer = recognizer else {
os_log("识别器为空,重置状态", log: log, type: .info)
// 仍需取消潜在的挂起启动任务,避免稍后被触发
cancelPendingAudioStart()
return true
}
do {
// 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动
cancelPendingAudioStart()
@ -457,7 +483,12 @@ public class AzureAsrHelper: NSObject {
os_log("音频停止失败", log: self.log, type: .error)
}
}
guard let recognizer = recognizer else {
os_log("识别器为空,重置状态", log: log, type: .info)
// 仍需取消潜在的挂起启动任务,避免稍后被触发
cancelPendingAudioStart()
return true
}
// 停止连续识别
try recognizer.stopContinuousRecognition()
self.recognizer = nil
@ -480,6 +511,12 @@ public class AzureAsrHelper: NSObject {
// 尝试安全地停止识别器
do {
guard let recognizer = recognizer else {
os_log("识别器为空,重置状态", log: log, type: .info)
// 仍需取消潜在的挂起启动任务,避免稍后被触发
cancelPendingAudioStart()
return true
}
try recognizer.stopContinuousRecognition()
self.recognizer = nil
} catch {

7
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() {
}

14
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)
}
}
/**
* 重置音频捕获状态

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

@ -324,7 +324,6 @@ public func restoreOriginalAudioState() {
public func stopMicrophoneCapture() {
guard _isWriting==true else { return }
_isWriting=false
micCapture.stopCapture()

4
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()

Loading…
Cancel
Save