Browse Source

Merge remote-tracking branch 'onewaygame/main' into lxm

weicu
lxm 2 years ago
parent
commit
9babd69890
  1. 2
      .metadata
  2. 8
      android/app/build.gradle.kts
  3. 396
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  4. 43
      android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt
  5. 24
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  6. 2
      android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt
  7. BIN
      android/app/src/main/res/raw/sample.mp3
  8. 21
      azure/LICENSE
  9. 66
      azure/README.md
  10. 757
      azure/ios/Classes/AzureAsrHelper.swift
  11. 18
      azure/ios/Classes/AzureSpeechRecognitionPlugin.swift
  12. 427
      azure/ios/Classes/AzureTtsHelper.swift
  13. 259
      azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift
  14. 24
      azure/ios/azure_speech_recognition.podspec
  15. 6
      azure/lib/azure_speech_recognition.dart
  16. 23
      azure/pubspec.yaml
  17. 6
      ios/Podfile
  18. 64
      ios/Podfile.lock
  19. 218
      ios/Runner.xcodeproj/project.pbxproj
  20. 730
      ios/Runner/AppDelegate.swift
  21. 585
      ios/Runner/AudioSessionManager.swift
  22. 813
      ios/Runner/AzureAsrHelper.swift
  23. 454
      ios/Runner/AzureTtsHelper.swift
  24. 13
      ios/Runner/Base.lproj/Main.storyboard
  25. 464
      ios/Runner/BluetoothMediaButtonHelper.swift
  26. 243
      ios/Runner/ClassicBluetoothHelper.swift
  27. 32
      ios/Runner/Info.plist
  28. 0
      ios/Runner/Resources/.gitkeep
  29. BIN
      ios/Runner/Resources/sample.mp3
  30. BIN
      ios/Runner/Resources/silence.mp3
  31. 6
      ios/Runner/Runner-Bridging-Header.h
  32. 565
      ios/Runner/VoiceInteractionService.swift
  33. 391
      ios/Runner/VolcanoAIService.swift
  34. BIN
      ios/Runner/sample.mp3
  35. BIN
      ios/Runner/silence.mp3
  36. 15
      lib/core/bindings/initial_binding.dart
  37. 4
      lib/data/models/agent.dart
  38. 29
      lib/data/models/events/voice_interaction_event.dart
  39. 58
      lib/data/models/message_model.dart
  40. 3
      lib/data/providers/agent_provider.dart
  41. 310
      lib/data/services/bluetooth_media_button_service.dart
  42. 4
      lib/data/services/chat_history_service.dart
  43. 445
      lib/data/services/classic_bluetooth_service.dart
  44. 3
      lib/data/services/flutter_tts_service.dart
  45. 29
      lib/data/services/speech_factory.dart
  46. 51
      lib/data/services/speech_impl/azure_asr_service.dart
  47. 269
      lib/data/services/speech_impl/azure_tts_service.dart
  48. 54
      lib/data/services/speech_impl/flutter_asr_service.dart
  49. 393
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  50. 340
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  51. 467
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  52. 376
      lib/data/services/voice_interaction_service.dart
  53. 226
      lib/modules/chat/controllers/chat_controller.dart
  54. 163
      lib/modules/home/controllers/home_controller.dart
  55. 34
      lib/modules/home/views/home_view.dart
  56. 181
      lib/modules/pairing/controllers/pairing_controller.dart
  57. 4
      lib/modules/pairing/views/pairing_view.dart
  58. 19
      lib/modules/settings/controllers/settings_controller.dart
  59. 26
      lib/modules/settings/views/settings_view.dart
  60. 297
      lib/modules/timbre/controllers/timbre_controller.dart
  61. 116
      lib/modules/timbre/views/timbre_view.dart
  62. 11
      lib/modules/translation/bindings/translation_binding.dart
  63. 182
      lib/modules/translation/controllers/translation_controller.dart
  64. 170
      lib/modules/translation/views/translation_view.dart
  65. 24
      macos/Podfile.lock
  66. 8
      pubspec.yaml
  67. 30
      test/widget_test.dart

2
.metadata

@ -15,7 +15,7 @@ migration:
- platform: root - platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android - platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1

8
android/app/build.gradle.kts

@ -3,9 +3,8 @@ import java.io.FileInputStream
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") }
url = uri("https://artifact.bytedance.com/repository/Volcengine/") maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") }
}
} }
plugins { plugins {
@ -84,6 +83,9 @@ android {
} }
dependencies { dependencies {
// WebRTC依赖已移除
// implementation("io.antmedia:webrtc-android-framework:2.8.0-SNAPSHOT")
// 添加OkHttp依赖 // 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3") implementation("com.squareup.okhttp3:okhttp:4.9.3")

396
android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt

@ -1,11 +1,21 @@
package com.yunqiinnovation.deepsound package com.yunqiinnovation.deepsound
import android.content.Context import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.os.Process
import com.yunqiinnovation.deepsound.core.utils.FileLogger import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler import com.microsoft.cognitiveservices.speech.util.EventHandler
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
// 移除WebRTC相关导入
class AzureAsrHelper(private val context: Context) { class AzureAsrHelper(private val context: Context) {
private var recognizer: SpeechRecognizer? = null private var recognizer: SpeechRecognizer? = null
@ -18,6 +28,14 @@ class AzureAsrHelper(private val context: Context) {
private var isAutoDetectLanguage = false private var isAutoDetectLanguage = false
private var supportedLanguages = arrayOf("zh-CN", "en-US") private var supportedLanguages = arrayOf("zh-CN", "en-US")
// 是否使用回音消除 - 内部控制常量
private val useEchoCancellation = true
// 自定义音频处理相关
private var customAudioProcessor: CustomAudioProcessor? = null
private var pushStream: PushAudioInputStream? = null
private var audioConfig: AudioConfig? = null
// 初始化SDK并创建recognizer // 初始化SDK并创建recognizer
fun initialize(subscriptionKey: String, serviceRegion: String, fun initialize(subscriptionKey: String, serviceRegion: String,
supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US")): Boolean { supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US")): Boolean {
@ -63,17 +81,31 @@ class AzureAsrHelper(private val context: Context) {
// 创建识别器 // 创建识别器
try { try {
if (isAutoDetectLanguage) { if (useEchoCancellation) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) // 如果使用回音消除,创建自定义音频输入流
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig) setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else { } else {
recognizer = SpeechRecognizer(speechConfig) // 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
} }
FileLogger.d(TAG, "Azure 语音服务初始化成功") FileLogger.d(TAG, "Azure 语音服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "创建识别器失败: ${e.message}") FileLogger.e(TAG, "创建识别器失败: ${e.message}")
stopCustomAudioProcessing()
return false return false
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -89,13 +121,29 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
// 停止当前的音频处理
stopCustomAudioProcessing()
// 使用现有配置重新创建 recognizer // 使用现有配置重新创建 recognizer
if (speechConfig != null) { if (speechConfig != null) {
if (isAutoDetectLanguage) { if (useEchoCancellation) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) // 如果使用回音消除,创建自定义音频输入流
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig) setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else { } else {
recognizer = SpeechRecognizer(speechConfig) // 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
} }
return true return true
} else { } else {
@ -122,17 +170,24 @@ class AzureAsrHelper(private val context: Context) {
} }
try { try {
// 启动音频处理
startCustomAudioProcessing()
// 执行识别 // 执行识别
val result = recognizer?.recognizeOnceAsync()?.get() val result = recognizer?.recognizeOnceAsync()?.get()
// 停止音频处理
stopCustomAudioProcessing()
if (result != null && result.reason == ResultReason.RecognizedSpeech) { if (result != null && result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language
callback.onResult(result.text, detectedLanguage ?: "") callback.onResult(result.text, detectedLanguage ?: "")
} else { } else {
callback.onError("未能识别语音") callback.onError("未能识别语音")
} }
} catch (e: Exception) { } catch (e: Exception) {
// 停止音频处理
stopCustomAudioProcessing()
callback.onError("识别异常: ${e.message}") callback.onError("识别异常: ${e.message}")
} }
} }
@ -156,6 +211,9 @@ class AzureAsrHelper(private val context: Context) {
} }
try { try {
// 启动音频处理
startCustomAudioProcessing()
// 设置识别事件处理 // 设置识别事件处理
// 最终识别结果 // 最终识别结果
recognizer?.recognized?.addEventListener( recognizer?.recognized?.addEventListener(
@ -166,6 +224,7 @@ class AzureAsrHelper(private val context: Context) {
} else { } else {
currentLanguage currentLanguage
} }
// FileLogger.d(TAG, "最终识别结果: ${event.result.text}")
callback.onResult(event.result.text, detectedLanguage) callback.onResult(event.result.text, detectedLanguage)
} }
} }
@ -180,6 +239,7 @@ class AzureAsrHelper(private val context: Context) {
} else { } else {
currentLanguage currentLanguage
} }
// FileLogger.d(TAG, "识别中结果: ${event.result.text}")
callback.onRecognizing(event.result.text, detectedLanguage) callback.onRecognizing(event.result.text, detectedLanguage)
} }
} }
@ -217,6 +277,7 @@ class AzureAsrHelper(private val context: Context) {
} catch (e: Exception) { } catch (e: Exception) {
callback.onError("开始连续识别失败: ${e.message}") callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopCustomAudioProcessing()
return false return false
} }
} }
@ -228,13 +289,18 @@ class AzureAsrHelper(private val context: Context) {
} }
try { try {
recognizer?.stopContinuousRecognitionAsync()?.get() recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
callback.onSessionStopped() callback.onSessionStopped()
// 停止音频处理
stopCustomAudioProcessing()
return true return true
} catch (e: Exception) { } catch (e: Exception) {
callback.onError("停止连续识别失败: ${e.message}") callback.onError("停止连续识别失败: ${e.message}")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopCustomAudioProcessing()
return false return false
} }
} }
@ -244,9 +310,89 @@ class AzureAsrHelper(private val context: Context) {
return isContinuousRecognitionActive return isContinuousRecognitionActive
} }
// 设置自定义音频处理
private fun setupCustomAudioProcessing() {
if (!useEchoCancellation) {
return
}
try {
// 1. 创建PushAudioInputStream
pushStream = PushAudioInputStream.create()
// 2. 创建AudioConfig
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 3. 创建自定义音频处理器
customAudioProcessor = CustomAudioProcessor(pushStream)
FileLogger.d(TAG, "自定义音频处理设置完成")
} catch (e: Exception) {
FileLogger.e(TAG, "设置自定义音频处理失败: ${e.message}")
releaseCustomAudioProcessing()
// 降级处理:如果自定义处理设置失败,尝试使用默认麦克风
try {
FileLogger.d(TAG, "尝试降级到默认麦克风输入")
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
} catch (e2: Exception) {
FileLogger.e(TAG, "默认麦克风输入设置也失败: ${e2.message}")
audioConfig = null
}
}
}
// 启动自定义音频处理
private fun startCustomAudioProcessing() {
if (!useEchoCancellation || customAudioProcessor == null) {
return
}
try {
customAudioProcessor?.startRecording()
FileLogger.d(TAG, "自定义音频处理已启动")
} catch (e: Exception) {
FileLogger.e(TAG, "启动自定义音频处理失败: ${e.message}")
}
}
// 停止自定义音频处理
private fun stopCustomAudioProcessing() {
if (!useEchoCancellation || customAudioProcessor == null) {
return
}
try {
customAudioProcessor?.stopRecording()
FileLogger.d(TAG, "自定义音频处理已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止自定义音频处理失败: ${e.message}")
}
}
// 释放自定义音频处理资源
private fun releaseCustomAudioProcessing() {
stopCustomAudioProcessing()
try {
customAudioProcessor = null
pushStream?.close()
pushStream = null
audioConfig?.close()
audioConfig = null
FileLogger.d(TAG, "自定义音频处理资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放自定义音频处理资源时出错: ${e.message}")
}
}
// 释放所有资源 // 释放所有资源
fun dispose() { fun dispose() {
try { try {
// 停止和释放音频处理
releaseCustomAudioProcessing()
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
@ -261,6 +407,235 @@ class AzureAsrHelper(private val context: Context) {
} }
} }
// 自定义音频处理器 - 使用Android原生回音消除
private inner class CustomAudioProcessor(private val pushStream: PushAudioInputStream?) {
private val SAMPLE_RATE = 16000
private val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
private val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
private val BUFFER_SIZE = SAMPLE_RATE * 2 // 简化缓冲区大小计算,更稳定
private var audioRecord: AudioRecord? = null
private var echoCanceler: AcousticEchoCanceler? = null
private val isRecording = AtomicBoolean(false)
private var recordingThread: Thread? = null
// 启动录音并处理音频数据
fun startRecording() {
if (isRecording.get() || pushStream == null) {
return
}
try {
// 使用Builder模式构建AudioFormat
val audioFormat = AudioFormat.Builder()
.setSampleRate(SAMPLE_RATE)
.setEncoding(AUDIO_FORMAT)
.setChannelMask(CHANNEL_CONFIG)
.build()
// 使用Builder模式创建AudioRecord实例
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(BUFFER_SIZE)
.build()
// 检查AudioRecord初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
FileLogger.e(TAG, "AudioRecord初始化失败,状态: ${audioRecord?.state}")
// 尝试使用DEFAULT音频源重试一次
audioRecord?.release()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(BUFFER_SIZE)
.build()
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
FileLogger.e(TAG, "AudioRecord初始化第二次尝试也失败,放弃")
releaseAudioResources()
return
} else {
FileLogger.d(TAG, "使用默认音频源成功初始化AudioRecord")
}
}
// 启用音频效果(回音消除、噪声抑制等)
enableAudioEffects()
// 启动录音
audioRecord?.startRecording()
isRecording.set(true)
// 创建录音线程
recordingThread = Thread({
val buffer = ByteArray(BUFFER_SIZE)
while (isRecording.get()) {
try {
val readSize = audioRecord?.read(buffer, 0, BUFFER_SIZE) ?: 0
if (readSize > 0) {
try {
// 将处理后的音频数据推送到流
if (readSize == buffer.size) {
// 如果读取的大小等于buffer的大小,直接写入整个buffer
pushStream.write(buffer)
} else {
// 如果只读取了部分数据,创建新的数组只包含有效数据
val validData = buffer.copyOfRange(0, readSize)
pushStream.write(validData)
}
} catch (e: Exception) {
FileLogger.e(TAG, "写入音频数据失败: ${e.message}")
break
}
} else if (readSize == 0) {
// 读取为0,可能是临时的,等待一下继续尝试
Thread.sleep(10)
} else {
// 负值表示错误
FileLogger.e(TAG, "读取音频数据失败,错误码: $readSize")
break
}
} catch (e: Exception) {
FileLogger.e(TAG, "录音线程异常: ${e.message}")
break
}
}
}, "AudioRecordingThread")
// 设置线程优先级并启动
recordingThread?.priority = Thread.MAX_PRIORITY
recordingThread?.start()
FileLogger.d(TAG, "音频录制已启动" + (if(echoCanceler?.enabled == true) ",回音消除已启用" else ""))
} catch (e: Exception) {
FileLogger.e(TAG, "启动音频录制失败: ${e.message}")
releaseAudioResources()
}
}
// 启用音频效果(回音消除、噪声抑制等)
private fun enableAudioEffects() {
try {
val audioSessionId = audioRecord?.audioSessionId ?: -1
if (audioSessionId != -1) {
// 启用回音消除
if (AcousticEchoCanceler.isAvailable()) {
try {
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler != null) {
echoCanceler?.enabled = true
FileLogger.d(TAG, "回音消除已启用,会话ID: $audioSessionId")
} else {
FileLogger.w(TAG, "回音消除器创建返回null")
}
} catch (e: Exception) {
FileLogger.e(TAG, "创建回音消除器时出错: ${e.message}")
}
} else {
FileLogger.d(TAG, "设备不支持回音消除")
}
// 以下功能暂时不启用,可根据需要取消注释
/*
// 启用噪声抑制
if (NoiseSuppressor.isAvailable()) {
try {
val ns = NoiseSuppressor.create(audioSessionId)
ns?.enabled = true
FileLogger.d(TAG, "噪声抑制已启用")
} catch (e: Exception) {
FileLogger.e(TAG, "创建噪声抑制器时出错: ${e.message}")
}
}
// 启用自动增益控制
if (AutomaticGainControl.isAvailable()) {
try {
val agc = AutomaticGainControl.create(audioSessionId)
agc?.enabled = true
FileLogger.d(TAG, "自动增益控制已启用")
} catch (e: Exception) {
FileLogger.e(TAG, "创建自动增益控制时出错: ${e.message}")
}
}
*/
} else {
FileLogger.w(TAG, "无效的音频会话ID,无法启用音频效果")
}
} catch (e: Exception) {
FileLogger.e(TAG, "启用音频效果时出错: ${e.message}")
}
}
// 停止录音
fun stopRecording() {
if (!isRecording.get()) {
return
}
isRecording.set(false)
try {
// 等待录音线程结束
recordingThread?.join(1000)
// 释放资源
releaseAudioResources()
FileLogger.d(TAG, "音频录制已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止音频录制失败: ${e.message}")
}
}
// 释放音频资源
private fun releaseAudioResources() {
try {
// 停止录音
try {
if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) {
audioRecord?.stop()
}
} catch (e: Exception) {
// 忽略可能的IllegalStateException
FileLogger.w(TAG, "停止AudioRecord时出错: ${e.message}")
}
// 释放回音消除器
try {
if (echoCanceler != null) {
echoCanceler?.enabled = false
echoCanceler?.release()
echoCanceler = null
}
} catch (e: Exception) {
FileLogger.w(TAG, "释放回音消除器时出错: ${e.message}")
} finally {
echoCanceler = null
}
// 释放音频记录器
try {
audioRecord?.release()
} catch (e: Exception) {
FileLogger.w(TAG, "释放AudioRecord时出错: ${e.message}")
} finally {
audioRecord = null
}
// 重置线程
recordingThread = null
} catch (e: Exception) {
FileLogger.e(TAG, "释放音频资源失败: ${e.message}")
}
}
}
// 一次性识别回调接口 // 一次性识别回调接口
interface RecognizeCallback { interface RecognizeCallback {
fun onResult(result: String, detectedLanguage: String = "") fun onResult(result: String, detectedLanguage: String = "")
@ -276,5 +651,4 @@ class AzureAsrHelper(private val context: Context) {
fun onCanceled(reason: String, errorDetails: String) fun onCanceled(reason: String, errorDetails: String)
fun onError(error: String) fun onError(error: String)
} }
} }

43
android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt

@ -24,6 +24,9 @@ class AzureTtsHelper(private val context: Context) {
private var isInitialized = false private var isInitialized = false
private var audioManager: AudioManager? = null private var audioManager: AudioManager? = null
// 跟踪当前是否正在播放语音
private var speaking = false
// 音频输出设备类型 // 音频输出设备类型
enum class AudioOutputType { enum class AudioOutputType {
SPEAKER, // 扬声器 SPEAKER, // 扬声器
@ -74,8 +77,14 @@ class AzureTtsHelper(private val context: Context) {
synthesizer = SpeechSynthesizer(speechConfig, audioConfig) synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
// 设置事件监听 // 设置事件监听
synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCompleted?.addEventListener { _, _ ->
synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() } speaking = false
semaphore.release()
}
synthesizer?.SynthesisCanceled?.addEventListener { _, _ ->
speaking = false
semaphore.release()
}
isInitialized = true isInitialized = true
FileLogger.d(TAG, "TTS 引擎初始化成功") FileLogger.d(TAG, "TTS 引擎初始化成功")
@ -189,8 +198,14 @@ class AzureTtsHelper(private val context: Context) {
synthesizer = SpeechSynthesizer(speechConfig, audioConfig) synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
// 重新设置事件监听 // 重新设置事件监听
synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCompleted?.addEventListener { _, _ ->
synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() } speaking = false
semaphore.release()
}
synthesizer?.SynthesisCanceled?.addEventListener { _, _ ->
speaking = false
semaphore.release()
}
FileLogger.d(TAG, "已重新创建语音合成器") FileLogger.d(TAG, "已重新创建语音合成器")
} catch (e: Exception) { } catch (e: Exception) {
@ -264,7 +279,7 @@ class AzureTtsHelper(private val context: Context) {
*/ */
fun speakText(text: String, callback: TTSCallback) { fun speakText(text: String, callback: TTSCallback) {
if (!isInitialized) { if (!isInitialized) {
callback.onError("TTS 引擎尚未初始化") callback.onError("TTS 引擎尚未初始化===")
return return
} }
@ -323,6 +338,9 @@ class AzureTtsHelper(private val context: Context) {
// 异步合成语音 // 异步合成语音
val task: Future<SpeechSynthesisResult> = synthesizer!!.SpeakSsmlAsync(ssml) val task: Future<SpeechSynthesisResult> = synthesizer!!.SpeakSsmlAsync(ssml)
// 标记为正在播放
speaking = true
// 等待合成完成 // 等待合成完成
Thread { Thread {
try { try {
@ -343,8 +361,8 @@ class AzureTtsHelper(private val context: Context) {
callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}")
} }
else -> { else -> {
FileLogger.e(TAG, "语音合成失败: ${result.reason}") FileLogger.e(TAG, "语音合成失败kt: ${result.reason}")
callback.onError("语音合成失败: ${result.reason}") callback.onError("语音合成失败kt: ${result.reason}")
} }
} }
@ -373,6 +391,7 @@ class AzureTtsHelper(private val context: Context) {
try { try {
synthesizer?.StopSpeakingAsync() synthesizer?.StopSpeakingAsync()
speaking = false
FileLogger.d(TAG, "已停止语音合成") FileLogger.d(TAG, "已停止语音合成")
return true return true
@ -399,6 +418,7 @@ class AzureTtsHelper(private val context: Context) {
} }
isInitialized = false isInitialized = false
speaking = false
FileLogger.d(TAG, "TTS 引擎已释放") FileLogger.d(TAG, "TTS 引擎已释放")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "释放 TTS 引擎失败: ${e.message}") FileLogger.e(TAG, "释放 TTS 引擎失败: ${e.message}")
@ -406,6 +426,15 @@ class AzureTtsHelper(private val context: Context) {
} }
} }
/**
* 检查当前是否正在播放语音
*
* @return 是否正在播放语音
*/
fun isSpeaking(): Boolean {
return speaking
}
/** /**
* TTS 回调接口 * TTS 回调接口
*/ */

24
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt

@ -48,7 +48,7 @@ class MainActivity: FlutterActivity() {
val timestamp = intent.getLongExtra("timestamp", 0) val timestamp = intent.getLongExtra("timestamp", 0)
Log.d(TAG, "收到语音识别启动广播: timestamp=$timestamp") Log.d(TAG, "收到语音识别启动广播: timestamp=$timestamp")
sendVoiceInteractionEvent(mapOf( sendVoiceInteractionEvent(mapOf(
"type" to "recognitionStarted", "type" to "recognition_started",
"timestamp" to timestamp "timestamp" to timestamp
)) ))
} }
@ -311,7 +311,6 @@ class MainActivity: FlutterActivity() {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "语音交互事件通道开始监听") Log.d(TAG, "语音交互事件通道开始监听")
voiceInteractionEventSink = events voiceInteractionEventSink = events
events?.success(mapOf("type" to "channelReady"))
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
@ -326,7 +325,6 @@ class MainActivity: FlutterActivity() {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "ASR事件通道开始监听") Log.d(TAG, "ASR事件通道开始监听")
azureAsrEventSink = events azureAsrEventSink = events
events?.success(mapOf("type" to "channelReady"))
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
@ -341,11 +339,6 @@ class MainActivity: FlutterActivity() {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "蓝牙事件通道开始监听") Log.d(TAG, "蓝牙事件通道开始监听")
bluetoothEventSink = events bluetoothEventSink = events
// 发送就绪事件
events?.success(mapOf(
"type" to "channelReady",
"timestamp" to System.currentTimeMillis()
))
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
@ -542,6 +535,9 @@ class MainActivity: FlutterActivity() {
"stopSpeaking" -> { "stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking()) result.success(azureTtsHelper.stopSpeaking())
} }
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
"dispose" -> { "dispose" -> {
azureTtsHelper.dispose() azureTtsHelper.dispose()
result.success(true) result.success(true)
@ -555,14 +551,14 @@ class MainActivity: FlutterActivity() {
// 设置语音交互方法通道 // 设置语音交互方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"startVoiceInteractionService" -> { "startService" -> {
startVoiceInteractionService(call, result) startVoiceInteractionService(call, result)
} }
"stopVoiceInteractionService" -> { "stopService" -> {
stopVoiceInteractionService() stopVoiceInteractionService()
result.success(true) result.success(true)
} }
"isVoiceInteractionServiceRunning" -> { "isServiceRunning" -> {
result.success(VoiceInteractionService.isServiceRunning()) result.success(VoiceInteractionService.isServiceRunning())
} }
"pauseVoiceInteraction" -> { "pauseVoiceInteraction" -> {
@ -644,6 +640,10 @@ class MainActivity: FlutterActivity() {
result.error("BLUETOOTH_ERROR", "检查蓝牙状态失败: ${e.message}", e.toString()) result.error("BLUETOOTH_ERROR", "检查蓝牙状态失败: ${e.message}", e.toString())
} }
} }
"initialize" -> {
result.success(true)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -728,7 +728,7 @@ class MainActivity: FlutterActivity() {
runOnUiThread { runOnUiThread {
try { try {
val eventData = mapOf( val eventData = mapOf(
"type" to "chatHistory", "type" to "chat_history_updated",
"agentId" to agentId, "agentId" to agentId,
"userMessage" to userMessage, "userMessage" to userMessage,
"assistantMessage" to assistantMessage, "assistantMessage" to assistantMessage,

2
android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt

@ -785,7 +785,7 @@ class VoiceInteractionService : Service() {
* 用于从 Flutter 端控制服务 * 用于从 Flutter 端控制服务
*/ */
private fun pauseVoiceInteraction() { private fun pauseVoiceInteraction() {
FileLogger.d(TAG, "暂停语音交互(来自Flutter的请求)") FileLogger.d(TAG, "暂停后台语音交互(来自Flutter的请求)")
// 停止当前TTS播放 // 停止当前TTS播放
stopCurrentTTS() stopCurrentTTS()

BIN
android/app/src/main/res/raw/sample.mp3

Binary file not shown.

21
azure/LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Your Company
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
azure/README.md

@ -0,0 +1,66 @@
# Azure Speech Recognition
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
## Features
- Speech-to-text (Azure Speech Recognition)
- Text-to-speech (Azure Speech Synthesis)
- Support for multiple languages
- Language detection
- Continuous recognition
- Streaming synthesis
## Getting Started
### Prerequisites
- Azure Speech service subscription key
- Azure Speech service region
### Installation
Add this to your package's `pubspec.yaml` file:
```yaml
dependencies:
azure_speech_recognition:
path: ./azure
```
### Usage
```dart
import 'package:azure_speech_recognition/azure_speech_recognition.dart';
// Initialize the service
await AzureSpeechRecognition.initialize(
subscriptionKey: 'your_subscription_key',
region: 'your_region',
supportedLanguages: ['zh-CN', 'en-US'],
);
// Start continuous recognition
await AzureSpeechRecognition.startContinuousRecognition();
// Listen for recognition events
AzureSpeechRecognition.onRecognitionEvent.listen((event) {
if (event['type'] == 'result') {
print('Recognized: ${event['text']}');
print('Detected language: ${event['detectedLanguage']}');
}
});
// Stop recognition when done
await AzureSpeechRecognition.stopContinuousRecognition();
// Speak text
await AzureSpeechRecognition.speakText('Hello, world!');
// Clean up
await AzureSpeechRecognition.dispose();
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

757
azure/ios/Classes/AzureAsrHelper.swift

@ -0,0 +1,757 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
import AudioToolbox
/// 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 pushStream: SPXPushAudioInputStream?
/// 音频处理相关
private var audioProcessor: CustomAudioProcessor?
private var isProcessingAudio = false
private var audioProcessingTimer: Timer?
/// 状态标志
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()
// 创建自定义推送流,替代默认的麦克风输入
pushStream = try SPXPushAudioInputStream()
audioConfig = try SPXAudioConfiguration(streamInput: pushStream!)
// 初始化自定义音频处理器
audioProcessor = CustomAudioProcessor()
// 设置语言配置
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, .mixWithOthers])
// 设置首选的输入和输出
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 {
// 启动音频处理
startAudioProcessing()
// 通知会话开始
eventHandler("sessionStarted", [:])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
guard let self = self else { return }
// 停止音频处理
self.stopAudioProcessing()
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)"])
stopAudioProcessing()
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 {
// 启动音频处理
startAudioProcessing()
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别开始")
return true
} catch {
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
stopAudioProcessing()
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
// 停止音频处理
stopAudioProcessing()
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] 释放资源")
// 停止音频处理
stopAudioProcessing()
// 停止连续识别
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 释放音频会话
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("[AzureAsrHelper] 警告: 释放音频会话失败: \(error.localizedDescription)")
}
// 释放资源
recognizer = nil
speechConfig = nil
audioConfig = nil
pushStream = nil
audioProcessor = 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
}
}
// MARK: - 音频处理
/// 开始音频处理
private func startAudioProcessing() {
guard !isProcessingAudio, let audioProcessor = audioProcessor else { return }
isProcessingAudio = true
// 启动音频处理器
if !audioProcessor.startRecord() {
print("[AzureAsrHelper] 错误: 启动音频处理器失败")
eventHandler("error", ["message": "启动音频处理器失败"])
return
}
// 启动音频处理定时器
audioProcessingTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in
guard let self = self, self.isProcessingAudio, let processor = self.audioProcessor, let stream = self.pushStream else {
return
}
// 读取处理后的音频数据
var bytes = [UInt8](repeating: 0, count: 2560)
let bytesRead = processor.read(bytes: &bytes)
if bytesRead > 0 {
// 推送数据到Azure语音服务
let data = Data(bytes: bytes, count: bytesRead)
stream.write(data)
// 通知音频数据可用
self.eventHandler("audioData", ["data": bytes])
}
}
print("[AzureAsrHelper] 音频处理已启动")
}
/// 停止音频处理
private func stopAudioProcessing() {
// 停止定时器
audioProcessingTimer?.invalidate()
audioProcessingTimer = nil
// 停止音频处理器
audioProcessor?.stopRecord()
isProcessingAudio = false
print("[AzureAsrHelper] 音频处理已停止")
}
}
// MARK: - 自定义音频处理器
@available(iOS 13.0, *)
class CustomAudioProcessor: NSObject {
// 音频单元
private var ioUnit: AudioUnit?
// 音频格式
private var audioFormat: AudioStreamBasicDescription
// 音频缓冲
private var audioBufferList: AudioBufferList
private var audioList: [Float] = []
private let audioListQueue = DispatchQueue(label: "audioListQueue")
// 回音消除状态
private var isEchoCancellationEnabled = true
override init() {
// 设置音频格式 - 16kHz, 16位, 单声道
audioFormat = AudioStreamBasicDescription(
mSampleRate: 16000.0,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked,
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0
)
// 初始化音频缓冲
audioBufferList = AudioBufferList(
mNumberBuffers: 1,
mBuffers: AudioBuffer(
mNumberChannels: 1,
mDataByteSize: 4096,
mData: malloc(4096)
)
)
super.init()
}
deinit {
stopRecord()
free(audioBufferList.mBuffers.mData)
}
/// 启动音频处理
/// - Returns: 是否成功启动
func startRecord() -> Bool {
print("[CustomAudioProcessor] 配置音频单元")
// 创建音频组件描述 - 使用VoiceProcessingIO类型获取回音消除
var ioUnitDescription = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_VoiceProcessingIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
// 查找音频组件
guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else {
print("[CustomAudioProcessor] 错误: 未找到音频组件")
return false
}
// 创建音频单元实例
if checkError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建音频单元") {
ioUnit = nil
return false
}
// 启用输入端口
var enableInput: UInt32 = 1
let kInputBus: AudioUnitElement = 1
let kOutputBus: AudioUnitElement = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, kInputBus, &enableInput,
UInt32(MemoryLayout<UInt32>.size)), "启用输入端口") {
return false
}
// 禁用输出端口 (我们只需要输入)
var enableOutput: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, kOutputBus,
&enableOutput, UInt32(MemoryLayout<UInt32>.size)), "禁用输出端口") {
return false
}
// 设置缓冲区分配标志
var flag: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout<UInt32>.size)), "设置缓冲区分配标志") {
return false
}
// 设置音频格式
let size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线输出范围的流格式") {
return false
}
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线输入范围的流格式") {
return false
}
// 启用回音消除
if isEchoCancellationEnabled {
var echoCancellation: UInt32 = 1
AudioUnitSetProperty(ioUnit!, kAUVoiceIOProperty_BypassVoiceProcessing,
kAudioUnitScope_Global, 0, &echoCancellation, UInt32(MemoryLayout<UInt32>.size))
}
// 设置输入回调 - 当有新音频数据时调用
var inputCallback = AURenderCallbackStruct(
inputProc: CustomAudioProcessor.onAudioDataAvailable,
inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
if checkError(AudioUnitSetProperty(ioUnit!,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, kInputBus,
&inputCallback, UInt32(MemoryLayout<AURenderCallbackStruct>.size)), "设置输入回调") {
return false
}
// 初始化音频单元
var hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
while hasError {
Thread.sleep(forTimeInterval: 0.1)
hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
}
// 启动音频单元
hasError = checkError(AudioOutputUnitStart(ioUnit!), "启动音频单元")
print("[CustomAudioProcessor] 音频处理器已启动,回音消除\(isEchoCancellationEnabled ? "已启用" : "已禁用")")
return !hasError
}
/// 停止音频处理
func stopRecord() {
print("[CustomAudioProcessor] 停止音频处理器")
if let ioUnit = ioUnit {
// 停止音频单元
_ = checkError(AudioOutputUnitStop(ioUnit), "停止音频单元")
// 关闭音频单元
_ = checkError(AudioUnitUninitialize(ioUnit), "反初始化音频单元")
_ = checkError(AudioComponentInstanceDispose(ioUnit), "释放音频单元")
self.ioUnit = nil
}
// 清空音频数据缓冲
audioListQueue.sync {
audioList.removeAll()
}
}
/// 音频数据回调 - 当有新的音频数据可用时调用
private static let onAudioDataAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in
// 获取实例
let processor = Unmanaged<CustomAudioProcessor>.fromOpaque(inRefCon).takeUnretainedValue()
// 计算预期数据大小
let expectedDataByteSize = inNumberFrames * processor.audioFormat.mBytesPerFrame
// 确保缓冲区足够大
if processor.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize {
processor.audioBufferList.mBuffers.mData = realloc(processor.audioBufferList.mBuffers.mData, Int(expectedDataByteSize))
processor.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize
}
// 渲染音频数据
let status = processor.checkOSStatus(AudioUnitRender(processor.ioUnit!, ioActionFlags, inTimeStamp,
inBusNumber, inNumberFrames, &processor.audioBufferList),
"渲染音频数据")
// 将Int16数据转换为浮点数据进行处理
var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames))
let buffer = processor.audioBufferList.mBuffers
let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self)
for j in 0..<Int(buffer.mDataByteSize / UInt32(MemoryLayout<Int16>.size)) {
// 归一化到[-1.0, 1.0]范围
audioDataFloat[j] = Float(bufferData[j]) / 32768.0
}
// 应用附加处理 (如有需要)
// processor.applyAdditionalProcessing(&audioDataFloat)
// 保存处理后的数据
if status == noErr {
processor.audioListQueue.async {
processor.audioList.append(contentsOf: audioDataFloat)
}
}
return status
}
/// 读取处理后的音频数据
/// - Parameter bytes: 输出字节数组
/// - Returns: 读取的字节数
func read(bytes: inout [UInt8]) -> Int {
return audioListQueue.sync {
// 如果没有数据,返回0
if audioList.isEmpty {
return 0
}
// 确保有足够的数据 (至少1280个样本)
if audioList.count < 1280 {
return 0
}
// 读取一帧数据 (1280个样本)
let frameLength = 1280
let buffer = Array(audioList.prefix(frameLength))
audioList.removeFirst(frameLength)
// 将浮点数据转回Int16格式
var int16Data = buffer.map { Int16($0 * 32767) }
// 转换为字节数组
let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count))
bytes = [UInt8](data)
// 每个样本2字节 (16位PCM)
return frameLength * 2
}
}
/// 检查错误并打印日志
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 是否发生错误
private func checkError(_ status: OSStatus, _ operation: String) -> Bool {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
return true
}
return false
}
/// 检查OSStatus并返回状态
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 原始状态
private func checkOSStatus(_ status: OSStatus, _ operation: String) -> OSStatus {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
}
return status
}
}

18
azure/ios/Classes/AzureSpeechRecognitionPlugin.swift

@ -0,0 +1,18 @@
import Flutter
import UIKit
public class AzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
if #available(iOS 13.0, *) {
SwiftAzureSpeechRecognitionPlugin.register(with: registrar)
} else {
// 如果低于iOS 13.0,返回不支持的错误
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler { (call, result) in
result(FlutterError(code: "UNSUPPORTED",
message: "需要iOS 13.0及以上系统",
details: nil))
}
}
}
}

427
azure/ios/Classes/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
}
}

259
azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift

@ -0,0 +1,259 @@
import Flutter
import UIKit
import MicrosoftCognitiveServicesSpeech
import AVFoundation
@available(iOS 13.0, *)
public class SwiftAzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
private var azureChannel: FlutterMethodChannel
private var ttsChannel: FlutterMethodChannel
private var asrHelper: AzureAsrHelper
private var ttsHelper: AzureTtsHelper
private static var eventStreamHandler: AzureEventStreamHandler?
// 创建方法到通道的映射
private static var ttsMethodHandlers = [String: FlutterMethodCallHandler]()
private static var asrMethodHandlers = [String: FlutterMethodCallHandler]()
public static func register(with registrar: FlutterPluginRegistrar) {
// ASR通道
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
// TTS通道
let ttsChannel = FlutterMethodChannel(name: "com.deep_voice.azure_tts", binaryMessenger: registrar.messenger())
// 设置ASR事件通道
let eventChannel = FlutterEventChannel(name: "com.deep_voice.azure_asr_events", binaryMessenger: registrar.messenger())
eventStreamHandler = AzureEventStreamHandler()
eventChannel.setStreamHandler(eventStreamHandler)
let instance = SwiftAzureSpeechRecognitionPlugin(
azureChannel: channel,
ttsChannel: ttsChannel,
eventStreamHandler: eventStreamHandler!
)
// 直接设置各自通道的处理器
channel.setMethodCallHandler(instance.handleAsrMethodCalls)
ttsChannel.setMethodCallHandler(instance.handleTtsMethodCalls)
}
// 新增直接处理方法调用的函数
private func handleTtsMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
handleTtsMethod(call, result)
}
private func handleAsrMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
handleAsrMethod(call, result)
}
init(azureChannel: FlutterMethodChannel, ttsChannel: FlutterMethodChannel, eventStreamHandler: AzureEventStreamHandler) {
self.azureChannel = azureChannel
self.ttsChannel = ttsChannel
// 创建辅助类实例,使用自定义事件回调处理器
let eventHandler: (String, [String: Any]) -> Void = { eventName, arguments in
DispatchQueue.main.async {
if let eventSink = SwiftAzureSpeechRecognitionPlugin.eventStreamHandler?.eventSink {
var eventData = arguments
eventData["type"] = eventName
eventSink(eventData)
}
}
}
asrHelper = AzureAsrHelper(eventHandler: eventHandler)
ttsHelper = AzureTtsHelper(eventHandler: eventHandler)
super.init()
}
private func handleAsrMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时读取必要参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let supportedLanguages = args?["supportedLanguages"] as? [String] ?? []
let success = asrHelper.initialize(
speechSubscriptionKey: speechSubscriptionKey,
serviceRegion: serviceRegion,
supportedLanguages: supportedLanguages.isEmpty ? nil : supportedLanguages
)
result(success)
case "startContinuousRecognition":
// 只有使用参数时才验证
let success = asrHelper.startContinuousRecognition()
result(success)
case "stopContinuousRecognition":
// 不需要额外参数
let success = asrHelper.stopContinuousRecognition()
result(success)
case "recognizeOnce":
// 只有使用参数时才验证
let success = asrHelper.recognizeOnce()
result(success)
case "isContinuousRecognitionActive":
// 不需要额外参数
result(asrHelper.isContinuousRecognitionActive())
case "dispose":
// 不需要额外参数
print("[AzurePlugin] 释放ASR资源")
asrHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知ASR方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
private func handleTtsMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时验证参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let language = args?["language"] as? String ?? "zh-CN"
print("[AzurePlugin] 初始化TTS,语言: \(language)")
let success = ttsHelper.initialize(speechSubscriptionKey: speechSubscriptionKey, serviceRegion: serviceRegion, language: language)
result(success)
case "setVoice":
// 仅获取voice参数
guard let voiceName = args?["voiceName"] as? String, !voiceName.isEmpty else {
let errorMsg = "声音名称不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_VOICE", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 设置声音: \(voiceName)")
let success = ttsHelper.setVoice(voiceName: voiceName)
result(success)
case "speakText":
// 仅获取text参数
let text = args?["text"] as? String ?? ""
if text.isEmpty {
print("[AzurePlugin] 警告: 要播放的文本为空")
result("OK")
return
}
print("[AzurePlugin] 播放文本: \(text.prefix(50))...")
let success = ttsHelper.speakText(text: text)
result(success ? "OK" : "ERROR")
case "speakSsml":
// 仅获取ssml参数
guard let ssml = args?["ssml"] as? String, !ssml.isEmpty else {
let errorMsg = "SSML内容不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SSML", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 播放SSML: \(ssml.prefix(100))...")
// 由于我们移除了speakSsml方法,这里改用speakText方法
// Azure SDK内部会自动检测是普通文本还是SSML
let success = ttsHelper.speakText(text: ssml)
result(success)
case "stopSpeaking":
// 不需要参数
print("[AzurePlugin] 停止播放")
let success = ttsHelper.stopSpeaking()
result(success)
case "isSpeaking":
// 不需要参数
result(ttsHelper.isSpeaking())
case "setSpeechParams":
// 仅获取语音参数
let rate = args?["rate"] as? Int ?? 0
let pitch = args?["pitch"] as? Int ?? 0
let volume = args?["volume"] as? Int ?? 100
print("[AzurePlugin] 设置语音参数: rate=\(rate), pitch=\(pitch), volume=\(volume)")
let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume)
result(success)
case "dispose":
// 释放TTS资源
print("[AzurePlugin] 释放TTS资源")
ttsHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知TTS方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
}
// 用于处理事件流的辅助类
@available(iOS 13.0, *)
class AzureEventStreamHandler: NSObject, FlutterStreamHandler {
var eventSink: FlutterEventSink?
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 通知Flutter端事件通道已准备好
DispatchQueue.main.async {
events(["type": "channelReady"])
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}

24
azure/ios/azure_speech_recognition.podspec

@ -0,0 +1,24 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint azure_speech_recognition.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'azure_speech_recognition'
s.version = '0.1.0'
s.summary = 'Azure Speech Recognition plugin for Flutter'
s.description = <<-DESC
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
DESC
s.homepage = 'https://github.com/yourusername/azure_speech_recognition'
s.license = { :type => 'MIT', :file => '../LICENSE' }
s.author = { 'Your Company' => 'your-email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

6
azure/lib/azure_speech_recognition.dart

@ -0,0 +1,6 @@
// This is a placeholder file that exports nothing.
// The actual implementation is in the app's services folder.
// This file exists just to satisfy the Flutter plugin structure requirements.
// Empty library to satisfy plugin structure
library azure_speech_recognition;

23
azure/pubspec.yaml

@ -0,0 +1,23 @@
name: azure_speech_recognition
description: Azure Speech Recognition and Text-to-Speech services Flutter plugin
version: 0.1.0
homepage: https://github.com/yourusername/azure_speech_recognition
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
flutter:
plugin:
platforms:
ios:
pluginClass: AzureSpeechRecognitionPlugin

6
ios/Podfile

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '12.0' platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -31,6 +31,10 @@ target 'Runner' do
use_frameworks! use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# 添加 Microsoft Cognitive Services Speech SDK
pod 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
target 'RunnerTests' do target 'RunnerTests' do
inherit! :search_paths inherit! :search_paths
end end

64
ios/Podfile.lock

@ -1,21 +1,24 @@
PODS: PODS:
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1): - audio_session (0.0.1):
- Flutter - Flutter
- azure_speech_recognition_null_safety (0.0.1): - audio_waveforms (0.0.1):
- Flutter - Flutter
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.29.0)
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_local_notifications (0.0.1): - flutter_local_notifications (0.0.1):
- Flutter - Flutter
- flutter_tts (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- just_audio (0.0.1): - just_audio (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- MicrosoftCognitiveServicesSpeech-iOS (1.29.0) - MicrosoftCognitiveServicesSpeech-iOS (1.34.0)
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -23,40 +26,53 @@ PODS:
- Flutter - Flutter
- record_darwin (1.0.0): - record_darwin (1.0.0):
- Flutter - Flutter
- sqflite_darwin (0.0.4): - share_plus (0.0.1):
- Flutter
- speech_to_text (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Try
- Try (2.1.1)
DEPENDENCIES: DEPENDENCIES:
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`)
- azure_speech_recognition_null_safety (from `.symlinks/plugins/azure_speech_recognition_null_safety/ios`) - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`) - just_audio (from `.symlinks/plugins/just_audio/darwin`)
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`) - record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- MicrosoftCognitiveServicesSpeech-iOS - MicrosoftCognitiveServicesSpeech-iOS
- Try
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_service:
:path: ".symlinks/plugins/audio_service/darwin"
audio_session: audio_session:
:path: ".symlinks/plugins/audio_session/ios" :path: ".symlinks/plugins/audio_session/ios"
azure_speech_recognition_null_safety: audio_waveforms:
:path: ".symlinks/plugins/azure_speech_recognition_null_safety/ios" :path: ".symlinks/plugins/audio_waveforms/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_local_notifications: flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios" :path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_tts:
:path: ".symlinks/plugins/flutter_tts/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
just_audio: just_audio:
:path: ".symlinks/plugins/just_audio/darwin" :path: ".symlinks/plugins/just_audio/darwin"
path_provider_foundation: path_provider_foundation:
@ -65,23 +81,29 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
record_darwin: record_darwin:
:path: ".symlinks/plugins/record_darwin/ios" :path: ".symlinks/plugins/record_darwin/ios"
sqflite_darwin: share_plus:
:path: ".symlinks/plugins/sqflite_darwin/darwin" :path: ".symlinks/plugins/share_plus/ios"
speech_to_text:
:path: ".symlinks/plugins/speech_to_text/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
azure_speech_recognition_null_safety: aed1d7ededc73a1d0b7a342b1f4803ea166e2747 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
MicrosoftCognitiveServicesSpeech-iOS: 0de20a8ebb6bbd87decaeaa9a707dd40830106a6 MicrosoftCognitiveServicesSpeech-iOS: 723eccb3e459bbb530df3d9bfe84a291074b09eb
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
PODFILE CHECKSUM: 359b0564325fb2f8842a19a47bd7e63cbdcd9c04 PODFILE CHECKSUM: f35c1d63b4b8ce9c2eb88107f162ac59923c4839
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

218
ios/Runner.xcodeproj/project.pbxproj

@ -7,15 +7,24 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.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 */; };
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 */; };
0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
993AA22300B2BA544C45E2B2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */; }; E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A9190D0838960F143BDC158 /* Pods_Runner.framework */; };
F3E628587CFA36508BAB3B56 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 735C476F36391FA72F7CA27A /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -42,18 +51,30 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.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>"; };
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>"; };
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>"; };
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; 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>"; }; 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>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
478E46ED4F3AE11E50872120 /* 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>"; }; 4A9190D0838960F143BDC158 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
735C476F36391FA72F7CA27A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -61,26 +82,23 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
C0DB3B999FA4C3E6CFA90079 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F83C5E570162235C4FE20CCB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
6AE85031FDE82288FAE3C673 /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
993AA22300B2BA544C45E2B2 /* Pods_RunnerTests.framework in Frameworks */, E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
97C146EB1CF9000F007C117D /* Frameworks */ = { B4459155A2535A0ED1D6C7D8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F3E628587CFA36508BAB3B56 /* Pods_Runner.framework in Frameworks */, 3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -95,6 +113,19 @@
path = RunnerTests; path = RunnerTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
776E5AB1CE0A62CBDBAB3996 /* Pods */ = {
isa = PBXGroup;
children = (
8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */,
77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */,
0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */,
930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */,
02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */,
9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -113,8 +144,8 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
9DDB11208866A014130CF5A9 /* Pods */, 776E5AB1CE0A62CBDBAB3996 /* Pods */,
D1DDBFE3838710CC012E9C02 /* Frameworks */, A5A9FDE94E78257E9A48E2E8 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -130,6 +161,15 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */,
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */,
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */,
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */,
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */,
0ADE29672D8C25660032BFCB /* sample.mp3 */,
0ADE29682D8C25660032BFCB /* silence.mp3 */,
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */,
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@ -142,25 +182,11 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9DDB11208866A014130CF5A9 /* Pods */ = { A5A9FDE94E78257E9A48E2E8 /* Frameworks */ = {
isa = PBXGroup;
children = (
F83C5E570162235C4FE20CCB /* Pods-Runner.debug.xcconfig */,
C0DB3B999FA4C3E6CFA90079 /* Pods-Runner.release.xcconfig */,
478E46ED4F3AE11E50872120 /* Pods-Runner.profile.xcconfig */,
5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */,
BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */,
671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D1DDBFE3838710CC012E9C02 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
735C476F36391FA72F7CA27A /* Pods_Runner.framework */, 4A9190D0838960F143BDC158 /* Pods_Runner.framework */,
D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */, 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -172,10 +198,10 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
4CED93D53BF7D79C894B39B7 /* [CP] Check Pods Manifest.lock */, 1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
6AE85031FDE82288FAE3C673 /* Frameworks */, B4459155A2535A0ED1D6C7D8 /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@ -191,15 +217,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
CB1E1186EB3C828E55784697 /* [CP] Check Pods Manifest.lock */, 8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */, 4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */,
E839EB3BEF50366A68B2A46D /* [CP] Copy Pods Resources */, D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -264,6 +290,8 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */,
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -271,23 +299,7 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4CED93D53BF7D79C894B39B7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -309,22 +321,40 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
9740EEB61CF901F6004384FC /* Run Script */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Run Script"; name = "Thin Binary";
outputPaths = ( outputPaths = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
}; };
CB1E1186EB3C828E55784697 /* [CP] Check Pods Manifest.lock */ = { 8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -346,24 +376,22 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; name = "Run Script";
outputFileListPaths = ( outputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
showEnvVarsInLog = 0;
}; };
E839EB3BEF50366A68B2A46D /* [CP] Copy Pods Resources */ = { D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -395,7 +423,14 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */,
0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */,
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */,
0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */,
0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */,
0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -473,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;
@ -489,13 +524,15 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -505,50 +542,64 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
}; };
name = Debug; name = Debug;
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
}; };
name = Release; name = Release;
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
}; };
name = Profile; name = Profile;
@ -602,7 +653,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;
@ -653,7 +704,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;
@ -670,15 +721,20 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -693,13 +749,15 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

730
ios/Runner/AppDelegate.swift

@ -1,13 +1,743 @@
import Flutter import Flutter
import UIKit import UIKit
import AVFoundation
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
private var classicBluetoothHelper: ClassicBluetoothHelper?
private var mediaButtonHelper: Any?
// 添加Azure语音服务辅助类
private var azureAsrHelper: 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 {
// 获取flutter控制器
guard let controller = window?.rootViewController as? FlutterViewController else {
NSLog("AppDelegate: Flutter控制器不可用")
return false
}
// 初始化音频会话管理器
audioSessionManager = AudioSessionManager.shared
// 统一注册所有方法通道
registerMethodChannel()
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
private func registerMethodChannel() {
let controller = window?.rootViewController as? FlutterViewController
guard let controller = controller else {
NSLog("AppDelegate: FlutterViewController 不可用")
return
}
// 注册经典蓝牙方法通道
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",
binaryMessenger: controller.binaryMessenger
)
classicBluetoothChannel.setMethodCallHandler { [weak self, weak classicBluetoothHelper] (call, result) in
guard let _ = self, let classicBluetoothHelper = classicBluetoothHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "经典蓝牙辅助实例不可用", details: nil))
return
}
switch call.method {
case "isBluetoothEnabled":
result(classicBluetoothHelper.isBluetoothEnabled())
case "getConnectedHeadsetDevices":
// 获取已连接的耳机设备
classicBluetoothHelper.getConnectedHeadsetDevices { devices, error in
if let error = error {
result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil))
} else {
result(devices)
}
}
case "initialize":
classicBluetoothHelper.initialize()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册蓝牙流通道
let classicBluetoothStreamChannel = FlutterEventChannel(
name: "com.deep_voice.classic_bluetooth_events",
binaryMessenger: controller.binaryMessenger
)
// 创建蓝牙流处理器(作为局部变量)
let bluetoothStreamHandler = BluetoothStreamHandler(bluetoothHelper: classicBluetoothHelper)
classicBluetoothStreamChannel.setStreamHandler(bluetoothStreamHandler)
}
// MARK: - 消息通道设置
// MARK: - 蓝牙媒体按键通道
@available(iOS 13.0, *)
private func registerMediaButtonChannels(controller: FlutterViewController) {
// 初始化媒体按钮辅助类
if mediaButtonHelper == nil {
mediaButtonHelper = BluetoothMediaButtonHelper()
}
// guard let mediaButtonHelper = self.mediaButtonHelper as? BluetoothMediaButtonHelper else {
// NSLog("AppDelegate: 媒体按钮辅助实例不可用")
// return
// }
// mediaButtonHelper.startButtonListening()
// 注册媒体按钮方法通道
let mediaButtonChannel = FlutterMethodChannel(
name: "com.deep_voice.bluetooth_media_button",
binaryMessenger: controller.binaryMessenger
)
mediaButtonChannel.setMethodCallHandler { [weak self] (call, result) in
guard let _ = self, let mediaButtonHelper = self?.mediaButtonHelper as? BluetoothMediaButtonHelper 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 mediaButtonStreamChannel = FlutterEventChannel(
name: "com.deep_voice.bluetooth_media_button_events",
binaryMessenger: controller.binaryMessenger
)
// 创建媒体按钮流处理器
let mediaButtonStreamHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper as! BluetoothMediaButtonHelper)
mediaButtonStreamChannel.setStreamHandler(mediaButtonStreamHandler)
}
// 注册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()
// 设置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
}
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 azureAsrStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_asr_events",
binaryMessenger: controller.binaryMessenger
)
// 创建ASR流处理器
let asrStreamHandler = AsrStreamHandler(azureAsrHelper: azureAsrHelper as! AzureAsrHelper)
azureAsrStreamChannel.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()
// 设置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
}
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 azureTtsStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_tts_events",
binaryMessenger: controller.binaryMessenger
)
// 创建TTS流处理器
let ttsStreamHandler = TtsStreamHandler(azureTtsHelper: azureTtsHelper as! AzureTtsHelper)
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["azure_speech_key"] as? String,
let azureRegion = args["azure_speech_region"] as? String,
let volcanoKey = args["volcano_ai_api_key"] 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)
}
}
// 蓝牙事件流处理器
class BluetoothStreamHandler: NSObject, FlutterStreamHandler, BluetoothEventHandler {
private let bluetoothHelper: ClassicBluetoothHelper
private var eventSink: FlutterEventSink?
init(bluetoothHelper: ClassicBluetoothHelper) {
self.bluetoothHelper = bluetoothHelper
super.init()
// 设置事件处理器
bluetoothHelper.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
// NSLog("BluetoothStreamHandler: 开始监听蓝牙事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
// NSLog("BluetoothStreamHandler: 取消监听蓝牙事件")
eventSink = nil
return nil
}
// 实现BluetoothEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("BluetoothStreamHandler: 接收到蓝牙事件: \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// 媒体按钮事件流处理器
@available(iOS 13.0, *)
class MediaButtonStreamHandler: NSObject, FlutterStreamHandler, MediaButtonEventHandler {
private let mediaButtonHelper: BluetoothMediaButtonHelper
private var eventSink: FlutterEventSink?
init(mediaButtonHelper: BluetoothMediaButtonHelper) {
self.mediaButtonHelper = mediaButtonHelper
super.init()
// 设置事件处理器
mediaButtonHelper.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("MediaButtonStreamHandler: 开始监听媒体按钮事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("MediaButtonStreamHandler: 取消监听媒体按钮事件")
eventSink = nil
return nil
}
// 实现MediaButtonEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("MediaButtonStreamHandler: 收到按钮事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// ASR事件处理器协议
@available(iOS 13.0, *)
protocol AsrEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// ASR事件流处理器
@available(iOS 13.0, *)
class AsrStreamHandler: NSObject, FlutterStreamHandler, AsrEventHandler {
private let azureAsrHelper: AzureAsrHelper
private var eventSink: FlutterEventSink?
init(azureAsrHelper: AzureAsrHelper) {
self.azureAsrHelper = azureAsrHelper
super.init()
// 设置事件处理器
azureAsrHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("AsrStreamHandler: 开始监听ASR事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("AsrStreamHandler: 取消监听ASR事件")
eventSink = nil
return nil
}
// 实现AsrEventHandler协议
func sendEvent(_ event: [String: Any]) {
// NSLog("AsrStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// TTS事件处理器协议
@available(iOS 13.0, *)
protocol TtsEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// TTS事件流处理器
@available(iOS 13.0, *)
class TtsStreamHandler: NSObject, FlutterStreamHandler, TtsEventHandler {
private let azureTtsHelper: AzureTtsHelper
private var eventSink: FlutterEventSink?
init(azureTtsHelper: AzureTtsHelper) {
self.azureTtsHelper = azureTtsHelper
super.init()
// 设置事件处理器
azureTtsHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("TtsStreamHandler: 开始监听TTS事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("TtsStreamHandler: 取消监听TTS事件")
eventSink = nil
return nil
}
// 实现TtsEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("TtsStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// 火山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)
}
}
} }

585
ios/Runner/AudioSessionManager.swift

@ -0,0 +1,585 @@
import Foundation
import AVFoundation
import UIKit
/// 统一音频会话管理类
/// 用于统一管理所有组件的音频会话配置
@objc class AudioSessionManager: NSObject {
// MARK: - 单例实现
@objc static let shared = AudioSessionManager()
// 音频会话
private let audioSession = AVAudioSession.sharedInstance()
// 音频会话状态
private var isConfigured = false
private var isActive = false
// 当前配置类型
private var currentCategory: AVAudioSession.Category?
private var currentMode: AVAudioSession.Mode?
private var currentOptions: AVAudioSession.CategoryOptions?
// 中断监听器
private var interruptionObserver: NSObjectProtocol?
// 路由变化监听器
private var routeChangeObserver: NSObjectProtocol?
// 私有初始化方法,确保只能通过shared访问
private override init() {
super.init()
NSLog("[AudioSessionManager] 音频会话管理器已初始化")
setupNotifications()
}
deinit {
removeNotifications()
}
// MARK: - 通知设置
private func setupNotifications() {
// 监听中断事件
interruptionObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.handleInterruption(notification)
}
// 监听路由变化事件
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)
}
if let routeChangeObserver = routeChangeObserver {
NotificationCenter.default.removeObserver(routeChangeObserver)
}
NotificationCenter.default.removeObserver(self)
}
// MARK: - 通知处理
private func handleInterruption(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
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) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
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() {
NSLog("[AudioSessionManager] 应用进入后台")
// 应用进入后台时可能需要调整音频会话
}
@objc private func handleAppWillEnterForeground() {
NSLog("[AudioSessionManager] 应用即将进入前台")
// 应用返回前台,检查并恢复音频会话
if isConfigured && !isActive {
try? activateSession()
}
}
// MARK: - 音频会话配置
/// 统一配置方法 - 所有音频配置都通过此方法进行
@objc func configureSession(category: AVAudioSession.Category,
mode: AVAudioSession.Mode,
options: AVAudioSession.CategoryOptions,
force: Bool = false) throws {
NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode), 选项=\(options), 强制=\(force)")
// 如果已配置且不强制重新配置,则直接返回
if isConfigured && !force &&
currentCategory == category &&
currentMode == mode &&
currentOptions == options {
NSLog("[AudioSessionManager] 音频会话已配置,跳过配置")
return
}
do {
// NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode)")
// 暂时停用当前会话(如果正在活动)
if isActive {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
}
// 设置新的分类、模式和选项
try audioSession.setCategory(category, mode: mode, options: options)
// 设置采样率和缓冲区大小(对于语音识别优化)
// if mode == .spokenAudio {
// try audioSession.setPreferredSampleRate(16000)
// try audioSession.setPreferredIOBufferDuration(0.01) // 10ms
// }
// 重新激活会话
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
isActive = true
// 记录当前配置
currentCategory = category
currentMode = mode
currentOptions = options
isConfigured = true
// 检查并设置首选的输入设备(如果需要)
if category == .playAndRecord || category == .record {
try configurePreferredInput()
}
return
} catch {
NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)")
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
// )
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: false // 是否强制重新配置
)
} 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
// )
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: false // 是否强制重新配置
)
}
// 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)
}
}
/// 获取当前音频会话的配置信息
@objc func getCurrentConfiguration() -> [String: Any] {
return [
"isConfigured": isConfigured,
"isActive": isActive,
"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 hasBluetoothHeadsetConnected() -> Bool {
let outputs = audioSession.currentRoute.outputs
return outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE }
}
/// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
// 在iOS中无法直接检查蓝牙是否启用,我们可以通过检查音频会话配置来推断
// 检查音频会话是否配置了允许蓝牙
if let options = currentOptions {
return options.contains(.allowBluetooth) || options.contains(.allowBluetoothA2DP)
}
// 如果尚未配置音频会话,尝试检查当前路由中是否有蓝牙设备
let outputs = audioSession.currentRoute.outputs
let hasBluetooth = outputs.contains {
$0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE
}
return hasBluetooth
}
/// 获取当前音频路由
@objc func getCurrentAudioRoute() -> String {
let outputs = audioSession.currentRoute.outputs
if let output = outputs.first {
return "\(output.portType.rawValue) (\(output.portName))"
}
return "unknown"
}
/// 获取当前输入设备
@objc func getCurrentInputDevice() -> String {
let inputs = audioSession.currentRoute.inputs
if let input = inputs.first {
return "\(input.portType.rawValue) (\(input.portName))"
}
return "unknown"
}
// MARK: - 路由管理和监听
/// 获取当前路由信息
@objc func getCurrentRoute() -> AVAudioSessionRouteDescription {
return audioSession.currentRoute
}
/// 添加路由变更监听器
/// - Parameters:
/// - observer: 监听对象
/// - selector: 监听方法选择器
@objc func addRouteChangeListener(_ observer: Any, selector: Selector) {
NotificationCenter.default.addObserver(
observer,
selector: selector,
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
/// 移除路由变更监听器
/// - Parameter observer: 要移除的监听对象
@objc func removeRouteChangeListener(_ observer: Any) {
NotificationCenter.default.removeObserver(
observer,
name: AVAudioSession.routeChangeNotification,
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"
}
}
// /// 为Azure语音识别专门配置的音频会话
// /// 此函数优化Azure语音识别的音频处理配置,提供更强的回音消除能力
// /// - Parameters:
// /// - force: 是否强制重新配置
// /// - configureAdditionalSettings: 配置完成后进行额外的音频设置
// /// - Returns: 配置是否成功
// @objc func configureForAzureSpeechRecognition(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool {
// NSLog("[AudioSessionManager] 配置Azure ASR语音识别的音频会话")
// do {
// // 配置音频会话以优化语音识别
// try configureSession(
// category: .playAndRecord, // 允许同时录音和播放
// mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
// options: [
// .allowBluetooth, // 允许蓝牙设备
// .defaultToSpeaker, // 默认使用扬声器
// .mixWithOthers // 允许与其他应用混音
// ],
// force: force // 是否强制重新配置
// )
// // 额外的优化配置
// if configureAdditionalSettings {
// // 获取当前是否使用耳机
// let currentRoute = audioSession.currentRoute
// let hasHeadphones = currentRoute.outputs.contains {
// $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
// }
// // 设置采样率为16kHz(Azure Speech API推荐)
// try audioSession.setPreferredSampleRate(16000.0)
// // 设置较小的缓冲区大小以减少延迟
// try audioSession.setPreferredIOBufferDuration(0.01)
// // 根据是否有耳机连接调整输入增益
// if !hasHeadphones {
// // 无耳机时降低输入增益以减少回音
// try audioSession.setInputGain(0.8)
// NSLog("[AudioSessionManager] 启用扬声器回音消除优化")
// } else {
// // 使用耳机时可以使用较高增益
// try audioSession.setInputGain(1.0)
// NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式")
// }
// }
// NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话")
// return true
// } catch {
// NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)")
// return false
// }
// }
@objc func configureForVoiceInteraction(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool {
NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话")
do {
// 配置音频会话以优化语音识别
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: force // 是否强制重新配置
)
// 额外的优化配置
if configureAdditionalSettings {
// 获取当前是否使用耳机
let currentRoute = audioSession.currentRoute
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 设置采样率为16kHz(Azure Speech API推荐)
try audioSession.setPreferredSampleRate(16000.0)
// 设置较小的缓冲区大小以减少延迟
try audioSession.setPreferredIOBufferDuration(0.01)
// 根据是否有耳机连接调整输入增益
if !hasHeadphones {
// 无耳机时降低输入增益以减少回音
try audioSession.setInputGain(0.8)
NSLog("[AudioSessionManager] 启用扬声器回音消除优化")
} else {
// 使用耳机时可以使用较高增益
try audioSession.setInputGain(1.0)
NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式")
}
}
NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于语音交互(同时支持ASR和TTS)的音频会话
/// 此函数综合优化语音识别和语音合成,适用于需要双向交互的场景
/// - Returns: 配置是否成功
// @objc func configureForVoiceInteraction(force: Bool = false) -> Bool {
// do {
// // 配置音频会话以支持语音交互
// // 使用playAndRecord类别允许同时录音和播放
// // 使用spokenAudio模式优化语音交互
// try configureSession(
// category: .playAndRecord,
// mode: .voiceChat,
// options: [
// // .allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
// .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
// .defaultToSpeaker, // 默认使用扬声器输出
// .mixWithOthers
// ],
// force: force // 强制重新配置,确保设置生效
// )
// // 设置首选输入设备
// try configurePreferredInput()
// NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话")
// return true
// } catch {
// NSLog("[AudioSessionManager] 配置语音交互(ASR+TTS)的音频会话失败: \(error.localizedDescription)")
// return false
// }
// }
}

813
ios/Runner/AzureAsrHelper.swift

@ -0,0 +1,813 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
import AudioToolbox
/// Azure ASR工具类,负责实现语音识别服务接口
@available(iOS 13.0, *)
class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 是否使用自定义音频处理器 - 设置为false时将使用系统默认麦克风输入
private let useCustomAudioProcessor = true
/// 事件处理回调
private var eventHandler: (([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 pushStream: SPXPushAudioInputStream?
private var audioProcessor: CustomAudioProcessor?
private var isProcessingAudio = false
private var audioProcessingTimer: Timer?
/// 状态标志
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
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// MARK: - 初始化
override init() {
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?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 检查语言参数
if let languages = supportedLanguages, languages.isEmpty {
print("[AzureAsrHelper] 警告: 传入的支持语言列表为空,将使用默认语言")
}
// 释放之前的资源
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()
// 根据设置决定是否使用自定义音频处理器
if useCustomAudioProcessor {
// 创建自定义音频流和处理器,替代默认的麦克风输入
pushStream = try SPXPushAudioInputStream()
audioConfig = try SPXAudioConfiguration(streamInput: pushStream!)
// 初始化自定义音频处理器
audioProcessor = CustomAudioProcessor()
} else {
// 使用默认麦克风输入
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?(["type": "error", "message": "创建识别器失败: \(error.localizedDescription)"])
return false
}
}
/// 设置音频会话
private func setupAudioSession() throws {
print("[AzureAsrHelper] 开始配置音频会话...")
// 使用AudioSessionManager配置音频会话,使用专门为Azure ASR优化的配置
let success = audioSessionManager.configureForVoiceInteraction()
if !success {
print("[AzureAsrHelper] 警告: 通过AudioSessionManager配置音频会话失败")
}
}
/// 设置所有回调
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?(["type": "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?(["type": "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?(["type": "sessionStarted"])
}
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已结束")
self._isContinuousRecognitionActive = false
self.eventHandler?(["type": "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?(["type": "canceled", "reason": reason, "errorDetails": errorDetails])
self._isContinuousRecognitionActive = false
}
}
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 如果正在连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
do {
// 启动音频处理
startAudioProcessing()
// 通知会话开始
eventHandler?(["type": "sessionStarted"])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
guard let self = self else { return }
// 停止音频处理
self.stopAudioProcessing()
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
self.eventHandler?(["type": "result",
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.noMatch {
print("[AzureAsrHelper] 无匹配结果")
self.eventHandler?(["type": "noMatch"])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler?(["type": "error", "message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler?(["type": "error", "message": "识别取消,无法获取详细原因"])
}
}
}
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "识别异常: \(error.localizedDescription)"])
stopAudioProcessing()
return false
}
}
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 检查是否已经在识别
if _isContinuousRecognitionActive {
print("[AzureAsrHelper] 已经在进行连续识别中,忽略请求")
return true
}
// 确保识别器已创建
if recognizer == nil {
print("[AzureAsrHelper] 尝试重新创建识别器...")
if !createRecognizerAndSetupCallbacks() {
eventHandler?(["type": "error", "message": "无法创建语音识别器"])
return false
}
}
// 启动音频处理
startAudioProcessing()
// 尝试启动连续识别
do {
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别已启动")
return true
} catch {
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
// 停止音频处理
stopAudioProcessing()
// 发送错误通知
eventHandler?(["type": "error", "message": "开始连续识别失败: \(error.localizedDescription)"])
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
// 停止音频处理
stopAudioProcessing()
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
if !_isContinuousRecognitionActive {
print("[AzureAsrHelper] 未进行连续识别,忽略停止请求")
return true
}
print("[AzureAsrHelper] 停止连续语音识别")
// 防止空指针异常
guard let recognizer = recognizer else {
print("[AzureAsrHelper] 警告: 识别器为空,但状态显示活跃")
_isContinuousRecognitionActive = false
eventHandler?(["type": "sessionStopped"])
return true
}
// 先标记为非活跃状态,防止重复调用
_isContinuousRecognitionActive = false
// 异步执行停止操作,避免阻塞主线程
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
// 在后台线程执行停止操作
try recognizer.stopContinuousRecognition()
// 主线程回调通知结果
DispatchQueue.main.async {
print("[AzureAsrHelper] 连续识别已停止")
self.eventHandler?(["type": "sessionStopped"])
}
} catch {
// 主线程回调通知错误
DispatchQueue.main.async {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
// 停止失败,恢复状态
self._isContinuousRecognitionActive = true
self.eventHandler?(["type": "error", "message": "停止识别失败: \(error.localizedDescription)"])
}
}
}
// 立即返回,不阻塞调用方
return true
}
/// 检查连续识别是否活跃
/// - Returns: 连续识别是否处于活跃状态
func isContinuousRecognitionActive() -> Bool {
return _isContinuousRecognitionActive
}
/// 释放资源
func dispose() {
// 停止音频处理
stopAudioProcessing()
// 尝试停止所有识别操作
if _isContinuousRecognitionActive {
do {
if let recognizer = recognizer {
try recognizer.stopContinuousRecognition()
print("[AzureAsrHelper] 连续识别已停止")
}
} catch {
print("[AzureAsrHelper] 警告: 停止连续识别失败: \(error.localizedDescription)")
}
_isContinuousRecognitionActive = false
}
// 释放识别器
recognizer = nil
audioConfig = nil
speechConfig = nil
pushStream = nil
audioProcessor = nil
// 重置状态
isInitialized = false
print("[AzureAsrHelper] 资源已释放")
}
/// 从结果中获取检测到的语言
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
}
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
// MARK: - 音频处理
/// 开始音频处理
private func startAudioProcessing() {
// 如果未使用自定义音频处理器,跳过
if !useCustomAudioProcessor {
return
}
guard !isProcessingAudio, let audioProcessor = audioProcessor else { return }
isProcessingAudio = true
// 启动音频处理器
if !audioProcessor.startRecord() {
print("[AzureAsrHelper] 错误: 启动音频处理器失败")
eventHandler?(["type": "error", "message": "启动音频处理器失败"])
return
}
// 启动音频处理定时器
audioProcessingTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in
guard let self = self, self.isProcessingAudio, let processor = self.audioProcessor, let stream = self.pushStream else {
return
}
// 读取处理后的音频数据
var bytes = [UInt8](repeating: 0, count: 2560)
let bytesRead = processor.read(bytes: &bytes)
if bytesRead > 0 {
// 推送数据到Azure语音服务
let data = Data(bytes: bytes, count: bytesRead)
stream.write(data)
// 通知音频数据可用(可选)
// self.eventHandler?(["type": "audioData", "data": bytes])
}
}
print("[AzureAsrHelper] 音频处理已启动")
}
/// 停止音频处理
private func stopAudioProcessing() {
// 如果未使用自定义音频处理器,跳过
if !useCustomAudioProcessor {
return
}
// 停止定时器
audioProcessingTimer?.invalidate()
audioProcessingTimer = nil
// 停止音频处理器
audioProcessor?.stopRecord()
isProcessingAudio = false
print("[AzureAsrHelper] 音频处理已停止")
}
}
// MARK: - 自定义音频处理器
@available(iOS 13.0, *)
class CustomAudioProcessor: NSObject {
// 音频单元
private var ioUnit: AudioUnit?
// 音频格式
private var audioFormat: AudioStreamBasicDescription
// 音频缓冲
private var audioBufferList: AudioBufferList
private var audioList: [Float] = []
private let audioListQueue = DispatchQueue(label: "audioListQueue")
// 回音消除状态
private var isEchoCancellationEnabled = true
override init() {
// 设置音频格式 - 16kHz, 16位, 单声道
audioFormat = AudioStreamBasicDescription(
mSampleRate: 16000.0,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked,
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0
)
// 初始化音频缓冲
audioBufferList = AudioBufferList(
mNumberBuffers: 1,
mBuffers: AudioBuffer(
mNumberChannels: 1,
mDataByteSize: 4096,
mData: malloc(4096)
)
)
super.init()
}
deinit {
stopRecord()
free(audioBufferList.mBuffers.mData)
}
/// 启动音频处理
/// - Returns: 是否成功启动
func startRecord() -> Bool {
print("[CustomAudioProcessor] 配置音频单元")
// 创建音频组件描述 - 使用VoiceProcessingIO类型获取回音消除
var ioUnitDescription = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_VoiceProcessingIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
// 查找音频组件
guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else {
print("[CustomAudioProcessor] 错误: 未找到音频组件")
return false
}
// 创建音频单元实例
if checkError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建音频单元") {
ioUnit = nil
return false
}
// 启用输入端口
var enableInput: UInt32 = 1
let kInputBus: AudioUnitElement = 1
let kOutputBus: AudioUnitElement = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, kInputBus, &enableInput,
UInt32(MemoryLayout<UInt32>.size)), "启用输入端口") {
return false
}
// 禁用输出端口 (我们只需要输入)
var enableOutput: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, kOutputBus,
&enableOutput, UInt32(MemoryLayout<UInt32>.size)), "禁用输出端口") {
return false
}
// 设置缓冲区分配标志
var flag: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout<UInt32>.size)), "设置缓冲区分配标志") {
return false
}
// 设置音频格式
let size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线输出范围的流格式") {
return false
}
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线输入范围的流格式") {
return false
}
// 启用回音消除 - 注意: kAUVoiceIOProperty_BypassVoiceProcessing值为1时表示绕过处理,值为0表示启用处理
if isEchoCancellationEnabled {
var echoCancellation: UInt32 = 0 // 0表示不绕过,即启用回音消除
AudioUnitSetProperty(ioUnit!, kAUVoiceIOProperty_BypassVoiceProcessing,
kAudioUnitScope_Global, 0, &echoCancellation, UInt32(MemoryLayout<UInt32>.size))
}
// 设置输入回调 - 当有新音频数据时调用
var inputCallback = AURenderCallbackStruct(
inputProc: CustomAudioProcessor.onAudioDataAvailable,
inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
if checkError(AudioUnitSetProperty(ioUnit!,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, kInputBus,
&inputCallback, UInt32(MemoryLayout<AURenderCallbackStruct>.size)), "设置输入回调") {
return false
}
// 初始化音频单元
var hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
while hasError {
Thread.sleep(forTimeInterval: 0.1)
hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
}
// 启动音频单元
hasError = checkError(AudioOutputUnitStart(ioUnit!), "启动音频单元")
print("[CustomAudioProcessor] 音频处理器已启动,回音消除\(isEchoCancellationEnabled ? "已启用" : "已禁用")")
return !hasError
}
/// 停止音频处理
func stopRecord() {
print("[CustomAudioProcessor] 停止音频处理器")
if let ioUnit = ioUnit {
// 停止音频单元
_ = checkError(AudioOutputUnitStop(ioUnit), "停止音频单元")
// 关闭音频单元
_ = checkError(AudioUnitUninitialize(ioUnit), "反初始化音频单元")
_ = checkError(AudioComponentInstanceDispose(ioUnit), "释放音频单元")
self.ioUnit = nil
}
// 清空音频数据缓冲
audioListQueue.sync {
audioList.removeAll()
}
}
/// 音频数据回调 - 当有新的音频数据可用时调用
private static let onAudioDataAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in
// 获取实例
let processor = Unmanaged<CustomAudioProcessor>.fromOpaque(inRefCon).takeUnretainedValue()
// 计算预期数据大小
let expectedDataByteSize = inNumberFrames * processor.audioFormat.mBytesPerFrame
// 确保缓冲区足够大
if processor.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize {
processor.audioBufferList.mBuffers.mData = realloc(processor.audioBufferList.mBuffers.mData, Int(expectedDataByteSize))
processor.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize
}
// 渲染音频数据
let status = processor.checkOSStatus(AudioUnitRender(processor.ioUnit!, ioActionFlags, inTimeStamp,
inBusNumber, inNumberFrames, &processor.audioBufferList),
"渲染音频数据")
// 将Int16数据转换为浮点数据进行处理
var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames))
let buffer = processor.audioBufferList.mBuffers
let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self)
for j in 0..<Int(buffer.mDataByteSize / UInt32(MemoryLayout<Int16>.size)) {
// 归一化到[-1.0, 1.0]范围
audioDataFloat[j] = Float(bufferData[j]) / 32768.0
}
// 保存处理后的数据
if status == noErr {
processor.audioListQueue.async {
processor.audioList.append(contentsOf: audioDataFloat)
}
}
return status
}
/// 读取处理后的音频数据
/// - Parameter bytes: 输出字节数组
/// - Returns: 读取的字节数
func read(bytes: inout [UInt8]) -> Int {
return audioListQueue.sync {
// 如果没有数据,返回0
if audioList.isEmpty {
return 0
}
// 确保有足够的数据 (至少1280个样本)
if audioList.count < 1280 {
return 0
}
// 读取一帧数据 (1280个样本)
let frameLength = 1280
let buffer = Array(audioList.prefix(frameLength))
audioList.removeFirst(frameLength)
// 将浮点数据转回Int16格式
var int16Data = buffer.map { Int16($0 * 32767) }
// 转换为字节数组
let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count))
bytes = [UInt8](data)
// 每个样本2字节 (16位PCM)
return frameLength * 2
}
}
/// 检查错误并打印日志
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 是否发生错误
private func checkError(_ status: OSStatus, _ operation: String) -> Bool {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
return true
}
return false
}
/// 检查OSStatus并返回状态
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 原始状态
private func checkOSStatus(_ status: OSStatus, _ operation: String) -> OSStatus {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
}
return status
}
}

454
ios/Runner/AzureTtsHelper.swift

@ -0,0 +1,454 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure TTS工具类,负责实现TTS服务接口
@available(iOS 13.0, *)
class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (([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
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// 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: - 初始化
override init() {
super.init()
}
deinit {
dispose()
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
// 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?(["type": "error", "message": "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?(["type": "error", "message": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
/// 配置音频会话
private func configureAudioSession() -> Bool {
do {
// 使用音频会话管理器配置媒体播放模式
try audioSessionManager.configureForVoiceInteraction()
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?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler?(["type": "error", "message": "声音名称不能为空"])
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?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
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?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
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?(["type": "error", "message": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler?(["type": "started"])
Task {
do {
// 使用异步方法进行合成并直接播放
_ = try synthesizer.startSpeakingSsml(text)
} catch {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "error", "message": "语音合成失败: \(error.localizedDescription)"])
}
}
}
return true
}
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
if !_isSpeaking {
print("[AzureTtsHelper] 未进行语音播放,忽略停止请求")
return true
}
// 停止合成
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler?(["type": "canceled"])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
/// 检查是否正在播放
/// - Returns: 当前是否正在播放语音
func isSpeaking() -> Bool {
return _isSpeaking
}
/// 释放资源
func dispose() {
// 尝试停止所有语音合成
if _isSpeaking {
do {
if let synthesizer = synthesizer {
try synthesizer.stopSpeaking()
print("[AzureTtsHelper] 语音合成已停止")
}
} catch {
print("[AzureTtsHelper] 警告: 停止语音合成失败: \(error.localizedDescription)")
}
_isSpeaking = false
}
// 释放合成器
synthesizer = nil
speechConfig = nil
// 重置状态
isInitialized = false
isAudioSessionConfigured = false
print("[AzureTtsHelper] 资源已释放")
}
// 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?(["type": "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?(["type": "error", "message": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "error", "message": "语音合成被取消"])
}
}
}
// 合成开始事件
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
}
}

13
ios/Runner/Base.lproj/Main.storyboard

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Flutter View Controller--> <!--Flutter View Controller-->
@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-26" y="-77"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

464
ios/Runner/BluetoothMediaButtonHelper.swift

@ -0,0 +1,464 @@
import Foundation
import MediaPlayer
import AVFoundation
import UIKit
/// 媒体按钮事件处理协议
@available(iOS 13.0, *)
@objc protocol MediaButtonEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
/// 蓝牙媒体按键事件处理类
/// 该类仅在iOS 13.0及以上版本可用
@available(iOS 13.0, *)
class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
// MARK: - 属性
@objc static let shared = BluetoothMediaButtonHelper()
/// 日志标签
private let TAG = "BluetoothMediaButtonHelper"
/// 远程控制中心
private var remoteCommandCenter: MPRemoteCommandCenter
/// 事件处理器 - 用于发送事件到Flutter
private weak var eventHandler: MediaButtonEventHandler?
/// 是否已启用监听
private var isListening = false
/// 静音音频播放器 - 用于保持音频会话活跃
private var silentAudioPlayer: AVAudioPlayer?
/// 播放计时器 - 用于定期更新播放信息
private var playbackTimer: Timer?
/// 当前播放时间 - 模拟播放进度
private var currentPlaybackTime: TimeInterval = 0
/// 总时长 - 模拟
private let totalDuration: TimeInterval = 300.0
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// MARK: - 初始化
override init() {
// 获取远程控制中心单例
self.remoteCommandCenter = MPRemoteCommandCenter.shared()
super.init()
NSLog("\(TAG): 初始化媒体按钮处理器")
// 初始化静音音频播放器
initSilentAudioPlayer()
}
deinit {
stopButtonListening()
NSLog("\(TAG): 媒体按钮处理器已释放")
}
// MARK: - 公共方法
/// 设置事件处理器
@objc func setEventHandler(_ handler: MediaButtonEventHandler) {
eventHandler = handler
NSLog("\(TAG): 已设置媒体按钮事件处理器")
}
/// 开始监听蓝牙媒体按钮事件
@objc func startButtonListening() -> Bool {
if isListening {
NSLog("\(TAG): 已经在监听蓝牙媒体按钮事件")
return true
}
NSLog("\(TAG): 开始监听蓝牙媒体按钮事件")
// 立即注册远程命令 - 这不需要音频会话
registerRemoteCommands()
// 标记为已在监听
isListening = true
// 确保NowPlaying信息可用 - 这也不依赖于活跃的音频会话
setupNowPlaying(isPlaying: true)
// 尝试启用静音音频播放(但即使失败,远程命令仍然可用)
do {
// try audioSessionManager.configureForBluetoothMediaButtons()
startSilentAudio()
// 延时1秒
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.setupNowPlaying(isPlaying: false)
self.stopSilentAudio()
}
// startUpdatingPlaybackInfo()
} catch {
NSLog("\(TAG): 音频设置或播放失败: \(error.localizedDescription)")
// 继续执行,因为远程命令在某些设备上不需要播放音频也能工作
}
NSLog("\(TAG): 蓝牙媒体按钮监听已启动")
return true
}
/// 停止监听蓝牙媒体按钮事件
@objc func stopButtonListening() -> Bool {
if !isListening {
return true
}
NSLog("\(TAG): 停止监听蓝牙媒体按钮事件")
// 停止播放静音音频
stopSilentAudio()
// 停止更新播放信息
stopUpdatingPlaybackInfo()
// 取消注册远程命令
unregisterRemoteCommands()
// 清除NowPlaying信息
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
}
isListening = false
NSLog("\(TAG): 蓝牙媒体按钮监听已停止")
return true
}
// MARK: - 私有方法
/// 初始化静音音频播放器
private func initSilentAudioPlayer() {
do {
// 尝试加载sample.mp3文件
if let audioPath = Bundle.main.path(forResource: "sample", ofType: "mp3") {
let audioUrl = URL(fileURLWithPath: audioPath)
silentAudioPlayer = try AVAudioPlayer(contentsOf: audioUrl)
silentAudioPlayer?.numberOfLoops = -1 // 无限循环播放
silentAudioPlayer?.volume = 0.0 // 设置为全音量
silentAudioPlayer?.prepareToPlay() // 预加载
silentAudioPlayer?.delegate = self
NSLog("\(TAG): 音频播放器使用sample.mp3初始化成功")
return
}
NSLog("\(TAG): 未找到sample.mp3文件,使用生成的静音数据")
} catch {
NSLog("\(TAG): 初始化静音音频播放器失败: \(error.localizedDescription)")
}
}
/// 开始播放静音音频
private func startSilentAudio() {
guard let player = silentAudioPlayer else {
NSLog("\(TAG): 无法开始播放,播放器未初始化")
return
}
if !player.isPlaying {
let success = player.play()
if success {
NSLog("\(TAG): 开始播放静音音频")
} else {
NSLog("\(TAG): 播放启动失败")
}
} else {
NSLog("\(TAG): 静音音频已在播放中")
}
}
/// 停止播放静音音频
private func stopSilentAudio() {
silentAudioPlayer?.stop()
NSLog("\(TAG): 停止播放静音音频")
}
/// 开始更新播放信息
private func startUpdatingPlaybackInfo() {
// 初始设置
setupNowPlaying(isPlaying: true)
// 定期更新播放信息 - 恢复定时器,确保持续更新播放状态
playbackTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
// 更新播放时间(模拟)
self.currentPlaybackTime += 1.0
if self.currentPlaybackTime >= self.totalDuration {
self.currentPlaybackTime = 0.0
}
// 更新NowPlaying信息
self.setupNowPlaying(isPlaying: true)
}
NSLog("\(TAG): 开始更新播放信息")
}
/// 停止更新播放信息
private func stopUpdatingPlaybackInfo() {
playbackTimer?.invalidate()
playbackTimer = nil
// 清空NowPlaying信息
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
}
NSLog("\(TAG): 停止更新播放信息")
}
/// 设置NowPlaying信息
private func setupNowPlaying(isPlaying: Bool) {
NSLog("\(TAG): 设置NowPlaying信息, isPlaying=\(isPlaying)")
// 创建音频专辑图片
let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片
var nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyTitle: "DeepVoice 语音助手",
MPMediaItemPropertyArtist: "DeepVoice",
MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime,
MPMediaItemPropertyPlaybackDuration: totalDuration
]
// 如果有专辑图片,添加到NowPlaying信息中
if let albumArt = albumArt {
let artwork = MPMediaItemArtwork(boundsSize: albumArt.size) { size in
return albumArt
}
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
}
// 设置NowPlaying信息 (在主线程执行)
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
NSLog("\(self.TAG): NowPlaying信息设置完成")
}
}
/// 注册远程命令
private func registerRemoteCommands() {
let commandCenter = MPRemoteCommandCenter.shared()
// 播放/暂停命令
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
// 下一曲/上一曲命令
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action: #selector(handleNextTrackCommand(_:)))
commandCenter.previousTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.addTarget(self, action: #selector(handlePreviousTrackCommand(_:)))
// 快进/快退命令
commandCenter.seekForwardCommand.isEnabled = true
commandCenter.seekForwardCommand.addTarget(self, action: #selector(handleSeekForwardCommand(_:)))
commandCenter.seekBackwardCommand.isEnabled = true
commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:)))
// 停止命令
commandCenter.stopCommand.isEnabled = true
commandCenter.stopCommand.addTarget(self, action: #selector(handleStopCommand(_:)))
NSLog("\(TAG): 远程命令已注册")
}
/// 取消注册远程命令
private func unregisterRemoteCommands() {
NSLog("\(TAG): 取消注册远程命令")
// 基本控制命令
remoteCommandCenter.playCommand.isEnabled = false
remoteCommandCenter.playCommand.removeTarget(nil)
remoteCommandCenter.pauseCommand.isEnabled = false
remoteCommandCenter.pauseCommand.removeTarget(nil)
remoteCommandCenter.togglePlayPauseCommand.isEnabled = false
remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil)
remoteCommandCenter.stopCommand.isEnabled = false
remoteCommandCenter.stopCommand.removeTarget(nil)
}
/// 发送按钮事件到Flutter
private func sendButtonEvent(type: String) {
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": type,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
}
// MARK: - 命令处理方法
/// 处理播放/暂停命令
@objc private func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到播放/暂停命令")
// 发送事件到Flutter
sendButtonEvent(type: "play_pause")
// 调用VoiceInteractionService处理媒体按钮事件
if VoiceInteractionService.shared.isServiceRunning() {
NSLog("\(TAG): 转发到VoiceInteractionService处理")
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理下一曲命令
@objc private func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到下一曲命令")
// 发送事件到Flutter
sendButtonEvent(type: "next_track")
// 也可以转发到VoiceInteractionService
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理上一曲命令
@objc private func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到上一曲命令")
// 发送事件到Flutter
sendButtonEvent(type: "previous_track")
// 也可以转发到VoiceInteractionService
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理快进命令
@objc private func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快进命令")
// 发送事件到Flutter
sendButtonEvent(type: "seek_forward")
return .success
}
/// 处理快退命令
@objc private func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快退命令")
// 发送事件到Flutter
sendButtonEvent(type: "seek_backward")
return .success
}
/// 处理停止命令
@objc private func handleStopCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到停止命令")
// 发送事件到Flutter
sendButtonEvent(type: "stop")
// 调用VoiceInteractionService处理媒体按钮事件
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
// MARK: - AVAudioPlayerDelegate方法
/// 音频播放结束回调
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
NSLog("\(TAG): 音频播放结束, 成功: \(flag)")
// 如果仍在监听状态,重新开始播放
if isListening && player == silentAudioPlayer {
player.play()
}
}
/// 音频播放错误回调
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
if let error = error {
NSLog("\(TAG): 音频解码错误: \(error.localizedDescription)")
} else {
NSLog("\(TAG): 音频解码发生未知错误")
}
// 重新初始化播放器
if player == silentAudioPlayer && isListening {
initSilentAudioPlayer()
startSilentAudio()
}
}
/// 清理和释放资源
func dispose() {
NSLog("\(TAG): 释放BluetoothMediaButtonHelper资源")
// 停止监听
stopButtonListening()
// 释放播放器和计时器资源
silentAudioPlayer = nil
if let timer = playbackTimer {
timer.invalidate()
playbackTimer = nil
}
// 清除引用
eventHandler = nil
NSLog("\(TAG): 资源已释放")
}
}
extension UInt32 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt32>.size)
}
}
extension UInt16 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
}

243
ios/Runner/ClassicBluetoothHelper.swift

@ -0,0 +1,243 @@
import Foundation
import CoreBluetooth
import AVFoundation
import UIKit
/// 蓝牙事件处理协议
@objc protocol BluetoothEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
// MARK: - 属性
// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// 蓝牙管理器
private var centralManager: CBCentralManager?
// 事件处理器
private weak var eventHandler: BluetoothEventHandler?
// MARK: - 初始化与设置
override init() {
super.init()
}
// 设置事件处理器
@objc func setEventHandler(_ handler: BluetoothEventHandler) {
eventHandler = handler
}
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
let stateString = getBluetoothStateString(state: central.state)
sendBluetoothStateEvent(state: stateString)
}
// MARK: - 设备监控
// 获取蓝牙状态字符串
private func getBluetoothStateString(state: CBManagerState) -> String {
switch state {
case .poweredOn:
return "on"
case .poweredOff:
return "off"
default:
return "unknown"
}
}
// 发送蓝牙状态事件
private func sendBluetoothStateEvent(state: String) {
let event: [String: Any] = [
"type": "bluetoothStateChanged",
"state": state,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// 启动蓝牙设备监控
@objc func initialize() {
if centralManager == nil {
centralManager = CBCentralManager(delegate: self, queue: nil)
}
// 发送当前蓝牙状态
if let manager = centralManager {
let stateString = getBluetoothStateString(state: manager.state)
sendBluetoothStateEvent(state: stateString)
}
// 移除之前的监听器
NotificationCenter.default.removeObserver(self)
// 添加新的监听器
audioSessionManager.addRouteChangeListener(self, selector: #selector(handleRouteChange(_:)))
// BluetoothMediaButtonHelper.shared.startButtonListening()
}
// 处理音频路由变化
@objc private func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
// 获取当前设备
let currentDevices = getConnectedAudioDevices()
// 检查原因
switch reason {
case .newDeviceAvailable:
// 新设备已连接
for device in currentDevices {
// 发送设备连接事件
NSLog("发送设备连接事件 - 设备: \(device["name"] ?? "未知设备") [\(device["address"] ?? "未知地址")]")
sendDeviceConnectedEvent(device: device)
}
case .oldDeviceUnavailable:
// 获取断开之前的路由
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
// 过滤蓝牙相关设备
for output in previousRoute.outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid)
]
// 发送设备断开事件
sendDeviceDisconnectedEvent(device: device)
}
}
default:
break
}
}
// 检查是否为蓝牙输出
private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE
}
// MARK: - 设备事件
// 发送设备连接事件
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] = [
"type": "deviceConnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// 发送设备断开事件
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] = [
"type": "deviceDisconnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// MARK: - 公共API
// 获取当前连接的耳机设备
@objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) {
// 检查蓝牙状态
if centralManager?.state != .poweredOn {
completion(nil, "蓝牙未启用")
return
}
do {
// 获取设备
let devices = getConnectedAudioDevices()
completion(devices, nil)
} catch {
completion(nil, "获取设备失败: \(error.localizedDescription)")
}
}
// 获取当前连接的音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// 获取当前音频路由
let outputs = audioSessionManager.getCurrentRoute().outputs
// 过滤蓝牙相关设备
for output in outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid) // 处理设备地址,移除后缀
]
result.append(device)
}
return result
}
// 释放资源
@objc func dispose() {
audioSessionManager.removeRouteChangeListener(self)
eventHandler = nil
}
// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
return centralManager?.state == .poweredOn
}
// 清理设备地址,移除后缀
private func cleanDeviceAddress(_ address: String) -> String {
// 检查是否包含常见分隔符
if let range = address.range(of: "-") ?? address.range(of: ":") ?? address.range(of: "_") {
// 只保留分隔符之前的部分
return String(address[..<range.lowerBound])
}
// iOS设备UID有时会包含额外信息,尝试只保留标准MAC地址格式部分
// MAC地址通常是12个十六进制字符,可能带有分隔符
let hexPattern = "[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}|[0-9A-Fa-f]{12}"
if let regex = try? NSRegularExpression(pattern: hexPattern, options: []),
let match = regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) {
let matchRange = match.range
if let range = Range(matchRange, in: address) {
return String(address[range])
}
}
return address
}
}

32
ios/Runner/Info.plist

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -24,13 +26,20 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>需要使用麦克风来进行语音交互</string> <string>需要麦克风权限来进行语音识别</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>需要语音识别权限来提供语音交互服务</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string> <string>audio</string>
<string>fetch</string> <string>voip</string>
<string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
@ -39,14 +48,15 @@
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>NSUserNotificationsUsageDescription</key>
<string>需要通知权限来保持与AI助手的实时对话</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

0
ios/Runner/Resources/.gitkeep

BIN
ios/Runner/Resources/sample.mp3

Binary file not shown.

BIN
ios/Runner/Resources/silence.mp3

Binary file not shown.

6
ios/Runner/Runner-Bridging-Header.h

@ -1 +1,7 @@
#ifndef Runner_Bridging_Header_h
#define Runner_Bridging_Header_h
#import "GeneratedPluginRegistrant.h" #import "GeneratedPluginRegistrant.h"
#import <MicrosoftCognitiveServicesSpeech/SPXSpeechApi.h>
#endif /* Runner_Bridging_Header_h */

565
ios/Runner/VoiceInteractionService.swift

@ -0,0 +1,565 @@
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 lastUserMessage = ""
private var lastAssistantMessage = ""
// 服务组件
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)
// 启动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
lastUserMessage = ""
lastAssistantMessage = ""
}
/**
* 初始化服务
*/
private func initServices(azureKey: String, azureRegion: String, volcanoKey: String) {
NSLog("%@: 初始化服务组件", TAG)
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper()
if let helper = azureAsrHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理ASR事件
self?.handleAsrEvent(eventData: eventData)
})
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper()
if let helper = azureTtsHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理TTS事件
self?.handleTtsEvent(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(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理ASR事件: %@", TAG, type)
switch type {
case "recognizing":
if let text = eventData["text"] as? String, !text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
} else if let data = eventData["data"] as? [String: Any],
let text = data["text"] as? String,
!text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
}
case "result":
var text: String?
// 直接读取text或从data中读取
if let directText = eventData["text"] as? String, !directText.isEmpty {
text = directText
} else if let data = eventData["data"] as? [String: Any],
let dataText = data["text"] as? String,
!dataText.isEmpty {
text = dataText
}
if let finalText = text {
updateLastActivityTime()
// 保存用户消息
lastUserMessage = finalText
processWithVolcanoAI(finalText)
}
// 重置状态,继续识别
hasSpeechDetected = false
case "sessionStarted":
updateLastActivityTime()
case "sessionStopped":
isRecognitionActive = false
case "canceled":
isRecognitionActive = false
case "error":
isRecognitionActive = false
if let message = eventData["message"] as? String {
NSLog("%@: ASR错误: %@", TAG, message)
}
playNotification("语音识别出错")
default:
break
}
}
}
/**
* 处理TTS事件
*/
private func handleTtsEvent(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理TTS事件: %@", TAG, type)
switch type {
case "started":
isTtsSpeaking = true
case "completed":
isTtsSpeaking = false
updateLastActivityTime()
case "canceled":
isTtsSpeaking = false
case "error":
isTtsSpeaking = false
if let message = eventData["message"] as? String {
NSLog("%@: TTS错误: %@", TAG, message)
}
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()
// 检查应用是否在后台
if UIApplication.shared.applicationState == .background {
NSLog("%@: 应用在后台,无法开始语音识别", TAG)
playNotification("请打开应用后重试!")
return
}
// 播放提示音
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)
// 保存助手回复
self.lastAssistantMessage = response
// 播放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)"
}
}
}

BIN
ios/Runner/sample.mp3

Binary file not shown.

BIN
ios/Runner/silence.mp3

Binary file not shown.

15
lib/core/bindings/initial_binding.dart

@ -2,11 +2,12 @@ import 'package:get/get.dart';
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 '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import '../../data/services/voice_interaction_service.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings { class InitialBinding extends Bindings {
@ -31,14 +32,18 @@ class InitialBinding extends Bindings {
// 火山AI服务 // 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true); Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 注意:VoiceInteractionService 在 HomeController 中初始化
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
fenix: true);
// 经典蓝牙服务 // 经典蓝牙服务
Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(), Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(),
fenix: true); fenix: true);
// 蓝牙媒体按钮服务
Get.lazyPut<BluetoothMediaButtonService>(
() => BluetoothMediaButtonService(),
fenix: true);
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
fenix: true);
// // 语言管理器(需要最先初始化) // // 语言管理器(需要最先初始化)
// Get.put<LoginService>(LoginService(), permanent: true); // Get.put<LoginService>(LoginService(), permanent: true);
// await LoginService().FirebaseInit(); // await LoginService().FirebaseInit();

4
lib/data/models/agent.dart

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

29
lib/data/models/events/voice_interaction_event.dart

@ -0,0 +1,29 @@
import 'package:meta/meta.dart';
/// 语音交互事件基类
abstract class VoiceInteractionEvent {
final int timestamp;
VoiceInteractionEvent({required this.timestamp});
}
/// 聊天历史事件
class ChatHistoryEvent extends VoiceInteractionEvent {
final String agentId;
final String userMessage;
final String assistantMessage;
ChatHistoryEvent({
required this.agentId,
required this.userMessage,
required this.assistantMessage,
required int timestamp,
}) : super(timestamp: timestamp);
}
/// 语音识别开始事件
class RecognitionStartedEvent extends VoiceInteractionEvent {
RecognitionStartedEvent({
required int timestamp,
}) : super(timestamp: timestamp);
}

58
lib/data/models/message_model.dart

@ -0,0 +1,58 @@
class Message {
final String role; // 'user' or 'assistant'
final String content;
final DateTime timestamp;
final bool isLoading;
Message({
required this.role,
required this.content,
required this.timestamp,
this.isLoading = false,
});
// 从JSON构造函数
factory Message.fromJson(Map<String, dynamic> json) {
return Message(
role: json['role'] as String,
content: json['content'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
isLoading: json['isLoading'] as bool? ?? false,
);
}
// 转换为JSON
Map<String, dynamic> toJson() {
return {
'role': role,
'content': content,
'timestamp': timestamp.toIso8601String(),
'isLoading': isLoading,
};
}
// 创建一个加载中的消息
factory Message.loading() {
return Message(
role: 'assistant',
content: '',
timestamp: DateTime.now(),
isLoading: true,
);
}
// 复制并修改
Message copyWith({
String? role,
String? content,
DateTime? timestamp,
bool? isLoading,
}) {
return Message(
role: role ?? this.role,
content: content ?? this.content,
timestamp: timestamp ?? this.timestamp,
isLoading: isLoading ?? this.isLoading,
);
}
}

3
lib/data/providers/agent_provider.dart

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

310
lib/data/services/bluetooth_media_button_service.dart

@ -0,0 +1,310 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import 'voice_interaction_service.dart';
/// 蓝牙媒体按钮事件类型
enum MediaButtonType {
/// 播放/暂停按钮
playPause,
/// 未知按钮
unknown
}
/// 蓝牙媒体按钮事件
class MediaButtonEvent {
/// 按钮类型
final MediaButtonType buttonType;
/// 事件发生时间戳
final int timestamp;
MediaButtonEvent({
required this.buttonType,
required this.timestamp,
});
factory MediaButtonEvent.fromMap(Map<dynamic, dynamic> map) {
MediaButtonType buttonType;
switch (map['buttonType']) {
case 'playPause':
buttonType = MediaButtonType.playPause;
break;
default:
// 将所有其他事件类型映射为 playPause
if (map['buttonType'] == 'play' ||
map['buttonType'] == 'pause' ||
map['buttonType'] == 'togglePlayPause') {
buttonType = MediaButtonType.playPause;
} else {
// 忽略其他类型(next, previous, seekForward, seekBackward)
buttonType = MediaButtonType.unknown;
}
}
return MediaButtonEvent(
buttonType: buttonType,
timestamp: map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch,
);
}
@override
String toString() {
switch (buttonType) {
case MediaButtonType.playPause:
return '蓝牙媒体按钮: 播放/暂停';
case MediaButtonType.unknown:
return '蓝牙媒体按钮: 未知';
}
}
}
/// 蓝牙媒体按钮服务
class BluetoothMediaButtonService extends GetxService {
static BluetoothMediaButtonService get to => Get.find<BluetoothMediaButtonService>();
// 方法通道名称
final MethodChannel _channel = const MethodChannel('com.deep_voice.bluetooth_media_button');
// 事件通道名称
final EventChannel _eventChannel = const EventChannel('com.deep_voice.bluetooth_media_button_events');
/// 按钮事件流控制器
final _buttonEventController = StreamController<MediaButtonEvent>.broadcast();
/// 按钮事件流
Stream<MediaButtonEvent> get buttonEvents => _buttonEventController.stream;
/// 是否已启用监听
final _isListening = false.obs;
bool get isListening => _isListening.value;
/// 是否正在加载
final isLoading = false.obs;
/// 错误信息
final errorMessage = ''.obs;
/// 按钮事件流订阅
StreamSubscription? _buttonEventSubscription;
/// 服务是否已初始化
bool _isInitialized = false;
/// 是否支持媒体按钮服务(iOS 13.0及以上)
bool get isSupported {
if (!Platform.isIOS) return true; // 非iOS平台默认支持
// 获取平台版本信息
final String version = Platform.operatingSystemVersion;
// 尝试提取iOS版本号 (格式如: "Version 14.5 (Build 18E182)")
try {
// 提取版本号
final RegExp versionRegex = RegExp(r'Version\s+(\d+)\.(\d+)');
final match = versionRegex.firstMatch(version);
if (match != null) {
final int majorVersion = int.parse(match.group(1) ?? '0');
return majorVersion >= 13; // iOS 13及以上支持
}
} catch (e) {
Logger.error('解析iOS版本失败: $e');
}
// 无法确定版本时,假设不支持
return false;
}
/// 初始化服务
BluetoothMediaButtonService() {
Logger.info('创建 BluetoothMediaButtonService 实例');
}
/// 初始化媒体按钮服务
Future<bool> initialize() async {
if (_isInitialized) {
Logger.info('BluetoothMediaButtonService 已经初始化');
return true;
}
// 检查系统版本是否支持
if (!isSupported) {
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)');
return false;
}
try {
Logger.info('正在初始化 BluetoothMediaButtonService...');
// 设置事件监听器
_setupButtonEventListener();
_isInitialized = true;
Logger.info('BluetoothMediaButtonService 初始化成功');
return true;
} catch (e) {
Logger.error('初始化 BluetoothMediaButtonService 失败: $e');
errorMessage.value = '初始化蓝牙媒体按钮服务失败';
return false;
}
}
/// 设置按钮事件监听器
void _setupButtonEventListener() {
_buttonEventSubscription?.cancel();
// 设置事件通道监听器
_buttonEventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleButtonEvent, onError: _handleEventStreamError);
Logger.info('已设置蓝牙媒体按钮事件监听器');
}
/// 处理按钮事件
void _handleButtonEvent(dynamic event) {
Logger.info('BluetoothMediaButtonService 收到原始事件: $event');
if (event is! Map) {
Logger.error('收到无效的蓝牙媒体按钮事件 (非Map类型): $event');
return;
}
final Map<dynamic, dynamic> eventMap = event;
try {
if (eventMap['type'] == 'mediaButtonEvent') {
// 获取按钮类型
final String buttonType = eventMap['buttonType'] as String? ?? 'unknown';
// 判断是否为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);
// 广播事件
_buttonEventController.add(buttonEvent);
Logger.info('发送playPause按钮事件: $buttonEvent');
} else {
// 忽略其他类型的按钮事件
Logger.info('忽略非playPause按钮事件: $buttonType');
}
} else {
Logger.warning('收到未知类型的事件: ${eventMap['type']}');
}
} catch (e) {
Logger.error('处理蓝牙媒体按钮事件时出错: $e');
}
}
/// 处理事件流错误
void _handleEventStreamError(dynamic error) {
Logger.error('蓝牙媒体按钮事件流错误: $error');
errorMessage.value = '蓝牙媒体按钮监听出错';
}
/// 开始监听蓝牙媒体按钮事件
Future<bool> startButtonListening() async {
// 检查系统版本是否支持
if (!isSupported) {
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)');
return false;
}
if (!_isInitialized && !(await initialize())) {
return false;
}
if (_isListening.value) {
Logger.info('已经在监听蓝牙媒体按钮事件');
return true;
}
isLoading.value = true;
try {
final result = await _channel.invokeMethod<bool>('startButtonListening') ?? false;
if (result) {
_isListening.value = true;
Logger.info('开始监听蓝牙媒体按钮事件成功');
} else {
errorMessage.value = '开始监听蓝牙媒体按钮事件失败';
Logger.error('开始监听蓝牙媒体按钮事件失败');
}
isLoading.value = false;
return result;
} catch (e) {
isLoading.value = false;
errorMessage.value = '开始监听蓝牙媒体按钮事件出错: $e';
Logger.error('开始监听蓝牙媒体按钮事件出错: $e');
return false;
}
}
/// 停止监听蓝牙媒体按钮事件
Future<bool> stopButtonListening() async {
if (!_isInitialized) {
return false;
}
if (!_isListening.value) {
return true;
}
isLoading.value = true;
try {
final result = await _channel.invokeMethod<bool>('stopButtonListening') ?? false;
if (result) {
_isListening.value = false;
Logger.info('停止监听蓝牙媒体按钮事件成功');
} else {
errorMessage.value = '停止监听蓝牙媒体按钮事件失败';
Logger.error('停止监听蓝牙媒体按钮事件失败');
}
isLoading.value = false;
return result;
} catch (e) {
isLoading.value = false;
errorMessage.value = '停止监听蓝牙媒体按钮事件出错: $e';
Logger.error('停止监听蓝牙媒体按钮事件出错: $e');
return false;
}
}
/// 释放资源
void dispose() {
_buttonEventSubscription?.cancel();
_buttonEventSubscription = null;
_buttonEventController.close();
_isInitialized = false;
_isListening.value = false;
Logger.info('BluetoothMediaButtonService 已释放');
}
@override
void onClose() {
dispose();
super.onClose();
}
}

4
lib/data/services/chat_history_service.dart

@ -10,7 +10,7 @@ class ChatHistoryService extends GetxService {
// 存储键名 // 存储键名
static const String _historyKeyPrefix = 'chat_history_'; static const String _historyKeyPrefix = 'chat_history_';
static const int _maxHistoryCount = 20; // 每个对话最多保存的消息数 static const int _maxHistoryCount = 10; // 每个对话最多保存的消息数
@override @override
void onInit() { void onInit() {
@ -41,7 +41,7 @@ class ChatHistoryService extends GetxService {
// 保存到存储 // 保存到存储
await _storage.write(_getHistoryKey(agentId), jsonString); await _storage.write(_getHistoryKey(agentId), jsonString);
Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}'); // Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}');
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败', e); Logger.error('保存聊天记录失败', e);

445
lib/data/services/classic_bluetooth_service.dart

@ -2,8 +2,8 @@ import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import 'dart:io';
/// 蓝牙设备信息 /// 蓝牙设备信息
class BluetoothDevice { class BluetoothDevice {
@ -11,228 +11,104 @@ class BluetoothDevice {
final String address; final String address;
final String type; final String type;
BluetoothDevice({ BluetoothDevice(
required this.name, {required this.name, required this.address, required this.type});
required this.address,
required this.type
});
@override @override
String toString() => 'BluetoothDevice(name: $name, address: $address, type: $type)'; String toString() =>
} 'BluetoothDevice(name: $name, address: $address, type: $type)';
/// 蓝牙事件类型
enum BluetoothEventType {
/// 设备已连接
deviceConnected,
/// 设备已断开
deviceDisconnected,
/// 蓝牙状态改变(开启/关闭)
bluetoothStateChanged
}
/// 蓝牙连接状态事件
class BluetoothConnectionEvent {
/// 事件类型
final BluetoothEventType type;
/// 相关设备(可能为空,比如蓝牙状态改变事件)
final BluetoothDevice? device;
/// 蓝牙状态(仅适用于bluetoothStateChanged事件)
final String? state;
/// 事件发生时间戳
final int timestamp;
BluetoothConnectionEvent({
required this.type,
this.device,
this.state,
required this.timestamp,
});
factory BluetoothConnectionEvent.fromMap(Map<dynamic, dynamic> map) {
BluetoothEventType eventType;
switch (map['type']) {
case 'deviceConnected':
eventType = BluetoothEventType.deviceConnected;
break;
case 'deviceDisconnected':
eventType = BluetoothEventType.deviceDisconnected;
break;
case 'bluetoothStateChanged':
eventType = BluetoothEventType.bluetoothStateChanged;
break;
default:
throw ArgumentError('未知事件类型: ${map['type']}');
}
final deviceMap = map['device'] as Map<dynamic, dynamic>?;
final BluetoothDevice? device = deviceMap != null
? BluetoothDevice(
name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '',
type: deviceMap['type'] ?? 'unknown'
)
: null;
return BluetoothConnectionEvent(
type: eventType,
device: device,
state: map['state'] as String?,
timestamp: map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch,
);
}
@override
String toString() {
switch (type) {
case BluetoothEventType.deviceConnected:
return '蓝牙设备已连接: ${device?.name}';
case BluetoothEventType.deviceDisconnected:
return '蓝牙设备已断开: ${device?.name}';
case BluetoothEventType.bluetoothStateChanged:
return '蓝牙状态改变: $state';
}
}
} }
/// 经典蓝牙服务 /// 经典蓝牙服务
class ClassicBluetoothService extends GetxService { class ClassicBluetoothService extends GetxService {
static ClassicBluetoothService get to => Get.find<ClassicBluetoothService>(); static ClassicBluetoothService get to => Get.find<ClassicBluetoothService>();
// 注意: MainActivity.kt中定义的通道名称为 com.deep_voice.classic_bluetooth // 通道名称
final MethodChannel _channel = const MethodChannel('com.deep_voice.classic_bluetooth'); final MethodChannel _channel =
final EventChannel _eventChannel = const EventChannel('com.deep_voice.classic_bluetooth_events'); const MethodChannel('com.deep_voice.classic_bluetooth');
final EventChannel _eventChannel =
/// 蓝牙事件流控制器 const EventChannel('com.deep_voice.classic_bluetooth_events');
final _connectionEventController = StreamController<BluetoothConnectionEvent>.broadcast();
/// 蓝牙事件流
Stream<BluetoothConnectionEvent> get connectionEvents => _connectionEventController.stream;
/// 当前连接的设备 /// 当前连接的设备
final currentDevice = Rx<BluetoothDevice?>(null); final currentDevice = Rx<BluetoothDevice?>(null);
/// 已连接设备列表 /// 已连接耳机设备列表
final connectedDevices = <BluetoothDevice>[].obs; final connectedDevices = <BluetoothDevice>[].obs;
/// 蓝牙是否可用 /// 蓝牙设备状态监听
final _isBluetoothEnabled = true.obs;
bool get isBluetoothEnabled => _isBluetoothEnabled.value;
/// 是否正在加载
final isLoading = false.obs;
/// 错误信息
final errorMessage = ''.obs;
/// 蓝牙状态变化事件流
StreamSubscription? _bluetoothStateListener; StreamSubscription? _bluetoothStateListener;
/// 设备连接回调 /// 蓝牙开关状态
final List<Function(BluetoothDevice)> _deviceConnectedCallbacks = []; final bluetoothState = 'unknown'.obs;
/// 设备断开回调
final List<Function(BluetoothDevice)> _deviceDisconnectedCallbacks = [];
/// 蓝牙状态变化回调 /// 蓝牙是否可用
final List<Function(String)> _bluetoothStateChangedCallbacks = []; bool get isBluetoothOn => bluetoothState.value == 'on';
/// 服务是否已初始化 /// 服务是否已初始化
bool _isInitialized = false; bool _isInitialized = false;
ClassicBluetoothService() { /// 初始化蓝牙服务 - 单一入口点
// 创建服务时不立即初始化,避免还没有原生支持时就调用方法
Logger.info('创建 ClassicBluetoothService 实例');
}
/// 初始化蓝牙服务
/// 必须在使用任何方法前调用此方法确保通道已设置
Future<bool> initialize() async { Future<bool> initialize() async {
if (_isInitialized) { if (_isInitialized) {
Logger.info('ClassicBluetoothService 已经初始化'); Logger.info('蓝牙服务已经初始化,跳过');
return true; return true;
} }
try { try {
Logger.info('正在初始化 ClassicBluetoothService...'); Logger.info('正在初始化蓝牙服务...');
// 获取当前蓝牙状态
final bool isEnabled =
await _channel.invokeMethod<bool>('isBluetoothEnabled') ?? false;
bluetoothState.value = isEnabled ? 'on' : 'off';
Logger.info('初始蓝牙状态: ${bluetoothState.value}');
await _channel.invokeMethod('initialize');
// 设置事件监听器 // 设置事件监听
_setupBluetoothStateListener(); _setupBluetoothStateListener();
_isInitialized = true; _isInitialized = true;
Logger.info('ClassicBluetoothService 初始化成功'); Logger.info('蓝牙服务初始化成功');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('初始化 ClassicBluetoothService 失败: $e'); Logger.error('初始化蓝牙服务失败: $e');
errorMessage.value = '初始化蓝牙服务失败';
return false; return false;
} }
} }
/// 检查并请求蓝牙权限 /// 检查蓝牙权限
Future<bool> checkAndRequestPermissions() async { Future<bool> checkPermissions() async {
try { // 检查蓝牙相关权限
// 首先确保服务已初始化 Map<Permission, PermissionStatus> statuses = await [
if (!_isInitialized && !(await initialize())) { Permission.bluetooth,
Logger.error('无法初始化蓝牙服务'); Permission.bluetoothConnect,
errorMessage.value = '无法初始化蓝牙服务'; Permission.bluetoothScan,
return false; Permission.microphone,
} ].request();
// 检查蓝牙相关权限
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetooth,
Permission.bluetoothConnect,
Permission.bluetoothScan,
Permission.location,
Permission.microphone,
].request();
// 检查蓝牙连接权限
if (statuses[Permission.bluetoothConnect] != PermissionStatus.granted) {
Logger.error('蓝牙连接权限被拒绝');
errorMessage.value = '蓝牙连接权限被拒绝';
return false;
}
return true; return statuses[Permission.bluetoothConnect] == PermissionStatus.granted;
} catch (e) {
Logger.error('检查蓝牙权限失败: $e');
errorMessage.value = '检查蓝牙权限失败';
return false;
}
} }
/// 刷新已连接的蓝牙设备 /// 刷新已连接的蓝牙耳机设备
Future<bool> refreshConnectedDevices() async { Future<bool> refreshConnectedDevices() async {
try { // 确保服务已初始化
// 首先确保服务已初始化 if (!_isInitialized && !(await initialize())) {
if (!_isInitialized && !(await initialize())) { return false;
Logger.error('无法初始化蓝牙服务'); }
errorMessage.value = '无法初始化蓝牙服务';
return false;
}
errorMessage.value = '';
// 获取A2DP设备 // 如果蓝牙未开启,无法获取设备
final a2dpDevices = await getConnectedA2dpDevices(); if (!isBluetoothOn) {
Logger.info('已连接的A2DP设备: ${a2dpDevices.length}'); Logger.info('蓝牙未开启,无法获取设备');
return false;
}
// 获取耳机设备 try {
final headsetDevices = await getConnectedHeadsetDevices(); // 只获取耳机设备
Logger.info('已连接的耳机设备: ${headsetDevices.length}'); final devices = await getConnectedHeadsetDevices();
// 更新设备列表 // 更新设备列表
connectedDevices.value = [...a2dpDevices, ...headsetDevices]; connectedDevices.value = devices;
// 更新当前设备 // 更新当前设备
if (connectedDevices.isNotEmpty && currentDevice.value == null) { if (connectedDevices.isNotEmpty && currentDevice.value == null) {
@ -242,60 +118,27 @@ class ClassicBluetoothService extends GetxService {
return connectedDevices.isNotEmpty; return connectedDevices.isNotEmpty;
} catch (e) { } catch (e) {
Logger.error('刷新已连接设备失败: $e'); Logger.error('刷新已连接设备失败: $e');
errorMessage.value = '获取设备信息失败';
return false; return false;
} }
} }
/// 获取已连接的A2DP设备(音频设备)
Future<List<BluetoothDevice>> getConnectedA2dpDevices() async {
try {
// 确保服务已初始化
if (!_isInitialized && !(await initialize())) {
Logger.error('无法初始化蓝牙服务');
return [];
}
final result = await _channel.invokeMethod('getConnectedA2dpDevices');
Logger.info('获取已连接的A2DP设备结果: $result');
if (result is List) {
final devices = result.map((device) => BluetoothDevice(
name: device['name'] ?? '未知设备',
address: device['address'] ?? '',
type: 'a2dp'
)).toList();
return devices;
}
return [];
} catch (e) {
Logger.error('获取已连接的A2DP设备失败: $e');
return [];
}
}
/// 获取已连接的耳机设备 /// 获取已连接的耳机设备
Future<List<BluetoothDevice>> getConnectedHeadsetDevices() async { Future<List<BluetoothDevice>> getConnectedHeadsetDevices() async {
try { try {
// 确保服务已初始化 // 确保服务已初始化
if (!_isInitialized && !(await initialize())) { if (!_isInitialized && !(await initialize())) {
Logger.error('无法初始化蓝牙服务');
return []; return [];
} }
final result = await _channel.invokeMethod('getConnectedHeadsetDevices'); final result = await _channel.invokeMethod('getConnectedHeadsetDevices');
Logger.info('获取已连接的耳机设备结果: $result');
if (result is List) { if (result is List) {
final devices = result.map((device) => BluetoothDevice( return result
name: device['name'] ?? '未知设备', .map((device) => BluetoothDevice(
address: device['address'] ?? '', name: device['name'] ?? '未知设备',
type: 'headset' address: device['address'] ?? '',
)).toList(); type: 'headset'))
.toList();
return devices;
} }
return []; return [];
@ -307,125 +150,87 @@ class ClassicBluetoothService extends GetxService {
/// 设置蓝牙状态监听 /// 设置蓝牙状态监听
void _setupBluetoothStateListener() { void _setupBluetoothStateListener() {
try { _bluetoothStateListener?.cancel();
_bluetoothStateListener = _eventChannel
.receiveBroadcastStream()
.listen((event) {
Logger.info('收到蓝牙事件: $event');
if (event is Map) {
final type = event['type'] as String?;
if (type == 'bluetoothStateChanged') {
final state = event['state'] as String?;
if (state != null) {
_handleBluetoothStateChange(state);
}
} else if (type == 'deviceConnected') {
final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final device = BluetoothDevice(
name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '',
type: deviceMap['type'] ?? 'unknown'
);
_handleDeviceConnected(device);
}
} else if (type == 'deviceDisconnected') {
final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final device = BluetoothDevice(
name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '',
type: deviceMap['type'] ?? 'unknown'
);
_handleDeviceDisconnected(device);
}
}
}
}, onError: (error) {
Logger.error('蓝牙状态监听错误: $error');
});
} catch (e) {
Logger.error('设置蓝牙状态监听失败: $e');
}
}
/// 处理蓝牙状态变化 _bluetoothStateListener =
void _handleBluetoothStateChange(String state) { _eventChannel.receiveBroadcastStream().listen((event) {
Logger.info('----蓝牙状态变化: $state'); if (event is Map) {
Logger.info('蓝牙状态监听: $event');
final type = event['type'] as String?;
// 如果蓝牙关闭,清除设备信息 if (type == 'bluetoothStateChanged') {
if (state == 'off') { final state = event['state'] as String?;
connectedDevices.clear(); if (state != null) {
currentDevice.value = null; _handleBluetoothStateChange(state);
} }
} else if (type == 'deviceConnected') {
// 通知所有回调 _handleDeviceConnected(event);
for (var callback in _bluetoothStateChangedCallbacks) { } else if (type == 'deviceDisconnected') {
callback(state); _handleDeviceDisconnected(event);
} }
}
}, onError: (error) {
Logger.error('蓝牙状态监听错误: $error');
});
} }
/// 处理设备连接 /// 处理设备连接事件
void _handleDeviceConnected(BluetoothDevice device) { void _handleDeviceConnected(Map event) {
Logger.info('设备已连接: ${device.name}'); final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final device = BluetoothDevice(
name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '',
type: deviceMap['type'] ?? 'unknown');
// 更新设备列表 // 更新设备列表
if (!connectedDevices.any((d) => d.address == device.address)) { if (!connectedDevices.any((d) => d.address == device.address)) {
connectedDevices.add(device); connectedDevices.add(device);
} }
// 更新当前设备 // 更新当前设备
currentDevice.value = device; currentDevice.value = device;
// 通知所有回调 Logger.info('设备已连接: ${device.name}');
for (var callback in _deviceConnectedCallbacks) {
callback(device);
} }
} }
/// 处理设备断开 /// 处理设备断开事件
void _handleDeviceDisconnected(BluetoothDevice device) { void _handleDeviceDisconnected(Map event) {
Logger.info('设备已断开: ${device.name}'); final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final address = deviceMap['address'] as String?;
// 更新设备列表 if (address != null) {
connectedDevices.removeWhere((d) => d.address == device.address); // 更新设备列表
connectedDevices.removeWhere((d) => d.address == address);
// 如果是当前设备,清除当前设备 // 如果是当前设备,清除当前设备
if (currentDevice.value?.address == device.address) { if (currentDevice.value?.address == address) {
currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first; currentDevice.value =
} connectedDevices.isEmpty ? null : connectedDevices.first;
Logger.info('当前设备已断开连接');
// 通知所有回调 }
for (var callback in _deviceDisconnectedCallbacks) { }
callback(device);
} }
} }
/// 注册设备连接回调 /// 处理蓝牙状态变化
void onDeviceConnected(Function(BluetoothDevice) callback) { void _handleBluetoothStateChange(String state) {
_deviceConnectedCallbacks.add(callback); Logger.info('----蓝牙状态变化: $state');
}
/// 注册设备断开回调
void onDeviceDisconnected(Function(BluetoothDevice) callback) {
_deviceDisconnectedCallbacks.add(callback);
}
/// 注册蓝牙状态变化回调 // 更新蓝牙状态
void onBluetoothStateChanged(Function(String) callback) { bluetoothState.value = state;
_bluetoothStateChangedCallbacks.add(callback);
}
/// 打开蓝牙设置 if (state == 'off') {
Future<bool> openBluetoothSettings() async { // 蓝牙关闭,清空设备信息
try { connectedDevices.clear();
await AppSettings.openAppSettings(type: AppSettingsType.bluetooth); currentDevice.value = null;
return true; } else if (state == 'on') {
} catch (e) { // 蓝牙重新打开,延迟一小段时间后刷新设备
Logger.error('打开蓝牙设置失败: $e'); Future.delayed(const Duration(milliseconds: 1000), () {
return false; refreshConnectedDevices();
});
} }
} }
@ -433,12 +238,6 @@ class ClassicBluetoothService extends GetxService {
void onClose() { void onClose() {
// 取消蓝牙状态监听 // 取消蓝牙状态监听
_bluetoothStateListener?.cancel(); _bluetoothStateListener?.cancel();
// 清除回调
_deviceConnectedCallbacks.clear();
_deviceDisconnectedCallbacks.clear();
_bluetoothStateChangedCallbacks.clear();
super.onClose(); super.onClose();
} }
} }

3
lib/data/services/flutter_tts_service.dart

@ -1,3 +0,0 @@
// 重新导出FlutterTtsService类
// 这个文件作为兼容层,将speech_impl/flutter_tts_service.dart中的服务导出
export 'speech_impl/flutter_tts_service.dart';

29
lib/data/services/speech_factory.dart

@ -1,10 +1,13 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'asr_service.dart'; import 'asr_service.dart';
import 'speech_impl/voice_clone_tts_service.dart';
import 'tts_service.dart'; import 'tts_service.dart';
import 'speech_impl/azure_asr_service.dart'; import 'speech_impl/azure_asr_service.dart';
import 'speech_impl/azure_tts_service.dart'; import 'speech_impl/azure_tts_service.dart';
import 'speech_impl/flutter_asr_service.dart'; import 'speech_impl/flutter_asr_service.dart';
import 'speech_impl/flutter_tts_service.dart'; import 'speech_impl/flutter_tts_service.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
/// 语音服务类型 /// 语音服务类型
@ -22,7 +25,6 @@ enum SpeechServiceType {
class SpeechFactory extends GetxService { class SpeechFactory extends GetxService {
static SpeechFactory get to => Get.find<SpeechFactory>(); static SpeechFactory get to => Get.find<SpeechFactory>();
/// 当前使用的语音服务类型 /// 当前使用的语音服务类型
SpeechServiceType _currentServiceType = SpeechServiceType.azure; SpeechServiceType _currentServiceType = SpeechServiceType.azure;
@ -40,6 +42,12 @@ class SpeechFactory extends GetxService {
} }
} }
/// 设置语音音色服务类型
void setTimbreServiceType() {
// 重新注册服务
_registerServices();
}
/// 注册语音服务 /// 注册语音服务
void _registerServices() { void _registerServices() {
// 注销现有服务 // 注销现有服务
@ -51,16 +59,26 @@ class SpeechFactory extends GetxService {
Get.delete<TtsService>(force: true); Get.delete<TtsService>(force: true);
} }
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1';
// 根据类型注册新服务 // 根据类型注册新服务
switch (_currentServiceType) { switch (_currentServiceType) {
case SpeechServiceType.azure: case SpeechServiceType.azure:
Get.lazyPut<AsrService>(() => AzureAsrService()); Get.lazyPut<AsrService>(() => AzureAsrService());
Get.lazyPut<TtsService>(() => AzureTtsService()); if (timbre == '1') {
Get.lazyPut<TtsService>(() => AzureTtsService());
} else {
Get.lazyPut<TtsService>(() => VoiceCloneTtsService());
}
break; break;
case SpeechServiceType.flutter: case SpeechServiceType.flutter:
Get.lazyPut<AsrService>(() => FlutterAsrService()); Get.lazyPut<AsrService>(() => FlutterAsrService());
Get.lazyPut<TtsService>(() => FlutterTtsService()); if (timbre == '1') {
Get.lazyPut<TtsService>(() => FlutterTtsService());
} else {
Get.lazyPut<TtsService>(() => VoiceCloneTtsService());
}
break; break;
} }
} }
@ -83,7 +101,8 @@ class SpeechFactory extends GetxService {
// await asrService.initialize(); // await asrService.initialize();
// await ttsService.initialize(); // await ttsService.initialize();
Logger.info('语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); Logger.info(
'语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) { } catch (e) {
Logger.error('语音服务初始化失败: $e'); Logger.error('语音服务初始化失败: $e');
rethrow; rethrow;

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

@ -17,19 +17,15 @@ class AzureAsrService extends GetxService implements AsrService {
late final String _subscriptionKey; late final String _subscriptionKey;
late final String _serviceRegion; late final String _serviceRegion;
final List<String> _supportedLanguages = ['zh-CN']; final List<String> _defaultSupportedLanguages = ['zh-CN'];
@override @override
List<String> get supportedLanguages => _supportedLanguages; List<String> get supportedLanguages => _defaultSupportedLanguages;
// 连续识别相关 // 连续识别相关
bool _isContinuousRecognitionActive = false; bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController; StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
// 事件通道状态
bool _isEventChannelReady = false;
Completer<void>? _eventChannelReadyCompleter;
// 最新的识别结果 // 最新的识别结果
String _latestRecognizedText = ''; String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText; String get latestRecognizedText => _latestRecognizedText;
@ -40,7 +36,6 @@ class AzureAsrService extends GetxService implements AsrService {
AzureAsrService() { AzureAsrService() {
_loadConfig(); _loadConfig();
_setupEventChannel();
} }
/// 从环境变量加载配置 /// 从环境变量加载配置
@ -55,50 +50,21 @@ class AzureAsrService extends GetxService implements AsrService {
/// 设置事件通道 /// 设置事件通道
void _setupEventChannel() { void _setupEventChannel() {
_eventChannelReadyCompleter = Completer<void>();
_eventSubscription = _eventChannel _eventSubscription = _eventChannel
.receiveBroadcastStream() .receiveBroadcastStream()
.listen((event) { .listen((event) {
if (event is Map) { if (event is Map) {
final String eventType = event['type'] as String? ?? '';
// 处理通道准备好的事件
if (eventType == 'channelReady') {
_isEventChannelReady = true;
if (!_eventChannelReadyCompleter!.isCompleted) {
_eventChannelReadyCompleter!.complete();
}
return;
}
_handleRecognitionEvent(event); _handleRecognitionEvent(event);
} }
}, onError: _handleRecognitionError); }, 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 @override
Future<bool> initialize({ Future<bool> initialize({
List<String>? supportedLanguages, List<String>? supportedLanguages,
}) async { }) async {
try { try {
if (supportedLanguages != null && supportedLanguages.isNotEmpty) { final List<String> languages = supportedLanguages ?? _defaultSupportedLanguages;
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 检查是否需要重新初始化 // 检查是否需要重新初始化
if (_isInitialized) { if (_isInitialized) {
@ -108,8 +74,9 @@ class AzureAsrService extends GetxService implements AsrService {
final bool result = await _channel.invokeMethod('initialize', { final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey, 'subscriptionKey': _subscriptionKey,
'region': _serviceRegion, 'region': _serviceRegion,
'supportedLanguages': _supportedLanguages, 'supportedLanguages': languages,
}); });
_setupEventChannel();
_isInitialized = result; _isInitialized = result;
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
@ -159,11 +126,6 @@ class AzureAsrService extends GetxService implements AsrService {
await initialize(); await initialize();
} }
// 确保事件通道已准备好
if (!await _waitForEventChannel()) {
throw Exception('事件通道未准备好,无法开始连续识别');
}
if (_isContinuousRecognitionActive) { if (_isContinuousRecognitionActive) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
@ -217,6 +179,9 @@ class AzureAsrService extends GetxService implements AsrService {
final Map<dynamic, dynamic> eventMap = event; final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? ''; final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) { switch (eventType) {
case 'result': case 'result':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';

269
lib/data/services/speech_impl/azure_tts_service.dart

@ -1,25 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../tts_service.dart'; import '../tts_service.dart';
/// 音频输出设备类型
enum AudioOutputType {
/// 扬声器
speaker,
/// 听筒
earpiece,
/// 自动选择(有耳机用耳机,没有则用听筒)
auto
}
/// 微软 Text-to-Speech 服务 /// 微软 Text-to-Speech 服务
/// ///
/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, /// 该服务通过平台通道与原生 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。 /// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService { class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService()); static final AzureTtsService to = Get.put(AzureTtsService());
@ -34,7 +23,6 @@ class AzureTtsService extends GetxService implements TtsService {
int _currentRate = 0; int _currentRate = 0;
int _currentPitch = 0; int _currentPitch = 0;
int _currentVolume = 100; int _currentVolume = 100;
AudioOutputType _currentAudioOutputType = AudioOutputType.auto;
// 支持的语音列表 // 支持的语音列表
final List<String> _supportedVoices = [ final List<String> _supportedVoices = [
@ -49,29 +37,40 @@ class AzureTtsService extends GetxService implements TtsService {
final List<_SpeechItem> _textQueue = []; final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false; bool _isProcessingQueue = false;
// 事件流控制器
final StreamController<TtsEvent> _eventController = StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
// 可观察状态 // 可观察状态
final isEnabled = true.obs; final isEnabled = true.obs;
final _isSpeaking = false.obs;
final audioOutputType = AudioOutputType.auto.obs;
// 流式文本缓冲区 // 流式文本缓冲区
String _streamBuffer = ''; String _streamBuffer = '';
/// 缓存的播放状态
bool _speakingCache = false;
DateTime _lastSpeakingCheck = DateTime.now();
@override @override
String get currentVoice => _currentVoice; String get currentVoice => _currentVoice;
@override @override
bool get isSpeaking => _isSpeaking.value; bool get isSpeaking {
// 如果未初始化,直接返回false
if (!_isInitialized) return false;
// 如果距离上次检查不到50ms,则使用缓存值
if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) {
return _speakingCache;
}
// 否则异步更新缓存(但仍返回当前缓存值)
_checkSpeakingStatus();
return _speakingCache;
}
@override
Stream<TtsEvent> get onEvent => const Stream.empty();
@override @override
List<String> get supportedVoices => _supportedVoices; List<String> get supportedVoices => _supportedVoices;
AudioOutputType get currentAudioOutputType => _currentAudioOutputType;
AzureTtsService() { AzureTtsService() {
_loadConfig(); _loadConfig();
@ -87,7 +86,10 @@ class AzureTtsService extends GetxService implements TtsService {
Future<bool> initialize({ Future<bool> initialize({
List<String>? supportedLanguages, List<String>? supportedLanguages,
}) async { }) async {
if (_isInitialized) return true; // 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
try { try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
@ -95,15 +97,11 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
// 始终优先使用中文作为默认语言
final String defaultLanguage = 'zh-CN';
// 更新支持的语音列表(如果提供) // 更新支持的语音列表(如果提供)
if (supportedLanguages != null && supportedLanguages.isNotEmpty) { final String language = supportedLanguages?.first ?? 'zh-CN';
// 如果支持中文,优先使用中文,否则使用提供的第一种语言
String language = supportedLanguages.contains(defaultLanguage)
? defaultLanguage
: supportedLanguages.first;
final result = await _channel.invokeMethod('initialize', { final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey, 'subscriptionKey': _subscriptionKey,
@ -111,41 +109,12 @@ class AzureTtsService extends GetxService implements TtsService {
'language': language, 'language': language,
}); });
_isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
// 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值
// 只需要确保 Dart 层的状态与原生层一致
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
_isInitialized = true;
return result; return result;
} else {
// 使用默认语言初始化
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': 'zh-CN',
});
_isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
return result;
}
} catch (e) { } catch (e) {
Logger.error('初始化失败: $e'); Logger.error('初始化失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('TTS初始化失败: $e'));
return false; return false;
} }
} }
@ -164,36 +133,6 @@ class AzureTtsService extends GetxService implements TtsService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音失败: $e'); Logger.error('设置语音失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音失败: $e'));
return false;
}
}
/// 设置音频输出设备类型
Future<bool> setAudioOutputType(AudioOutputType outputType) async {
if (!_isInitialized) await initialize();
try {
final String outputTypeStr = outputType.toString().split('.').last;
final result = await _channel.invokeMethod('setAudioOutputType', {
'outputType': outputTypeStr,
});
if (result) {
_currentAudioOutputType = outputType;
audioOutputType.value = outputType;
}
return result;
} catch (e) {
Logger.error('设置音频输出设备失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置音频输出设备失败: $e'));
return false; return false;
} }
} }
@ -218,10 +157,6 @@ class AzureTtsService extends GetxService implements TtsService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音参数失败: $e'); Logger.error('设置语音参数失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e'));
return false; return false;
} }
} }
@ -232,26 +167,19 @@ class AzureTtsService extends GetxService implements TtsService {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
_isSpeaking.value = true; // 主动设置缓存状态为true
_speakingCache = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
final result = await _channel.invokeMethod('speakText', {'text': text}); final result = await _channel.invokeMethod('speakText', {'text': text});
_isSpeaking.value = false; // 调用完成后主动检查状态
_checkSpeakingStatus();
// 发送完成事件
_eventController.add(TtsEvent(type: TtsEventType.completed));
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
_isSpeaking.value = false; // 发生错误时重置状态
Logger.error('语音合成失败: $e'); _speakingCache = false;
Logger.error('1语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
return false; return false;
} }
} }
@ -262,30 +190,44 @@ class AzureTtsService extends GetxService implements TtsService {
if (!isEnabled.value || ssml.isEmpty) return false; if (!isEnabled.value || ssml.isEmpty) return false;
try { try {
_isSpeaking.value = true; // 主动设置缓存状态为true
_speakingCache = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
_isSpeaking.value = false; // 调用完成后主动检查状态
_checkSpeakingStatus();
// 发送完成事件
_eventController.add(TtsEvent(type: TtsEventType.completed));
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
_isSpeaking.value = false; // 发生错误时重置状态
_speakingCache = false;
Logger.error('SSML语音合成失败: $e'); Logger.error('SSML语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('SSML语音合成失败: $e'));
return false; return false;
} }
} }
/// 等待语音播放完成
Future<void> waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async {
// 首先检查当前状态
_checkSpeakingStatus();
if (!_speakingCache) return;
final stopwatch = Stopwatch()..start();
while (_speakingCache && stopwatch.elapsed < timeout) {
// 短暂等待后再次检查
await Future.delayed(const Duration(milliseconds: 100));
// 检查状态
await _channel.invokeMethod<bool>('isSpeaking').then((result) {
if (result != null) {
_speakingCache = result;
_lastSpeakingCheck = DateTime.now();
}
});
}
}
@override @override
Future<bool> speakStream(String text) async { Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
@ -343,10 +285,6 @@ class AzureTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
return false; return false;
} }
} }
@ -380,18 +318,16 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成 // 等待队列处理完成
while (_isProcessingQueue || _isSpeaking.value) { await _checkSpeakingStatus();
while (_isProcessingQueue || _speakingCache) {
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await _checkSpeakingStatus();
} }
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('刷新TTS流失败: $e'); Logger.error('刷新TTS流失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
return false; return false;
} }
} }
@ -421,12 +357,12 @@ class AzureTtsService extends GetxService implements TtsService {
// 使用speakOnce播放当前项 // 使用speakOnce播放当前项
await speakOnce(item.text); await speakOnce(item.text);
// 等待播放完成
await waitForSpeechCompletion();
} }
} catch (e) { } catch (e) {
Logger.error('处理语音队列出错: $e'); Logger.error('处理语音队列出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
} finally { } finally {
_isProcessingQueue = false; _isProcessingQueue = false;
} }
@ -437,23 +373,16 @@ class AzureTtsService extends GetxService implements TtsService {
try { try {
_textQueue.clear(); _textQueue.clear();
if (_isSpeaking.value) { if (_speakingCache) {
final result = await _channel.invokeMethod('stopSpeaking'); final result = await _channel.invokeMethod('stopSpeaking');
_isSpeaking.value = false; // 重置缓存状态
_speakingCache = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
return result; return result;
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false; return false;
} }
} }
@ -492,25 +421,6 @@ class AzureTtsService extends GetxService implements TtsService {
if (!isEnabled.value) stop(); if (!isEnabled.value) stop();
} }
/// 切换音频输出设备类型
Future<bool> toggleAudioOutputType() async {
AudioOutputType newType;
switch (_currentAudioOutputType) {
case AudioOutputType.speaker:
newType = AudioOutputType.earpiece;
break;
case AudioOutputType.earpiece:
newType = AudioOutputType.auto;
break;
case AudioOutputType.auto:
newType = AudioOutputType.speaker;
break;
}
return await setAudioOutputType(newType);
}
@override @override
Future<void> dispose() async { Future<void> dispose() async {
if (!_isInitialized) return; if (!_isInitialized) return;
@ -518,40 +428,27 @@ class AzureTtsService extends GetxService implements TtsService {
try { try {
await stop(); await stop();
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
// 关闭事件流控制器
await _eventController.close();
_isInitialized = false; _isInitialized = false;
} catch (e) { } catch (e) {
Logger.error('释放资源失败: $e'); Logger.error('释放资源失败: $e');
} }
} }
/// 处理AI返回的文本流,将其转换为语音 /// 异步检查并更新播放状态缓存
/// Future<bool> _checkSpeakingStatus() async {
/// [textStream] 文本流 _lastSpeakingCheck = DateTime.now();
/// 返回一个Future,当流处理完成时完成
Future<bool> processAiTextStream(Stream<String> textStream) async {
if (!isEnabled.value) {
Logger.error('TTS服务未启用');
return false;
}
try { try {
await for (final text in textStream) { final result = await _channel.invokeMethod<bool>('isSpeaking');
if (text.isEmpty) continue; if (result != null) {
await speakStream(text); _speakingCache = result;
} }
return _speakingCache;
// 处理完流后,刷新缓冲区中的剩余文本
await flushStream();
return true;
} catch (e) { } catch (e) {
Logger.error('处理文本流时出错: $e'); Logger.error('获取播放状态失败: $e');
return false; return _speakingCache;
} }
} }
} }
/// 语音合成项目 /// 语音合成项目

54
lib/data/services/speech_impl/flutter_asr_service.dart

@ -165,12 +165,22 @@ class FlutterAsrService extends GetxService implements AsrService {
// 如果当前正在监听,先停止 // 如果当前正在监听,先停止
if (_speech.isListening) { if (_speech.isListening) {
await _speech.stop(); await _speech.stop();
// 短暂延迟确保资源释放 // 延长等待时间,确保资源完全释放
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 800));
} }
// 在启动前再次检查状态
if (_speech.isListening) {
Logger.warning('停止识别后,仍处于监听状态,可能有系统冲突');
await _speech.stop();
await Future.delayed(const Duration(milliseconds: 500));
}
// 启动语音识别前确保状态正确
_asrState.value = AsrState.listening;
// 启动语音识别 // 启动语音识别
return await _speech.listen( final result = await _speech.listen(
onResult: onResult, onResult: onResult,
localeId: _currentLocale.value, localeId: _currentLocale.value,
listenFor: listenFor, listenFor: listenFor,
@ -178,10 +188,20 @@ class FlutterAsrService extends GetxService implements AsrService {
listenOptions: stt.SpeechListenOptions( listenOptions: stt.SpeechListenOptions(
partialResults: true, partialResults: true,
autoPunctuation: autoPunctuation, autoPunctuation: autoPunctuation,
// 使用独白模式,减少与系统交互冲突
listenMode: stt.ListenMode.dictation,
), ),
) ?? false; ) ?? false;
if (!result) {
Logger.error('语音识别启动失败');
_asrState.value = AsrState.error;
}
return result;
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: $e');
_asrState.value = AsrState.error;
return false; return false;
} }
} }
@ -195,12 +215,29 @@ class FlutterAsrService extends GetxService implements AsrService {
return; return;
} }
// 短暂延迟后再重新启动 // 检查系统状态,避免频繁重启
await Future.delayed(const Duration(milliseconds: 300)); if (_speech.isListening) {
Logger.info('当前已在监听中,无需重启');
return;
}
// 延长等待时间,避免与系统资源冲突
await Future.delayed(const Duration(milliseconds: 1000));
// 再次检查状态
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) {
Logger.info('延迟期间控制器已关闭,跳过重启');
return;
}
Logger.info('尝试重新启动连续识别');
// 使用统一方法启动识别 // 使用统一方法启动识别
final success = await _startListening( final success = await _startListening(
onResult: _onSpeechResult, onResult: _onSpeechResult,
// 增加超时时间和暂停时间
listenFor: const Duration(seconds: 30),
pauseFor: const Duration(seconds: 4),
); );
if (success) { if (success) {
@ -208,11 +245,16 @@ class FlutterAsrService extends GetxService implements AsrService {
} else { } else {
Logger.error('连续识别重启失败'); Logger.error('连续识别重启失败');
_continuousRecognitionController?.add( _continuousRecognitionController?.add(
RecognitionEvent.error('重启语音识别失败') RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')
); );
} }
} catch (e) { } catch (e) {
Logger.error('重启连续识别失败: $e'); Logger.error('重启连续识别失败: $e');
// 发送错误给控制器
_continuousRecognitionController?.add(
RecognitionEvent.error('重启失败: $e')
);
} }
} }

393
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -0,0 +1,393 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:just_audio/just_audio.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../core/utils/logger.dart';
import '../tts_service.dart';
import 'volcano_tts_api_service.dart';
/// 声音复刻服务,负责基本的文本转语音功能
///
/// 使用讯飞声音复刻实现TtsService接口
class VoiceCloneTtsService extends GetxService implements TtsService {
final String hostUrl = "wss://cn-huabei-1.xf-yun.com/v1/private/voice_clone";
final String appId = '43d87d22';
final String apiKey = '2bd01ce06ac35957fc0c082f9b6284ec';
final String apiSecret = 'ODU3OTUyYjhjMjY5NDhkMmMxYWY2Zjlm';
WebSocketChannel? _channel;
final AudioPlayer _player = AudioPlayer();
late ConcatenatingAudioSource _playlist;
// 当前语音
final RxString _currentVoice = ''.obs;
@override
String get currentVoice => _currentVoice.value;
// 播放状态
final RxBool _isSpeaking = false.obs;
@override
bool get isSpeaking => _isSpeaking.value;
// 事件流控制器
final StreamController<TtsEvent> _eventController =
StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
bool _isInitialized = false;
String _resId = '';
// 语音合成队列
final List _textQueue = [];
bool _isProcessingQueue = false;
BytesBuilder _audioBytes = BytesBuilder();
Completer<void> _completer = Completer();
final isEnabled = true.obs;
// 流式文本缓冲区
String _streamBuffer = '';
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
if (_isInitialized) return true;
try {
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre');
_resId = timbre;
_playlist = ConcatenatingAudioSource(
useLazyPreparation: true,
children: [],
);
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
return false;
}
}
@override
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
try {
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
return false;
}
}
// 语音合成
Future<void> _speechSynthesis(String text) async {
await _tryConnecting();
_channel?.sink.add(
json.encode({
"header": {"app_id": appId, "status": 2, "res_id": _resId},
"parameter": {
"tts": {
"vcn": "x5_clone",
"audio": {"encoding": "lame", "sample_rate": 16000}
}
},
"payload": {
"text": {
"status": 2,
"seq": 0,
"text": base64Encode(utf8.encode(text))
}
},
}),
);
}
Future<void> _tryConnecting() async {
try {
final url = _geturl();
_channel = WebSocketChannel.connect(Uri.parse(url));
_channel?.stream.timeout(const Duration(seconds: 10)); //设置超时时间
await _channel?.ready;
_channel?.stream.listen(
(message) {
_handle(message);
},
onError: (error) {
Logger.error('连接失败: $error');
},
onDone: () {
Logger.error('WebSocket 连接已关闭!');
},
cancelOnError: true,
);
} catch (e) {
Logger.error('初始化异常: $e');
}
}
String _geturl() {
// 解析 URL
final uri = Uri.parse(hostUrl);
// 获取当前时间并格式化为 RFC1123 格式
final date = HttpDate.format(DateTime.now().toUtc());
// 参与签名的字段
final signString = [
"host: ${uri.host}",
"date: $date",
"GET ${uri.path} HTTP/1.1",
];
// 拼接签名字符串
final sgin = signString.join("\n");
// 使用 HMAC-SHA256 签名
final key = utf8.encode(apiSecret);
final bytes = utf8.encode(sgin);
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(bytes);
final sha = base64Encode(digest.bytes);
// 构建请求参数
final authUrl =
'api_key="$apiKey", algorithm="hmac-sha256", headers="host date request-line", signature="$sha"';
// 将请求参数使用 base64 编码
final authorization = base64Encode(utf8.encode(authUrl));
// 构建查询参数
final queryParams = {
"host": uri.host,
"date": date,
"authorization": authorization,
};
// 将查询参数编码并拼接到 URL 后面
final callurl =
Uri.parse(hostUrl).replace(queryParameters: queryParams).toString();
return callurl;
}
void _handle(data) async {
final result = jsonDecode(data);
int code = result['header']['code'];
if (code == 0) {
int status = result['header']['status'];
if (status == 1 || status == 2) {
Uint8List audio = base64Decode(result['payload']['audio']['audio']);
_audioBytes.add(audio);
}
if (status == 2) {
final newSource = BytesAudioSource(_audioBytes.toBytes());
await _playlist.add(newSource);
if (!_isSpeaking.value) {
_audioPlay();
}
_completer.complete();
_completer = Completer();
}
}
}
void _audioPlay() async {
_isSpeaking.value = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
await _player.setAudioSource(_playlist);
await _player.play();
}
@override
Future<bool> speakOnce(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
_audioBytes.clear();
await _speechSynthesis(text);
await _completer.future;
return true;
} catch (e) {
_isSpeaking.value = false;
Logger.error('语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
return false;
}
}
@override
Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])');
bool hasProcessed = false;
// 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = [];
int lastMatchEnd = 0;
for (final match in matches) {
// 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim();
if (sentence != null && sentence.isNotEmpty) {
sentences.add(sentence);
lastMatchEnd = match.end;
}
}
// 处理找到的句子
for (final sentence in sentences) {
_textQueue.add(sentence);
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_streamBuffer);
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
return true;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
return false;
}
}
/// 处理语音合成队列
Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true;
try {
while (_textQueue.isNotEmpty) {
if (!isEnabled.value) {
_textQueue.clear();
break;
}
final item = _textQueue.removeAt(0);
// 使用speakOnce播放当前项
await speakOnce(item);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
} finally {
_isProcessingQueue = false;
}
}
@override
Future<bool> stop() async {
try {
_textQueue.clear();
_player.stop();
_playlist.clear();
_audioBytes = BytesBuilder();
if (_isSpeaking.value) {
_isSpeaking.value = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false;
}
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
// 关闭事件流控制器
await _eventController.close();
_isInitialized = false;
_player.dispose();
} catch (e) {
Logger.error('释放资源失败: $e');
}
}
}

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

@ -0,0 +1,340 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../modules/chat/models/message_model.dart';
import '../../models/events/voice_interaction_event.dart';
import '../chat_history_service.dart';
/// Android语音交互服务
///
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口,
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件
class AndroidVoiceInteractionService extends GetxService {
static AndroidVoiceInteractionService get to => Get.find();
// 方法通道和事件通道
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction');
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events');
// 服务状态
final _isServiceRunning = false.obs;
bool get isServiceRunning => _isServiceRunning.value;
// 事件流控制器
StreamController<VoiceInteractionEvent>? _eventStreamController;
Stream<VoiceInteractionEvent>? _eventStream;
@override
Stream<VoiceInteractionEvent> get eventStream => _eventStream ?? Stream.empty();
// 事件通道状态
StreamSubscription? _eventSubscription;
// 配置信息
late String _azureSpeechKey;
late String _azureSpeechRegion;
late String _volcanoAiApiKey;
// 初始化状态标志
static bool _isInitialized = false;
// 当前会话历史
final List<Map<String, String>> _messageHistory = [];
// 服务ID
static const String _serviceId = 'android_assistant';
/// 构造函数
AndroidVoiceInteractionService() {
}
/// 从环境变量加载配置
void _loadConfig() {
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
_volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
if (_volcanoAiApiKey.isEmpty) {
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY');
}
}
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen((event) {
if (event is Map) {
final String eventType = event['type'] as String? ?? '';
// 直接处理事件,不再等待channelReady
_handleVoiceInteractionEvent(event);
}
}, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
});
}
/// 创建事件流
void _createEventStream() {
_eventStreamController = StreamController<VoiceInteractionEvent>.broadcast();
_eventStream = _eventStreamController?.stream;
}
/// 初始化服务
@override
Future<AndroidVoiceInteractionService> initialize() async {
try {
if (_isInitialized) {
return this;
}
_isInitialized = true;
_loadConfig();
_setupEventChannel();
_createEventStream();
// 检查服务是否正在运行
await _checkServiceStatus();
await _startService();
Logger.info('语音交互服务初始化完成');
return this;
} catch (e) {
Logger.error('语音交互服务初始化失败: $e');
return this;
}
}
@override
void onWakeup() {
Logger.info('语音交互服务唤醒');
}
/// 检查服务状态
Future<void> _checkServiceStatus() async {
try {
final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false;
_isServiceRunning.value = isRunning;
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}');
} catch (e) {
Logger.error('检查语音交互服务状态失败: $e');
_isServiceRunning.value = false;
}
}
/// 启动语音交互服务
Future<bool> _startService() async {
if (_isServiceRunning.value) {
Logger.info('语音交互服务已经在运行');
return true;
}
try {
final bool result = await _channel.invokeMethod('startVoiceInteractionService', {
'azure_speech_key': _azureSpeechKey,
'azure_speech_region': _azureSpeechRegion,
'volcano_ai_api_key': _volcanoAiApiKey,
}) ?? false;
if (result) {
_isServiceRunning.value = true;
Logger.info('语音交互服务启动成功');
} else {
Logger.error('语音交互服务启动失败');
}
return result;
} catch (e) {
Logger.error('启动语音交互服务失败: $e');
return false;
}
}
/// 停止语音交互服务
Future<bool> _stopService() async {
if (!_isServiceRunning.value) {
Logger.info('语音交互服务未运行');
return true;
}
try {
final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false;
if (result) {
_isServiceRunning.value = false;
Logger.info('语音交互服务停止成功');
} else {
Logger.error('语音交互服务停止失败');
}
return result;
} catch (e) {
Logger.error('停止语音交互服务失败: $e');
return false;
}
}
/// 暂停语音交互(停止TTS和语音识别,但保持服务运行)
@override
Future<bool> pauseVoiceInteraction() async {
if (!_isServiceRunning.value) {
Logger.info('语音交互服务未运行,无法暂停');
return false;
}
try {
final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false;
if (result) {
Logger.info('语音交互暂停成功');
} else {
Logger.error('语音交互暂停失败');
}
return result;
} catch (e) {
Logger.error('暂停语音交互失败: $e');
return false;
}
}
/// 处理来自原生端的语音交互事件
void _handleVoiceInteractionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
final int timestamp = eventMap['timestamp'] as int? ?? 0;
// 添加时间戳日志,帮助调试
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
switch (eventType) {
case 'chatHistory':
final String agentId = eventMap['agentId'] as String? ?? '';
final String userMessage = eventMap['userMessage'] as String? ?? '';
final String assistantMessage = eventMap['assistantMessage'] as String? ?? '';
// 添加到消息历史
if (userMessage.isNotEmpty) {
_messageHistory.add({'role': 'user', 'content': userMessage});
_messageHistory.add({'role': 'assistant', 'content': assistantMessage});
// 保持历史记录在一定长度
while (_messageHistory.length > 10) {
_messageHistory.removeAt(0);
}
}
_eventStreamController?.add(ChatHistoryEvent(
agentId: agentId,
userMessage: userMessage,
assistantMessage: assistantMessage,
timestamp: timestamp,
));
// 保存聊天记录
_saveChatHistory(agentId, userMessage, assistantMessage, timestamp);
Logger.info('收到聊天历史事件: agentId=$agentId');
break;
case 'recognitionStarted':
_eventStreamController?.add(RecognitionStartedEvent(
timestamp: timestamp,
));
break;
default:
Logger.warning('收到未知类型的语音交互事件: $eventType');
break;
}
}
@override
void onClose() {
// 清理资源
_stopService();
_eventSubscription?.cancel();
_eventStreamController?.close();
super.onClose();
}
/// 保存聊天记录
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
try {
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
// 创建用户消息和助手消息
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);
} catch (e) {
Logger.error('保存聊天记录失败: $e');
}
}
/// 是否处于活跃状态
@override
bool isActive() {
return _isServiceRunning.value;
}
/// 获取当前对话历史
@override
List<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
/// 获取当前服务ID
@override
String getServiceId() {
return _serviceId;
}
}
/// 聊天历史事件
class ChatHistoryEvent extends VoiceInteractionEvent {
final String agentId;
final String userMessage;
final String assistantMessage;
ChatHistoryEvent({
required this.agentId,
required this.userMessage,
required this.assistantMessage,
required int timestamp,
}) : super(timestamp: timestamp);
}
/// 语音识别开始事件
class RecognitionStartedEvent extends VoiceInteractionEvent {
RecognitionStartedEvent({
required int timestamp,
}) : super(timestamp: timestamp);
}

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

@ -0,0 +1,467 @@
import 'dart:async';
import 'package:get/get.dart';
import '../asr_service.dart';
import '../tts_service.dart';
import '../volcano_ai_service.dart';
import '../chat_history_service.dart';
import '../../../modules/chat/models/message_model.dart';
import '../../models/events/voice_interaction_event.dart';
import '../../../core/utils/logger.dart';
enum VoiceInteractionState {
idle, // 等待唤醒
active, // 活跃状态 - 可以同时识别和播放
}
class IosVoiceInteractionService extends GetxService {
final AsrService _asrService = Get.find<AsrService>();
final TtsService _ttsService = Get.find<TtsService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final ChatHistoryService _chatHistoryService = Get.find<ChatHistoryService>();
// 当前状态
final Rx<VoiceInteractionState> state = VoiceInteractionState.idle.obs;
// 是否识别到用户语音
final RxBool isSpeechDetected = false.obs;
// 是否正在处理AI响应
final RxBool isProcessingAI = false.obs;
// 是否正在播放TTS
final RxBool isSpeaking = false.obs;
// 对话历史记录
final List<Map<String, String>> _messageHistory = [];
// 最大历史记录数
static const int _maxHistorySize = 10;
// 超时计时器
Timer? _inactivityTimer;
// 流订阅
StreamSubscription? _recognitionSubscription;
// 当前识别的文本
String _currentRecognizedText = '';
// 事件流控制器
final StreamController<VoiceInteractionEvent> _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast();
// 获取事件流
@override
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
// 标识符,用于存储聊天历史
final String _agentId = 'ios_assistant';
/// 初始化服务
@override
Future<IosVoiceInteractionService> initialize() async {
try {
// 保持idle状态,不自动启动语音识别
Logger.info('iOS语音交互服务初始化完成,处于idle状态');
} catch (e) {
Logger.error('iOS语音交互服务初始化失败: $e');
}
return this;
}
// 处理唤醒事件
void onWakeup() {
_ttsService.speakOnce('我在呢!');
isSpeaking.value = true;
if (state.value == VoiceInteractionState.idle) {
_activateInteraction();
}
}
// 激活交互
void _activateInteraction() {
// 设置状态为活跃
state.value = VoiceInteractionState.active;
// 开始连续监听
_startContinuousListening();
// 启动不活动计时器
_startInactivityTimer();
// 发送识别开始事件
_sendRecognitionStartedEvent();
}
// 开始连续监听
Future<void> _startContinuousListening() async {
// 停止之前的监听
_recognitionSubscription?.cancel();
try {
// 开始连续识别
final recognitionStream = await _asrService.startContinuousRecognition();
// 监听识别结果
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
onError: (error) {
Logger.error('语音识别错误: $error');
_startContinuousListening(); // 尝试重新启动
}
);
Logger.info('开始连续语音识别');
} catch (e) {
Logger.error('启动语音识别失败: $e');
}
}
// 处理识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
// 更新活动时间
_updateActivityTime();
switch (event.type) {
case RecognitionEventType.intermediateResult:
// 检测用户是否开始说话
if (event.text.isNotEmpty && !isSpeechDetected.value) {
isSpeechDetected.value = true;
// 用户开始讲话时,立即中断当前响应
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
break;
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
// 最终结果,处理用户输入
_processUserInput(event.text);
}
// 重置语音检测状态
isSpeechDetected.value = false;
break;
case RecognitionEventType.sessionStarted:
Logger.info('语音识别会话开始');
break;
case RecognitionEventType.sessionStopped:
Logger.info('语音识别会话结束');
break;
case RecognitionEventType.error:
case RecognitionEventType.canceled:
Logger.error('语音识别错误: ${event.error}');
break;
}
}
// 中断当前响应
void _interruptCurrentResponse(String reason) {
Logger.info(reason);
// 如果正在活跃状态,需要中断当前操作
if (state.value == VoiceInteractionState.active) {
// 停止TTS播放
_ttsService.stop();
isSpeaking.value = false;
isProcessingAI.value = false;
}
}
// 处理用户输入
Future<void> _processUserInput(String text) async {
Logger.info('处理用户输入: $text');
// 设置为处理状态
isProcessingAI.value = true;
// 添加用户消息到历史记录
final userMessage = {'role': 'user', 'content': text};
_addToHistory(userMessage);
// 创建用户消息对象
final userMsg = Message(
role: 'user',
content: text,
timestamp: DateTime.now(),
);
// AI响应处理标志
bool isProcessingCancelled = false;
try {
// 构建消息历史
final messageHistory = _buildMessageHistory();
// 调用AI服务获取响应
final responseStream = _aiService.sendMessageStream(
messages: messageHistory,
systemPrompt: "你是一个智能助手,请简明扼要地回答问题。",
);
String aiResponse = '';
// 等待AI响应
await for (final chunk in responseStream) {
// 检查是否被用户打断
if (isSpeechDetected.value) {
// 用户开始说话,标记处理被取消
isProcessingCancelled = true;
Logger.info('AI响应生成过程中被用户打断');
break;
}
// 如果状态已改变(可能由其他原因导致),停止处理
if (state.value != VoiceInteractionState.active) {
isProcessingCancelled = true;
break;
}
aiResponse += chunk;
// 立即播放当前文本块,实现边生成边播放
_speakStreamResponse(chunk);
}
// 如果处理被取消,不继续后续操作
if (isProcessingCancelled) {
isProcessingAI.value = false;
return;
}
// 确保完整响应被处理
if (aiResponse.isNotEmpty) {
// 将AI响应添加到历史记录
final assistantMessage = {'role': 'assistant', 'content': aiResponse};
_addToHistory(assistantMessage);
// 创建助手消息对象
final assistantMsg = Message(
role: 'assistant',
content: aiResponse,
timestamp: DateTime.now(),
);
// 保存聊天历史
_saveChatHistory(userMsg, assistantMsg);
// 发送聊天历史事件
_sendChatHistoryEvent(userMsg.content, assistantMsg.content);
// 刷新TTS流,确保所有文本都被播放
await _ttsService.flushStream();
isSpeaking.value = false;
}
// 处理完成
isProcessingAI.value = false;
} catch (e) {
Logger.error('AI响应处理失败: $e');
// 错误恢复
isProcessingAI.value = false;
}
}
// 构建用于AI服务的消息历史
List<Map<String, String>> _buildMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 添加消息到历史记录
void _addToHistory(Map<String, String> message) {
_messageHistory.add(message);
// 限制历史记录大小
while (_messageHistory.length > _maxHistorySize) {
_messageHistory.removeAt(0);
}
}
// 播放流式响应
void _speakStreamResponse(String chunk) {
if (chunk.isEmpty) return;
// 更新播放状态
isSpeaking.value = true;
// 使用流式TTS播放
_ttsService.speakStream(chunk);
// 更新活动时间
_updateActivityTime();
}
// 停止TTS
void _stopTts() {
_ttsService.stop();
isSpeaking.value = false;
}
// 更新最后活动时间
DateTime _lastActivityTime = DateTime.now();
void _updateActivityTime() {
_lastActivityTime = DateTime.now();
}
// 启动不活动计时器(长时间无交互会切换到空闲状态)
void _startInactivityTimer() {
_cancelInactivityTimer();
_inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) {
// 计算空闲时间
final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds;
// 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲
if (idleTime > 8 &&
!isSpeaking.value &&
!isProcessingAI.value &&
!isSpeechDetected.value) {
_resetToIdle();
timer.cancel();
}
});
}
// 取消不活动计时器
void _cancelInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = null;
}
// 重置到空闲状态
void _resetToIdle() {
_ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!');
_cancelInactivityTimer();
// 停止TTS
_stopTts();
// 停止语音识别
_recognitionSubscription?.cancel();
_asrService.stopContinuousRecognition();
// 重置状态
state.value = VoiceInteractionState.idle;
isSpeechDetected.value = false;
isProcessingAI.value = false;
isSpeaking.value = false;
// 清空历史记录
_messageHistory.clear();
Logger.info('语音交互已重置为空闲状态');
}
// 暂停语音交互
@override
void pauseVoiceInteraction() {
if (state.value != VoiceInteractionState.idle) {
_stopTts();
_recognitionSubscription?.cancel();
_asrService.stopContinuousRecognition();
// 重置所有状态
state.value = VoiceInteractionState.idle;
isSpeechDetected.value = false;
isProcessingAI.value = false;
isSpeaking.value = false;
// 清空历史记录
_messageHistory.clear();
}
}
// 恢复语音交互
void resumeVoiceInteraction() {
if (state.value == VoiceInteractionState.idle) {
_activateInteraction();
}
}
// 手动停止交互
void stopInteraction() {
_resetToIdle();
}
// 获取当前状态
VoiceInteractionState getCurrentState() {
return state.value;
}
// 是否处于活跃状态
@override
bool isActive() {
return state.value != VoiceInteractionState.idle;
}
// 获取当前对话历史
@override
List<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 获取当前服务ID
@override
String getServiceId() {
return _agentId;
}
// 释放资源
@override
void onClose() {
_cancelInactivityTimer();
_recognitionSubscription?.cancel();
_resetToIdle();
_eventStreamController.close();
super.onClose();
}
// 保存聊天历史
void _saveChatHistory(Message userMsg, Message assistantMsg) {
try {
// 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(_agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(_agentId, existingMessages);
Logger.info('保存聊天历史记录成功');
} catch (e) {
Logger.error('保存聊天历史记录失败: $e');
}
}
// 发送聊天历史事件
void _sendChatHistoryEvent(String userMessage, String assistantMessage) {
final event = ChatHistoryEvent(
agentId: _agentId,
userMessage: userMessage,
assistantMessage: assistantMessage,
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(event);
Logger.info('已发送聊天历史事件');
}
// 发送识别开始事件
void _sendRecognitionStartedEvent() {
final event = RecognitionStartedEvent(
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(event);
Logger.info('已发送识别开始事件');
}
}

376
lib/data/services/voice_interaction_service.dart

@ -1,331 +1,275 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/events/voice_interaction_event.dart';
import '../../core/utils/logger.dart';
import '../../modules/chat/models/message_model.dart'; import '../../modules/chat/models/message_model.dart';
import 'chat_history_service.dart'; import 'chat_history_service.dart';
import 'speech_impl/azure_asr_service.dart';
import 'speech_impl/azure_tts_service.dart';
import 'asr_service.dart';
import 'tts_service.dart';
/// 语音交互服务 /// 语音交互服务接口
/// ///
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, /// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件
class VoiceInteractionService extends GetxService { class VoiceInteractionService extends GetxService {
static VoiceInteractionService get to => Get.find(); static VoiceInteractionService get to => Get.find<VoiceInteractionService>();
// 方法通道和事件通道
// 方法通道
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction');
// 事件通道
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events');
// 服务状态 // 服务状态
final _isServiceRunning = false.obs; final _isServiceRunning = false.obs;
bool get isServiceRunning => _isServiceRunning.value; bool get isServiceRunning => _isServiceRunning.value;
// 事件流控制器 // 流控制器
StreamController<VoiceInteractionEvent>? _eventStreamController; final _eventStreamController = StreamController<VoiceInteractionEvent>.broadcast();
Stream<VoiceInteractionEvent>? _eventStream;
Stream<VoiceInteractionEvent>? get eventStream => _eventStream; // 事件流
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
// 事件通道状态 // 事件通道订阅
bool _isEventChannelReady = false;
Completer<void>? _eventChannelReadyCompleter;
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
// 标记是否初始化
bool _isInitialized = false;
// 配置信息 // 配置信息
late String _azureSpeechKey; late String _azureSpeechKey;
late String _azureSpeechRegion; late String _azureSpeechRegion;
late String _volcanoAiApiKey; late String _volcanoAiKey;
// 初始化状态标志 // 聊天历史服务
static bool _isInitialized = false; late final ChatHistoryService _chatHistoryService;
/// 构造函数
VoiceInteractionService() {
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleVoiceInteractionEvent, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
});
} }
/// 从环境变量加载配置 /// 从环境变量加载配置
void _loadConfig() { void _loadConfig() {
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
_volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? ''; _volcanoAiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
} }
if (_volcanoAiApiKey.isEmpty) { if (_volcanoAiKey.isEmpty) {
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY'); Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY');
} }
} }
/// 设置事件通道 /// 处理来自原生层的事件
void _setupEventChannel() { void _handleVoiceInteractionEvent(dynamic event) {
_eventChannelReadyCompleter = Completer<void>(); if (event is! Map) return;
_eventSubscription = _eventChannel final eventMap = event as Map<dynamic, dynamic>;
.receiveBroadcastStream() final String eventType = eventMap['type'] as String? ?? '';
.listen((event) { // final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch;
if (event is Map) {
final String eventType = event['type'] as String? ?? '';
// 处理通道准备好的事件
if (eventType == 'channelReady') {
_isEventChannelReady = true;
if (!_eventChannelReadyCompleter!.isCompleted) {
_eventChannelReadyCompleter!.complete();
}
return;
}
_handleVoiceInteractionEvent(event);
}
}, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
});
}
/// 创建事件流 Logger.info('收到语音交互事件: $eventType');
void _createEventStream() {
_eventStreamController = StreamController<VoiceInteractionEvent>.broadcast();
_eventStream = _eventStreamController?.stream;
}
/// 等待事件通道准备好 switch (eventType) {
Future<bool> _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async { case 'recognition_started':
if (_isEventChannelReady) return true; // 语音识别开始事件
final recognitionEvent = RecognitionStartedEvent(
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(recognitionEvent);
break;
try { case 'chat_history_updated':
await _eventChannelReadyCompleter!.future.timeout(timeout); // 聊天历史更新事件
return true; final String agentId = eventMap['agentId'] as String? ?? '';
} on TimeoutException { final String userMessage = eventMap['userMessage'] as String? ?? '';
Logger.error('等待语音交互事件通道准备好超时'); final String assistantMessage = eventMap['assistantMessage'] as String? ?? '';
return false;
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;
} }
} }
/// 初始化服务 /// 保存聊天历史
Future<VoiceInteractionService> initialize() async { void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
try { try {
if (_isInitialized) { // 检查参数有效性
return this; if (userMessage.isEmpty) {
return;
} }
_isInitialized = true;
_loadConfig(); // 创建用户消息和助手消息
_setupEventChannel(); final userMsg = Message(
_createEventStream(); role: 'user',
// 检查服务是否正在运行 content: userMessage,
await _checkServiceStatus(); timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp),
);
// 等待事件通道准备好 final assistantMsg = Message(
await _waitForEventChannel(); role: 'assistant',
content: assistantMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息
);
Logger.info('语音交互服务初始化完成'); // 加载现有历史记录
return this; final existingMessages = _chatHistoryService.loadHistory(agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) { } catch (e) {
Logger.error('语音交互服务初始化失败: $e'); Logger.error('保存聊天历史失败: $e');
return this;
} }
} }
/// 检查服务状态 /// 初始化服务
Future<void> _checkServiceStatus() async { Future<bool> initialize() async {
if (_isInitialized) return true;
try { try {
final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; Logger.info('正在初始化语音交互服务...');
_isServiceRunning.value = isRunning;
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); // 获取聊天历史服务
try {
_chatHistoryService = Get.find<ChatHistoryService>();
} catch (e) {
Logger.warning('获取ChatHistoryService失败,将创建新实例');
_chatHistoryService = Get.put(ChatHistoryService());
}
// 设置事件通道
_setupEventChannel();
// 加载配置
_loadConfig();
// 检查服务是否已在运行
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) { } catch (e) {
Logger.error('检查语音交互服务状态失败: $e'); Logger.error('语音交互服务初始化失败: $e');
_isServiceRunning.value = false; return false;
} }
} }
/// 启动语音交互服务 /// 启动语音交互服务
Future<bool> startService() async { Future<bool> startService() async {
if (_isServiceRunning.value) {
Logger.info('语音交互服务已经在运行');
return true;
}
try { try {
final bool result = await _channel.invokeMethod('startVoiceInteractionService', { Logger.info('启动语音交互服务...');
// 使用已加载的配置信息
final result = await _channel.invokeMethod<bool>('startService', {
'azure_speech_key': _azureSpeechKey, 'azure_speech_key': _azureSpeechKey,
'azure_speech_region': _azureSpeechRegion, 'azure_speech_region': _azureSpeechRegion,
'volcano_ai_api_key': _volcanoAiApiKey, 'volcano_ai_api_key': _volcanoAiKey,
}) ?? false; }) ?? false;
if (result) { if (result) {
_isServiceRunning.value = true; _isServiceRunning.value = true;
Logger.info('语音交互服务启动成功'); Logger.info('语音交互服务已启动');
} else { } else {
Logger.error('语音交互服务启动失败'); Logger.error('启动语音交互服务失败');
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动语音交互服务失败: $e'); Logger.error('启动语音交互服务时发生错误: $e');
return false; return false;
} }
} }
/// 停止语音交互服务 /// 停止语音交互服务
Future<bool> stopService() async { Future<bool> stopService() async {
if (!_isServiceRunning.value) {
Logger.info('语音交互服务未运行');
return true;
}
try { try {
final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; Logger.info('停止语音交互服务...');
final result = await _channel.invokeMethod<bool>('stopService') ?? false;
if (result) { if (result) {
_isServiceRunning.value = false; _isServiceRunning.value = false;
Logger.info('语音交互服务停止成功'); Logger.info('语音交互服务已停止');
} else { } else {
Logger.error('语音交互服务停止失败'); Logger.error('停止语音交互服务失败');
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止语音交互服务失败: $e'); Logger.error('停止语音交互服务时发生错误: $e');
return false; return false;
} }
} }
/// 暂停语音交互(停止TTS和语音识别,但保持服务运行) /// 检查服务是否运行
Future<bool> pauseVoiceInteraction() async { Future<bool> checkServiceStatus() async {
if (!_isServiceRunning.value) { try {
Logger.info('语音交互服务未运行,无法暂停'); final bool result = await _channel.invokeMethod<bool>('isServiceRunning') ?? false;
_isServiceRunning.value = result;
return result;
} catch (e) {
Logger.error('检查服务状态时发生错误: $e');
return false; return false;
} }
}
/// 暂停语音交互
Future<bool> pauseVoiceInteraction() async {
try { try {
final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; Logger.info('暂停后台语音交互...');
final result = await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false;
if (result) { if (result) {
Logger.info('语音交互暂停成功'); Logger.info('语音交互已暂停');
} else { } else {
Logger.error('语音交互暂停失败'); Logger.error('暂停语音交互失败');
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('暂停语音交互失败: $e'); Logger.error('暂停语音交互时发生错误: $e');
return false; return false;
} }
} }
/// 处理来自原生端的语音交互事件 /// 资源释放
void _handleVoiceInteractionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
final int timestamp = eventMap['timestamp'] as int? ?? 0;
// 添加时间戳日志,帮助调试
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
switch (eventType) {
case 'chatHistory':
final String agentId = eventMap['agentId'] as String? ?? '';
final String userMessage = eventMap['userMessage'] as String? ?? '';
final String assistantMessage = eventMap['assistantMessage'] as String? ?? '';
_eventStreamController?.add(ChatHistoryEvent(
agentId: agentId,
userMessage: userMessage,
assistantMessage: assistantMessage,
timestamp: timestamp,
));
// 保存聊天记录
_saveChatHistory(agentId, userMessage, assistantMessage, timestamp);
Logger.info('收到聊天历史事件: agentId=$agentId');
break;
case 'recognitionStarted':
_eventStreamController?.add(RecognitionStartedEvent(
timestamp: timestamp,
));
break;
default:
Logger.warning('收到未知类型的语音交互事件: $eventType');
break;
}
}
@override @override
void onClose() { void onClose() {
// 清理资源
_eventSubscription?.cancel(); _eventSubscription?.cancel();
_eventStreamController?.close(); _eventStreamController.close();
super.onClose(); super.onClose();
} }
/// 保存聊天记录
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
try {
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
// 创建用户消息和助手消息
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);
} catch (e) {
Logger.error('保存聊天记录失败: $e');
}
}
}
/// 语音交互事件基类
abstract class VoiceInteractionEvent {
final int timestamp;
VoiceInteractionEvent({required this.timestamp});
}
/// 聊天历史事件
class ChatHistoryEvent extends VoiceInteractionEvent {
final String agentId;
final String userMessage;
final String assistantMessage;
ChatHistoryEvent({
required this.agentId,
required this.userMessage,
required this.assistantMessage,
required int timestamp,
}) : super(timestamp: timestamp);
}
/// 语音识别开始事件
class RecognitionStartedEvent extends VoiceInteractionEvent {
RecognitionStartedEvent({
required int timestamp,
}) : super(timestamp: timestamp);
} }

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

@ -2,19 +2,21 @@ import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../models/message_model.dart'; import '../models/message_model.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/volcano_ai_service.dart'; import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/tts_service.dart'; import '../../../data/services/tts_service.dart';
import '../../../data/providers/agent_provider.dart'; import '../../../data/providers/agent_provider.dart';
import '../../../data/models/agent.dart'; import '../../../data/models/agent.dart';
import '../../../core/utils/logger.dart'; 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/services/voice_interaction_service.dart'; import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/asr_service.dart';
class ChatController extends GetxController { class ChatController extends GetxController {
// 服务 // 服务
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
late TtsService _ttsService; final TtsService _ttsService = Get.find<TtsService>();
late AsrService _asrService; final AsrService _asrService = Get.find<AsrService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>(); final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>(); final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>();
@ -36,6 +38,9 @@ class ChatController extends GetxController {
final RxBool isTtsEnabled = true.obs; final RxBool isTtsEnabled = true.obs;
final RxBool isVoiceRecognitionActive = false.obs; final RxBool isVoiceRecognitionActive = false.obs;
// 语音交互模式
bool _isVoiceInteractionMode = false;
// 语音识别状态 // 语音识别状态
final RxString recognizedText = ''.obs; final RxString recognizedText = ''.obs;
final RxBool isListening = false.obs; final RxBool isListening = false.obs;
@ -44,6 +49,9 @@ class ChatController extends GetxController {
// 临时消息索引,用于更新临时消息 // 临时消息索引,用于更新临时消息
int _tempMessageIndex = -1; int _tempMessageIndex = -1;
// 防抖计时器
Timer? _scrollDebounceTimer;
// 流式响应控制 // 流式响应控制
StreamSubscription? _aiResponseSubscription; StreamSubscription? _aiResponseSubscription;
StreamSubscription? _recognitionSubscription; StreamSubscription? _recognitionSubscription;
@ -52,6 +60,12 @@ class ChatController extends GetxController {
// 取消标志 // 取消标志
bool _shouldCancelAiResponse = false; bool _shouldCancelAiResponse = false;
// 自动停止语音识别计时器
Timer? _autoStopTimer;
// 记录最后一次活动时间
DateTime _lastActivityTime = DateTime.now();
@override @override
Future<void> onInit() async { Future<void> onInit() async {
super.onInit(); super.onInit();
@ -60,7 +74,7 @@ class ChatController extends GetxController {
// 从路由参数获取Agent信息 // 从路由参数获取Agent信息
final Map<String, dynamic> args = Get.arguments ?? {}; final Map<String, dynamic> args = Get.arguments ?? {};
final String agentId = args['agentId'] ?? 'personal_assistant'; final String agentId = args['agent'] ?? args['agentId'] ?? 'personal_assistant';
// 获取Agent // 获取Agent
final Agent? foundAgent = AgentProvider.getAgentById(agentId); final Agent? foundAgent = AgentProvider.getAgentById(agentId);
@ -72,8 +86,11 @@ class ChatController extends GetxController {
agent = foundAgent; agent = foundAgent;
_ttsService = Get.find<TtsService>(); _ttsService.initialize();
_asrService = Get.find<AsrService>(); _asrService.initialize();
// 设置语音交互模式
_isVoiceInteractionMode = args['isVoiceInteraction'] == true;
// 设置TTS服务 // 设置TTS服务
await _ttsService.setVoice(agent.voice); await _ttsService.setVoice(agent.voice);
@ -94,9 +111,11 @@ class ChatController extends GetxController {
// } // }
} }
// 如果playVoiceOnEnter为true,自动激活语音识别 // 如果isVoiceInteraction为true,自动激活语音识别
if (args['playVoiceOnEnter'] == true) { if (args['isVoiceInteraction'] == true) {
startVoiceRecognition(); _ttsService.speakOnce(agent.voiceWelcomeMessage ?? agent.welcomeMessage);
startVoiceRecognition();
// 自动停止计时器会在startVoiceRecognition中启动
} }
// 订阅VoiceInteractionService事件 // 订阅VoiceInteractionService事件
@ -111,87 +130,65 @@ class ChatController extends GetxController {
/// 订阅VoiceInteractionService事件 /// 订阅VoiceInteractionService事件
void _subscribeToVoiceInteractionEvents() { void _subscribeToVoiceInteractionEvents() {
if (_voiceInteractionService.eventStream != null) { if (_voiceInteractionService.eventStream != null) {
_voiceInteractionSubscription = _voiceInteractionService.eventStream!.listen((event) { _voiceInteractionSubscription = _voiceInteractionService.eventStream.listen((event) {
if (event is RecognitionStartedEvent) { if (event is RecognitionStartedEvent) {
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS // 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS
_handleRecognitionStartedEvent(); _stopCurrentInteraction();
} else if (event is ChatHistoryEvent) { } else if (event is ChatHistoryEvent) {
// 收到chatHistory事件时,更新聊天界面 // 收到聊天历史事件,添加到聊天记录
_handleChatHistoryEvent(event); _processChatHistoryEvent(event);
} }
}); });
} }
} }
/// 处理recognitionStarted事件 /// 处理聊天历史事件
void _handleRecognitionStartedEvent() { void _processChatHistoryEvent(ChatHistoryEvent event) {
Logger.info('收到后台语音识别开始事件,停止当前AI交互');
// 停止当前AI交互
if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应");
}
// 停止TTS
_ttsService.stop();
// 停止语音识别
if (isVoiceRecognitionActive.value) {
stopVoiceRecognition();
}
}
/// 处理chatHistory事件
void _handleChatHistoryEvent(ChatHistoryEvent event) {
Logger.info('收到后台聊天历史事件: agentId=${event.agentId}'); Logger.info('收到后台聊天历史事件: agentId=${event.agentId}');
// 检查是否是当前Agent的消息 // 如果消息不是空的且不是当前Agent的,不处理
if (event.agentId != agent.id) { if (event.userMessage.isEmpty || event.agentId != agent.id) {
Logger.info('收到的消息不属于当前Agent,忽略');
return; return;
} }
// 创建用户消息和助手消息 // 添加用户消息
final userMsg = Message( final userMsg = Message(
role: 'user', role: 'user',
content: event.userMessage, content: event.userMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp), timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp),
); );
// 添加助手消息
final assistantMsg = Message( final assistantMsg = Message(
role: 'assistant', role: 'assistant',
content: event.assistantMessage, content: event.assistantMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 确保助手消息时间戳晚于用户消息 timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒
); );
// 检查是否已存在相同内容的消息(避免重复添加) // 防止消息重复
final bool userMsgExists = messages.any((m) => if (!_isDuplicateMessage(userMsg) && !_isDuplicateMessage(assistantMsg)) {
m.role == userMsg.role &&
m.content == userMsg.content &&
(m.timestamp.millisecondsSinceEpoch - userMsg.timestamp.millisecondsSinceEpoch).abs() < 5000
);
final bool assistantMsgExists = messages.any((m) =>
m.role == assistantMsg.role &&
m.content == assistantMsg.content &&
(m.timestamp.millisecondsSinceEpoch - assistantMsg.timestamp.millisecondsSinceEpoch).abs() < 5000
);
// 如果消息不存在,则添加到聊天记录中
if (!userMsgExists) {
messages.add(userMsg); messages.add(userMsg);
}
if (!assistantMsgExists) {
messages.add(assistantMsg); messages.add(assistantMsg);
} }
_scrollToBottom();
}
// 如果有新消息添加,刷新列表并滚动到底部 /// 停止当前交互
if (!userMsgExists || !assistantMsgExists) { void _stopCurrentInteraction() {
messages.refresh(); Logger.info('收到后台语音识别开始事件,停止当前AI交互');
// 停止当前AI交互
if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应");
} }
_scrollToBottom();
// 停止TTS
_ttsService.stop();
// 停止语音识别
if (isVoiceRecognitionActive.value) {
stopVoiceRecognition();
}
} }
@override @override
@ -203,6 +200,8 @@ class ChatController extends GetxController {
_aiResponseSubscription?.cancel(); _aiResponseSubscription?.cancel();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_voiceInteractionSubscription?.cancel(); _voiceInteractionSubscription?.cancel();
_autoStopTimer?.cancel();
_scrollDebounceTimer?.cancel(); // 清理防抖计时器
// 停止TTS // 停止TTS
_ttsService.stop(); _ttsService.stop();
@ -211,22 +210,28 @@ class ChatController extends GetxController {
_asrService.stopContinuousRecognition(); _asrService.stopContinuousRecognition();
} }
// 保存当前聊天记录
if (agent.id != null) {
_historyService.saveHistory(agent.id!, messages.toList());
}
super.onClose(); super.onClose();
} }
/// 滚动到列表底部方法 /// 滚动到列表底部方法,添加防抖功能
void _scrollToBottom() { void _scrollToBottom() {
if (scrollController.hasClients && messages.isNotEmpty) {
try { if (scrollController.hasClients && messages.isNotEmpty) {
scrollController.animateTo( try {
scrollController.position.maxScrollExtent, scrollController.animateTo(
duration: Duration(milliseconds: 300), scrollController.position.maxScrollExtent,
curve: Curves.easeOut, duration: Duration(milliseconds: 300),
); curve: Curves.easeOut,
} catch (e) { );
Logger.error('滚动到底部失败', e); } catch (e) {
Logger.error('滚动到底部失败', e);
}
} }
}
} }
/// 中断当前响应 /// 中断当前响应
@ -272,7 +277,7 @@ class ChatController extends GetxController {
timestamp: DateTime.now(), timestamp: DateTime.now(),
); );
messages.add(userMessage); messages.add(userMessage);
_scrollToBottom();
// 清空输入框 // 清空输入框
inputText.value = ''; inputText.value = '';
@ -280,7 +285,10 @@ class ChatController extends GetxController {
// 添加加载中的消息 // 添加加载中的消息
final loadingMessage = Message.loading(); final loadingMessage = Message.loading();
messages.add(loadingMessage); messages.add(loadingMessage);
_scrollToBottom();
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
isSending.value = true; isSending.value = true;
@ -422,7 +430,7 @@ class ChatController extends GetxController {
_voiceInteractionService.pauseVoiceInteraction(); _voiceInteractionService.pauseVoiceInteraction();
try { try {
// 启动连续语音识别 // 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
final recognitionStream = await _asrService.startContinuousRecognition(); final recognitionStream = await _asrService.startContinuousRecognition();
@ -433,7 +441,8 @@ class ChatController extends GetxController {
// 重置临时消息索引 // 重置临时消息索引
_tempMessageIndex = -1; _tempMessageIndex = -1;
// 启动自动停止计时器
_startAutoStopTimer();
// 监听识别事件 // 监听识别事件
_recognitionSubscription = recognitionStream.listen( _recognitionSubscription = recognitionStream.listen(
@ -449,6 +458,9 @@ class ChatController extends GetxController {
// 处理语音识别事件 // 处理语音识别事件
void _handleRecognitionEvent(RecognitionEvent event) { void _handleRecognitionEvent(RecognitionEvent event) {
// 更新活动时间
_updateActivityTime();
switch (event.type) { switch (event.type) {
case RecognitionEventType.intermediateResult: case RecognitionEventType.intermediateResult:
_handleRecognizing(event.text); _handleRecognizing(event.text);
@ -465,6 +477,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) { void _handleRecognizing(String text) {
if (text.isEmpty) return; if (text.isEmpty) return;
@ -476,6 +524,9 @@ class ChatController extends GetxController {
isSpeechDetected.value = true; isSpeechDetected.value = true;
} }
// 更新活动时间
_updateActivityTime();
// 用户开始讲话时,立即停止当前TTS和中断AI响应 // 用户开始讲话时,立即停止当前TTS和中断AI响应
if (isSending.value || _aiResponseSubscription != null) { if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); _interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
@ -499,8 +550,9 @@ class ChatController extends GetxController {
_tempMessageIndex = messages.length - 1; _tempMessageIndex = messages.length - 1;
messages.refresh(); messages.refresh();
} }
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
});
} }
// 处理语音识别最终结果 // 处理语音识别最终结果
@ -510,6 +562,9 @@ class ChatController extends GetxController {
Logger.info('处理语音识别最终结果: $text'); Logger.info('处理语音识别最终结果: $text');
recognizedText.value = text; recognizedText.value = text;
// 更新活动时间
_updateActivityTime();
// 查找并移除临时消息 // 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
messages.removeAt(_tempMessageIndex); messages.removeAt(_tempMessageIndex);
@ -627,4 +682,19 @@ class ChatController extends GetxController {
_saveChatHistory(); _saveChatHistory();
_scrollToBottom(); _scrollToBottom();
} }
void _stopVoiceRecognitionAutomatically() {
final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15;
Logger.info('${timeoutSeconds}秒无活动,自动停止语音识别');
stopVoiceRecognition();
}
/// 检查是否为重复消息
bool _isDuplicateMessage(Message message) {
return messages.any((m) =>
m.role == message.role &&
m.content == message.content &&
(m.timestamp.millisecondsSinceEpoch - message.timestamp.millisecondsSinceEpoch).abs() < 5000
);
}
} }

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

@ -1,11 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../data/services/classic_bluetooth_service.dart' as classic; import '../../../data/services/classic_bluetooth_service.dart';
import '../../../data/services/bluetooth_media_button_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_service.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../explore/controllers/explore_controller.dart'; import '../../explore/controllers/explore_controller.dart';
// 连接状态枚举 // 连接状态枚举
@ -21,7 +20,13 @@ class HomeController extends GetxController {
final ExploreController exploreController = Get.find<ExploreController>(); final ExploreController exploreController = Get.find<ExploreController>();
// 经典蓝牙服务 // 经典蓝牙服务
late classic.ClassicBluetoothService _bluetoothService; late ClassicBluetoothService _bluetoothService;
// 蓝牙媒体按钮服务
late BluetoothMediaButtonService _mediaButtonService;
// 媒体按钮订阅
StreamSubscription<MediaButtonEvent>? _mediaButtonSubscription;
// 设备连接状态 // 设备连接状态
final _isConnected = false.obs; final _isConnected = false.obs;
@ -47,9 +52,14 @@ class HomeController extends GetxController {
String get connectionErrorMsg => _connectionErrorMsg.value; String get connectionErrorMsg => _connectionErrorMsg.value;
// 当前连接的经典蓝牙设备 // 当前连接的经典蓝牙设备
final _connectedClassicDevices = <classic.BluetoothDevice>[].obs; List<BluetoothDevice> get connectedClassicDevices =>
List<classic.BluetoothDevice> get connectedClassicDevices => _bluetoothService.connectedDevices;
_connectedClassicDevices;
// 蓝牙状态
RxString get bluetoothState => _bluetoothService.bluetoothState;
// 状态更新标志,防止多次重复更新
bool _isUpdatingStatus = false;
@override @override
void onInit() { void onInit() {
@ -58,6 +68,9 @@ class HomeController extends GetxController {
// 初始化蓝牙服务和状态监控 // 初始化蓝牙服务和状态监控
_initBluetoothService(); _initBluetoothService();
// 初始化蓝牙媒体按钮服务
// _initMediaButtonService();
// 电池模拟更新(实际应用中可以通过蓝牙获取) // 电池模拟更新(实际应用中可以通过蓝牙获取)
_simulateBatteryUpdate(); _simulateBatteryUpdate();
} }
@ -66,7 +79,7 @@ class HomeController extends GetxController {
void _initBluetoothService() { void _initBluetoothService() {
try { try {
// 获取经典蓝牙服务实例 // 获取经典蓝牙服务实例
_bluetoothService = Get.find<classic.ClassicBluetoothService>(); _bluetoothService = Get.find<ClassicBluetoothService>();
// 更新当前连接状态 // 更新当前连接状态
_updateConnectionStatus(); _updateConnectionStatus();
@ -79,6 +92,45 @@ class HomeController extends GetxController {
} }
} }
// 初始化蓝牙媒体按钮服务
void _initMediaButtonService() {
try {
// 获取媒体按钮服务实例
_mediaButtonService = Get.find<BluetoothMediaButtonService>();
// 初始化媒体按钮服务
_mediaButtonService.initialize().then((success) {
if (success) {
// 启动按钮监听
_mediaButtonService.startButtonListening();
// 设置按钮事件监听
_setupMediaButtonListener();
Logger.info('蓝牙媒体按钮服务初始化成功');
} else {
Logger.warning('蓝牙媒体按钮服务初始化失败');
}
});
} catch (e) {
Logger.error('初始化蓝牙媒体按钮服务失败: $e');
}
}
// 设置媒体按钮事件监听
void _setupMediaButtonListener() {
// 取消之前的订阅
_mediaButtonSubscription?.cancel();
// 设置新的订阅
_mediaButtonSubscription = _mediaButtonService.buttonEvents.listen((event) {
Logger.info('收到媒体按钮事件: $event');
// 处理按钮事件
if (event.buttonType == MediaButtonType.playPause) {}
});
}
// 电池电量模拟更新 // 电池电量模拟更新
void _simulateBatteryUpdate() { void _simulateBatteryUpdate() {
// 每5分钟更新一次电池电量(模拟) // 每5分钟更新一次电池电量(模拟)
@ -97,44 +149,49 @@ class HomeController extends GetxController {
// 更新连接状态 // 更新连接状态
void _updateConnectionStatus() { void _updateConnectionStatus() {
// 检查当前设备 // 如果正在更新,则跳过
final currentDevice = _bluetoothService.currentDevice.value; if (_isUpdatingStatus) return;
if (currentDevice != null) { _isUpdatingStatus = true;
_isConnected.value = true;
_connectionStatus.value = ConnectionStatus.connected; try {
} else { // 检查当前设备
_isConnected.value = false; final currentDevice = _bluetoothService.currentDevice.value;
_connectionStatus.value = ConnectionStatus.disconnected;
} if (currentDevice != null) {
_isConnected.value = true;
_connectionStatus.value = ConnectionStatus.connected;
} else {
_isConnected.value = false;
_connectionStatus.value = ConnectionStatus.disconnected;
}
update(); // 通知UI刷新 update(); // 通知UI刷新
} finally {
_isUpdatingStatus = false;
}
} }
// 设置蓝牙监听器 // 设置蓝牙监听器
void _setupBluetoothListeners() { void _setupBluetoothListeners() {
// 只监听当前设备和蓝牙状态
// 监听设备连接
_bluetoothService.onDeviceConnected((device) { // 监听当前设备状态
Logger.info('监听到蓝牙设备连接: ${device.name}'); ever(_bluetoothService.currentDevice, (device) {
_isConnected.value = true; if (device != null) {
_connectionStatus.value = ConnectionStatus.connected; _isConnected.value = true;
update(); _connectionStatus.value = ConnectionStatus.connected;
}); } else if (_isConnected.value) {
// 如果之前连接着设备但现在变为null,那么设备已断开
// 监听设备断开 _isConnected.value = false;
_bluetoothService.onDeviceDisconnected((device) { _connectionStatus.value = ConnectionStatus.disconnected;
Logger.info('监听到蓝牙设备断开: ${device.name}'); _deviceBattery.value = null;
_isConnected.value = false; }
_connectionStatus.value = ConnectionStatus.disconnected;
_deviceBattery.value = null;
update(); update();
}); });
// 监听蓝牙状态变化 // 蓝牙状态变化监听
_bluetoothService.onBluetoothStateChanged((state) { ever(_bluetoothService.bluetoothState, (state) {
Logger.info('监听到蓝牙状态变化: $state');
if (state == 'off') { if (state == 'off') {
_isConnected.value = false; _isConnected.value = false;
_connectionStatus.value = ConnectionStatus.disconnected; _connectionStatus.value = ConnectionStatus.disconnected;
@ -146,6 +203,12 @@ class HomeController extends GetxController {
// 刷新设备信息 // 刷新设备信息
Future<void> refreshDeviceInfo() async { Future<void> refreshDeviceInfo() async {
return;
// 如果正在更新,则跳过
if (_isUpdatingStatus) return;
_isUpdatingStatus = true;
try { try {
// 设置状态为连接中 // 设置状态为连接中
_connectionStatus.value = ConnectionStatus.connecting; _connectionStatus.value = ConnectionStatus.connecting;
@ -160,10 +223,7 @@ class HomeController extends GetxController {
// 根据结果更新状态 // 根据结果更新状态
if (!hasDevices) { if (!hasDevices) {
Logger.info('刷新设备:未找到设备'); Logger.info('刷新设备:未找到设备');
// 这里可以设置状态变量而不是使用snackbar _connectionErrorMsg.value = '未找到已连接的设备';
_connectionErrorMsg.value = _bluetoothService.errorMessage.value.isEmpty
? 'noDeviceFound'.tr // 未找到已连接的设备
: _bluetoothService.errorMessage.value;
} else { } else {
_connectionErrorMsg.value = ''; _connectionErrorMsg.value = '';
Logger.info('刷新设备:成功'); Logger.info('刷新设备:成功');
@ -173,6 +233,8 @@ class HomeController extends GetxController {
_connectionStatus.value = ConnectionStatus.failed; _connectionStatus.value = ConnectionStatus.failed;
_connectionErrorMsg.value = 'failedDeviceInformation'.tr; // 无法获取设备信息 _connectionErrorMsg.value = 'failedDeviceInformation'.tr; // 无法获取设备信息
update(); update();
} finally {
_isUpdatingStatus = false;
} }
} }
@ -188,7 +250,7 @@ class HomeController extends GetxController {
// 打开AI伴侣功能 // 打开AI伴侣功能
void openAIGirlfriend() { void openAIGirlfriend() {
exploreController.onAIFeatureTap(2);//进入AI女友页面 exploreController.onAIFeatureTap(2); //进入AI女友页面
} }
// 展示AI功能 // 展示AI功能
@ -214,6 +276,11 @@ class HomeController extends GetxController {
// 获取连接状态对应的文本 // 获取连接状态对应的文本
String getConnectionStatusText() { String getConnectionStatusText() {
// 如果蓝牙关闭,优先显示蓝牙状态
if (_bluetoothService.bluetoothState.value == 'off') {
return '蓝牙已关闭';
}
switch (_connectionStatus.value) { switch (_connectionStatus.value) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
return 'connected'.tr; // 已连接 return 'connected'.tr; // 已连接
@ -256,4 +323,12 @@ class HomeController extends GetxController {
final device = _bluetoothService.currentDevice.value; final device = _bluetoothService.currentDevice.value;
return device?.address ?? 'unknown'.tr; // 未知 return device?.address ?? 'unknown'.tr; // 未知
} }
@override
void onClose() {
// 取消按钮事件流订阅
_mediaButtonSubscription?.cancel();
super.onClose();
}
} }

34
lib/modules/home/views/home_view.dart

@ -102,6 +102,8 @@ class HomeView extends GetView<HomeController> {
_buildSectionTitle(isDarkMode), _buildSectionTitle(isDarkMode),
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildPremiumAILayout(), _buildPremiumAILayout(),
], ],
), ),
), ),
@ -667,14 +669,16 @@ class HomeView extends GetView<HomeController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
Text( Flexible(
description, child: Text(
style: TextStyle( description,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], style: TextStyle(
fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 12.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
@ -698,14 +702,16 @@ class HomeView extends GetView<HomeController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox(height: 2.h), SizedBox(height: 2.h),
Text( Flexible(
description, child: Text(
style: TextStyle( description,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], style: TextStyle(
fontSize: 10.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 10.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),

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

@ -1,18 +1,23 @@
import 'package:deep_voice/data/services/voice_interaction_service.dart'; import 'package:app_settings/app_settings.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' as classic; import '../../../data/services/classic_bluetooth_service.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart'; import 'dart:io';
import 'package:permission_handler/permission_handler.dart'; import '../../../data/services/voice_interaction_service.dart';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 /// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
class PairingController extends GetxController { class PairingController extends GetxController {
static PairingController get to => Get.find(); static PairingController get to => Get.find();
// 经典蓝牙服务 // 经典蓝牙服务
late classic.ClassicBluetoothService _bluetoothService; final ClassicBluetoothService _bluetoothService =
Get.find<ClassicBluetoothService>();
// 语音交互服务
final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>();
// 是否开始搜索 // 是否开始搜索
final _isSearching = false.obs; final _isSearching = false.obs;
@ -27,21 +32,17 @@ class PairingController extends GetxController {
bool get isBluetoothServiceAvailable => _isBluetoothServiceAvailable.value; bool get isBluetoothServiceAvailable => _isBluetoothServiceAvailable.value;
// 当前连接设备 // 当前连接设备
Rx<classic.BluetoothDevice?> get currentDevice => Rx<BluetoothDevice?> get currentDevice => _bluetoothService.currentDevice;
_bluetoothService.currentDevice;
// 连接设备列表 // 连接设备列表
RxList<classic.BluetoothDevice> get connectedDevices => RxList<BluetoothDevice> get connectedDevices =>
_bluetoothService.connectedDevices; _bluetoothService.connectedDevices;
// 错误信息 // 蓝牙状态
RxString get errorMessage => _bluetoothService.errorMessage; RxString get bluetoothState => _bluetoothService.bluetoothState;
// 连接设备列表(兼容旧代码) // 错误信息
List<classic.BluetoothDevice> get connectedClassicDevices => final errorMessage = ''.obs;
_bluetoothService.currentDevice.value != null
? [_bluetoothService.currentDevice.value!]
: [];
// 状态描述(用于UI显示) // 状态描述(用于UI显示)
final statusDescription = 'initializing'.tr.obs; // 正在初始化... final statusDescription = 'initializing'.tr.obs; // 正在初始化...
@ -49,23 +50,70 @@ class PairingController extends GetxController {
// 加载状态 // 加载状态
final isLoading = false.obs; final isLoading = false.obs;
// 是否正在跳转到主页
bool _isNavigatingToHome = false;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initialize(); _initialize();
} }
/// 完整的初始化流程 /// 设置状态变化监听
void _setupStatusListeners() {
if (!_isBluetoothServiceAvailable.value) return;
// 监听蓝牙开关状态
ever(_bluetoothService.bluetoothState, (state) {
_updateStatusDescription();
});
// 监听当前设备状态
ever(_bluetoothService.currentDevice, (device) {
_updateStatusDescription();
// 如果有设备连接,自动跳转到主页
if (device != null) {
_autoNavigateToHome();
}
});
}
/// 更新状态描述
void _updateStatusDescription() {
if (_bluetoothService.bluetoothState.value == 'off') {
statusDescription.value = '蓝牙已关闭,请打开蓝牙';
return;
}
if (_bluetoothService.currentDevice.value != null) {
statusDescription.value =
'已连接到设备: ${_bluetoothService.currentDevice.value!.name}';
return;
}
if (_bluetoothService.connectedDevices.isNotEmpty) {
statusDescription.value =
'找到已连接的设备: ${_bluetoothService.connectedDevices.first.name}';
return;
}
if (isLoading.value) {
statusDescription.value = '正在搜索设备...';
return;
}
statusDescription.value = '未找到已连接的蓝牙设备';
}
/// 初始化流程
Future<void> _initialize() async { Future<void> _initialize() async {
try { try {
VoiceInteractionService.to.initialize(); // 初始化语音交互服务
VoiceInteractionService.to.startService(); _voiceInteractionService.initialize();
isLoading.value = true; isLoading.value = true;
statusDescription.value = 'initializing'.tr; // 正在初始化... statusDescription.value = '正在初始化...';
// 1. 获取蓝牙服务
_bluetoothService = Get.find<classic.ClassicBluetoothService>();
// 确保蓝牙服务已初始化 // 确保蓝牙服务已初始化
if (!await _bluetoothService.initialize()) { if (!await _bluetoothService.initialize()) {
@ -76,33 +124,24 @@ class PairingController extends GetxController {
isLoading.value = false; isLoading.value = false;
return; return;
} }
_setupStatusListeners();
// 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题 // 检查权限
_setupDeviceListeners(); if (Platform.isAndroid) {
statusDescription.value = '正在检查权限...';
// 2. 检查权限 final hasPermission = await _bluetoothService.checkPermissions();
statusDescription.value = 'checkingPermissions'.tr; // 正在检查权限... if (!hasPermission) {
final hasPermission = statusDescription.value = '缺少蓝牙权限';
await _bluetoothService.checkAndRequestPermissions(); isLoading.value = false;
if (!hasPermission) { return;
statusDescription.value = 'lackPermissions'.tr; // 缺少蓝牙权限 }
isLoading.value = false;
return;
} }
// 3. 刷新设备列表 // 刷新设备列表
statusDescription.value = 'searchingDevice'.tr; // 正在搜索设备... statusDescription.value = '正在搜索设备...';
await refreshConnectedDevices(); await refreshConnectedDevices();
// 4. 处理结果 // 更新状态描述
if (_bluetoothService.connectedDevices.isNotEmpty) { _updateStatusDescription();
statusDescription.value = 'findConnectedDevice'.tr; // 找到已连接的设备
// 延迟跳转到主页
_autoNavigateToHome();
} else {
// 没有找到设备
statusDescription.value = 'notFound'.tr; // 未找到已连接的蓝牙设备
}
} catch (e) { } catch (e) {
Logger.error('初始化配对控制器失败: $e'); Logger.error('初始化配对控制器失败: $e');
statusDescription.value = 'initializationFailure'.tr; // 初始化失败 statusDescription.value = 'initializationFailure'.tr; // 初始化失败
@ -112,32 +151,6 @@ class PairingController extends GetxController {
} }
} }
/// 设置设备监听器
void _setupDeviceListeners() {
try {
if (!_isBluetoothServiceAvailable.value) return;
// 监听设备连接事件
_bluetoothService.onDeviceConnected((device) {
Logger.info('检测到蓝牙设备连接: ${device.name}');
// 更新状态并跳转到主页
statusDescription.value =
'${'connectedDevice'.tr} ${device.name}'; // 已连接到设备:
// 立即跳转到主页
_navigateToHome();
});
} catch (e) {
Logger.error('设置设备监听器失败: $e');
}
}
/// 立即跳转到主页
void _navigateToHome() {
Get.offAllNamed(Routes.home);
}
/// 刷新设备 /// 刷新设备
Future<void> refreshConnectedDevices() async { Future<void> refreshConnectedDevices() async {
try { try {
@ -146,14 +159,8 @@ class PairingController extends GetxController {
final hasDevices = await _bluetoothService.refreshConnectedDevices(); final hasDevices = await _bluetoothService.refreshConnectedDevices();
if (hasDevices) { // 更新状态描述
statusDescription.value = 'findConnectedDevice'.tr; // 找到已连接的设备 _updateStatusDescription();
// 延迟跳转到主页
_autoNavigateToHome();
} else {
// 没有找到设备
statusDescription.value = 'notFound'.tr; // 未找到已连接的蓝牙设备
}
} catch (e) { } catch (e) {
Logger.error('刷新设备失败: $e'); Logger.error('刷新设备失败: $e');
statusDescription.value = 'failedRefreshDevice'.tr; // 刷新设备失败 statusDescription.value = 'failedRefreshDevice'.tr; // 刷新设备失败
@ -164,7 +171,12 @@ class PairingController extends GetxController {
/// 自动跳转到主页 /// 自动跳转到主页
void _autoNavigateToHome() { void _autoNavigateToHome() {
if (_bluetoothService.connectedDevices.isEmpty) return; // 如果已经在跳转或设备列表为空,则不执行
if (_isNavigatingToHome || _bluetoothService.connectedDevices.isEmpty)
return;
// 设置跳转标志
_isNavigatingToHome = true;
Logger.info('已找到连接的设备,自动跳转到主页'); Logger.info('已找到连接的设备,自动跳转到主页');
@ -175,7 +187,12 @@ class PairingController extends GetxController {
/// 打开系统蓝牙设置 /// 打开系统蓝牙设置
void openBluetoothSettings() { void openBluetoothSettings() {
_bluetoothService.openBluetoothSettings(); try {
// 使用app_settings插件打开蓝牙设置
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} catch (e) {
Logger.error('打开蓝牙设置失败: $e');
}
} }
/// 刷新设备(兼容旧代码) /// 刷新设备(兼容旧代码)
@ -185,6 +202,8 @@ class PairingController extends GetxController {
/// 跳过连接 /// 跳过连接
void skipConnecting() { void skipConnecting() {
// 设置跳转标志防止自动跳转再次触发
_isNavigatingToHome = true;
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);
} }
} }

4
lib/modules/pairing/views/pairing_view.dart

@ -200,7 +200,7 @@ class PairingView extends GetView<PairingController> {
), ),
// 设备列表(有设备时才显示) // 设备列表(有设备时才显示)
if (controller.connectedClassicDevices.isNotEmpty) ...[ if (controller.connectedDevices.isNotEmpty) ...[
SizedBox(height: 24.h), SizedBox(height: 24.h),
_buildConnectedDevicesList(isDarkMode), _buildConnectedDevicesList(isDarkMode),
], ],
@ -212,7 +212,7 @@ class PairingView extends GetView<PairingController> {
// 已连接设备列表 // 已连接设备列表
Widget _buildConnectedDevicesList(bool isDarkMode) { Widget _buildConnectedDevicesList(bool isDarkMode) {
return Obx(() { return Obx(() {
final devices = controller.connectedClassicDevices; final devices = controller.connectedDevices;
if (devices.isEmpty) { if (devices.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();

19
lib/modules/settings/controllers/settings_controller.dart

@ -59,6 +59,8 @@ class SettingsController extends GetxController {
}, },
]; ];
RxString timbreName = '默认音色'.obs;
//用户信息 //用户信息
static const String userKey = 'user_info'; static const String userKey = 'user_info';
//final _storage=GetStorage(); //final _storage=GetStorage();
@ -72,6 +74,8 @@ class SettingsController extends GetxController {
// 获取系统语言 // 获取系统语言
_getLanguage(); _getLanguage();
_getTimbre();
// 从存储中加载上次保存的主题设置 // 从存储中加载上次保存的主题设置
final savedDarkMode = _themeService.isSavedDarkMode(); final savedDarkMode = _themeService.isSavedDarkMode();
@ -144,6 +148,21 @@ class SettingsController extends GetxController {
language.value = data; language.value = data;
} }
// 获取音色
void _getTimbre() {
List? timbreList = _storage.read('timbreList');
String? timbre = _storage.read('timbre');
if (timbreList != null && timbre != null) {
Map data = timbreList.firstWhere(
(element) => element['type'] == timbre,
orElse: () => {},
);
if (data.isNotEmpty) {
timbreName.value = data['name'];
}
}
}
void setLanguage(LanguageModel data) { void setLanguage(LanguageModel data) {
if (data.value != 'system') { if (data.value != 'system') {
List languageList = data.value.split('_'); List languageList = data.value.split('_');

26
lib/modules/settings/views/settings_view.dart

@ -339,6 +339,32 @@ class SettingsView extends GetView<SettingsController> {
// 语音服务选择 // 语音服务选择
_buildSpeechServiceSetting(isDarkMode), _buildSpeechServiceSetting(isDarkMode),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
Obx(
() => _buildNavigationSetting(
title: '音色服务',
subtitle: '选择音色语音合成服务',
icon: Icons.photo_camera_front_outlined,
iconBgColor: isDarkMode
? Colors.cyan[900]!.withOpacity(0.3)
: Colors.cyan[100]!,
iconColor:
isDarkMode ? Colors.cyan[300]! : Colors.cyan[600]!,
value: controller.timbreName.value,
valueStyle: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[500],
),
onTap: () {
Get.toNamed(Routes.timbre);
},
isDarkMode: isDarkMode,
),
),
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode

297
lib/modules/timbre/controllers/timbre_controller.dart

@ -1,14 +1,303 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
import '../../../data/services/speech_factory.dart';
import '../../settings/controllers/settings_controller.dart';
class TimbreController extends GetxController { class TimbreController extends GetxController {
RxList dataList = [ final SettingsController settingsController = Get.find<SettingsController>();
{'type': '1', 'name': '默认音色', 'description': '微软云端音色服务'},
{'type': '2', 'name': '声音复刻', 'description': '复刻本人音色服务'} final GetStorage _storage = GetStorage();
].obs; final _record = AudioRecorder();
final String _appId = "43d87d22";
final String _appKey = "2bd01ce06ac35957fc0c082f9b6284ec";
RxList dataList = [].obs;
RxString type = '1'.obs; RxString type = '1'.obs;
RxString text = ''.obs;
String _token = '';
int _textId = 0;
int _segId = 0;
String _taskId = '';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_getDataList();
}
@override
void onClose() {
_record.dispose();
super.onClose();
}
void _getDataList() {
List? timbreList = _storage.read('timbreList');
String? timbre = _storage.read('timbre');
if (timbreList != null) {
dataList.value = timbreList;
} else {
dataList.add({
'type': '1',
'name': '默认音色',
'description': '微软云端音色服务',
'isUsable': true
});
_storage.write('timbreList', [
{
'type': '1',
'name': '默认音色',
'description': '微软云端音色服务',
'isUsable': true
}
]);
}
if (timbre != null) {
type.value = timbre;
}
}
// 录制声音
void startRecorder() async {
if (_token.isEmpty) {
await _getToken();
}
await _getTraintext();
await _addTask();
final directory = await getApplicationDocumentsDirectory();
final path =
'${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
await _record.start(
const RecordConfig(
encoder: AudioEncoder.wav,
sampleRate: 24000,
numChannels: 1,
),
path: path,
);
}
// 获取token
Future<void> _getToken() async {
int timeStamp = DateTime.now().millisecondsSinceEpoch;
String data =
'{"base":{"appid":"$_appId","version":"v1","timestamp":"$timeStamp"},"model":"remote"}';
String keySignInput = _appKey + timeStamp.toString();
List<int> keySignBytes = utf8.encode(keySignInput);
Digest keySignDigest = md5.convert(keySignBytes);
String keySign = keySignDigest.toString();
String signInput = keySign + data;
List<int> signBytes = utf8.encode(signInput);
Digest signDigest = md5.convert(signBytes);
String sign = signDigest.toString();
final response = await http.post(
Uri.parse('http://avatar-hci.xfyousheng.com/aiauth/v1/token'),
headers: {
'Content-Type': 'application/json',
'Authorization': sign,
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['retcode'] == '000000') {
_token = body['accesstoken'];
}
}
}
// 获取训练文本
Future<void> _getTraintext() async {
String data = '{"textId":"5001"}';
Map sign = _sign(data);
final response = await http.post(
Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/traintext'),
headers: {
'Content-Type': 'application/json',
'X-Sign': sign['sign'],
'X-Token': _token,
'X-AppId': _appId,
'X-Time': sign['timeStamp'],
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['code'] == 0) {
_textId = body['data']['textId'];
Map textSegs = body['data']['textSegs'].first;
_segId = textSegs['segId'];
text.value = _decodeCorruptedText(textSegs['segText']);
}
}
}
// 创建训练任务
Future<void> _addTask() async {
String data = '{"resourceType":"12"}';
Map sign = _sign(data);
final response = await http.post(
Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/add'),
headers: {
'Content-Type': 'application/json',
'X-Sign': sign['sign'],
'X-Token': _token,
'X-AppId': _appId,
'X-Time': sign['timeStamp'],
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['code'] == 0) {
_taskId = body['data'];
}
}
}
Map _sign(String data) {
int timeStamp = DateTime.now().millisecondsSinceEpoch;
String keySignInput = _appKey + timeStamp.toString();
List<int> keySignBytes = utf8.encode(keySignInput);
Digest keySignDigest = md5.convert(keySignBytes);
String keySign = keySignDigest.toString();
String signInput = keySign + data;
List<int> signBytes = utf8.encode(signInput);
Digest signDigest = md5.convert(signBytes);
String sign = signDigest.toString();
return {'sign': sign, 'timeStamp': timeStamp.toString()};
}
String _decodeCorruptedText(String corruptedText) {
List<int> latin1Bytes =
corruptedText.codeUnits.map((ch) => ch & 0xFF).toList();
return utf8.decode(latin1Bytes);
}
// 完成录制
void completeRecorder() async {
final audioPath = await _record.stop();
String data =
'{"taskId":"$_taskId","textId":"$_textId","textSegId":"$_segId"}';
Map sign = _sign(data);
final uri = Uri.parse(
'http://opentrain.xfyousheng.com/voice_train/task/submitWithAudio');
final request = http.MultipartRequest('POST', uri);
request.files.add(await http.MultipartFile.fromPath(
'file',
audioPath!,
filename: path.basename(audioPath),
));
request.fields.addAll({
'taskId': _taskId,
'textId': '$_textId',
'textSegId': '$_segId',
});
request.headers.addAll({
"X-Sign": sign['sign'],
"X-Token": _token,
"X-AppId": _appId,
"X-Time": sign['timeStamp'],
});
final response = await request.send();
if (response.statusCode == 200) {
final responseBody = await response.stream.bytesToString();
final body = json.decode(responseBody);
if (body['code'] == 0) {
text.value = '';
dataList.add({
'type': _taskId,
'name': '声音复刻${dataList.length}',
'description': '复刻本人音色服务',
'isUsable': false
});
await _getTaskResult(_taskId);
}
}
}
// 查询训练任务状态
Future<void> _getTaskResult(String id) async {
String data = '{"taskId":"$id"}';
Map sign = _sign(data);
final response = await http.post(
Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/result'),
headers: {
'Content-Type': 'application/json',
'X-Sign': sign['sign'],
'X-Token': _token,
'X-AppId': _appId,
'X-Time': sign['timeStamp'],
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['code'] == 0) {
if (body['data']['trainStatus'] == -1) {
await _getTaskResult(id);
}
if (body['data']['trainStatus'] == 1) {
Map? dataMap =
dataList.firstWhereOrNull((item) => item['type'] == id);
if (dataMap != null) {
dataMap['type'] = body['data']['assetId'];
dataMap['isUsable'] = true;
dataList.refresh();
_storage.write('timbreList', dataList);
}
}
if (body['data']['trainStatus'] == 0) {
Get.snackbar(
'error'.tr, // 错误
_decodeCorruptedText(body['data']['failedDesc']),
snackPosition: SnackPosition.TOP,
);
}
}
}
}
// 取消
void cancel() {
text.value = '';
_record.stop();
Get.back();
}
// 选择音色
void selectTone(Map data) {
if (!data['isUsable'] || type.value == data['type']) return;
type.value = data['type'];
settingsController.timbreName.value = data['name'];
_storage.write('timbre', data['type']);
setSpeechServiceType();
}
// 设置语音音色服务类型
void setSpeechServiceType() {
try {
final factory = Get.find<SpeechFactory>();
factory.setTimbreServiceType();
} catch (e) {
// 处理错误
}
} }
} }

116
lib/modules/timbre/views/timbre_view.dart

@ -60,7 +60,69 @@ class TimbreView extends GetView<TimbreController> {
), ),
), ),
GestureDetector( GestureDetector(
onTap: () {}, onTap: () {
controller.startRecorder();
Get.dialog(
barrierDismissible: false,
AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'请朗读',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Obx(
() => controller.text.value.isNotEmpty
? Text(
controller.text.value,
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
)
: Container(
width: 40.w,
height: 40.w,
alignment: Alignment.center,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.blue),
),
),
),
actions: [
TextButton(
onPressed: () {
controller.cancel();
},
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
Obx(() => controller.text.value.isNotEmpty
? TextButton(
onPressed: () {
Get.back();
controller.completeRecorder();
},
child: Text(
'完成录制',
style: TextStyle(
color: isDarkMode
? Colors.red[300]
: Colors.red[600],
),
),
)
: const SizedBox()),
],
),
);
},
child: Container( child: Container(
width: 130.w, width: 130.w,
height: 48.w, height: 48.w,
@ -89,7 +151,7 @@ class TimbreView extends GetView<TimbreController> {
Widget _item(bool isDarkMode, Map data) { Widget _item(bool isDarkMode, Map data) {
return InkWell( return InkWell(
onTap: () { onTap: () {
controller.type.value = data['type']; controller.selectTone(data);
}, },
child: Obx( child: Obx(
() => Container( () => Container(
@ -139,26 +201,36 @@ class TimbreView extends GetView<TimbreController> {
], ],
), ),
), ),
Container( data['isUsable']
width: 24.w, ? Container(
height: 24.w, width: 24.w,
margin: EdgeInsets.only(left: 12.w), height: 24.w,
decoration: BoxDecoration( margin: EdgeInsets.only(left: 12.w),
color: controller.type.value == data['type'] decoration: BoxDecoration(
? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) color: controller.type.value == data['type']
: (isDarkMode ? (isDarkMode ? Colors.teal[300] : Colors.teal[600])
? Colors.white.withOpacity(0.1) : (isDarkMode
: Colors.grey[300]), ? Colors.white.withOpacity(0.1)
shape: BoxShape.circle, : Colors.grey[300]),
), shape: BoxShape.circle,
child: controller.type.value == data['type'] ),
? Icon( child: controller.type.value == data['type']
Icons.check, ? Icon(
size: 16.w, Icons.check,
color: Colors.white, size: 16.w,
) color: Colors.white,
: null, )
), : null,
)
: SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor:
const AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
], ],
), ),
), ),

11
lib/modules/translation/bindings/translation_binding.dart

@ -1,19 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
import '../../../data/services/speech_impl/azure_asr_service.dart';
import '../../../data/services/speech_impl/azure_tts_service.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/language_manager.dart';
class TranslationBinding implements Bindings { class TranslationBinding implements Bindings {
@override @override
void dependencies() { void dependencies() {
Get.put(LanguageManager(), permanent: true);
Get.lazyPut<AsrService>(() => AzureAsrService(), fenix: true);
Get.lazyPut<TtsService>(() => AzureTtsService(), fenix: true);
Get.put(VolcanoTranslationService(), permanent: true);
Get.put(TranslationController()); Get.put(TranslationController());
} }
} }

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

@ -7,6 +7,7 @@ import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart'; import '../../../data/services/tts_service.dart';
import '../../../data/services/language_manager.dart'; import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/voice_interaction_service.dart'; import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/asr_service.dart'; import '../../../data/services/asr_service.dart';
class TranslationController extends GetxController { class TranslationController extends GetxController {
@ -22,7 +23,7 @@ class TranslationController extends GetxController {
static const String _historyKey = 'translation_history'; static const String _historyKey = 'translation_history';
static const String _sourceLanguageKey = 'translation_source_language'; static const String _sourceLanguageKey = 'translation_source_language';
static const String _targetLanguageKey = 'translation_target_language'; static const String _targetLanguageKey = 'translation_target_language';
static const int _maxHistoryItems = 100; static const int _maxHistoryItems = 10;
// 状态变量 // 状态变量
final isRecognizing = false.obs; final isRecognizing = false.obs;
@ -38,9 +39,16 @@ class TranslationController extends GetxController {
// 翻译历史和当前项 // 翻译历史和当前项
final translationHistory = <TranslationItem>[].obs; final translationHistory = <TranslationItem>[].obs;
final Rx<TranslationItem?> currentItem = Rx<TranslationItem?>(null);
String? currentSessionId; String? currentSessionId;
// 兼容旧版视图的计算属性
Rx<TranslationItem?> get currentItem {
if (translationHistory.isNotEmpty && translationHistory.last.isIntermediate) {
return Rx<TranslationItem?>(translationHistory.last);
}
return Rx<TranslationItem?>(null);
}
// UI相关 // UI相关
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
@ -70,8 +78,8 @@ class TranslationController extends GetxController {
targetLanguageCode.value = targetAsrCode; targetLanguageCode.value = targetAsrCode;
} }
// 订阅VoiceInteractionService事件 // 订阅原生服务事件
_subscribeToVoiceInteractionEvents(); _subscribeToVoiceInteractionService();
// 加载历史记录 // 加载历史记录
loadTranslationHistory(); loadTranslationHistory();
@ -83,20 +91,15 @@ class TranslationController extends GetxController {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
// 额外的延迟滚动,确保在复杂视图中也能正确滚动
Future.delayed(const Duration(milliseconds: 500), () {
_scrollToBottom();
});
}); });
} }
/// 订阅VoiceInteractionService事件 /// 订阅原生服务事件
void _subscribeToVoiceInteractionEvents() { void _subscribeToVoiceInteractionService() {
if (_voiceInteractionService.eventStream != null) { if (_voiceInteractionService.eventStream != null) {
_voiceInteractionSubscription = _voiceInteractionService.eventStream!.listen((event) { _voiceInteractionSubscription = _voiceInteractionService.eventStream!.listen((event) {
if (event is RecognitionStartedEvent) { if (event is RecognitionStartedEvent) {
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS _handleVoiceInteractionStarted();
stopAll();
} }
}); });
} }
@ -126,7 +129,6 @@ class TranslationController extends GetxController {
Future<void> _initServices() async { Future<void> _initServices() async {
try { try {
await _translationService.initialize(); await _translationService.initialize();
await _ttsService.initialize();
// 初始化 ASR 服务时,明确指定需要支持的语言 // 初始化 ASR 服务时,明确指定需要支持的语言
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
@ -173,16 +175,20 @@ class TranslationController extends GetxController {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// 初始化新的翻译项 // // 初始化新的翻译项
currentItem.value = TranslationItem( // final newItem = TranslationItem(
sourceText: '', // sourceText: '',
translatedText: '', // translatedText: '',
sourceLanguageCode: sourceLanguageCode.value, // sourceLanguageCode: sourceLanguageCode.value,
targetLanguageCode: targetLanguageCode.value, // targetLanguageCode: targetLanguageCode.value,
timestamp: DateTime.now(), // timestamp: DateTime.now(),
sessionId: currentSessionId!, // sessionId: currentSessionId!,
isFirstInSession: true, // isFirstInSession: true,
); // isIntermediate: true,
// );
// // 添加到历史记录
// translationHistory.add(newItem);
// 开始连续语音识别 // 开始连续语音识别
final recognitionStream = await _asrService.startContinuousRecognition(); final recognitionStream = await _asrService.startContinuousRecognition();
@ -227,21 +233,20 @@ class TranslationController extends GetxController {
isRecognizing.value = false; isRecognizing.value = false;
// 如果当前有中间结果,将其作为最终结果处理 // 如果当前有中间结果,将其作为最终结果处理
if (currentItem.value != null && currentItem.value!.sourceText.isNotEmpty) { if (translationHistory.isNotEmpty && translationHistory.last.isIntermediate && translationHistory.last.sourceText.isNotEmpty) {
// 标记为最终结果 // 标记为最终结果
currentItem.value!.isIntermediate = false; translationHistory.last.isIntermediate = false;
translationHistory.refresh();
// 如果还没有翻译结果,尝试翻译 // 如果还没有翻译结果,尝试翻译
if (currentItem.value!.translatedText.isEmpty) { if (translationHistory.last.translatedText.isEmpty) {
await translateText(currentItem.value!.sourceText); await translateText(translationHistory.last.sourceText, translationHistory.last.timestamp, isFinal: true);
} }
translationHistory.add(currentItem.value!);
saveTranslationHistory(); saveTranslationHistory();
} }
currentSessionId = null; currentSessionId = null;
currentItem.value = null;
} catch (e) { } catch (e) {
Logger.error('停止语音识别失败: $e'); Logger.error('停止语音识别失败: $e');
} }
@ -251,9 +256,9 @@ class TranslationController extends GetxController {
void handleIntermediateResult(String text) { void handleIntermediateResult(String text) {
if (text.isEmpty) return; if (text.isEmpty) return;
if (currentItem.value == null) { if (translationHistory.isEmpty || !translationHistory.last.isIntermediate) {
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
currentItem.value = TranslationItem( final newItem = TranslationItem(
sourceText: text, sourceText: text,
translatedText: '', translatedText: '',
sourceLanguageCode: sourceLanguageCode.value, sourceLanguageCode: sourceLanguageCode.value,
@ -263,13 +268,14 @@ class TranslationController extends GetxController {
isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId, isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId,
isIntermediate: true, isIntermediate: true,
); );
translationHistory.add(newItem);
// 添加新项后滚动到底部 // 添加新项后滚动到底部
_scrollToBottom(); _scrollToBottom();
} else { } else {
// 更新当前项 // 更新当前项
currentItem.value!.sourceText = text; translationHistory.last.sourceText = text;
currentItem.value!.isIntermediate = true; translationHistory.refresh();
currentItem.refresh();
// 更新后滚动到底部 // 更新后滚动到底部
_scrollToBottom(); _scrollToBottom();
} }
@ -277,45 +283,53 @@ class TranslationController extends GetxController {
// 防抖处理翻译请求 // 防抖处理翻译请求
_translationDebounceTimer?.cancel(); _translationDebounceTimer?.cancel();
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () { _translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
if (currentItem.value != null && currentItem.value!.sourceText.isNotEmpty) { if (translationHistory.isNotEmpty && translationHistory.last.isIntermediate && translationHistory.last.sourceText.isNotEmpty) {
translateText(currentItem.value!.sourceText); translateText(translationHistory.last.sourceText, translationHistory.last.timestamp, isFinal: false);
} }
}); });
} }
// 处理最终识别结果 // 处理最终识别结果
Future<void> handleFinalResult(String text) async { Future<void> handleFinalResult(String text) async {
if (text.isEmpty || currentItem.value == null) return; if (text.isEmpty || translationHistory.isEmpty) return;
// 更新当前项,但不立即添加到历史记录 // 更新当前项
currentItem.value!.sourceText = text; if (translationHistory.last.isIntermediate) {
currentItem.value!.isIntermediate = false; translationHistory.last.sourceText = text;
currentItem.refresh(); translationHistory.last.isIntermediate = false;
// 更新后滚动到底部 translationHistory.refresh();
_scrollToBottom(); // 更新后滚动到底部
_scrollToBottom();
saveTranslationHistory();
// 先翻译,保持UI稳定 // 使用项目的时间戳进行翻译
final translationResult = await translateText(text); translateText(text, translationHistory.last.timestamp, isFinal: true);
} else {
// 如果最后一项不是中间结果,创建新项
final newItem = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguageCode: sourceLanguageCode.value,
targetLanguageCode: targetLanguageCode.value,
timestamp: DateTime.now(),
sessionId: currentSessionId ?? DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId,
isIntermediate: false,
);
// 翻译完成后再添加到历史记录 translationHistory.add(newItem);
if (currentItem.value != null) { translationHistory.refresh();
translationHistory.add(currentItem.value!);
saveTranslationHistory();
// 添加到历史记录后滚动到底部
_scrollToBottom(); _scrollToBottom();
saveTranslationHistory();
// 播放TTS
if (translationResult != null && isTtsEnabled.value) {
await playTranslatedText(translationResult);
}
// 重置当前项,准备下一次输入 // 使用新项目的时间戳进行翻译
currentItem.value = null; translateText(text, newItem.timestamp, isFinal: true);
} }
} }
// 翻译文本 // 翻译文本
Future<String?> translateText(String sourceText) async { Future<String?> translateText(String sourceText, DateTime timestamp, {bool isFinal = false}) async {
if (sourceText.isEmpty) return null; if (sourceText.isEmpty) return null;
isTranslating.value = true; isTranslating.value = true;
@ -335,27 +349,28 @@ class TranslationController extends GetxController {
); );
if (translationResult != null) { if (translationResult != null) {
// 更新翻译结果 // 查找匹配的项目 - 优先使用timestamp查找
if (currentItem.value != null && currentItem.value!.sourceText.contains(sourceText)) { int index = -1;
// 更新当前项
currentItem.value!.translatedText = translationResult; index = translationHistory.indexWhere((item) =>
currentItem.value!.sourceLanguageCode = detectedSourceLanguageCode; item.timestamp.isAtSameMomentAs(timestamp));
currentItem.value!.targetLanguageCode = detectedTargetLanguageCode; if (index != -1) {
currentItem.refresh(); // 更新历史项
translationHistory[index].translatedText = translationResult;
translationHistory[index].sourceLanguageCode = detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode = detectedTargetLanguageCode;
translationHistory.refresh();
saveTranslationHistory();
// 更新后滚动到底部 // 更新后滚动到底部
_scrollToBottom(); _scrollToBottom();
} else {
// 更新历史记录 // 播放TTS - 只在最终结果时播放
final index = translationHistory.indexWhere((item) => item.sourceText == sourceText); if (isTtsEnabled.value && isFinal) {
if (index != -1) { // Logger.info('播放翻译文本: ${translationResult}');
translationHistory[index].translatedText = translationResult; await playTranslatedText(translationResult);
translationHistory[index].sourceLanguageCode = detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode = detectedTargetLanguageCode;
translationHistory.refresh();
saveTranslationHistory();
// 更新后滚动到底部
_scrollToBottom();
} }
} else {
Logger.info('未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}');
} }
} }
} catch (e) { } catch (e) {
@ -428,7 +443,7 @@ class TranslationController extends GetxController {
// 滚动到底部 // 滚动到底部
void _scrollToBottom() { void _scrollToBottom() {
if (scrollController.hasClients && (translationHistory.isNotEmpty || currentItem.value != null)) { if (scrollController.hasClients && (translationHistory.isNotEmpty || currentSessionId != null)) {
try { try {
scrollController.animateTo( scrollController.animateTo(
scrollController.position.maxScrollExtent, scrollController.position.maxScrollExtent,
@ -541,7 +556,14 @@ class TranslationController extends GetxController {
if (historyJson != null) { if (historyJson != null) {
try { try {
final List<dynamic> historyList = jsonDecode(historyJson); final List<dynamic> historyList = jsonDecode(historyJson);
translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList(); final List<TranslationItem> allHistory = historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 只保留最新的10条记录
if (allHistory.length > _maxHistoryItems) {
translationHistory.value = allHistory.sublist(allHistory.length - _maxHistoryItems);
} else {
translationHistory.value = allHistory;
}
// 使用微任务确保在视图完全渲染后滚动到底部 // 使用微任务确保在视图完全渲染后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -578,6 +600,12 @@ class TranslationController extends GetxController {
void scrollToBottom({bool animate = true}) { void scrollToBottom({bool animate = true}) {
_scrollToBottom(); _scrollToBottom();
} }
/// 处理原生语音交互开始事件
void _handleVoiceInteractionStarted() {
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS
stopAll();
}
} }
// 翻译项模型 // 翻译项模型

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

@ -167,128 +167,96 @@ class TranslationView extends GetView<TranslationController> {
return Obx(() { return Obx(() {
final itemCount = controller.translationHistory.length; final itemCount = controller.translationHistory.length;
// 使用 currentItem 来判断是否显示当前项
final hasCurrentItem = controller.currentItem.value != null &&
controller
.currentItem.value!.sourceText.isNotEmpty; // 只有当有实际内容时才显示当前项
// 计算底部内边距,确保内容不会被底部框遮住
final bottomPadding = controller.bottomBarHeight + 20.h; // 使用控制栏高度加上额外间距
return ListView.builder( return ListView.builder(
controller: controller.scrollController, controller: controller.scrollController,
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 16.w, right: 16.w, top: 16.h, bottom: bottomPadding), left: 16.w,
itemCount: itemCount + (hasCurrentItem ? 1 : 0), right: 16.w,
top: 16.h,
bottom: 16.h,
),
itemCount: itemCount,
itemBuilder: (context, index) { itemBuilder: (context, index) {
// Current item (being translated) // 获取当前项
if (hasCurrentItem && index == itemCount) {
return _buildTranslationItemWidget(
controller.currentItem.value!.sourceText,
controller.currentItem.value!.translatedText,
true,
isIntermediate: controller.currentItem.value!.isIntermediate,
isDarkMode: isDarkMode,
);
}
// History items
final historyItem = controller.translationHistory[index]; final historyItem = controller.translationHistory[index];
return _buildTranslationItemWidget(
historyItem.sourceText, // 构建项目widget
historyItem.translatedText, return _buildTranslationItemWidget(context, historyItem);
false,
item: historyItem,
isIntermediate: false, // 历史项总是显示为最终结果
isDarkMode: isDarkMode,
);
}, },
); );
}); });
} }
Widget _buildTranslationItemWidget( Widget _buildTranslationItemWidget(
String sourceText, BuildContext context, TranslationItem item) {
String translatedText, return Container(
bool isCurrent, { margin: const EdgeInsets.only(bottom: 8),
TranslationItem? item, padding: const EdgeInsets.all(12),
bool isIntermediate = false,
required bool isDarkMode,
}) {
// 使用AnimatedContainer实现平滑过渡,但恢复原来的视觉样式
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.only(bottom: 24.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.grey[100], color: item.isIntermediate
borderRadius: BorderRadius.circular(12.r), ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色
: (item.sourceLanguageCode == controller.sourceLanguageCode.value
? Theme.of(context)
.colorScheme
.primary
.withOpacity(0.08) // 源语言匹配时使用主题色底色
: Theme.of(context)
.colorScheme
.secondary
.withOpacity(0.08)), // 否则使用次要色底色
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: item.isIntermediate
? Colors.grey.withOpacity(0.2)
: Colors.transparent,
width: 1,
),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Source text // 源文本
AnimatedContainer( Text(
duration: const Duration(milliseconds: 300), item.sourceText,
width: double.infinity, style: TextStyle(
padding: EdgeInsets.all(16.r), fontSize: 16,
child: Text( color: Theme.of(context).textTheme.bodyLarge?.color,
sourceText.isEmpty && isCurrent ? '等待语音输入...' : sourceText, ),
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
// 翻译文本
Text(
item.translatedText,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16,
color: sourceText.isEmpty && isCurrent color: Theme.of(context).textTheme.bodyMedium?.color,
? (isDarkMode ? Colors.grey[400] : Colors.grey) fontWeight: FontWeight.w500,
: (isDarkMode ? Colors.white : Colors.black),
fontWeight: FontWeight.normal,
), ),
), ),
), ],
// 语言信息和时间戳
// Divider const SizedBox(height: 4),
Divider( Row(
height: 1, mainAxisAlignment: MainAxisAlignment.spaceBetween,
thickness: 1, children: [
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), Text(
'${item.sourceLanguageName} → ${item.targetLanguageName}',
// Translated text style: TextStyle(
AnimatedContainer( fontSize: 12,
duration: const Duration(milliseconds: 300), color: Theme.of(context).textTheme.bodySmall?.color,
width: double.infinity,
padding: EdgeInsets.all(16.r),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
translatedText.isEmpty && isCurrent
? '翻译结果将显示在这里...'
: translatedText,
style: TextStyle(
fontSize: 16.sp,
color: translatedText.isEmpty && isCurrent
? (isDarkMode ? Colors.grey[400] : Colors.grey)
: (isDarkMode ? Colors.white : Colors.black),
fontStyle: FontStyle.normal,
),
),
), ),
if (translatedText.isNotEmpty) ),
IconButton( if (!item.isIntermediate)
icon: Icon( GestureDetector(
Icons.volume_up, onTap: () => controller.playTranslationItem(item),
color: isDarkMode ? AppColors.primary : Colors.blue, child: Icon(
size: 20.sp, Icons.volume_up,
), size: 18,
onPressed: () { color: Theme.of(context).colorScheme.primary,
if (isCurrent) {
controller.playTranslatedText(translatedText);
} else if (item != null) {
controller.playTranslationItem(item);
}
},
), ),
], ),
), ],
), ),
], ],
), ),

24
macos/Podfile.lock

@ -1,7 +1,4 @@
PODS: PODS:
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1): - audio_session (0.0.1):
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
@ -26,14 +23,15 @@ PODS:
- FlutterMacOS - FlutterMacOS
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- sqflite_darwin (0.0.4): - speech_to_text (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Try
- Try (2.1.1)
- window_size (0.0.2): - window_size (0.0.2):
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
@ -45,12 +43,14 @@ DEPENDENCIES:
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
SPEC REPOS:
trunk:
- Try
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_service:
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin
audio_session: audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
device_info_plus: device_info_plus:
@ -73,13 +73,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
sqflite_darwin: speech_to_text:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin
window_size: window_size:
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
@ -91,7 +90,8 @@ SPEC CHECKSUMS:
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9
PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073 PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073

8
pubspec.yaml

@ -16,14 +16,11 @@ dependencies:
get_storage: ^2.1.1 get_storage: ^2.1.1
http: ^1.3.0 http: ^1.3.0
flutter_screenutil: ^5.9.0 flutter_screenutil: ^5.9.0
just_audio: ^0.9.36
flutter_azure_tts: ^0.2.3
path_provider: ^2.1.5 path_provider: ^2.1.5
audio_session: ^0.1.18 audio_session: ^0.1.18
flutter_markdown: ^0.6.18+2 flutter_markdown: ^0.6.18+2
markdown: ^7.1.1 markdown: ^7.1.1
permission_handler: ^11.4.0 permission_handler: ^11.4.0
audio_service: ^0.18.12
web_socket_channel: ^3.0.2 web_socket_channel: ^3.0.2
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
synchronized: ^3.1.0 synchronized: ^3.1.0
@ -32,8 +29,9 @@ dependencies:
flutter_tts: ^4.2.2 flutter_tts: ^4.2.2
logger: ^2.0.2+1 logger: ^2.0.2+1
flutter_blue_plus: ^1.31.16 flutter_blue_plus: ^1.31.16
# shared_preferences: ^2.2.2 # 移除,使用 get_storage 代替 just_audio: ^0.9.36
# speech_to_text: ^7.0.0 # 移除,使用自定义的 ASR 服务
window_size: window_size:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git

30
test/widget_test.dart

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:deep_voice/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MainApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Loading…
Cancel
Save