Browse Source

v001fix

weicu
wolfplus 2 years ago
parent
commit
111759a76c
  1. 14
      ios/Runner.xcodeproj/project.pbxproj
  2. 408
      ios/Runner/AppDelegate.swift
  3. 634
      ios/Runner/AudioSessionManager.swift
  4. 72
      ios/Runner/AzureAsrHelper.swift
  5. 2
      ios/Runner/AzureTtsHelper.swift
  6. 170
      ios/Runner/BluetoothMediaButtonHelper.swift
  7. 82
      ios/Runner/ClassicBluetoothHelper.swift
  8. 514
      ios/Runner/VoiceInteractionService.swift
  9. 391
      ios/Runner/VolcanoAIService.swift
  10. 14
      lib/core/bindings/initial_binding.dart
  11. 7
      lib/data/services/bluetooth_media_button_service.dart
  12. 3
      lib/data/services/classic_bluetooth_service.dart
  13. 7
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  14. 5
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  15. 286
      lib/data/services/voice_interaction_service.dart
  16. 1
      lib/modules/chat/controllers/chat_controller.dart
  17. 75
      lib/modules/home/controllers/home_controller.dart
  18. 4
      lib/modules/pairing/controllers/pairing_controller.dart

14
ios/Runner.xcodeproj/project.pbxproj

@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; }; 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; };
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; }; 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; };
0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */; };
0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */; };
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29682D8C25660032BFCB /* silence.mp3 */; }; 0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29682D8C25660032BFCB /* silence.mp3 */; };
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; }; 0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; };
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */; }; 0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */; };
@ -53,6 +55,8 @@
0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = "<group>"; }; 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = "<group>"; };
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = "<group>"; }; 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = "<group>"; };
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceInteractionService.swift; sourceTree = "<group>"; };
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolcanoAIService.swift; sourceTree = "<group>"; };
0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = "<group>"; }; 0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = "<group>"; };
0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = "<group>"; }; 0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = "<group>"; };
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = "<group>"; }; 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = "<group>"; };
@ -157,6 +161,8 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */,
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */,
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */, 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */,
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */, 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */,
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */, 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */,
@ -422,6 +428,8 @@
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */, 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */, 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */,
0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */,
0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */,
0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */, 0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
); );
@ -500,7 +508,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -629,7 +637,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -680,7 +688,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;

408
ios/Runner/AppDelegate.swift

@ -1,5 +1,6 @@
import Flutter import Flutter
import UIKit import UIKit
import AVFoundation
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
@ -8,24 +9,28 @@ import UIKit
// 添加Azure语音服务辅助类 // 添加Azure语音服务辅助类
private var azureAsrHelper: Any? private var azureAsrHelper: Any?
private var azureTtsHelper: Any? private var azureTtsHelper: Any?
// 添加火山AI服务
private var volcanoAIService: VolcanoAIService?
// 语音交互服务
private var voiceInteractionService: VoiceInteractionService?
private var audioSessionManager: AudioSessionManager?
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
// 初始化蓝牙辅助类
classicBluetoothHelper = ClassicBluetoothHelper()
// 立即初始化蓝牙,不要等待Flutter调用
classicBluetoothHelper?.initBluetooth()
// 仅在iOS 13.0及以上版本初始化媒体按钮辅助类和Azure语音服务辅助类 // 获取flutter控制器
if #available(iOS 13.0, *) { guard let controller = window?.rootViewController as? FlutterViewController else {
mediaButtonHelper = BluetoothMediaButtonHelper() NSLog("AppDelegate: Flutter控制器不可用")
// 稍后在方法通道注册时初始化Azure语音服务辅助类 return false
} }
// 建立Flutter方法通道 // 初始化音频会话管理器
audioSessionManager = AudioSessionManager.shared
// 统一注册所有方法通道
registerMethodChannel() registerMethodChannel()
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
@ -39,25 +44,60 @@ import UIKit
return return
} }
// 注册经典蓝牙通道 // 注册经典蓝牙方法通道
let classicChannel = FlutterMethodChannel( registerClassicBluetoothChannels(controller: controller)
// 仅在iOS 13.0及以上版本注册媒体按钮通道和Azure语音服务通道
if #available(iOS 13.0, *) {
// 注册媒体按钮方法通道
registerMediaButtonChannels(controller: controller)
// 注册Azure ASR方法通道
registerAzureAsrChannels(controller: controller)
// 注册Azure TTS方法通道
registerAzureTtsChannels(controller: controller)
// 注册火山AI服务方法通道
registerVolcanoAIServiceChannels(controller: controller)
// 注册语音交互服务方法通道
registerVoiceInteractionChannels(controller: controller)
}
}
// MARK: - 经典蓝牙通道
private func registerClassicBluetoothChannels(controller: FlutterViewController) {
// 初始化蓝牙辅助类
if classicBluetoothHelper == nil {
classicBluetoothHelper = ClassicBluetoothHelper()
}
guard let classicBluetoothHelper = self.classicBluetoothHelper else {
NSLog("AppDelegate: 经典蓝牙辅助实例不可用")
return
}
// 注册经典蓝牙方法通道
let classicBluetoothChannel = FlutterMethodChannel(
name: "com.deep_voice.classic_bluetooth", name: "com.deep_voice.classic_bluetooth",
binaryMessenger: controller.binaryMessenger binaryMessenger: controller.binaryMessenger
) )
classicChannel.setMethodCallHandler { [weak self] (call, result) in classicBluetoothChannel.setMethodCallHandler { [weak self, weak classicBluetoothHelper] (call, result) in
guard let self = self else { guard let _ = self, let classicBluetoothHelper = classicBluetoothHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "AppDelegate实例不可用", details: nil)) result(FlutterError(code: "UNAVAILABLE", message: "经典蓝牙辅助实例不可用", details: nil))
return return
} }
switch call.method { switch call.method {
case "isBluetoothEnabled": case "isBluetoothEnabled":
result(self.classicBluetoothHelper?.isBluetoothEnabled() ?? false) result(classicBluetoothHelper.isBluetoothEnabled())
case "getConnectedHeadsetDevices": case "getConnectedHeadsetDevices":
// 获取已连接的耳机设备 // 获取已连接的耳机设备
self.classicBluetoothHelper?.getConnectedHeadsetDevices { devices, error in classicBluetoothHelper.getConnectedHeadsetDevices { devices, error in
if let error = error { if let error = error {
result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil)) result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil))
} else { } else {
@ -67,7 +107,7 @@ import UIKit
case "startBluetoothDeviceMonitoring": case "startBluetoothDeviceMonitoring":
// 启动蓝牙设备监控 // 启动蓝牙设备监控
self.classicBluetoothHelper?.initBluetooth() classicBluetoothHelper.startBluetoothDeviceMonitoring()
result(true) result(true)
default: default:
@ -75,44 +115,40 @@ import UIKit
} }
} }
// 注册蓝牙事件通道 // 注册蓝牙流通道
let classicEventChannel = FlutterEventChannel( let classicBluetoothStreamChannel = FlutterEventChannel(
name: "com.deep_voice.classic_bluetooth_events", name: "com.deep_voice.classic_bluetooth_events",
binaryMessenger: controller.binaryMessenger binaryMessenger: controller.binaryMessenger
) )
// 创建事件处理器 // 创建蓝牙流处理器(作为局部变量)
let bluetoothEventHandler = BluetoothStreamHandler(bluetoothHelper: self.classicBluetoothHelper!) let bluetoothStreamHandler = BluetoothStreamHandler(bluetoothHelper: classicBluetoothHelper)
classicEventChannel.setStreamHandler(bluetoothEventHandler) classicBluetoothStreamChannel.setStreamHandler(bluetoothStreamHandler)
// 仅在iOS 13.0及以上版本注册媒体按钮通道和Azure语音服务通道
if #available(iOS 13.0, *) {
// 注册媒体按钮方法通道
registerMediaButtonChannels(controller: controller)
// 注册Azure ASR方法通道
registerAzureAsrChannels(controller: controller)
// 注册Azure TTS方法通道
registerAzureTtsChannels(controller: controller)
}
} }
// 注册媒体按钮通道 // MARK: - 消息通道设置
// MARK: - 蓝牙媒体按键通道
@available(iOS 13.0, *) @available(iOS 13.0, *)
private func registerMediaButtonChannels(controller: FlutterViewController) { private func registerMediaButtonChannels(controller: FlutterViewController) {
guard let mediaButtonHelper = mediaButtonHelper as? BluetoothMediaButtonHelper else { // 初始化媒体按钮辅助类
return if mediaButtonHelper == nil {
mediaButtonHelper = BluetoothMediaButtonHelper()
} }
// guard let mediaButtonHelper = self.mediaButtonHelper as? BluetoothMediaButtonHelper else {
// NSLog("AppDelegate: 媒体按钮辅助实例不可用")
// return
// }
// mediaButtonHelper.startButtonListening()
// 注册媒体按钮方法通道 // 注册媒体按钮方法通道
let mediaButtonChannel = FlutterMethodChannel( let mediaButtonChannel = FlutterMethodChannel(
name: "com.deep_voice.bluetooth_media_button", name: "com.deep_voice.bluetooth_media_button",
binaryMessenger: controller.binaryMessenger binaryMessenger: controller.binaryMessenger
) )
mediaButtonChannel.setMethodCallHandler { [weak self, weak mediaButtonHelper] (call, result) in mediaButtonChannel.setMethodCallHandler { [weak self] (call, result) in
guard let _ = self, let mediaButtonHelper = mediaButtonHelper else { guard let _ = self, let mediaButtonHelper = self?.mediaButtonHelper as? BluetoothMediaButtonHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil)) result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil))
return return
} }
@ -131,17 +167,18 @@ import UIKit
} }
} }
// 注册媒体按钮事件通道 // 注册媒体按钮流通道
let mediaButtonEventChannel = FlutterEventChannel( let mediaButtonStreamChannel = FlutterEventChannel(
name: "com.deep_voice.bluetooth_media_button_events", name: "com.deep_voice.bluetooth_media_button_events",
binaryMessenger: controller.binaryMessenger binaryMessenger: controller.binaryMessenger
) )
// 创建媒体按钮事件处理器 // 创建媒体按钮流处理器
let mediaButtonEventHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper) let mediaButtonStreamHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper as! BluetoothMediaButtonHelper)
mediaButtonEventChannel.setStreamHandler(mediaButtonEventHandler) mediaButtonStreamChannel.setStreamHandler(mediaButtonStreamHandler)
} }
// 注册Azure ASR通道 // 注册Azure ASR通道
@available(iOS 13.0, *) @available(iOS 13.0, *)
private func registerAzureAsrChannels(controller: FlutterViewController) { private func registerAzureAsrChannels(controller: FlutterViewController) {
@ -203,15 +240,15 @@ import UIKit
} }
} }
// 注册Azure ASR事件通道 // 注册Azure ASR流通道
let azureAsrEventChannel = FlutterEventChannel( let azureAsrStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_asr_events", name: "com.deep_voice.azure_asr_events",
binaryMessenger: controller.binaryMessenger binaryMessenger: controller.binaryMessenger
) )
// 创建ASR事件处理器 // 创建ASR流处理器
let asrStreamHandler = AsrStreamHandler() let asrStreamHandler = AsrStreamHandler()
azureAsrEventChannel.setStreamHandler(asrStreamHandler) azureAsrStreamChannel.setStreamHandler(asrStreamHandler)
} }
// 注册Azure TTS通道 // 注册Azure TTS通道
@ -300,15 +337,156 @@ import UIKit
} }
} }
// 注册Azure TTS事件通道 // 注册Azure TTS流通道
let azureTtsEventChannel = FlutterEventChannel( let azureTtsStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_tts_events", name: "com.deep_voice.azure_tts_events",
binaryMessenger: controller.binaryMessenger binaryMessenger: controller.binaryMessenger
) )
// 创建TTS事件处理器 // 创建TTS流处理器
let ttsStreamHandler = TtsStreamHandler() let ttsStreamHandler = TtsStreamHandler()
azureTtsEventChannel.setStreamHandler(ttsStreamHandler) azureTtsStreamChannel.setStreamHandler(ttsStreamHandler)
}
// 注册火山AI服务方法通道
@available(iOS 13.0, *)
private func registerVolcanoAIServiceChannels(controller: FlutterViewController) {
guard let volcanoAIService = volcanoAIService else {
return
}
// 注册火山AI方法通道
let volcanoAIChannel = FlutterMethodChannel(
name: "com.deep_voice.volcano_ai",
binaryMessenger: controller.binaryMessenger
)
volcanoAIChannel.setMethodCallHandler { [weak self, weak volcanoAIService] (call, result) in
guard let _ = self, let volcanoAIService = volcanoAIService else {
result(FlutterError(code: "UNAVAILABLE", message: "火山AI服务实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
let apiKey = call.arguments as? String ?? ""
let success = volcanoAIService.initialize(apiKey: apiKey)
result(success)
case "sendMessage":
guard let arguments = call.arguments as? [String: Any],
let message = arguments["message"] as? String,
let systemPrompt = arguments["systemPrompt"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil))
return
}
if message.isEmpty {
result(FlutterError(code: "EMPTY_MESSAGE", message: "消息不能为空", details: nil))
return
}
// 异步发送消息
Task {
do {
// 创建用户消息
let userMessage = volcanoAIService.createUserMessage(content: message)
// 发送消息并等待响应
let response = try await volcanoAIService.sendMessage(
messages: [userMessage],
systemPrompt: systemPrompt
)
// 返回响应
DispatchQueue.main.async {
result(response)
}
} catch {
// 处理错误
DispatchQueue.main.async {
result(FlutterError(
code: "AI_ERROR",
message: "AI请求失败: \(error.localizedDescription)",
details: nil
))
}
}
}
default:
result(FlutterMethodNotImplemented)
}
}
// 注册火山AI流通道
let volcanoAIStreamChannel = FlutterEventChannel(
name: "com.deep_voice.volcano_ai_stream",
binaryMessenger: controller.binaryMessenger
)
// 创建火山AI流处理器(作为局部变量)
let volcanoAIStreamHandler = VolcanoAIStreamHandler(volcanoAIService: volcanoAIService)
volcanoAIStreamChannel.setStreamHandler(volcanoAIStreamHandler)
}
// 注册语音交互服务方法通道
@available(iOS 13.0, *)
private func registerVoiceInteractionChannels(controller: FlutterViewController) {
// 初始化语音交互服务
voiceInteractionService = VoiceInteractionService.shared
// 注册语音交互服务方法通道
let voiceInteractionChannel = FlutterMethodChannel(
name: "com.deep_voice.voice_interaction",
binaryMessenger: controller.binaryMessenger
)
voiceInteractionChannel.setMethodCallHandler { [weak self, weak voiceInteractionService] (call, result) in
guard let _ = self, let voiceInteractionService = voiceInteractionService else {
result(FlutterError(code: "UNAVAILABLE", message: "语音交互服务实例不可用", details: nil))
return
}
switch call.method {
case "startService":
guard let args = call.arguments as? [String: Any],
let azureKey = args["azureSpeechKey"] as? String,
let azureRegion = args["azureSpeechRegion"] as? String,
let volcanoKey = args["volcanoAiKey"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "启动服务参数无效", details: nil))
return
}
let success = voiceInteractionService.start(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey)
result(success)
case "stopService":
voiceInteractionService.stop()
result(true)
case "isServiceRunning":
let isRunning = voiceInteractionService.isServiceRunning()
result(isRunning)
case "pauseVoiceInteraction":
voiceInteractionService.pauseVoiceInteraction()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册语音交互流通道
let voiceInteractionStreamChannel = FlutterEventChannel(
name: "com.deep_voice.voice_interaction_events",
binaryMessenger: controller.binaryMessenger
)
// 创建语音交互流处理器(作为局部变量)
let voiceInteractionStreamHandler = VoiceInteractionStreamHandler(voiceInteractionService: voiceInteractionService!)
voiceInteractionStreamChannel.setStreamHandler(voiceInteractionStreamHandler)
} }
// 存储ASR事件 // 存储ASR事件
@ -362,13 +540,13 @@ class BluetoothStreamHandler: NSObject, FlutterStreamHandler, BluetoothEventHand
} }
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("BluetoothStreamHandler: 开始监听蓝牙事件") // NSLog("BluetoothStreamHandler: 开始监听蓝牙事件")
self.eventSink = events self.eventSink = events
return nil return nil
} }
func onCancel(withArguments arguments: Any?) -> FlutterError? { func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("BluetoothStreamHandler: 取消监听蓝牙事件") // NSLog("BluetoothStreamHandler: 取消监听蓝牙事件")
eventSink = nil eventSink = nil
return nil return nil
} }
@ -437,8 +615,11 @@ class AsrStreamHandler: NSObject, FlutterStreamHandler {
forName: Notification.Name("AzureAsrEvent"), forName: Notification.Name("AzureAsrEvent"),
object: nil, object: nil,
queue: .main) { [weak self] notification in queue: .main) { [weak self] notification in
if let userInfo = notification.userInfo as? [String: Any] { if let userInfo = notification.userInfo as? [String: Any], let eventSink = self?.eventSink {
self?.eventSink?(userInfo) // 确保在主线程上发送事件
DispatchQueue.main.async {
eventSink(userInfo)
}
} }
} }
@ -478,8 +659,11 @@ class TtsStreamHandler: NSObject, FlutterStreamHandler {
forName: Notification.Name("AzureTtsEvent"), forName: Notification.Name("AzureTtsEvent"),
object: nil, object: nil,
queue: .main) { [weak self] notification in queue: .main) { [weak self] notification in
if let userInfo = notification.userInfo as? [String: Any] { if let userInfo = notification.userInfo as? [String: Any], let eventSink = self?.eventSink {
self?.eventSink?(userInfo) // 确保在主线程上发送事件
DispatchQueue.main.async {
eventSink(userInfo)
}
} }
} }
@ -498,3 +682,107 @@ class TtsStreamHandler: NSObject, FlutterStreamHandler {
return nil return nil
} }
} }
// 火山AI事件流处理器
@available(iOS 13.0, *)
class VolcanoAIStreamHandler: NSObject, FlutterStreamHandler {
private let volcanoAIService: VolcanoAIService
private var eventSink: FlutterEventSink?
init(volcanoAIService: VolcanoAIService) {
self.volcanoAIService = volcanoAIService
super.init()
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
// 处理来自Flutter的流式请求
func handleStreamRequest(messages: [[String: Any]], systemPrompt: String) {
volcanoAIService.sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: VolcanoAIService.StreamCallback(
onToken: { [weak self] token in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "token",
"token": token
]
DispatchQueue.main.async {
eventSink(event)
}
},
onComplete: { [weak self] in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "complete"
]
DispatchQueue.main.async {
eventSink(event)
}
},
onError: { [weak self] error in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "error",
"error": error.localizedDescription
]
DispatchQueue.main.async {
eventSink(event)
}
}
))
}
}
// 语音交互事件处理器协议
@available(iOS 13.0, *)
protocol VoiceInteractionEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// 语音交互流处理器
@available(iOS 13.0, *)
class VoiceInteractionStreamHandler: NSObject, FlutterStreamHandler, VoiceInteractionEventHandler {
private let voiceInteractionService: VoiceInteractionService
private var eventSink: FlutterEventSink?
init(voiceInteractionService: VoiceInteractionService) {
self.voiceInteractionService = voiceInteractionService
super.init()
// 设置事件处理器
voiceInteractionService.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("VoiceInteractionStreamHandler: 开始监听语音交互事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("VoiceInteractionStreamHandler: 取消监听语音交互事件")
eventSink = nil
return nil
}
// 实现VoiceInteractionEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("VoiceInteractionStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}

634
ios/Runner/AudioSessionManager.swift

@ -1,5 +1,6 @@
import Foundation import Foundation
import AVFoundation import AVFoundation
import UIKit
/// 统一音频会话管理类 /// 统一音频会话管理类
/// 用于统一管理所有组件的音频会话配置 /// 用于统一管理所有组件的音频会话配置
@ -11,103 +12,211 @@ import AVFoundation
// 音频会话 // 音频会话
private let audioSession = AVAudioSession.sharedInstance() private let audioSession = AVAudioSession.sharedInstance()
// 是否已经配置过音频会话 // 音频会话状态
private var isConfigured = false private var isConfigured = false
private var isActive = false
// 当前配置类型 // 当前配置类型
private var currentCategory: AVAudioSession.Category = .playAndRecord private var currentCategory: AVAudioSession.Category?
private var currentMode: AVAudioSession.Mode = .default private var currentMode: AVAudioSession.Mode?
private var currentOptions: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP] private var currentOptions: AVAudioSession.CategoryOptions?
// 中断监听器
private var interruptionObserver: NSObjectProtocol?
// 路由变化监听器
private var routeChangeObserver: NSObjectProtocol?
// 私有初始化方法,确保只能通过shared访问 // 私有初始化方法,确保只能通过shared访问
private override init() { private override init() {
super.init() super.init()
NSLog("[AudioSessionManager] 音频会话管理器已初始化") NSLog("[AudioSessionManager] 音频会话管理器已初始化")
setupNotifications()
do {
// try configureSession(
// category: .playAndRecord,
// mode: .default,
// options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers, .defaultToSpeaker],
// force: true
// )
NSLog("[AudioSessionManager] 初始化音频会话配置成功")
} catch {
NSLog("[AudioSessionManager] 初始化音频会话配置失败: \(error.localizedDescription)")
}
// BluetoothMediaButtonHelper.shared.startButtonListening()
} }
// MARK: - 音频会话配置 deinit {
removeNotifications()
}
/// 在应用启动时调用一次性配置方法
/// - Parameters: // MARK: - 通知设置
/// - category: 音频会话类别
/// - mode: 音频会话模式 private func setupNotifications() {
/// - options: 音频会话选项 // 监听中断事件
/// - Returns: 是否配置成功 interruptionObserver = NotificationCenter.default.addObserver(
@objc func initialConfiguration(category: AVAudioSession.Category = .playAndRecord, forName: AVAudioSession.interruptionNotification,
mode: AVAudioSession.Mode = .default, object: nil,
options: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP]) -> Bool { queue: .main) { [weak self] notification in
if isConfigured { self?.handleInterruption(notification)
NSLog("[AudioSessionManager] 音频会话已经配置过,忽略初始化请求") }
return true
// 监听路由变化事件
routeChangeObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.routeChangeNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.handleRouteChange(notification)
}
// 监听应用进入后台/前台
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
private func removeNotifications() {
if let interruptionObserver = interruptionObserver {
NotificationCenter.default.removeObserver(interruptionObserver)
} }
do { if let routeChangeObserver = routeChangeObserver {
NSLog("[AudioSessionManager] 执行一次性音频会话配置: \(category)") NotificationCenter.default.removeObserver(routeChangeObserver)
try audioSession.setCategory(category, mode: mode, options: options)
try audioSession.setActive(true)
// 记录当前配置
currentCategory = category
currentMode = mode
currentOptions = options
isConfigured = true
return true
} catch {
NSLog("[AudioSessionManager] 一次性配置音频会话失败: \(error.localizedDescription)")
return false
} }
NotificationCenter.default.removeObserver(self)
} }
/// 配置音频会话,适用于语音识别和合成 // MARK: - 通知处理
@objc func configureForVoiceProcessing() throws {
if !applyConfigurationIfNeeded(category: .playAndRecord, private func handleInterruption(_ notification: Notification) {
mode: .spokenAudio, guard let userInfo = notification.userInfo,
options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers]) { let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过语音处理模式配置") let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
switch type {
case .began:
// 中断开始
NSLog("[AudioSessionManager] 音频会话被中断")
isActive = false
case .ended:
// 中断结束,尝试恢复
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
NSLog("[AudioSessionManager] 尝试恢复被中断的音频会话")
try? activateSession()
}
}
@unknown default:
break
} }
} }
/// 配置音频会话,适用于蓝牙耳机连接和监听 private func handleRouteChange(_ notification: Notification) {
@objc func configureForBluetoothHeadset() throws { guard let userInfo = notification.userInfo,
if !applyConfigurationIfNeeded(category: .playAndRecord, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
mode: .default, let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
options: [.allowBluetooth, .allowBluetoothA2DP]) { return
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过蓝牙耳机模式配置") }
switch reason {
case .newDeviceAvailable:
// 新设备连接
let currentRoute = audioSession.currentRoute
for output in currentRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP {
NSLog("[AudioSessionManager] 检测到蓝牙音频设备连接: \(output.portName)")
// 蓝牙设备连接,可能需要更新配置
if isActive && currentOptions?.contains(.allowBluetooth) == true {
try? forceReconfigure()
}
break
}
case .oldDeviceUnavailable:
// 设备断开连接
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP {
NSLog("[AudioSessionManager] 蓝牙音频设备断开连接: \(output.portName)")
// 蓝牙设备断开,重新配置到内置扬声器
if isActive {
try? forceReconfigure()
}
break
}
}
default:
break
} }
} }
/// 配置音频会话,适用于媒体播放和蓝牙媒体按钮 @objc private func handleAppDidEnterBackground() {
@objc func configureForMediaPlayback() throws { NSLog("[AudioSessionManager] 应用进入后台")
if !applyConfigurationIfNeeded(category: .playback, // 应用进入后台时可能需要调整音频会话
mode: .default,
options: [.allowBluetooth, .allowBluetoothA2DP]) {
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过媒体播放模式配置")
}
} }
/// 配置自定义音频会话 @objc private func handleAppWillEnterForeground() {
@objc func configure(category: AVAudioSession.Category, NSLog("[AudioSessionManager] 应用即将进入前台")
mode: AVAudioSession.Mode, // 应用返回前台,检查并恢复音频会话
options: AVAudioSession.CategoryOptions) throws { if isConfigured && !isActive {
if !applyConfigurationIfNeeded(category: category, mode: mode, options: options) { try? activateSession()
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过自定义模式配置")
} }
} }
/// 内部方法:如果尚未配置,应用新的配置 // MARK: - 音频会话配置
private func applyConfigurationIfNeeded(category: AVAudioSession.Category,
mode: AVAudioSession.Mode, /// 统一配置方法 - 所有音频配置都通过此方法进行
options: AVAudioSession.CategoryOptions) -> Bool { @objc func configureSession(category: AVAudioSession.Category,
// 如果已经配置过,不再重复配置 mode: AVAudioSession.Mode,
if isConfigured { options: AVAudioSession.CategoryOptions,
return false force: Bool = false) throws {
NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode), 选项=\(options), 强制=\(force)")
// 如果已配置且不强制重新配置,则直接返回
if isConfigured && !force &&
currentCategory == category &&
currentMode == mode &&
currentOptions == options {
NSLog("[AudioSessionManager] 音频会话已配置,跳过配置")
return
} }
do { do {
NSLog("[AudioSessionManager] 应用音频会话配置: \(category)") // NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode)")
// 暂时停用当前会话(如果正在活动)
// if isActive {
// try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
// }
// 设置新的分类、模式和选项
try audioSession.setCategory(category, mode: mode, options: options) try audioSession.setCategory(category, mode: mode, options: options)
try audioSession.setActive(true)
// 设置采样率和缓冲区大小(对于语音识别优化)
// if mode == .spokenAudio {
// try audioSession.setPreferredSampleRate(16000)
// try audioSession.setPreferredIOBufferDuration(0.01) // 10ms
// }
// 重新激活会话
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
isActive = true
// 记录当前配置 // 记录当前配置
currentCategory = category currentCategory = category
@ -115,10 +224,109 @@ import AVFoundation
currentOptions = options currentOptions = options
isConfigured = true isConfigured = true
return true // 检查并设置首选的输入设备(如果需要)
// if category == .playAndRecord || category == .record {
// try configurePreferredInput()
// }
return
} catch { } catch {
NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)") NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)")
return false throw error
}
}
/// 配置首选输入设备
private func configurePreferredInput() throws {
// 获取可用输入设备
guard let availableInputs = audioSession.availableInputs else {
NSLog("[AudioSessionManager] 无法获取可用输入设备")
return
}
// 首选顺序:有线耳机 > 蓝牙耳机 > 内置麦克风
var preferredInput: AVAudioSessionPortDescription? = nil
// 先尝试找到有线耳机
if let wiredHeadset = availableInputs.first(where: { $0.portType == .headsetMic }) {
preferredInput = wiredHeadset
NSLog("[AudioSessionManager] 设置首选输入设备: 有线耳机")
}
// 如果没有有线耳机,尝试使用蓝牙
else if let bluetooth = availableInputs.first(where: { $0.portType == .bluetoothHFP }) {
preferredInput = bluetooth
NSLog("[AudioSessionManager] 设置首选输入设备: 蓝牙耳机")
}
// 如果没有外部设备,使用内置麦克风
else if let builtIn = availableInputs.first(where: { $0.portType == .builtInMic }) {
preferredInput = builtIn
NSLog("[AudioSessionManager] 设置首选输入设备: 内置麦克风")
}
// 设置首选输入设备
if let input = preferredInput {
try audioSession.setPreferredInput(input)
}
}
/// 激活音频会话(如果已配置)
@objc func activateSession() throws {
if !isConfigured {
NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话,应用默认配置")
// 未配置,应用默认配置
try configureSession(
category: .playAndRecord,
mode: .spokenAudio,
options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers, .defaultToSpeaker],
force: true
)
} else if !isActive {
NSLog("[AudioSessionManager] 激活已配置的音频会话")
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
isActive = true
}
}
/// 停用音频会话(保留配置)
@objc func deactivateSession() throws {
if isActive {
NSLog("[AudioSessionManager] 停用音频会话")
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
isActive = false
}
}
/// 重新配置当前会话
@objc func forceReconfigure() throws {
NSLog("[AudioSessionManager] 强制重新配置音频会话")
try configureSession(
category: currentCategory ?? .playAndRecord,
mode: currentMode ?? .default,
options: currentOptions ?? [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker],
force: true
)
}
// MARK: - 状态检查
/// 检查麦克风权限
@objc func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted:
completion(true)
case .denied:
NSLog("[AudioSessionManager] 麦克风权限被拒绝")
completion(false)
case .undetermined:
NSLog("[AudioSessionManager] 请求麦克风权限")
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
completion(granted)
}
}
@unknown default:
completion(false)
} }
} }
@ -126,51 +334,68 @@ import AVFoundation
@objc func getCurrentConfiguration() -> [String: Any] { @objc func getCurrentConfiguration() -> [String: Any] {
return [ return [
"isConfigured": isConfigured, "isConfigured": isConfigured,
"category": "\(currentCategory)", "isActive": isActive,
"mode": "\(currentMode)" "category": String(describing: currentCategory),
"mode": String(describing: currentMode),
"bluetoothEnabled": currentOptions?.contains(.allowBluetooth) == true,
"currentInputs": audioSession.currentRoute.inputs.map { [$0.portType.rawValue: $0.portName] },
"currentOutputs": audioSession.currentRoute.outputs.map { [$0.portType.rawValue: $0.portName] }
] ]
} }
/// 仅在特殊情况下使用:强制重新配置音频会话 /// 检查当前是否有蓝牙耳机连接
@objc func forceReconfigure() -> Bool { @objc func hasBluetoothHeadsetConnected() -> Bool {
isConfigured = false let outputs = audioSession.currentRoute.outputs
return outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE }
}
/// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
// 在iOS中无法直接检查蓝牙是否启用,我们可以通过检查音频会话配置来推断
do { // 检查音频会话是否配置了允许蓝牙
NSLog("[AudioSessionManager] 强制重新配置音频会话") if let options = currentOptions {
try audioSession.setCategory(currentCategory, mode: currentMode, options: currentOptions) return options.contains(.allowBluetooth) || options.contains(.allowBluetoothA2DP)
try audioSession.setActive(true)
isConfigured = true
return true
} catch {
NSLog("[AudioSessionManager] 强制重新配置失败: \(error.localizedDescription)")
return false
} }
// 如果尚未配置音频会话,尝试检查当前路由中是否有蓝牙设备
let outputs = audioSession.currentRoute.outputs
let hasBluetooth = outputs.contains {
$0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE
}
return hasBluetooth
} }
/// 激活音频会话(如果已配置) /// 获取当前音频路由
@objc func activateSession() throws { @objc func getCurrentAudioRoute() -> String {
if isConfigured { let outputs = audioSession.currentRoute.outputs
try audioSession.setActive(true) if let output = outputs.first {
} else { return "\(output.portType.rawValue) (\(output.portName))"
NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话")
// 应用默认配置并激活
try applyConfigurationIfNeeded(category: .playAndRecord,
mode: .default,
options: [.allowBluetooth, .allowBluetoothA2DP])
} }
return "unknown"
} }
/// 停用音频会话(保留配置) /// 获取当前输入设备
@objc func deactivateSession() throws { @objc func getCurrentInputDevice() -> String {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation) let inputs = audioSession.currentRoute.inputs
if let input = inputs.first {
return "\(input.portType.rawValue) (\(input.portName))"
}
return "unknown"
} }
/// 获取当前音频会话路由描述 // MARK: - 路由管理和监听
/// 获取当前路由信息
@objc func getCurrentRoute() -> AVAudioSessionRouteDescription { @objc func getCurrentRoute() -> AVAudioSessionRouteDescription {
return audioSession.currentRoute return audioSession.currentRoute
} }
/// 添加路由变化监听器 /// 添加路由变更监听器
/// - Parameters:
/// - observer: 监听对象
/// - selector: 监听方法选择器
@objc func addRouteChangeListener(_ observer: Any, selector: Selector) { @objc func addRouteChangeListener(_ observer: Any, selector: Selector) {
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
observer, observer,
@ -180,7 +405,8 @@ import AVFoundation
) )
} }
/// 移除路由变化监听器 /// 移除路由变更监听器
/// - Parameter observer: 要移除的监听对象
@objc func removeRouteChangeListener(_ observer: Any) { @objc func removeRouteChangeListener(_ observer: Any) {
NotificationCenter.default.removeObserver( NotificationCenter.default.removeObserver(
observer, observer,
@ -188,4 +414,216 @@ import AVFoundation
object: nil object: nil
) )
} }
/// 获取设备类型描述
private func getDeviceType(portType: AVAudioSession.Port) -> String {
switch portType {
case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
return "bluetooth"
case .headphones, .headsetMic:
return "wired"
case .airPlay:
return "airplay"
default:
return "unknown"
}
}
// MARK: - 蓝牙媒体按钮配置
/// 配置用于获取蓝牙媒体按钮响应的音频会话
/// 此函数专门针对需要接收蓝牙耳机上媒体按钮事件的场景进行优化
/// - Returns: 配置是否成功
@objc func configureForBluetoothMediaButtons() -> Bool {
do {
// 配置音频会话以支持蓝牙媒体按钮
// 使用playback类别确保可以接收远程控制事件
// 使用spokenAudio模式优化语音交互
try configureSession(
category: .playback,
mode: .default, // 使用default模式,避免与spokenAudio可能的冲突
options: [
.allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
.allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
.mixWithOthers // 允许与其他音频混合,避免中断
],
force: true // 强制重新配置,确保设置生效
)
NSLog("[AudioSessionManager] 已配置蓝牙媒体按钮响应的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置蓝牙媒体按钮响应的音频会话失败: \(error.localizedDescription)")
return false
}
}
// MARK: - Azure语音服务配置
/// 配置用于语音合成(TTS)的音频会话
/// 此函数针对TTS使用场景优化音频播放
/// - Returns: 配置是否成功
@objc func configureForTts() -> Bool {
do {
// 配置音频会话以支持语音合成
// 使用playback类别用于音频播放
try configureSession(
category: .playback,
mode: .spokenAudio, // 针对语音输出优化
options: [
.allowBluetooth, // 允许通过蓝牙连接进行输出
.allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
.mixWithOthers // 允许与其他音频混合,避免中断
],
force: false
)
NSLog("[AudioSessionManager] 已配置TTS的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置TTS的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于语音识别(ASR)的音频会话
/// 此函数针对ASR使用场景优化音频输入
/// - Returns: 配置是否成功
@objc func configureForAsr() -> Bool {
do {
// 配置音频会话以支持语音识别
// 使用record类别专注于录音
try configureSession(
category: .record,
mode: .measurement, // 使用measurement模式获取最高质量的音频输入
options: [
.allowBluetooth // 允许通过蓝牙SCO连接进行输入
],
force: false
)
// 设置首选输入设备
try configurePreferredInput()
NSLog("[AudioSessionManager] 已配置ASR的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置ASR的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于语音合成+音频播放的音频会话(同时支持TTS和音乐播放)
/// 此函数针对同时需要TTS和背景音乐的场景进行优化
/// - Returns: 配置是否成功
@objc func configureForTtsWithMusic() -> Bool {
do {
// 配置音频会话以支持TTS和音乐播放
try configureSession(
category: .playback,
mode: .default, // 使用默认模式以适应多种音频类型
options: [
.allowBluetooth, // 允许通过蓝牙连接进行输出
.allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
.mixWithOthers, // 允许与其他应用的音频混合
.allowAirPlay // 支持AirPlay设备
],
force: false
)
NSLog("[AudioSessionManager] 已配置TTS+音乐播放的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置TTS+音乐播放的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于全语音助手模式的音频会话(优先级最高)
/// 此函数针对需要完全控制音频体验的语音助手场景
/// - Returns: 配置是否成功
@objc func configureForVoiceAssistant() -> Bool {
do {
// 配置音频会话以支持完整语音助手功能
try configureSession(
category: .playAndRecord,
mode: .spokenAudio,
options: [
.allowBluetooth, // 允许通过蓝牙连接
.allowBluetoothA2DP, // 允许通过蓝牙A2DP连接
.defaultToSpeaker, // 默认使用扬声器输出
.duckOthers, // 降低其他应用的音量
.interruptSpokenAudioAndMixWithOthers // 中断其他语音音频并混合
],
force: true // 强制重新配置,确保设置生效
)
// 设置首选输入设备
try configurePreferredInput()
NSLog("[AudioSessionManager] 已配置语音助手模式的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置语音助手模式的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于多任务模式的音频会话(与其他应用友好共存)
/// 此函数针对需要在后台继续工作但不中断其他应用音频的场景
/// - Returns: 配置是否成功
@objc func configureForMultitasking() -> Bool {
do {
// 配置音频会话以支持多任务
try configureSession(
category: .ambient, // 使用ambient类别不会中断其他音频
mode: .default,
options: [
.mixWithOthers, // 允许与其他应用的音频混合
.allowBluetooth, // 允许通过蓝牙连接
.allowBluetoothA2DP // 允许通过蓝牙A2DP连接
],
force: false
)
NSLog("[AudioSessionManager] 已配置多任务模式的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置多任务模式的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于语音交互(同时支持ASR和TTS)的音频会话
/// 此函数综合优化语音识别和语音合成,适用于需要双向交互的场景
/// - Returns: 配置是否成功
@objc func configureForVoiceInteraction() -> Bool {
do {
// 配置音频会话以支持语音交互
// 使用playAndRecord类别允许同时录音和播放
// 使用spokenAudio模式优化语音交互
try configureSession(
category: .playAndRecord,
mode: .spokenAudio,
options: [
// .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
.allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
],
force: false // 强制重新配置,确保设置生效
)
// 设置首选输入设备
// try configurePreferredInput()
NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置语音交互(ASR+TTS)的音频会话失败: \(error.localizedDescription)")
return false
}
}
} }

72
ios/Runner/AzureAsrHelper.swift

@ -141,10 +141,10 @@ class AzureAsrHelper: NSObject {
/// 设置音频会话 /// 设置音频会话
private func setupAudioSession() throws { private func setupAudioSession() throws {
// 使用音频会话管理器配置语音处理模式 print("[AzureAsrHelper] 开始配置音频会话...")
try audioSessionManager.configureForVoiceProcessing()
audioSessionManager.configureForVoiceInteraction()
print("[AzureAsrHelper] 音频会话配置成功,已启用回音消除")
} }
/// 设置所有回调 /// 设置所有回调
@ -279,54 +279,84 @@ class AzureAsrHelper: NSObject {
return false return false
} }
// 如果已经在进行连续识别,先停止 // 检查是否已经在识别
if _isContinuousRecognitionActive { if _isContinuousRecognitionActive {
stopContinuousRecognition() print("[AzureAsrHelper] 已经在进行连续识别中,忽略请求")
return true
} }
// 确保识别器已创建 // 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() { if recognizer == nil {
return false print("[AzureAsrHelper] 尝试重新创建识别器...")
} if !createRecognizerAndSetupCallbacks() {
eventHandler("error", ["message": "无法创建语音识别器"])
// 重新确保音频设置正确 return false
do { }
try setupAudioSession()
} catch {
print("[AzureAsrHelper] 警告: 设置音频会话失败: \(error.localizedDescription)")
} }
// 尝试启动连续识别
do { do {
// 启动连续识别 // 启动连续识别
try recognizer?.startContinuousRecognition() try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true _isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别开始")
print("[AzureAsrHelper] 连续识别已启动")
return true return true
} catch { } catch {
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)") print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
// 发送错误通知
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"]) eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
return false return false
} }
} }
/// 停止连续语音识别 /// 停止连续语音识别
/// - Returns: 是否成功停止识别 /// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool { func stopContinuousRecognition() -> Bool {
if !_isContinuousRecognitionActive || recognizer == nil { if !_isContinuousRecognitionActive {
print("[AzureAsrHelper] 未进行连续识别,忽略停止请求")
return true return true
} }
print("[AzureAsrHelper] 停止连续语音识别")
do { do {
try recognizer?.stopContinuousRecognition() // 防止空指针异常
guard let recognizer = recognizer else {
print("[AzureAsrHelper] 警告: 识别器为空,但状态显示活跃")
_isContinuousRecognitionActive = false
eventHandler("sessionStopped", [:])
return true
}
// 尝试停止
try recognizer.stopContinuousRecognition()
// 标记为非活跃状态
_isContinuousRecognitionActive = false _isContinuousRecognitionActive = false
print("[AzureAsrHelper] 连续识别已停止") print("[AzureAsrHelper] 连续识别已停止")
return true return true
} catch { } catch {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)") print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "停止连续识别失败: \(error.localizedDescription)"])
// 即使失败,也将状态设置为非活跃
_isContinuousRecognitionActive = false _isContinuousRecognitionActive = false
// 发送停止事件,确保上层知道识别已经结束
eventHandler("sessionStopped", [:])
// 尝试释放和重新创建识别器
recognizer = nil
return false return false
} }
} }

2
ios/Runner/AzureTtsHelper.swift

@ -123,7 +123,7 @@ class AzureTtsHelper: NSObject {
private func configureAudioSession() -> Bool { private func configureAudioSession() -> Bool {
do { do {
// 使用音频会话管理器配置媒体播放模式 // 使用音频会话管理器配置媒体播放模式
try audioSessionManager.configureForMediaPlayback() try audioSessionManager.configureForVoiceInteraction()
isAudioSessionConfigured = true isAudioSessionConfigured = true
print("[AzureTtsHelper] 音频会话配置成功") print("[AzureTtsHelper] 音频会话配置成功")

170
ios/Runner/BluetoothMediaButtonHelper.swift

@ -15,6 +15,8 @@ import UIKit
class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
// MARK: - 属性 // MARK: - 属性
@objc static let shared = BluetoothMediaButtonHelper()
/// 日志标签 /// 日志标签
private let TAG = "BluetoothMediaButtonHelper" private let TAG = "BluetoothMediaButtonHelper"
@ -89,14 +91,14 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
// 尝试启用静音音频播放(但即使失败,远程命令仍然可用) // 尝试启用静音音频播放(但即使失败,远程命令仍然可用)
do { do {
// try passivelySetupAudioSession() // try audioSessionManager.configureForBluetoothMediaButtons()
startSilentAudio() // startSilentAudio()
//延时1秒 //延时1秒
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { // DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.setupNowPlaying(isPlaying: false) // self.setupNowPlaying(isPlaying: false)
self.stopSilentAudio() // self.stopSilentAudio()
} // }
// startUpdatingPlaybackInfo() // startUpdatingPlaybackInfo()
} catch { } catch {
@ -274,6 +276,12 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
commandCenter.seekBackwardCommand.isEnabled = true commandCenter.seekBackwardCommand.isEnabled = true
commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:))) commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:)))
// 停止命令
commandCenter.stopCommand.isEnabled = true
commandCenter.stopCommand.addTarget(self, action: #selector(handleStopCommand(_:)))
NSLog("\(TAG): 远程命令已注册") NSLog("\(TAG): 远程命令已注册")
} }
@ -281,7 +289,7 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
private func unregisterRemoteCommands() { private func unregisterRemoteCommands() {
NSLog("\(TAG): 取消注册远程命令") NSLog("\(TAG): 取消注册远程命令")
// 取消所有命令 // 基本控制命令
remoteCommandCenter.playCommand.isEnabled = false remoteCommandCenter.playCommand.isEnabled = false
remoteCommandCenter.playCommand.removeTarget(nil) remoteCommandCenter.playCommand.removeTarget(nil)
@ -291,147 +299,105 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
remoteCommandCenter.togglePlayPauseCommand.isEnabled = false remoteCommandCenter.togglePlayPauseCommand.isEnabled = false
remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil) remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil)
remoteCommandCenter.nextTrackCommand.isEnabled = false remoteCommandCenter.stopCommand.isEnabled = false
remoteCommandCenter.nextTrackCommand.removeTarget(nil) remoteCommandCenter.stopCommand.removeTarget(nil)
remoteCommandCenter.previousTrackCommand.isEnabled = false
remoteCommandCenter.previousTrackCommand.removeTarget(nil)
remoteCommandCenter.seekForwardCommand.isEnabled = false
remoteCommandCenter.seekForwardCommand.removeTarget(nil)
remoteCommandCenter.seekBackwardCommand.isEnabled = false
remoteCommandCenter.seekBackwardCommand.removeTarget(nil)
} }
/// 被动设置音频会话 - 只读取现有配置,不做侵入性修改 /// 发送按钮事件到Flutter
private func passivelySetupAudioSession() throws { private func sendButtonEvent(type: String) {
let audioSession = AVAudioSession.sharedInstance() // 创建事件数据
let eventData: [String: Any] = [
// 如果会话已经活跃,我们完全不修改它 "type": "mediaButtonEvent",
if audioSession.isOtherAudioPlaying || audioSession.isInputAvailable { "buttonType": type,
NSLog("\(TAG): 检测到活跃的音频会话,不进行任何修改") "timestamp": Int(Date().timeIntervalSince1970 * 1000)
return ]
}
// 检查应用是否已经拥有音频会话
if audioSession.category != .ambient && audioSession.category != .soloAmbient {
// 应用已经有一个有意义的类别,不要修改它
NSLog("\(TAG): 检测到应用已设置音频会话类别: \(audioSession.category)")
return
}
NSLog("\(TAG): 当前没有活跃的音频,设置最小化会话配置")
// 使用音频会话管理器配置媒体播放模式
try audioSessionManager.configure(category: .ambient, mode: .default, options: [])
NSLog("\(TAG): 被动音频会话配置完成") // 发送事件到处理器
eventHandler?.sendEvent(eventData)
} }
/// 检查是否有其他应用正在播放音频
private func isOtherAudioPlaying() -> Bool {
return AVAudioSession.sharedInstance().isOtherAudioPlaying
}
// MARK: - 命令处理方法 // MARK: - 命令处理方法
/// 处理播放/暂停命令 /// 处理播放/暂停命令
@objc func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { @objc private func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到播放/暂停命令") NSLog("\(TAG): 收到播放/暂停命令")
// 创建事件数据 // 发送事件到Flutter
let eventData: [String: Any] = [ sendButtonEvent(type: "play_pause")
"type": "mediaButtonEvent",
"buttonType": "playPause",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器 // 调用VoiceInteractionService处理媒体按钮事件
eventHandler?.sendEvent(eventData) if VoiceInteractionService.shared.isServiceRunning() {
NSLog("\(TAG): 转发到VoiceInteractionService处理")
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success return .success
} }
/// 处理下一曲命令 /// 处理下一曲命令
@objc func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { @objc private func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到下一曲命令") NSLog("\(TAG): 收到下一曲命令")
// 创建事件数据 // 发送事件到Flutter
let eventData: [String: Any] = [ sendButtonEvent(type: "next_track")
"type": "mediaButtonEvent",
"buttonType": "next",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器 // 也可以转发到VoiceInteractionService
eventHandler?.sendEvent(eventData) if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success return .success
} }
/// 处理上一曲命令 /// 处理上一曲命令
@objc func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { @objc private func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到上一曲命令") NSLog("\(TAG): 收到上一曲命令")
// 创建事件数据 // 发送事件到Flutter
let eventData: [String: Any] = [ sendButtonEvent(type: "previous_track")
"type": "mediaButtonEvent",
"buttonType": "previous",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器 // 也可以转发到VoiceInteractionService
eventHandler?.sendEvent(eventData) if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success return .success
} }
/// 处理快进命令 /// 处理快进命令
@objc func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { @objc private func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快进命令") NSLog("\(TAG): 收到快进命令")
// 创建事件数据 // 发送事件到Flutter
let eventData: [String: Any] = [ sendButtonEvent(type: "seek_forward")
"type": "mediaButtonEvent",
"buttonType": "seekForward",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
// 模拟快进
currentPlaybackTime += 10.0
if currentPlaybackTime > totalDuration {
currentPlaybackTime = totalDuration
}
setupNowPlaying(isPlaying: true)
return .success return .success
} }
/// 处理快退命令 /// 处理快退命令
@objc func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { @objc private func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快退命令") NSLog("\(TAG): 收到快退命令")
// 创建事件数据 // 发送事件到Flutter
let eventData: [String: Any] = [ sendButtonEvent(type: "seek_backward")
"type": "mediaButtonEvent",
"buttonType": "seekBackward",
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器 return .success
eventHandler?.sendEvent(eventData) }
/// 处理停止命令
@objc private func handleStopCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到停止命令")
// 模拟快退 // 发送事件到Flutter
currentPlaybackTime -= 10.0 sendButtonEvent(type: "stop")
if currentPlaybackTime < 0 {
currentPlaybackTime = 0 // 调用VoiceInteractionService处理媒体按钮事件
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
} }
setupNowPlaying(isPlaying: true)
return .success return .success
} }

82
ios/Runner/ClassicBluetoothHelper.swift

@ -8,15 +8,13 @@ import UIKit
func sendEvent(_ event: [String: Any]) func sendEvent(_ event: [String: Any])
} }
@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate { @objc class ClassicBluetoothHelper: NSObject {
// MARK: - 属性 // MARK: - 属性
// 音频会话管理器 // 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared private let audioSessionManager = AudioSessionManager.shared
// 蓝牙管理器
private var centralManager: CBCentralManager?
// 事件处理器 // 事件处理器
private weak var eventHandler: BluetoothEventHandler? private weak var eventHandler: BluetoothEventHandler?
@ -31,32 +29,6 @@ import UIKit
eventHandler = handler eventHandler = handler
} }
// 初始化蓝牙(主入口方法)
@objc func initBluetooth() {
// 初始化蓝牙中央管理器
if centralManager == nil {
centralManager = CBCentralManager(delegate: self, queue: nil)
}
// 配置音频会话
configureAudioSession()
// 启动设备监控
startBluetoothDeviceMonitoring()
// 发送当前蓝牙状态
if let manager = centralManager {
let stateString = getBluetoothStateString(state: manager.state)
sendBluetoothStateEvent(state: stateString)
}
}
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
let stateString = getBluetoothStateString(state: central.state)
sendBluetoothStateEvent(state: stateString)
}
// MARK: - 设备监控 // MARK: - 设备监控
@ -86,6 +58,7 @@ import UIKit
@objc func startBluetoothDeviceMonitoring() { @objc func startBluetoothDeviceMonitoring() {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
audioSessionManager.addRouteChangeListener(self, selector: #selector(handleRouteChange(_:))) audioSessionManager.addRouteChangeListener(self, selector: #selector(handleRouteChange(_:)))
BluetoothMediaButtonHelper.shared.startButtonListening()
} }
// 处理音频路由变化 // 处理音频路由变化
@ -135,41 +108,23 @@ import UIKit
// MARK: - 设备事件 // MARK: - 设备事件
// 发送设备连接事件 // 发送设备已连接事件
private func sendDeviceConnectedEvent(device: [String: String]) { private func sendDeviceConnectedEvent(device: [String: String]) {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "headset"
]
let event: [String: Any] = [ let event: [String: Any] = [
"type": "deviceConnected", "type": "deviceConnected",
"device": deviceMap, "device": device,
"timestamp": Int(Date().timeIntervalSince1970 * 1000) "timestamp": Int(Date().timeIntervalSince1970 * 1000)
] ]
eventHandler?.sendEvent(event) eventHandler?.sendEvent(event)
} }
// 发送设备断开事件 // 发送设备已断开事件
private func sendDeviceDisconnectedEvent(device: [String: String]) { private func sendDeviceDisconnectedEvent(device: [String: String]) {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "headset"
]
let event: [String: Any] = [ let event: [String: Any] = [
"type": "deviceDisconnected", "type": "deviceDisconnected",
"device": deviceMap, "device": device,
"timestamp": Int(Date().timeIntervalSince1970 * 1000) "timestamp": Int(Date().timeIntervalSince1970 * 1000)
] ]
eventHandler?.sendEvent(event) eventHandler?.sendEvent(event)
} }
@ -177,12 +132,7 @@ import UIKit
// 获取当前连接的耳机设备 // 获取当前连接的耳机设备
@objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) { @objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) {
// 检查蓝牙状态
if centralManager?.state != .poweredOn {
completion(nil, "蓝牙未启用")
return
}
do { do {
// 获取设备 // 获取设备
let devices = getConnectedAudioDevices() let devices = getConnectedAudioDevices()
@ -210,23 +160,11 @@ import UIKit
return result return result
} }
// 释放资源
@objc func dispose() {
audioSessionManager.removeRouteChangeListener(self)
eventHandler = nil
}
// 配置音频会话
private func configureAudioSession() {
do {
try audioSessionManager.configureForBluetoothHeadset()
} catch {}
}
// 检查蓝牙是否启用 // 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool { @objc func isBluetoothEnabled() -> Bool {
return centralManager?.state == .poweredOn return audioSessionManager.isBluetoothEnabled()
} }
// 清理设备地址,移除后缀 // 清理设备地址,移除后缀

514
ios/Runner/VoiceInteractionService.swift

@ -0,0 +1,514 @@
import Foundation
import UIKit
import AVFoundation
/**
* 语音交互服务,iOS原生实现
*
* 负责:
* 1) 监听蓝牙耳机按键
* 2) 处理语音识别
* 3) 与火山AI服务交互
* 4) 文本转语音播放
*/
@available(iOS 13.0, *)
class VoiceInteractionService: NSObject {
// 常量
private let TAG = "VoiceInteractionService"
private let RECOGNITION_TIMEOUT: TimeInterval = 8.0
// 单例
static let shared = VoiceInteractionService()
// 服务状态
private var isRunning = false
private var isActive = false
private var isRecognitionActive = false
private var isTimeoutPaused = false
private var hasSpeechDetected = false
private var isTtsSpeaking = false
// 事件处理器
private weak var eventHandler: VoiceInteractionEventHandler?
// 按键处理
private var lastKeyEventTime: TimeInterval = 0
private var keyEventCount = 0
// 活动时间
private var lastActivityTime: TimeInterval = 0
// 当前用户输入
private var currentUserInput = ""
// 服务组件
private var azureAsrHelper: AzureAsrHelper?
private var azureTtsHelper: AzureTtsHelper?
private var volcanoAIService: VolcanoAIService?
private var silenceTimer: Timer?
// 系统提示词
private let systemPrompt = """
你是一个智能语音助手,能够简洁明了地回答用户的问题。
请保持回答简短、准确,避免过长的解释。
如果用户的问题不清楚,请礼貌地请求澄清。
不要使用复杂的术语,除非用户明确要求。
用户用语音和你交互。
"""
// 初始化方法
private override init() {
super.init()
NSLog("%@: VoiceInteractionService 初始化中", TAG)
}
/**
* 启动服务
*
* @param apiKey Azure语音服务API密钥
* @param region Azure语音服务区域
* @param volcanoKey 火山AI服务API密钥
* @return 启动是否成功
*/
func start(azureKey: String, azureRegion: String, volcanoKey: String) -> Bool {
guard !isRunning else {
NSLog("%@: 服务已经在运行", TAG)
return true
}
NSLog("%@: 启动服务中", TAG)
// 重置状态
resetState()
// 初始化各个服务组件
initServices(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey)
// 播放静音音频,确保音频会话活跃
// startSilencePlayback()
// 启动monitorService定时器
silenceTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(monitorService), userInfo: nil, repeats: true)
NSLog("%@: monitorService定时器已启动", TAG)
// 标记服务为运行状态
isRunning = true
NSLog("%@: 服务启动完成,等待蓝牙按键事件", TAG)
return true
}
/**
* 停止服务
*/
func stop() {
guard isRunning else {
NSLog("%@: 服务未运行", TAG)
return
}
NSLog("%@: 停止服务中", TAG)
// 停止monitorService定时器
silenceTimer?.invalidate()
silenceTimer = nil
// 停止语音识别
if isRecognitionActive {
stopVoiceRecognition()
}
// 停止TTS
stopCurrentTTS()
// 释放服务实例
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 更新状态
resetState()
isRunning = false
NSLog("%@: 服务已停止", TAG)
}
/**
* 获取服务运行状态
*/
func isServiceRunning() -> Bool {
return isRunning
}
/**
* 重置状态
*/
private func resetState() {
isActive = false
isRecognitionActive = false
isTimeoutPaused = false
hasSpeechDetected = false
isTtsSpeaking = false
}
/**
* 初始化服务
*/
private func initServices(azureKey: String, azureRegion: String, volcanoKey: String) {
NSLog("%@: 初始化服务组件", TAG)
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper { [weak self] eventName, eventData in
// 处理ASR事件
self?.handleAsrEvent(eventName: eventName, eventData: eventData)
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper { [weak self] eventName, eventData in
// 处理TTS事件
self?.handleTtsEvent(eventName: eventName, eventData: eventData)
}
// 初始化火山AI服务
volcanoAIService = VolcanoAIService()
// 配置Azure ASR
if !azureKey.isEmpty && !azureRegion.isEmpty {
azureAsrHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, supportedLanguages: ["zh-CN"])
NSLog("%@: Azure ASR初始化完成", TAG)
} else {
NSLog("%@: Azure配置信息不完整,无法初始化Azure ASR", TAG)
}
// 配置Azure TTS
if !azureKey.isEmpty && !azureRegion.isEmpty {
azureTtsHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, language: "zh-CN")
NSLog("%@: Azure TTS初始化完成", TAG)
} else {
NSLog("%@: Azure配置信息不完整,无法初始化Azure TTS", TAG)
}
// 配置火山AI
if !volcanoKey.isEmpty {
volcanoAIService?.initialize(apiKey: volcanoKey)
NSLog("%@: 火山AI服务初始化完成", TAG)
} else {
NSLog("%@: 火山AI配置信息不完整,无法初始化火山AI服务", TAG)
}
}
/**
* 监控服务状态
*/
@objc private func monitorService() {
// 确保服务保持活跃状态
if !isActive {
isActive = true
}
// 检查语音识别状态
if isRecognitionActive {
let currentTime = Date().timeIntervalSince1970
let elapsedTime = currentTime - lastActivityTime
// NSLog("%@: 已过去时间: %@, 是否检测到语音: %@, 是否TTS播放: %@", TAG, String(elapsedTime), String(hasSpeechDetected), String(isTtsSpeaking), String(elapsedTime))
// 如果超过超时时间没有检测到语音,且不在TTS播放中,暂停语音识别
if !hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT {
// NSLog("%@: 超过%@秒未检测到语音,停止识别", TAG, String(RECOGNITION_TIMEOUT))
isTimeoutPaused = true
playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。")
// 停止语音识别并确保资源完全释放
stopVoiceRecognition()
// 清理识别状态
isRecognitionActive = false
hasSpeechDetected = false
}
}
}
/**
* 处理ASR事件
*/
private func handleAsrEvent(eventName: String, eventData: [String: Any]) {
// NSLog("%@: 处理ASR事件: %@", TAG, eventName)
switch eventName {
case "recognizing":
if let text = eventData["text"] as? String, !text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
}
case "result":
if let text = eventData["text"] as? String, !text.isEmpty {
updateLastActivityTime()
processWithVolcanoAI(text)
}
// 重置状态,继续识别
hasSpeechDetected = false
case "sessionStarted":
updateLastActivityTime()
case "sessionStopped":
isRecognitionActive = false
case "canceled":
isRecognitionActive = false
case "error":
isRecognitionActive = false
playNotification("语音识别出错")
default:
break
}
}
/**
* 处理TTS事件
*/
private func handleTtsEvent(eventName: String, eventData: [String: Any]) {
// NSLog("%@: 处理TTS事件: %@", TAG, eventName)
switch eventName {
case "started":
isTtsSpeaking = true
case "completed":
isTtsSpeaking = false
updateLastActivityTime()
case "canceled":
isTtsSpeaking = false
case "error":
isTtsSpeaking = false
NSLog("%@: TTS错误: %@", TAG, eventData["error"] as? String ?? "未知错误")
default:
break
}
}
/**
* 处理双击事件
*/
func handleMediaButtonAction() {
NSLog("%@: 处理媒体按钮事件", TAG)
// 屏蔽短时间重复响应的问题
let currentTime = Date().timeIntervalSince1970
let timeDiff = currentTime - lastKeyEventTime
if timeDiff < 0.5 {
NSLog("%@: 短时间重复响应,忽略", TAG)
return
}
lastKeyEventTime = currentTime
// 停止当前TTS播放
stopCurrentTTS()
// 播放提示音
playPrompt("我在!")
// 重置超时暂停标志
isTimeoutPaused = false
// 启动或重置语音识别
if !isRecognitionActive {
NSLog("%@: 语音识别未激活,开始启动", TAG)
startVoiceRecognition()
} else {
NSLog("%@: 语音识别已激活,更新活动时间", TAG)
updateLastActivityTime()
hasSpeechDetected = false
}
}
/**
* 开始语音识别
*/
private func startVoiceRecognition() {
if isRecognitionActive { return }
// 通知Flutter语音识别已启动
notifyVoiceRecognitionStarted()
isActive = true
isRecognitionActive = true
hasSpeechDetected = false
updateLastActivityTime()
if let asrHelper = azureAsrHelper {
let success = asrHelper.startContinuousRecognition()
if !success {
isRecognitionActive = false
NSLog("%@: 启动语音识别失败", TAG)
playNotification("启动语音识别失败")
}
} else {
isRecognitionActive = false
NSLog("%@: Azure ASR服务未初始化", TAG)
playNotification("语音识别服务未初始化")
}
}
/**
* 停止语音识别
*/
private func stopVoiceRecognition() {
if !isRecognitionActive { return }
NSLog("%@: 停止语音识别", TAG)
if let asrHelper = azureAsrHelper {
let success = asrHelper.stopContinuousRecognition()
if !success {
NSLog("%@: 停止语音识别失败", TAG)
}
}
isRecognitionActive = false
hasSpeechDetected = false
}
/**
* 使用火山AI处理语音识别结果
*/
private func processWithVolcanoAI(_ text: String) {
// 保存当前用户输入
currentUserInput = text
// 在后台线程处理
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self, let volcanoAIService = self.volcanoAIService else {
return
}
do {
// 创建消息
let message = volcanoAIService.createUserMessage(content: text)
let messages = [message]
// 发送请求
let response = try volcanoAIService.sendMessage(messages: messages, systemPrompt: self.systemPrompt)
// 播放AI回复
self.speakAIResponse(response)
// 同步聊天记录到Flutter端
self.notifyChatHistoryUpdated(userMessage: text, assistantMessage: response)
} catch {
NSLog("%@: AI处理出错: %@", self.TAG, error.localizedDescription)
self.playNotification("AI处理出错")
}
}
}
/**
* 播放AI回复
*/
private func speakAIResponse(_ text: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: text)
}
/**
* 播放提示音
*/
private func playPrompt(_ message: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: message)
}
/**
* 播放通知提示音
*/
private func playNotification(_ message: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: message)
}
/**
* 停止当前TTS播放
*/
private func stopCurrentTTS() {
if isTtsSpeaking {
azureTtsHelper?.stopSpeaking()
isTtsSpeaking = false
}
}
/**
* 更新最后活动时间
*/
private func updateLastActivityTime() {
lastActivityTime = Date().timeIntervalSince1970
}
/**
* 暂停语音交互
*/
func pauseVoiceInteraction() {
NSLog("%@: 暂停语音交互", TAG)
// 停止当前语音播放
stopCurrentTTS()
// 停止语音识别
if isRecognitionActive {
stopVoiceRecognition()
}
// 设置状态为暂停,但服务保持运行
isTimeoutPaused = true
}
/**
* 通知语音识别已开始
*/
private func notifyVoiceRecognitionStarted() {
// 发送识别开始事件
sendEvent(type: "recognition_started", data: [:])
}
/**
* 通知聊天历史更新
*/
private func notifyChatHistoryUpdated(userMessage: String, assistantMessage: String) {
// 发送聊天历史更新事件
sendEvent(type: "chat_history_updated", data: [
"agentId": "personal_assistant",
"userMessage": userMessage,
"assistantMessage": assistantMessage
])
}
/**
* 设置事件处理器
*/
func setEventHandler(_ handler: VoiceInteractionEventHandler) {
self.eventHandler = handler
}
/**
* 发送事件到Flutter端
*/
private func sendEvent(type: String, data: [String: Any] = [:]) {
var eventData = data
eventData["type"] = type
eventData["timestamp"] = Int(Date().timeIntervalSince1970 * 1000)
// 使用事件处理器发送事件
eventHandler?.sendEvent(eventData)
}
}

391
ios/Runner/VolcanoAIService.swift

@ -0,0 +1,391 @@
import Foundation
/**
* 火山AI服务的iOS原生实现
*
* 参考Android端的VolcanoAIService实现,提供同步和异步的API调用方式
*/
class VolcanoAIService {
private let TAG = "VolcanoAIService"
private let baseUrl = "https://ark.cn-beijing.volces.com/api/v3"
private let chatEndpoint = "/chat/completions"
private let session: URLSession
private var apiKey: String = ""
private var isInitialized = false
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 30.0
self.session = URLSession(configuration: config)
}
/**
* 初始化火山AI服务
*
* @param apiKey 火山AI API密钥
* @return 初始化是否成功
*/
func initialize(apiKey: String) -> Bool {
self.apiKey = apiKey
isInitialized = !apiKey.isEmpty
if !isInitialized {
NSLog("%@: 初始化失败:API key 不能为空", TAG)
} else {
NSLog("%@: 火山AI服务初始化成功", TAG)
}
return isInitialized
}
/**
* 生成个性化问候语
*
* @param agentName 代理名称
* @param systemPrompt 系统提示词
* @param callback 回调函数,返回生成的问候语
*/
func generateGreeting(agentName: String, systemPrompt: String, callback: @escaping (String?, Error?) -> Void) {
let messages: [[String: Any]] = [
["role": "system", "content": systemPrompt],
["role": "user", "content": "请用一句简短的话向我打个招呼,要符合你的身份和性格特点,不要超过18个字。"]
]
let messagesData = try? JSONSerialization.data(withJSONObject: messages, options: [])
let messagesArray = try? JSONSerialization.jsonObject(with: messagesData!, options: []) as? [[String: Any]]
var result = ""
sendMessageStream(messages: messagesArray!, systemPrompt: systemPrompt, streamCallback: StreamCallback(
onToken: { token in
result.append(token)
},
onComplete: {
callback(result, nil)
},
onError: { error in
callback(nil, error)
}
))
}
/**
* 发送消息(非流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回AI的回复
* @throws VolcanoAIError 如果API调用失败
*/
func sendMessage(messages: [[String: Any]], systemPrompt: String) throws -> String {
// 检查是否已初始化
if !isInitialized || apiKey.isEmpty {
throw VolcanoAIError.serviceNotInitialized
}
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
let requestBody: [String: Any] = [
"model": "doubao-1-5-lite-32k-250115",
"messages": fullMessages,
"temperature": 0.7,
"max_tokens": 2000,
"stream": false
]
guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else {
throw VolcanoAIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
throw VolcanoAIError.invalidRequestBody
}
let semaphore = DispatchSemaphore(value: 0)
var responseData: Data?
var responseError: Error?
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
responseError = VolcanoAIError.networkError(error.localizedDescription)
semaphore.signal()
return
}
guard let httpResponse = response as? HTTPURLResponse else {
responseError = VolcanoAIError.invalidResponse
semaphore.signal()
return
}
if !(200...299).contains(httpResponse.statusCode) {
var errorMessage = "Unknown error occurred"
if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? [String: Any],
let message = error["message"] as? String {
errorMessage = message
}
responseError = VolcanoAIError.apiError(errorMessage)
semaphore.signal()
return
}
responseData = data
semaphore.signal()
}
task.resume()
_ = semaphore.wait(timeout: .distantFuture)
if let error = responseError {
throw error
}
guard let data = responseData else {
throw VolcanoAIError.emptyResponse
}
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let choices = json["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let message = firstChoice["message"] as? [String: Any],
let content = message["content"] as? String else {
throw VolcanoAIError.invalidResponseFormat
}
return content
} catch {
throw VolcanoAIError.invalidResponseFormat
}
}
/**
* 发送消息(流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @param streamCallback 回调函数,用于接收流式输出的结果
*/
func sendMessageStream(messages: [[String: Any]], systemPrompt: String, streamCallback: StreamCallback) {
// 检查是否已初始化
if !isInitialized || apiKey.isEmpty {
streamCallback.onError(VolcanoAIError.serviceNotInitialized)
return
}
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
let requestBody: [String: Any] = [
"model": "doubao-1-5-lite-32k-250115",
"messages": fullMessages,
"temperature": 0.7,
"max_tokens": 2000,
"stream": true
]
guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else {
streamCallback.onError(VolcanoAIError.invalidURL)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("text/event-stream", forHTTPHeaderField: "Accept")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
streamCallback.onError(VolcanoAIError.invalidRequestBody)
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
streamCallback.onError(VolcanoAIError.networkError(error.localizedDescription))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
streamCallback.onError(VolcanoAIError.invalidResponse)
return
}
if !(200...299).contains(httpResponse.statusCode) {
var errorMessage = "Unknown error occurred"
if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? [String: Any],
let message = error["message"] as? String {
errorMessage = message
}
streamCallback.onError(VolcanoAIError.apiError(errorMessage))
return
}
guard let data = data else {
streamCallback.onError(VolcanoAIError.emptyResponse)
return
}
// 处理SSE流数据
let responseString = String(data: data, encoding: .utf8) ?? ""
let lines = responseString.components(separatedBy: "\n")
for line in lines {
if line.isEmpty { continue }
if line.hasPrefix("data: ") {
let data = String(line.dropFirst(6))
if data == "[DONE]" {
streamCallback.onComplete()
break
}
do {
if let jsonData = data.data(using: .utf8),
let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let choices = json["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let delta = firstChoice["delta"] as? [String: Any],
let content = delta["content"] as? String {
streamCallback.onToken(content)
}
} catch {
// 忽略无效的JSON数据
continue
}
}
}
}
task.resume()
}
/**
* 同步方式发送消息(流式输出)
*
* 注意:此方法会阻塞当前线程,请在后台线程中调用
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回完整的AI回复
* @throws VolcanoAIError 如果API调用失败
*/
func sendMessageStreamSync(messages: [[String: Any]], systemPrompt: String) throws -> String {
var result = ""
let semaphore = DispatchSemaphore(value: 0)
var responseError: Error?
sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: StreamCallback(
onToken: { token in
result.append(token)
},
onComplete: {
semaphore.signal()
},
onError: { error in
responseError = error
semaphore.signal()
}
))
// 等待流式输出完成或出错
_ = semaphore.wait(timeout: .now() + 60)
if let error = responseError {
throw error
}
return result
}
/**
* 创建用户消息
*/
func createUserMessage(content: String) -> [String: Any] {
return ["role": "user", "content": content]
}
/**
* 创建系统消息
*/
func createSystemMessage(content: String) -> [String: Any] {
return ["role": "system", "content": content]
}
/**
* 创建助手消息
*/
func createAssistantMessage(content: String) -> [String: Any] {
return ["role": "assistant", "content": content]
}
/**
* 流式输出回调类
*/
class StreamCallback {
let onToken: (String) -> Void
let onComplete: () -> Void
let onError: (Error) -> Void
init(onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void) {
self.onToken = onToken
self.onComplete = onComplete
self.onError = onError
}
}
}
/**
* 火山AI错误枚举
*/
enum VolcanoAIError: Error {
case serviceNotInitialized
case invalidURL
case invalidRequestBody
case networkError(String)
case invalidResponse
case emptyResponse
case invalidResponseFormat
case apiError(String)
var localizedDescription: String {
switch self {
case .serviceNotInitialized:
return "火山AI服务未初始化或API key为空,请先调用initialize方法"
case .invalidURL:
return "无效的URL"
case .invalidRequestBody:
return "无效的请求体"
case .networkError(let message):
return "网络错误: \(message)"
case .invalidResponse:
return "无效的响应"
case .emptyResponse:
return "空响应"
case .invalidResponseFormat:
return "无效的响应格式"
case .apiError(let message):
return "API错误: \(message)"
}
}
}

14
lib/core/bindings/initial_binding.dart

@ -1,16 +1,14 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:io';
import '../../data/services/speech_factory.dart'; import '../../data/services/speech_factory.dart';
import '../../data/services/chat_history_service.dart'; import '../../data/services/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_translation_service.dart';
import '../../data/services/voice_interaction_service.dart';
import '../../data/services/language_manager.dart'; import '../../data/services/language_manager.dart';
import '../../data/services/volcano_ai_service.dart'; import '../../data/services/volcano_ai_service.dart';
import '../../data/services/classic_bluetooth_service.dart'; import '../../data/services/classic_bluetooth_service.dart';
import '../../data/services/bluetooth_media_button_service.dart'; import '../../data/services/bluetooth_media_button_service.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import '../../data/services/voice_interaction_impl/ios_voice_interaction_service.dart'; import '../../data/services/voice_interaction_service.dart';
import '../../data/services/voice_interaction_impl/android_voice_interaction_service.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
@ -44,12 +42,8 @@ class InitialBinding extends Bindings {
// 根据平台注册对应的VoiceInteractionService实现
if (Platform.isIOS) {
Get.lazyPut<VoiceInteractionService>(() => IosVoiceInteractionService(), fenix: true);
} else { Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), fenix: true);
Get.lazyPut<VoiceInteractionService>(() => AndroidVoiceInteractionService(), fenix: true);
}
} }
} }

7
lib/data/services/bluetooth_media_button_service.dart

@ -193,14 +193,7 @@ class BluetoothMediaButtonService extends GetxService {
timestamp: eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch, timestamp: eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch,
); );
// 唤醒语音交互服务
try {
Get.find<VoiceInteractionService>().onWakeup();
} catch (e) {
Logger.error('唤醒语音交互服务失败: $e');
}
// 广播事件 // 广播事件
_buttonEventController.add(buttonEvent); _buttonEventController.add(buttonEvent);

3
lib/data/services/classic_bluetooth_service.dart

@ -153,6 +153,7 @@ class ClassicBluetoothService extends GetxService {
.receiveBroadcastStream() .receiveBroadcastStream()
.listen((event) { .listen((event) {
if (event is Map) { if (event is Map) {
Logger.info('蓝牙状态监听: $event');
final type = event['type'] as String?; final type = event['type'] as String?;
if (type == 'bluetoothStateChanged') { if (type == 'bluetoothStateChanged') {
@ -174,7 +175,7 @@ class ClassicBluetoothService extends GetxService {
/// 处理设备连接事件 /// 处理设备连接事件
void _handleDeviceConnected(Map event) { void _handleDeviceConnected(Map event) {
final deviceMap = event['device'] as Map?; final deviceMap = event['device'] as Map?;
if (deviceMap != null && (deviceMap['type'] == 'headset' || deviceMap['type'] == 'unknown')) { if (deviceMap != null) {
final device = BluetoothDevice( final device = BluetoothDevice(
name: deviceMap['name'] ?? '未知设备', name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '', address: deviceMap['address'] ?? '',

7
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -6,13 +6,12 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../modules/chat/models/message_model.dart'; import '../../../modules/chat/models/message_model.dart';
import '../../models/events/voice_interaction_event.dart'; import '../../models/events/voice_interaction_event.dart';
import '../chat_history_service.dart'; import '../chat_history_service.dart';
import '../voice_interaction_service.dart';
/// Android语音交互服务 /// Android语音交互服务
/// ///
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, /// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口,
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 /// 用于管理后台语音交互服务的生命周期和接收语音交互事件
class AndroidVoiceInteractionService extends GetxService implements VoiceInteractionService { class AndroidVoiceInteractionService extends GetxService {
static AndroidVoiceInteractionService get to => Get.find(); static AndroidVoiceInteractionService get to => Get.find();
// 方法通道和事件通道 // 方法通道和事件通道
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction');
@ -89,7 +88,7 @@ class AndroidVoiceInteractionService extends GetxService implements VoiceInterac
/// 初始化服务 /// 初始化服务
@override @override
Future<VoiceInteractionService> initialize() async { Future<AndroidVoiceInteractionService> initialize() async {
try { try {
if (_isInitialized) { if (_isInitialized) {
return this; return this;
@ -111,7 +110,7 @@ class AndroidVoiceInteractionService extends GetxService implements VoiceInterac
return this; return this;
} }
} }
@override @override
void onWakeup() { void onWakeup() {
Logger.info('语音交互服务唤醒'); Logger.info('语音交互服务唤醒');

5
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -7,14 +7,13 @@ import '../chat_history_service.dart';
import '../../../modules/chat/models/message_model.dart'; import '../../../modules/chat/models/message_model.dart';
import '../../models/events/voice_interaction_event.dart'; import '../../models/events/voice_interaction_event.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../voice_interaction_service.dart';
enum VoiceInteractionState { enum VoiceInteractionState {
idle, // 等待唤醒 idle, // 等待唤醒
active, // 活跃状态 - 可以同时识别和播放 active, // 活跃状态 - 可以同时识别和播放
} }
class IosVoiceInteractionService extends GetxService implements VoiceInteractionService { class IosVoiceInteractionService extends GetxService {
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final TtsService _ttsService = Get.find<TtsService>(); final TtsService _ttsService = Get.find<TtsService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
@ -60,7 +59,7 @@ class IosVoiceInteractionService extends GetxService implements VoiceInteraction
/// 初始化服务 /// 初始化服务
@override @override
Future<VoiceInteractionService> initialize() async { Future<IosVoiceInteractionService> initialize() async {
try { try {
// 保持idle状态,不自动启动语音识别 // 保持idle状态,不自动启动语音识别
Logger.info('iOS语音交互服务初始化完成,处于idle状态'); Logger.info('iOS语音交互服务初始化完成,处于idle状态');

286
lib/data/services/voice_interaction_service.dart

@ -1,33 +1,275 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/events/voice_interaction_event.dart'; import '../models/events/voice_interaction_event.dart';
import '../../core/utils/logger.dart';
import '../../modules/chat/models/message_model.dart';
import 'chat_history_service.dart';
/// 语音交互服务抽象基类 /// 语音交互服务接口
/// ///
/// 定义了iOS和Android平台共用的语音交互服务接口 /// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用
abstract class VoiceInteractionService { class VoiceInteractionService extends GetxService {
/// 事件流,用于发布语音交互相关事件 static VoiceInteractionService get to => Get.find<VoiceInteractionService>();
Stream<VoiceInteractionEvent> get eventStream;
/// 初始化服务 // 方法通道
/// static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction');
/// 返回服务实例自身,以支持链式调用
Future<VoiceInteractionService> initialize();
/// 暂停语音交互 // 事件通道
/// static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events');
/// 停止正在进行的语音识别和TTS播放
void pauseVoiceInteraction(); // 服务状态
final _isServiceRunning = false.obs;
bool get isServiceRunning => _isServiceRunning.value;
// 流控制器
final _eventStreamController = StreamController<VoiceInteractionEvent>.broadcast();
// 事件流
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
// 事件通道订阅
StreamSubscription? _eventSubscription;
// 标记是否初始化
bool _isInitialized = false;
// 配置信息
late String _azureSpeechKey;
late String _azureSpeechRegion;
late String _volcanoAiKey;
// 聊天历史服务
late final ChatHistoryService _chatHistoryService;
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleVoiceInteractionEvent, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
});
}
/// 从环境变量加载配置
void _loadConfig() {
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
_volcanoAiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
if (_volcanoAiKey.isEmpty) {
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY');
}
}
/// 处理来自原生层的事件
void _handleVoiceInteractionEvent(dynamic event) {
if (event is! Map) return;
final eventMap = event as Map<dynamic, dynamic>;
final String eventType = eventMap['type'] as String? ?? '';
// final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch;
Logger.info('收到语音交互事件: $eventType');
switch (eventType) {
case 'recognition_started':
// 语音识别开始事件
final recognitionEvent = RecognitionStartedEvent(
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(recognitionEvent);
break;
case 'chat_history_updated':
// 聊天历史更新事件
final String agentId = eventMap['agentId'] as String? ?? '';
final String userMessage = eventMap['userMessage'] as String? ?? '';
final String assistantMessage = eventMap['assistantMessage'] as String? ?? '';
final chatHistoryEvent = ChatHistoryEvent(
agentId: agentId,
userMessage: userMessage,
assistantMessage: assistantMessage,
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(chatHistoryEvent);
// 保存聊天历史到ChatHistoryService
_saveChatHistory(agentId, userMessage, assistantMessage, DateTime.now().millisecondsSinceEpoch);
break;
}
}
/// 保存聊天历史
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
try {
// 检查参数有效性
if (userMessage.isEmpty) {
return;
}
// 创建用户消息和助手消息
final userMsg = Message(
role: 'user',
content: userMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp),
);
final assistantMsg = Message(
role: 'assistant',
content: assistantMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息
);
// 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) {
Logger.error('保存聊天历史失败: $e');
}
}
/// 初始化服务
Future<bool> initialize() async {
if (_isInitialized) return true;
try {
Logger.info('正在初始化语音交互服务...');
// 获取聊天历史服务
try {
_chatHistoryService = Get.find<ChatHistoryService>();
} catch (e) {
Logger.warning('获取ChatHistoryService失败,将创建新实例');
_chatHistoryService = Get.put(ChatHistoryService());
}
// 设置事件通道
_setupEventChannel();
// 加载配置
_loadConfig();
/// 唤醒语音交互 // 检查服务是否已在运行
void onWakeup(); final bool running = await checkServiceStatus();
// 如果服务未运行,启动服务
if (!running) {
// 启动语音交互服务
final success = await startService();
if (!success) {
Logger.error('语音交互服务启动失败');
return false;
}
}
_isInitialized = true;
Logger.info('语音交互服务初始化完成');
return true;
} catch (e) {
Logger.error('语音交互服务初始化失败: $e');
return false;
}
}
/// 启动语音交互服务
Future<bool> startService() async {
try {
Logger.info('启动语音交互服务...');
// 使用已加载的配置信息
final result = await _channel.invokeMethod<bool>('startService', {
'azureSpeechKey': _azureSpeechKey,
'azureSpeechRegion': _azureSpeechRegion,
'volcanoAiKey': _volcanoAiKey,
}) ?? false;
if (result) {
_isServiceRunning.value = true;
Logger.info('语音交互服务已启动');
} else {
Logger.error('启动语音交互服务失败');
}
return result;
} catch (e) {
Logger.error('启动语音交互服务时发生错误: $e');
return false;
}
}
/// 是否处于活跃状态 /// 停止语音交互服务
bool isActive(); Future<bool> stopService() async {
try {
Logger.info('停止语音交互服务...');
final result = await _channel.invokeMethod<bool>('stopService') ?? false;
if (result) {
_isServiceRunning.value = false;
Logger.info('语音交互服务已停止');
} else {
Logger.error('停止语音交互服务失败');
}
return result;
} catch (e) {
Logger.error('停止语音交互服务时发生错误: $e');
return false;
}
}
/// 获取当前对话历史 /// 检查服务是否运行
List<Map<String, String>> getMessageHistory(); Future<bool> checkServiceStatus() async {
try {
final bool result = await _channel.invokeMethod<bool>('isServiceRunning') ?? false;
_isServiceRunning.value = result;
return result;
} catch (e) {
Logger.error('检查服务状态时发生错误: $e');
return false;
}
}
/// 暂停语音交互
Future<bool> pauseVoiceInteraction() async {
try {
Logger.info('暂停语音交互...');
final result = await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false;
if (result) {
Logger.info('语音交互已暂停');
} else {
Logger.error('暂停语音交互失败');
}
return result;
} catch (e) {
Logger.error('暂停语音交互时发生错误: $e');
return false;
}
}
/// 获取当前服务ID /// 资源释放
String getServiceId(); @override
void onClose() {
_eventSubscription?.cancel();
_eventStreamController.close();
super.onClose();
}
} }

1
lib/modules/chat/controllers/chat_controller.dart

@ -11,7 +11,6 @@ import '../../../data/services/asr_service.dart';
import '../../../data/services/chat_history_service.dart'; import '../../../data/services/chat_history_service.dart';
import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/voice_interaction_service.dart'; import '../../../data/services/voice_interaction_service.dart';
import 'dart:io' show Platform;
class ChatController extends GetxController { class ChatController extends GetxController {
// 服务 // 服务

75
lib/modules/home/controllers/home_controller.dart

@ -1,15 +1,12 @@
import 'package:deep_voice/data/services/speech_impl/azure_tts_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../data/services/classic_bluetooth_service.dart'; import '../../../data/services/classic_bluetooth_service.dart';
import '../../../data/services/bluetooth_media_button_service.dart'; import '../../../data/services/bluetooth_media_button_service.dart';
import '../../../data/services/voice_interaction_service.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothDevice;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../data/services/voice_interaction_impl/android_voice_interaction_service.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../data/providers/agent_provider.dart';
// 连接状态枚举 // 连接状态枚举
enum ConnectionStatus { enum ConnectionStatus {
disconnected, // 未连接 disconnected, // 未连接
@ -27,6 +24,9 @@ class HomeController extends GetxController {
// 蓝牙媒体按钮服务 // 蓝牙媒体按钮服务
late BluetoothMediaButtonService _mediaButtonService; late BluetoothMediaButtonService _mediaButtonService;
// 媒体按钮订阅
StreamSubscription<MediaButtonEvent>? _mediaButtonSubscription;
// 设备连接状态 // 设备连接状态
final _isConnected = false.obs; final _isConnected = false.obs;
bool get isConnected => _isConnected.value; bool get isConnected => _isConnected.value;
@ -56,9 +56,7 @@ class HomeController extends GetxController {
// 蓝牙状态 // 蓝牙状态
RxString get bluetoothState => _bluetoothService.bluetoothState; RxString get bluetoothState => _bluetoothService.bluetoothState;
// 按钮事件流订阅
StreamSubscription<MediaButtonEvent>? _mediaButtonEventSubscription;
// 状态更新标志,防止多次重复更新 // 状态更新标志,防止多次重复更新
bool _isUpdatingStatus = false; bool _isUpdatingStatus = false;
@ -70,7 +68,7 @@ class HomeController extends GetxController {
_initBluetoothService(); _initBluetoothService();
// 初始化蓝牙媒体按钮服务 // 初始化蓝牙媒体按钮服务
_initMediaButtonService(); // _initMediaButtonService();
// 电池模拟更新(实际应用中可以通过蓝牙获取) // 电池模拟更新(实际应用中可以通过蓝牙获取)
_simulateBatteryUpdate(); _simulateBatteryUpdate();
@ -100,32 +98,18 @@ class HomeController extends GetxController {
// 获取媒体按钮服务实例 // 获取媒体按钮服务实例
_mediaButtonService = Get.find<BluetoothMediaButtonService>(); _mediaButtonService = Get.find<BluetoothMediaButtonService>();
// 检查是否支持该功能 // 初始化媒体按钮服务
if (!_mediaButtonService.isSupported) {
Logger.info('当前设备不支持蓝牙媒体按钮功能');
return;
}
// 初始化服务
_mediaButtonService.initialize().then((success) { _mediaButtonService.initialize().then((success) {
if (success) { if (success) {
// 开始监听蓝牙媒体按钮事件 // 启动按钮监听
_mediaButtonService.startButtonListening(); _mediaButtonService.startButtonListening();
// 订阅按钮事件流 // 设置按钮事件监听
_mediaButtonEventSubscription = _mediaButtonService.buttonEvents.listen( _setupMediaButtonListener();
(event) {
Logger.info('HomeController: 收到蓝牙媒体按钮事件 $event');
_handleMediaButtonEvent(event);
},
onError: (error) {
Logger.error('HomeController: 蓝牙媒体按钮事件流错误: $error');
}
);
Logger.info('蓝牙媒体按钮服务初始化成功'); Logger.info('蓝牙媒体按钮服务初始化成功');
} else { } else {
Logger.error('蓝牙媒体按钮服务初始化失败'); Logger.warning('蓝牙媒体按钮服务初始化失败');
} }
}); });
} catch (e) { } catch (e) {
@ -133,6 +117,22 @@ class HomeController extends GetxController {
} }
} }
// 设置媒体按钮事件监听
void _setupMediaButtonListener() {
// 取消之前的订阅
_mediaButtonSubscription?.cancel();
// 设置新的订阅
_mediaButtonSubscription = _mediaButtonService.buttonEvents.listen((event) {
Logger.info('收到媒体按钮事件: $event');
// 处理按钮事件
if (event.buttonType == MediaButtonType.playPause) {
}
});
}
// 电池电量模拟更新 // 电池电量模拟更新
void _simulateBatteryUpdate() { void _simulateBatteryUpdate() {
// 每5分钟更新一次电池电量(模拟) // 每5分钟更新一次电池电量(模拟)
@ -325,29 +325,12 @@ class HomeController extends GetxController {
return device?.address ?? '未知'; return device?.address ?? '未知';
} }
// 处理蓝牙媒体按钮事件
void _handleMediaButtonEvent(MediaButtonEvent event) {
Logger.info('收到蓝牙媒体按钮事件: $event');
// 根据按钮类型执行相应操作
switch (event.buttonType) {
case MediaButtonType.playPause:
// AzureTtsService.to.speakOnce('我在呢');
break;
default:
// 处理其他按钮事件
break;
}
}
@override @override
void onClose() { void onClose() {
// 取消按钮事件流订阅 // 取消按钮事件流订阅
_mediaButtonEventSubscription?.cancel(); _mediaButtonSubscription?.cancel();
super.onClose(); super.onClose();
} }

4
lib/modules/pairing/controllers/pairing_controller.dart

@ -1,4 +1,3 @@
import 'package:deep_voice/data/services/voice_interaction_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
@ -6,6 +5,7 @@ import '../../../data/services/classic_bluetooth_service.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import 'dart:io'; import 'dart:io';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import '../../../data/services/voice_interaction_service.dart';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 /// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
@ -19,7 +19,7 @@ class PairingController extends GetxController {
// 语音交互服务 // 语音交互服务
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>(); final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>();
// 是否开始搜索 // 是否开始搜索
final _isSearching = false.obs; final _isSearching = false.obs;
bool get isSearching => _isSearching.value; bool get isSearching => _isSearching.value;

Loading…
Cancel
Save