Browse Source

deplugin

weicu
wolfplus 2 years ago
parent
commit
4f18a6426d
  1. 7
      ios/Podfile.lock
  2. 8
      ios/Runner.xcodeproj/project.pbxproj
  3. 370
      ios/Runner/AppDelegate.swift
  4. 408
      ios/Runner/AzureAsrHelper.swift
  5. 427
      ios/Runner/AzureTtsHelper.swift
  6. 15
      ios/Runner/BluetoothMediaButtonHelper.swift
  7. 4
      lib/data/models/agent.dart
  8. 3
      lib/data/providers/agent_provider.dart
  9. 96
      lib/data/services/bluetooth_media_button_service.dart
  10. 7
      lib/data/services/classic_bluetooth_service.dart
  11. 38
      lib/data/services/speech_impl/azure_asr_service.dart
  12. 85
      lib/modules/chat/controllers/chat_controller.dart
  13. 64
      lib/modules/home/controllers/home_controller.dart
  14. 17
      lib/modules/pairing/controllers/pairing_controller.dart
  15. 2
      pubspec.yaml

7
ios/Podfile.lock

@ -8,9 +8,6 @@ PODS:
- Flutter
- audio_waveforms (0.0.1):
- Flutter
- azure_speech_recognition (0.1.0):
- Flutter
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0)
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
@ -50,7 +47,6 @@ DEPENDENCIES:
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- azure_speech_recognition (from `.symlinks/plugins/azure_speech_recognition/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
@ -80,8 +76,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_session/ios"
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
azure_speech_recognition:
:path: ".symlinks/plugins/azure_speech_recognition/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
@ -114,7 +108,6 @@ SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
azure_speech_recognition: 063bf4f294dbabaf2741f529f76132b2c38b744b
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6

8
ios/Runner.xcodeproj/project.pbxproj

@ -11,6 +11,8 @@
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; };
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29682D8C25660032BFCB /* silence.mp3 */; };
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; };
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */; };
0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; };
@ -52,6 +54,8 @@
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; 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>"; };
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = "<group>"; };
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureTtsHelper.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@ -151,6 +155,8 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */,
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */,
0ADE29672D8C25660032BFCB /* sample.mp3 */,
0ADE29682D8C25660032BFCB /* silence.mp3 */,
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */,
@ -408,6 +414,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */,
0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */,
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */,

370
ios/Runner/AppDelegate.swift

@ -5,6 +5,9 @@ import UIKit
@objc class AppDelegate: FlutterAppDelegate {
private var classicBluetoothHelper: ClassicBluetoothHelper?
private var mediaButtonHelper: Any?
// 添加Azure语音服务辅助类
private var azureAsrHelper: Any?
private var azureTtsHelper: Any?
override func application(
_ application: UIApplication,
@ -16,9 +19,10 @@ import UIKit
// 立即初始化蓝牙,不要等待Flutter调用
classicBluetoothHelper?.initBluetooth()
// 仅在iOS 13.0及以上版本初始化媒体按钮辅助类
// 仅在iOS 13.0及以上版本初始化媒体按钮辅助类和Azure语音服务辅助类
if #available(iOS 13.0, *) {
mediaButtonHelper = BluetoothMediaButtonHelper()
// 稍后在方法通道注册时初始化Azure语音服务辅助类
}
// 建立Flutter方法通道
@ -81,44 +85,266 @@ import UIKit
let bluetoothEventHandler = BluetoothStreamHandler(bluetoothHelper: self.classicBluetoothHelper!)
classicEventChannel.setStreamHandler(bluetoothEventHandler)
// 仅在iOS 13.0及以上版本注册媒体按钮通道
if #available(iOS 13.0, *), let mediaButtonHelper = mediaButtonHelper as? BluetoothMediaButtonHelper {
// 仅在iOS 13.0及以上版本注册媒体按钮通道和Azure语音服务通道
if #available(iOS 13.0, *) {
// 注册媒体按钮方法通道
let mediaButtonChannel = FlutterMethodChannel(
name: "com.deep_voice.bluetooth_media_button",
binaryMessenger: controller.binaryMessenger
)
registerMediaButtonChannels(controller: controller)
// 注册Azure ASR方法通道
registerAzureAsrChannels(controller: controller)
mediaButtonChannel.setMethodCallHandler { [weak self, weak mediaButtonHelper] (call, result) in
guard let _ = self, let mediaButtonHelper = mediaButtonHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil))
// 注册Azure TTS方法通道
registerAzureTtsChannels(controller: controller)
}
}
// 注册媒体按钮通道
@available(iOS 13.0, *)
private func registerMediaButtonChannels(controller: FlutterViewController) {
guard let mediaButtonHelper = mediaButtonHelper as? BluetoothMediaButtonHelper else {
return
}
// 注册媒体按钮方法通道
let mediaButtonChannel = FlutterMethodChannel(
name: "com.deep_voice.bluetooth_media_button",
binaryMessenger: controller.binaryMessenger
)
mediaButtonChannel.setMethodCallHandler { [weak self, weak mediaButtonHelper] (call, result) in
guard let _ = self, let mediaButtonHelper = mediaButtonHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil))
return
}
switch call.method {
case "startButtonListening":
let success = mediaButtonHelper.startButtonListening()
result(success)
case "stopButtonListening":
let success = mediaButtonHelper.stopButtonListening()
result(success)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册媒体按钮事件通道
let mediaButtonEventChannel = FlutterEventChannel(
name: "com.deep_voice.bluetooth_media_button_events",
binaryMessenger: controller.binaryMessenger
)
// 创建媒体按钮事件处理器
let mediaButtonEventHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper)
mediaButtonEventChannel.setStreamHandler(mediaButtonEventHandler)
}
// 注册Azure ASR通道
@available(iOS 13.0, *)
private func registerAzureAsrChannels(controller: FlutterViewController) {
// 注册Azure ASR方法通道
let azureAsrChannel = FlutterMethodChannel(
name: "com.deep_voice.azure_asr",
binaryMessenger: controller.binaryMessenger
)
// 初始化Azure ASR辅助类
azureAsrHelper = AzureAsrHelper { [weak self] eventName, eventData in
// 推送事件到Flutter
self?.sendAsrEvent(eventName: eventName, eventData: eventData)
}
// 设置ASR方法处理器
azureAsrChannel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self,
let asrHelper = self.azureAsrHelper as? AzureAsrHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "Azure ASR辅助实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
guard let args = call.arguments as? [String: Any],
let key = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil))
return
}
switch call.method {
case "startButtonListening":
let success = mediaButtonHelper.startButtonListening()
result(success)
case "stopButtonListening":
let success = mediaButtonHelper.stopButtonListening()
result(success)
default:
result(FlutterMethodNotImplemented)
}
let languages = args["supportedLanguages"] as? [String]
let success = asrHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, supportedLanguages: languages)
result(success)
case "recognizeOnce":
let success = asrHelper.recognizeOnce()
result(success)
case "startContinuousRecognition":
let success = asrHelper.startContinuousRecognition()
result(success)
case "stopContinuousRecognition":
let success = asrHelper.stopContinuousRecognition()
result(success)
case "isContinuousRecognitionActive":
let isActive = asrHelper.isContinuousRecognitionActive()
result(isActive)
case "dispose":
asrHelper.dispose()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册Azure ASR事件通道
let azureAsrEventChannel = FlutterEventChannel(
name: "com.deep_voice.azure_asr_events",
binaryMessenger: controller.binaryMessenger
)
// 创建ASR事件处理器
let asrStreamHandler = AsrStreamHandler()
azureAsrEventChannel.setStreamHandler(asrStreamHandler)
}
// 注册Azure TTS通道
@available(iOS 13.0, *)
private func registerAzureTtsChannels(controller: FlutterViewController) {
// 注册Azure TTS方法通道
let azureTtsChannel = FlutterMethodChannel(
name: "com.deep_voice.azure_tts",
binaryMessenger: controller.binaryMessenger
)
// 初始化Azure TTS辅助类
azureTtsHelper = AzureTtsHelper { [weak self] eventName, eventData in
// 推送事件到Flutter
self?.sendTtsEvent(eventName: eventName, eventData: eventData)
}
// 设置TTS方法处理器
azureTtsChannel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self,
let ttsHelper = self.azureTtsHelper as? AzureTtsHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "Azure TTS辅助实例不可用", details: nil))
return
}
// 注册媒体按钮事件通道
let mediaButtonEventChannel = FlutterEventChannel(
name: "com.deep_voice.bluetooth_media_button_events",
binaryMessenger: controller.binaryMessenger
)
// 创建媒体按钮事件处理器
let mediaButtonEventHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper)
mediaButtonEventChannel.setStreamHandler(mediaButtonEventHandler)
switch call.method {
case "initialize":
guard let args = call.arguments as? [String: Any],
let key = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil))
return
}
let language = args["language"] as? String ?? "zh-CN"
let success = ttsHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, language: language)
result(success)
case "setVoice":
guard let args = call.arguments as? [String: Any],
let voiceName = args["voiceName"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "语音名称参数无效", details: nil))
return
}
let success = ttsHelper.setVoice(voiceName: voiceName)
result(success)
case "setSpeechParams":
guard let args = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGS", message: "语音参数无效", details: nil))
return
}
let rate = args["rate"] as? Int ?? 0
let pitch = args["pitch"] as? Int ?? 0
let volume = args["volume"] as? Int ?? 100
let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume)
result(success)
case "speakText":
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "文本参数无效", details: nil))
return
}
let success = ttsHelper.speakText(text: text)
result(success)
case "stopSpeaking":
let success = ttsHelper.stopSpeaking()
result(success)
case "isSpeaking":
let isSpeaking = ttsHelper.isSpeaking()
result(isSpeaking)
case "dispose":
ttsHelper.dispose()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册Azure TTS事件通道
let azureTtsEventChannel = FlutterEventChannel(
name: "com.deep_voice.azure_tts_events",
binaryMessenger: controller.binaryMessenger
)
// 创建TTS事件处理器
let ttsStreamHandler = TtsStreamHandler()
azureTtsEventChannel.setStreamHandler(ttsStreamHandler)
}
// 存储ASR事件
private var _cachedAsrEvent: [String: Any]?
// 发送ASR事件
@available(iOS 13.0, *)
private func sendAsrEvent(eventName: String, eventData: [String: Any]) {
// 调整格式:将event改为type,并将data的内容提升到顶层
var eventMap = eventData
eventMap["type"] = eventName
_cachedAsrEvent = eventMap
NotificationCenter.default.post(
name: Notification.Name("AzureAsrEvent"),
object: nil,
userInfo: eventMap
)
}
// 存储TTS事件
private var _cachedTtsEvent: [String: Any]?
// 发送TTS事件
@available(iOS 13.0, *)
private func sendTtsEvent(eventName: String, eventData: [String: Any]) {
// 调整格式:将event改为type,并将data的内容提升到顶层
var eventMap = eventData
eventMap["type"] = eventName
_cachedTtsEvent = eventMap
NotificationCenter.default.post(
name: Notification.Name("AzureTtsEvent"),
object: nil,
userInfo: eventMap
)
}
}
@ -190,3 +416,85 @@ class MediaButtonStreamHandler: NSObject, FlutterStreamHandler, MediaButtonEvent
}
}
}
// ASR事件流处理器
@available(iOS 13.0, *)
class AsrStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var notificationObserver: Any?
deinit {
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 注册通知观察者
notificationObserver = NotificationCenter.default.addObserver(
forName: Notification.Name("AzureAsrEvent"),
object: nil,
queue: .main) { [weak self] notification in
if let userInfo = notification.userInfo as? [String: Any] {
self?.eventSink?(userInfo)
}
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
// 移除通知观察者
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
notificationObserver = nil
}
return nil
}
}
// TTS事件流处理器
@available(iOS 13.0, *)
class TtsStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var notificationObserver: Any?
deinit {
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 注册通知观察者
notificationObserver = NotificationCenter.default.addObserver(
forName: Notification.Name("AzureTtsEvent"),
object: nil,
queue: .main) { [weak self] notification in
if let userInfo = notification.userInfo as? [String: Any] {
self?.eventSink?(userInfo)
}
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
// 移除通知观察者
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
notificationObserver = nil
}
return nil
}
}

408
ios/Runner/AzureAsrHelper.swift

@ -0,0 +1,408 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure ASR工具类,负责实现语音识别服务接口
@available(iOS 13.0, *)
class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音识别相关
private var speechConfig: SPXSpeechConfiguration?
private var recognizer: SPXSpeechRecognizer?
private var audioConfig: SPXAudioConfiguration?
/// 状态标志
private var isInitialized = false
private var _isContinuousRecognitionActive = false
/// 当前语言和支持的语言
private var currentLanguage = "zh-CN"
private var supportedLanguages: [String] = ["zh-CN", "en-US"]
private var isAutoDetectLanguage = false
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - ASR Service 接口实现
/// 初始化语音识别服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - supportedLanguages: 支持的语言代码数组 (可选)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String]? = nil) -> Bool {
print("[AzureAsrHelper] 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["message": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 设置语言
if let languages = supportedLanguages, !languages.isEmpty {
self.supportedLanguages = languages
}
// 根据支持的语言数量决定是否启用自动语言检测
isAutoDetectLanguage = self.supportedLanguages.count >= 2
// 如果只有一种语言,设置为当前语言
if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty {
currentLanguage = self.supportedLanguages[0]
}
// 创建识别器和设置回调
if !createRecognizerAndSetupCallbacks() {
return false
}
print("[AzureAsrHelper] Azure 语音服务初始化成功")
isInitialized = true
return true
}
/// 创建识别器并设置回调
private func createRecognizerAndSetupCallbacks() -> Bool {
// 释放之前的 recognizer
recognizer = nil
audioConfig = nil
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置音频输入参数
try setupAudioSession()
// 直接使用麦克风音频配置
audioConfig = try SPXAudioConfiguration()
// 设置语言配置
if isAutoDetectLanguage {
// 设置自动语言检测
speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
// 创建自动语言检测配置
let autoDetectSourceLanguageConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
// 创建识别器
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig,
audioConfiguration: audioConfig!
)
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
// 创建识别器
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
// 设置所有回调
setupAllCallbacks()
return true
} catch {
print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)")
eventHandler("error", ["message": "创建识别器失败: \(error.localizedDescription)"])
return false
}
}
/// 设置音频会话
private func setupAudioSession() throws {
let audioSession = AVAudioSession.sharedInstance()
// 使用playAndRecord类别允许同时录音和播放
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat, // 使用voiceChat模式能够更好地支持回音消除
options: [.allowBluetooth, .defaultToSpeaker, .allowAirPlay])
// 设置首选的输入和输出
let currentRoute = audioSession.currentRoute
// 获取当前是否连接了耳机或外部麦克风
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 如果没有耳机,明确启用内置麦克风和扬声器的回音消除
if !hasHeadphones {
try audioSession.setMode(.voiceChat) // 语音聊天模式有更强的回音消除
// 启用回音消除和噪声抑制
try audioSession.setInputGain(0.8) // 适当降低输入增益以减少扬声器音频被麦克风捕获的可能性
} else {
// 耳机模式,可以使用不同的设置
try audioSession.setMode(.voiceChat)
try audioSession.setInputGain(1.0)
}
// 设置合适的采样率
try audioSession.setPreferredSampleRate(16000.0) // Azure语音识别推荐的采样率
try audioSession.setPreferredIOBufferDuration(0.01) // 较小的缓冲区大小以减少延迟
// 激活音频会话
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
print("[AzureAsrHelper] 音频会话配置成功,已启用回音消除")
}
/// 设置所有回调
private func setupAllCallbacks() {
guard let recognizer = recognizer else { return }
// 最终识别结果
recognizer.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("result", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
// print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("recognizing", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 会话事件
recognizer.addSessionStartedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已开始")
self._isContinuousRecognitionActive = true
self.eventHandler("sessionStarted", [:])
}
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已结束")
self._isContinuousRecognitionActive = false
self.eventHandler("sessionStopped", [:])
}
// 取消事件
recognizer.addCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
let reason = event.reason.rawValue
let errorDetails = event.errorDetails ?? "未知错误"
print("[AzureAsrHelper] 识别取消: \(errorDetails)")
self.eventHandler("canceled", [
"reason": reason,
"errorDetails": errorDetails
])
self._isContinuousRecognitionActive = false
}
}
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果正在连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
do {
// 通知会话开始
eventHandler("sessionStarted", [:])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
guard let self = self else { return }
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
self.eventHandler("result", [
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.noMatch {
print("[AzureAsrHelper] 无匹配结果")
self.eventHandler("noMatch", [:])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler("error", ["message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler("error", ["message": "识别取消,无法获取详细原因"])
}
}
}
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler("error", ["message": "识别异常: \(error.localizedDescription)"])
return false
}
}
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果已经在进行连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
// 重新确保音频设置正确
do {
try setupAudioSession()
} catch {
print("[AzureAsrHelper] 警告: 设置音频会话失败: \(error.localizedDescription)")
}
do {
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别开始")
return true
} catch {
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
if !_isContinuousRecognitionActive || recognizer == nil {
return true
}
do {
try recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 连续识别已停止")
return true
} catch {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "停止连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
return false
}
}
/// 检查连续识别是否活跃
/// - Returns: 连续识别是否处于活跃状态
func isContinuousRecognitionActive() -> Bool {
return _isContinuousRecognitionActive
}
/// 释放资源
func dispose() {
print("[AzureAsrHelper] 释放资源")
// 停止连续识别
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 释放音频会话
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("[AzureAsrHelper] 警告: 释放音频会话失败: \(error.localizedDescription)")
}
// 释放资源
recognizer = nil
speechConfig = nil
audioConfig = nil
// 重置状态
_isContinuousRecognitionActive = false
isInitialized = false
}
/// 从结果中获取检测到的语言
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
if isAutoDetectLanguage {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
return langResult.language ?? currentLanguage
} catch {
print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)")
return currentLanguage
}
} else {
return currentLanguage
}
}
}

427
ios/Runner/AzureTtsHelper.swift

@ -0,0 +1,427 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure TTS工具类,负责实现TTS服务接口
@available(iOS 13.0, *)
class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音合成配置
private var speechConfig: SPXSpeechConfiguration?
/// 语音合成器
private var synthesizer: SPXSpeechSynthesizer?
/// 是否初始化成功
private var isInitialized = false
/// 当前是否正在播放
private var _isSpeaking = false
/// 音频会话配置
private var isAudioSessionConfigured = false
// MARK: - 语音设置
/// 当前语音
private var currentVoice = "zh-CN-XiaoxiaoNeural"
/// 支持的语音映射
private var voiceMap: [String: String] = [
"zh-CN": "zh-CN-XiaoxiaoNeural",
"en-US": "en-US-JennyNeural",
"ja-JP": "ja-JP-NanamiNeural",
"ko-KR": "ko-KR-SunHiNeural",
"zh-TW": "zh-TW-HsiaoChenNeural",
"zh-HK": "zh-HK-HiuMaanNeural"
]
/// 当前语音合成参数
private var currentSpeechRate = "0%"
private var currentPitch = "0%"
private var currentVolume = "100%"
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - TTS 接口实现
/// 初始化语音合成服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - language: 语言代码 (默认 zh-CN)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool {
print("[AzureTtsHelper] 初始化语音合成服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["error": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 配置音频会话
if !configureAudioSession() {
print("[AzureTtsHelper] 警告: 音频会话配置失败,将尝试继续初始化")
}
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置默认语音
let defaultVoice = getDefaultVoiceForLanguage(language)
currentVoice = defaultVoice
speechConfig?.speechSynthesisVoiceName = defaultVoice
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig!)
// 设置事件处理器
setupSynthesizerEvents()
isInitialized = true
print("[AzureTtsHelper] TTS 引擎初始化成功")
return true
} catch {
print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)")
eventHandler("error", ["error": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
/// 配置音频会话
private func configureAudioSession() -> Bool {
let audioSession = AVAudioSession.sharedInstance()
do {
// 使用playback类别,但支持混合和空中播放
try audioSession.setCategory(.playback,
mode: .spokenAudio,
options: [.mixWithOthers, .allowAirPlay, .duckOthers])
// 根据设备类型选择最佳配置
let currentRoute = audioSession.currentRoute
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 优化音频路由
if hasHeadphones {
// 耳机模式,使用默认设置
try audioSession.setPreferredIOBufferDuration(0.005) // 较小的缓冲区大小以减少延迟
} else {
// 扬声器模式
try audioSession.setPreferredIOBufferDuration(0.005)
}
// 避免完全激活音频会话,因为ASR可能已经激活
// 这里使用setActive(false)是为了不与ASR冲突
if !audioSession.isOtherAudioPlaying {
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
}
isAudioSessionConfigured = true
print("[AzureTtsHelper] 音频会话配置成功")
return true
} catch {
print("[AzureTtsHelper] 警告: 音频会话配置失败: \(error.localizedDescription)")
isAudioSessionConfigured = false
return false
}
}
/// 设置语音
/// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural")
/// - Returns: 设置是否成功
func setVoice(voiceName: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler("error", ["error": "声音名称不能为空"])
return false
}
if voiceName == currentVoice {
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
return true
}
print("[AzureTtsHelper] 设置声音: \(voiceName)")
currentVoice = voiceName
// 更新语音配置
if let speechConfig = speechConfig {
speechConfig.speechSynthesisVoiceName = voiceName
return true
}
return false
}
/// 设置语音合成参数
/// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0
/// - volume: 音量,范围 0 到 100,默认为 100
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
// 转换参数格式
currentSpeechRate = formatRateParam(rate)
currentPitch = formatPitchParam(pitch)
currentVolume = formatVolumeParam(volume)
print("[AzureTtsHelper] 已设置语音参数: 语速=\(currentSpeechRate), 音调=\(currentPitch), 音量=\(currentVolume)")
return true
}
/// 合成文本为语音并播放
/// - Parameter text: 要合成的文本
/// - Returns: 操作是否成功启动
func speakText(text: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if text.isEmpty {
print("[AzureTtsHelper] 警告: 要播放的文本为空")
return true
}
// 确保音频会话已配置
if !isAudioSessionConfigured {
_ = configureAudioSession()
}
print("[AzureTtsHelper] 开始语音合成: \(text.prefix(50))...")
// 生成SSML
let ssml = generateSsml(text: text)
// 直接进行SSML合成
return speakSsmlInternal(text: ssml)
}
/// 内部SSML合成和播放
private func speakSsmlInternal(text: String) -> Bool {
guard let synthesizer = synthesizer else {
print("[AzureTtsHelper] 错误: 合成器未初始化")
eventHandler("error", ["error": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler("started", [:])
Task {
do {
// 使用异步方法进行合成并直接播放
_ = try await synthesizer.startSpeakingSsml(text)
} catch {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成失败: \(error.localizedDescription)"])
}
}
}
return true
}
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
if !isInitialized || !_isSpeaking {
return true
}
// 停止合成
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler("canceled", [:])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler("error", ["error": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
/// 检查是否正在播放
/// - Returns: 当前是否正在播放语音
func isSpeaking() -> Bool {
return _isSpeaking
}
/// 释放资源
func dispose() {
try? stopSpeaking()
// 释放合成器和配置
synthesizer = nil
speechConfig = nil
isInitialized = false
_isSpeaking = false
isAudioSessionConfigured = false
print("[AzureTtsHelper] TTS 引擎已释放")
}
// MARK: - 私有辅助方法
/// 设置合成器事件处理
private func setupSynthesizerEvents() {
guard let synthesizer = synthesizer else { return }
// 添加书签到达事件处理
synthesizer.addBookmarkReachedEventHandler { _, e in
print("[AzureTtsHelper] 书签事件: 音频偏移: \((e.audioOffset + 5000) / 10000)ms, 文本: \"\(e.text)\"")
}
// 合成完成事件
synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in
guard let self = self else { return }
print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("completed", [:])
}
}
// 合成取消事件
synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in
guard let self = self else { return }
let result = e.result
do {
let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
print("[AzureTtsHelper] 语音合成取消: 原因: \(cancellationDetails.reason)")
if cancellationDetails.reason == SPXCancellationReason.error {
print("[AzureTtsHelper] 错误代码: \(cancellationDetails.errorCode)")
print("[AzureTtsHelper] 错误详情: \(cancellationDetails.errorDetails ?? "未知")")
}
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成被取消"])
}
}
}
// 合成开始事件
synthesizer.addSynthesisStartedEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成开始")
}
// 合成中事件
synthesizer.addSynthesizingEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成中")
}
}
/// 生成 SSML 文本
private func generateSsml(text: String) -> String {
return """
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'>
<voice name='\(currentVoice)'>
<prosody rate='\(currentSpeechRate)' pitch='\(currentPitch)' volume='\(currentVolume)'>
\(text)
</prosody>
</voice>
</speak>
"""
}
/// 格式化语速参数
private func formatRateParam(_ rate: Int) -> String {
let clampedRate = rate.clamp(min: -100, max: 100)
if clampedRate == 0 {
return "0%"
} else if clampedRate < 0 {
return "\(Int(Double(clampedRate) * 0.9))%"
} else {
return "+\(clampedRate)%"
}
}
/// 格式化音调参数
private func formatPitchParam(_ pitch: Int) -> String {
let clampedPitch = pitch.clamp(min: -100, max: 100)
if clampedPitch == 0 {
return "0%"
} else {
return "\(Int(Double(clampedPitch) * 0.5))%"
}
}
/// 格式化音量参数
private func formatVolumeParam(_ volume: Int) -> String {
let clampedVolume = volume.clamp(min: 0, max: 100)
return "\(clampedVolume)%"
}
/// 获取指定语言的默认语音
private func getDefaultVoiceForLanguage(_ language: String) -> String {
return voiceMap[language] ?? "zh-CN-XiaoxiaoNeural"
}
}
// MARK: - 扩展
extension Int {
func clamp(min: Int, max: Int) -> Int {
if self < min { return min }
if self > max { return max }
return self
}
}

15
ios/Runner/BluetoothMediaButtonHelper.swift

@ -82,12 +82,19 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
isListening = true
// 确保NowPlaying信息可用 - 这也不依赖于活跃的音频会话
setupNowPlaying(isPlaying: false)
setupNowPlaying(isPlaying: true)
// 尝试启用静音音频播放(但即使失败,远程命令仍然可用)
do {
try passivelySetupAudioSession()
// startSilentAudio()
startSilentAudio()
//延时1秒
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.setupNowPlaying(isPlaying: false)
self.stopSilentAudio()
}
// startUpdatingPlaybackInfo()
} catch {
NSLog("\(TAG): 音频设置或播放失败: \(error.localizedDescription)")
@ -136,7 +143,7 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
silentAudioPlayer = try AVAudioPlayer(contentsOf: audioUrl)
silentAudioPlayer?.numberOfLoops = -1 // 无限循环播放
silentAudioPlayer?.volume = 1.0 // 设置为全音量
silentAudioPlayer?.volume = 0.0 // 设置为全音量
silentAudioPlayer?.prepareToPlay() // 预加载
silentAudioPlayer?.delegate = self
@ -337,6 +344,8 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
// 检查是否有其他音频播放
// let audioSession = AVAudioSession.sharedInstance()
// let isOtherAudioPlaying = audioSession.isOtherAudioPlaying

4
lib/data/models/agent.dart

@ -8,6 +8,7 @@ class Agent {
final bool isOnline;
final String voice;
final String welcomeMessage;
final String? voiceWelcomeMessage;
final String systemPrompt;
const Agent({
@ -19,6 +20,7 @@ class Agent {
required this.isOnline,
required this.voice,
required this.welcomeMessage,
this.voiceWelcomeMessage,
required this.systemPrompt,
});
@ -49,6 +51,7 @@ class Agent {
'voice': voice,
'welcomeMessage': welcomeMessage,
'systemPrompt': systemPrompt,
'voiceWelcomeMessage': voiceWelcomeMessage,
};
}
@ -74,6 +77,7 @@ class Agent {
voice: voice ?? this.voice,
welcomeMessage: welcomeMessage ?? this.welcomeMessage,
systemPrompt: systemPrompt ?? this.systemPrompt,
voiceWelcomeMessage: voiceWelcomeMessage ?? this.voiceWelcomeMessage,
);
}
}

3
lib/data/providers/agent_provider.dart

@ -11,6 +11,7 @@ class AgentProvider {
isOnline: true,
voice: "zh-CN-XiaoxiaoNeural",
welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
voiceWelcomeMessage: "我在呢!",
systemPrompt: '''
你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。
@ -52,7 +53,7 @@ class AgentProvider {
- 及时承认不足并寻求改进
请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活, 不要输出表情符号。
请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活, 不要输出表情符号或非可读的符号。
'''),
Agent(

96
lib/data/services/bluetooth_media_button_service.dart

@ -6,26 +6,8 @@ import '../../core/utils/logger.dart';
/// 蓝牙媒体按钮事件类型
enum MediaButtonType {
/// 播放按钮
play,
/// 暂停按钮
pause,
/// 下一曲按钮
next,
/// 上一曲按钮
previous,
/// 快进按钮
seekForward,
/// 快退按钮
seekBackward,
/// 播放/暂停切换按钮
togglePlayPause,
/// 播放/暂停按钮
playPause,
/// 未知按钮
unknown
@ -48,29 +30,19 @@ class MediaButtonEvent {
MediaButtonType buttonType;
switch (map['buttonType']) {
case 'play':
buttonType = MediaButtonType.play;
break;
case 'pause':
buttonType = MediaButtonType.pause;
break;
case 'next':
buttonType = MediaButtonType.next;
break;
case 'previous':
buttonType = MediaButtonType.previous;
break;
case 'seekForward':
buttonType = MediaButtonType.seekForward;
break;
case 'seekBackward':
buttonType = MediaButtonType.seekBackward;
break;
case 'togglePlayPause':
buttonType = MediaButtonType.togglePlayPause;
case 'playPause':
buttonType = MediaButtonType.playPause;
break;
default:
buttonType = MediaButtonType.unknown;
// 将所有其他事件类型映射为 playPause
if (map['buttonType'] == 'play' ||
map['buttonType'] == 'pause' ||
map['buttonType'] == 'togglePlayPause') {
buttonType = MediaButtonType.playPause;
} else {
// 忽略其他类型(next, previous, seekForward, seekBackward)
buttonType = MediaButtonType.unknown;
}
}
return MediaButtonEvent(
@ -82,19 +54,7 @@ class MediaButtonEvent {
@override
String toString() {
switch (buttonType) {
case MediaButtonType.play:
return '蓝牙媒体按钮: 播放';
case MediaButtonType.pause:
return '蓝牙媒体按钮: 暂停';
case MediaButtonType.next:
return '蓝牙媒体按钮: 下一曲';
case MediaButtonType.previous:
return '蓝牙媒体按钮: 上一曲';
case MediaButtonType.seekForward:
return '蓝牙媒体按钮: 快进';
case MediaButtonType.seekBackward:
return '蓝牙媒体按钮: 快退';
case MediaButtonType.togglePlayPause:
case MediaButtonType.playPause:
return '蓝牙媒体按钮: 播放/暂停';
case MediaButtonType.unknown:
return '蓝牙媒体按钮: 未知';
@ -219,12 +179,30 @@ class BluetoothMediaButtonService extends GetxService {
try {
if (eventMap['type'] == 'mediaButtonEvent') {
final buttonEvent = MediaButtonEvent.fromMap(eventMap);
// 获取按钮类型
final String buttonType = eventMap['buttonType'] as String? ?? 'unknown';
// 广播事件
_buttonEventController.add(buttonEvent);
Logger.info('成功处理蓝牙媒体按钮事件: $buttonEvent');
// 判断是否为playPause相关事件
if (buttonType == 'playPause' ||
buttonType == 'play' ||
buttonType == 'pause' ||
buttonType == 'togglePlayPause') {
// 创建playPause事件并广播
final buttonEvent = MediaButtonEvent(
buttonType: MediaButtonType.playPause,
timestamp: eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch,
);
// 广播事件
_buttonEventController.add(buttonEvent);
Logger.info('发送playPause按钮事件: $buttonEvent');
} else {
// 忽略其他类型的按钮事件
Logger.info('忽略非playPause按钮事件: $buttonType');
}
} else {
Logger.warning('收到未知类型的事件: ${eventMap['type']}');
}

7
lib/data/services/classic_bluetooth_service.dart

@ -38,7 +38,7 @@ class ClassicBluetoothService extends GetxService {
StreamSubscription? _bluetoothStateListener;
/// 蓝牙开关状态
final bluetoothState = 'on'.obs;
final bluetoothState = 'unknown'.obs;
/// 蓝牙是否可用
bool get isBluetoothOn => bluetoothState.value == 'on';
@ -56,6 +56,11 @@ class ClassicBluetoothService extends GetxService {
try {
Logger.info('正在初始化蓝牙服务...');
// 获取当前蓝牙状态
final bool isEnabled = await _channel.invokeMethod<bool>('isBluetoothEnabled') ?? false;
bluetoothState.value = isEnabled ? 'on' : 'off';
Logger.info('初始蓝牙状态: ${bluetoothState.value}');
// 启动蓝牙设备监控 - 这将调用原生代码中的initBluetooth
await _channel.invokeMethod('startBluetoothDeviceMonitoring');

38
lib/data/services/speech_impl/azure_asr_service.dart

@ -26,10 +26,6 @@ class AzureAsrService extends GetxService implements AsrService {
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 事件通道状态
bool _isEventChannelReady = false;
Completer<void>? _eventChannelReadyCompleter;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
@ -55,41 +51,15 @@ class AzureAsrService extends GetxService implements AsrService {
/// 设置事件通道
void _setupEventChannel() {
_eventChannelReadyCompleter = Completer<void>();
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen((event) {
if (event is Map) {
final String eventType = event['type'] as String? ?? '';
// 处理通道准备好的事件
if (eventType == 'channelReady') {
_isEventChannelReady = true;
if (!_eventChannelReadyCompleter!.isCompleted) {
_eventChannelReadyCompleter!.complete();
}
return;
}
_handleRecognitionEvent(event);
}
}, onError: _handleRecognitionError);
}
/// 等待事件通道准备好
Future<bool> _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async {
if (_isEventChannelReady) return true;
try {
await _eventChannelReadyCompleter!.future.timeout(timeout);
return true;
} on TimeoutException {
Logger.error('等待事件通道准备好超时');
return false;
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
@ -159,11 +129,6 @@ class AzureAsrService extends GetxService implements AsrService {
await initialize();
}
// 确保事件通道已准备好
if (!await _waitForEventChannel()) {
throw Exception('事件通道未准备好,无法开始连续识别');
}
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
@ -217,6 +182,9 @@ class AzureAsrService extends GetxService implements AsrService {
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) {
case 'result':
final String text = eventMap['text'] as String? ?? '';

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

@ -10,6 +10,7 @@ import '../../../core/utils/logger.dart';
import '../../../data/services/chat_history_service.dart';
import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/asr_service.dart';
class ChatController extends GetxController {
// 服务
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
@ -35,6 +36,9 @@ class ChatController extends GetxController {
final RxBool isSending = false.obs;
final RxBool isTtsEnabled = true.obs;
final RxBool isVoiceRecognitionActive = false.obs;
// 语音交互模式
bool _isVoiceInteractionMode = false;
// 语音识别状态
final RxString recognizedText = ''.obs;
@ -51,6 +55,12 @@ class ChatController extends GetxController {
// 取消标志
bool _shouldCancelAiResponse = false;
// 自动停止语音识别计时器
Timer? _autoStopTimer;
// 记录最后一次活动时间
DateTime _lastActivityTime = DateTime.now();
@override
Future<void> onInit() async {
@ -60,7 +70,7 @@ class ChatController extends GetxController {
// 从路由参数获取Agent信息
final Map<String, dynamic> args = Get.arguments ?? {};
final String agentId = args['agentId'] ?? 'personal_assistant';
final String agentId = args['agent'] ?? args['agentId'] ?? 'personal_assistant';
// 获取Agent
final Agent? foundAgent = AgentProvider.getAgentById(agentId);
@ -74,6 +84,9 @@ class ChatController extends GetxController {
_ttsService = Get.find<TtsService>();
_asrService = Get.find<AsrService>();
// 设置语音交互模式
_isVoiceInteractionMode = args['isVoiceInteraction'] == true;
// 设置TTS服务
await _ttsService.setVoice(agent.voice);
@ -94,9 +107,11 @@ class ChatController extends GetxController {
// }
}
// 如果playVoiceOnEnter为true,自动激活语音识别
if (args['playVoiceOnEnter'] == true) {
startVoiceRecognition();
// 如果isVoiceInteraction为true,自动激活语音识别
if (args['isVoiceInteraction'] == true) {
_ttsService.speakOnce(agent.voiceWelcomeMessage ?? agent.welcomeMessage);
startVoiceRecognition();
// 自动停止计时器会在startVoiceRecognition中启动
}
// 订阅VoiceInteractionService事件
@ -105,6 +120,9 @@ class ChatController extends GetxController {
// 使用微任务在布局完成后自动滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
Future.delayed(const Duration(milliseconds: 500), () {
_scrollToBottom();
});
});
}
@ -203,6 +221,7 @@ class ChatController extends GetxController {
_aiResponseSubscription?.cancel();
_recognitionSubscription?.cancel();
_voiceInteractionSubscription?.cancel();
_autoStopTimer?.cancel();
// 停止TTS
_ttsService.stop();
@ -422,7 +441,7 @@ class ChatController extends GetxController {
_voiceInteractionService.pauseVoiceInteraction();
try {
// 启动连续语音识别
// 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
final recognitionStream = await _asrService.startContinuousRecognition();
@ -432,9 +451,10 @@ class ChatController extends GetxController {
// 重置临时消息索引
_tempMessageIndex = -1;
// 启动自动停止计时器
_startAutoStopTimer();
// 监听识别事件
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
@ -449,6 +469,9 @@ class ChatController extends GetxController {
// 处理语音识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
// 更新活动时间
_updateActivityTime();
switch (event.type) {
case RecognitionEventType.intermediateResult:
_handleRecognizing(event.text);
@ -465,6 +488,42 @@ class ChatController extends GetxController {
}
}
// 更新最后活动时间
void _updateActivityTime() {
_lastActivityTime = DateTime.now();
}
// 启动自动停止计时器
void _startAutoStopTimer() {
// 取消现有计时器
_autoStopTimer?.cancel();
// 初始化最后活动时间
_updateActivityTime();
// 根据模式设置超时时间
final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15;
// 创建新的计时器,每秒检查一次
_autoStopTimer = Timer.periodic(Duration(seconds: 1), (timer) {
// 检查是否有活动
bool isActive = _ttsService.isSpeaking || isSpeechDetected.value;
// 如果有活动,更新时间并继续等待
if (isActive) {
_updateActivityTime();
return;
}
// 检查空闲时间
int idleSeconds = DateTime.now().difference(_lastActivityTime).inSeconds;
if (idleSeconds >= timeoutSeconds) {
timer.cancel();
_stopVoiceRecognitionAutomatically();
}
});
}
// 处理语音识别中的状态
void _handleRecognizing(String text) {
if (text.isEmpty) return;
@ -476,6 +535,9 @@ class ChatController extends GetxController {
isSpeechDetected.value = true;
}
// 更新活动时间
_updateActivityTime();
// 用户开始讲话时,立即停止当前TTS和中断AI响应
if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
@ -509,6 +571,9 @@ class ChatController extends GetxController {
Logger.info('处理语音识别最终结果: $text');
recognizedText.value = text;
// 更新活动时间
_updateActivityTime();
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
@ -627,4 +692,10 @@ class ChatController extends GetxController {
_saveChatHistory();
_scrollToBottom();
}
void _stopVoiceRecognitionAutomatically() {
final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15;
Logger.info('${timeoutSeconds}秒无活动,自动停止语音识别');
stopVoiceRecognition();
}
}

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

@ -8,6 +8,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothDevice;
import 'package:flutter/material.dart';
import '../../../data/services/voice_interaction_service.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../data/providers/agent_provider.dart';
// 连接状态枚举
enum ConnectionStatus {
disconnected, // 未连接
@ -329,29 +330,13 @@ class HomeController extends GetxController {
// 根据按钮类型执行相应操作
switch (event.buttonType) {
case MediaButtonType.play:
// 处理播放按钮事件
_handlePlayButtonPressed();
break;
case MediaButtonType.pause:
// 处理暂停按钮事件
_handlePauseButtonPressed();
break;
case MediaButtonType.togglePlayPause:
// 处理播放/暂停切换按钮事件
_handleTogglePlayPause();
break;
case MediaButtonType.next:
// 处理下一曲按钮事件
_handleNextButtonPressed();
break;
case MediaButtonType.previous:
// 处理上一曲按钮事件
_handlePreviousButtonPressed();
case MediaButtonType.playPause:
// 跳转到chat页面,使用个人助手模式并开启语音交互
// Get.toNamed('/chat', arguments: {
// 'isVoiceInteraction': true,
// 'agent': 'personal_assistant'
// });
Logger.info('通过蓝牙按钮启动个人助手语音交互');
break;
default:
@ -360,39 +345,6 @@ class HomeController extends GetxController {
}
}
// 处理播放按钮事件
void _handlePlayButtonPressed() {
Logger.info('处理播放按钮事件');
// 在这里添加播放功能的实现
// 例如:启动语音助手
}
// 处理暂停按钮事件
void _handlePauseButtonPressed() {
Logger.info('处理暂停按钮事件');
// 在这里添加暂停功能的实现
// 例如:暂停语音交互
}
// 处理播放/暂停切换按钮事件
void _handleTogglePlayPause() {
Logger.info('处理播放/暂停切换按钮事件');
// 在这里添加播放/暂停切换功能的实现
}
// 处理下一曲按钮事件
void _handleNextButtonPressed() {
Logger.info('处理下一曲按钮事件');
// 在这里添加下一曲功能的实现
// 例如:跳到下一个问题或调整音量
}
// 处理上一曲按钮事件
void _handlePreviousButtonPressed() {
Logger.info('处理上一曲按钮事件');
// 在这里添加上一曲功能的实现
// 例如:回到上一个问题或调整音量
}
@override
void onClose() {

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

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import 'package:flutter/services.dart';
import 'dart:io';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
@ -129,13 +130,15 @@ class PairingController extends GetxController {
}
_setupStatusListeners();
// 检查权限
// statusDescription.value = '正在检查权限...';
// final hasPermission = await _bluetoothService.checkPermissions();
// if (!hasPermission) {
// statusDescription.value = '缺少蓝牙权限';
// isLoading.value = false;
// return;
// }
if (Platform.isAndroid) {
statusDescription.value = '正在检查权限...';
final hasPermission = await _bluetoothService.checkPermissions();
if (!hasPermission) {
statusDescription.value = '缺少蓝牙权限';
isLoading.value = false;
return;
}
}
// 刷新设备列表
statusDescription.value = '正在搜索设备...';

2
pubspec.yaml

@ -29,8 +29,6 @@ dependencies:
logger: ^2.0.2+1
flutter_blue_plus: ^1.31.16
just_audio: ^0.9.36
azure_speech_recognition:
path: ./azure
window_size:

Loading…
Cancel
Save