Browse Source

v007

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
063702bb81
  1. 3
      android/app/build.gradle.kts
  2. 6
      android/app/src/main/AndroidManifest.xml
  3. 597
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  4. 83
      android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt
  5. 172
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  6. 450
      android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt
  7. 237
      android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt
  8. 2
      lib/core/bindings/initial_binding.dart
  9. 117
      lib/data/services/azure_asr_service.dart
  10. 3
      lib/data/services/azure_tts_service.dart
  11. 10
      lib/data/services/classic_bluetooth_service.dart
  12. 2
      lib/modules/chat/controllers/chat_controller.dart
  13. 64
      lib/modules/pairing/controllers/pairing_controller.dart
  14. 32
      lib/modules/test/controllers/azure_asr_test_controller.dart
  15. 47
      lib/modules/translation/controllers/translation_controller.dart

3
android/app/build.gradle.kts

@ -65,6 +65,9 @@ dependencies {
// 添加legacy-support-v4依赖 // 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
// 添加 AndroidX Security 加密 SharedPreferences 依赖
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// // 添加火山语音合成SDK依赖 // // 添加火山语音合成SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8") // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")

6
android/app/src/main/AndroidManifest.xml

@ -17,8 +17,10 @@
<!-- Android 13+ 通知权限 --> <!-- Android 13+ 通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- 添加存储权限 --> <!-- 添加存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- 添加Android 11+的存储权限 --> <!-- 添加Android 11+的存储权限 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />

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

@ -1,20 +1,15 @@
package com.example.deep_voice package com.example.deep_voice
import android.util.Log import android.content.Context
import com.example.deep_voice.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 android.media.audiofx.AcousticEchoCanceler
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.AudioFormat
import java.util.concurrent.Executors
class AzureAsrHelper { class AzureAsrHelper(private val context: Context) {
private var recognizer: SpeechRecognizer? = null private var recognizer: SpeechRecognizer? = null
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var audioConfig: AudioConfig? = null
private val TAG = "AzureAsrHelper" private val TAG = "AzureAsrHelper"
private var isContinuousRecognitionActive = false private var isContinuousRecognitionActive = false
private var currentLanguage = "zh-CN" private var currentLanguage = "zh-CN"
@ -23,33 +18,25 @@ class AzureAsrHelper {
private var isAutoDetectLanguage = false private var isAutoDetectLanguage = false
private var supportedLanguages = arrayOf("zh-CN", "en-US") private var supportedLanguages = arrayOf("zh-CN", "en-US")
// 回音消除相关 // 初始化SDK并创建recognizer
private var echoCanceler: AcousticEchoCanceler? = null
private var audioSessionId: Int = -1
private var isEchoCancellationEnabled = false
// 直接管理 AudioRecord 和 PushStream
private var audioRecord: AudioRecord? = null
private var pushStream: PushAudioInputStream? = null
private var isRecording = false
private val executorService = Executors.newSingleThreadExecutor()
// 初始化 SDK
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 {
try { try {
Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, supportedLanguages=${supportedLanguages.joinToString()}") FileLogger.d(TAG, "初始化 Azure 语音服务")
// 检查配置是否为空 // 检查配置是否为空
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) { if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) {
Log.e(TAG, "Azure 配置信息不完整: subscriptionKey=${subscriptionKey.isEmpty()}, serviceRegion=${serviceRegion.isEmpty()}") FileLogger.e(TAG, "Azure 配置信息不完整")
return false return false
} }
// 释放之前的资源
dispose()
this.subscriptionKey = subscriptionKey this.subscriptionKey = subscriptionKey
this.serviceRegion = serviceRegion this.serviceRegion = serviceRegion
// 设置支持的语言列表 // 设置语言
if (supportedLanguages.isNotEmpty()) { if (supportedLanguages.isNotEmpty()) {
this.supportedLanguages = supportedLanguages this.supportedLanguages = supportedLanguages
} }
@ -62,320 +49,178 @@ class AzureAsrHelper {
this.currentLanguage = supportedLanguages[0] this.currentLanguage = supportedLanguages[0]
} }
// 释放之前的资源
dispose()
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
// 设置为 CONVERSATION 模式 // 设置语言配置
speechConfig?.setProperty(PropertyId.SpeechServiceConnection_RecoMode, "CONVERSATION")
if (isAutoDetectLanguage) { if (isAutoDetectLanguage) {
// 设置自动语言检测模式为连续检测 // 设置自动语言检测
speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous") speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous")
} else { } else {
// 设置指定的识别语言 // 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage speechConfig?.speechRecognitionLanguage = currentLanguage
} }
// 创建 PushAudioInputStream 和 AudioConfig // 创建识别器
try { try {
setupAudioInput() if (isAutoDetectLanguage) {
Log.d(TAG, "成功创建音频配置") val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
FileLogger.d(TAG, "Azure 语音服务初始化成功")
return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "创建音频配置失败: ${e.message}", e) FileLogger.e(TAG, "创建识别器失败: ${e.message}")
return false return false
} }
// 创建识别器
Log.d(TAG, "创建语音识别器")
if (isAutoDetectLanguage) {
// 创建自动语言检测配置
val autoDetectSourceLanguageConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectSourceLanguageConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
Log.d(TAG, "Azure 语音服务初始化成功")
return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "初始化失败: ${e.message}", e) FileLogger.e(TAG, "初始化失败: ${e.message}")
return false return false
} }
} }
// 设置音频输入 // 重置 recognizer
private fun setupAudioInput() { private fun resetRecognizer(): Boolean {
// 释放之前的资源 try {
releaseAudioResources() // 释放之前的 recognizer
recognizer?.close()
// 创建 PushAudioInputStream recognizer = null
pushStream = AudioInputStream.createPushStream()
// 创建 AudioConfig
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 创建 AudioRecord
val sampleRate = 16000
val channelConfig = AudioFormat.CHANNEL_IN_MONO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
if (bufferSize <= 0) {
Log.e(TAG, "无法获取最小缓冲区大小")
throw RuntimeException("无法获取最小缓冲区大小")
}
audioRecord = AudioRecord( // 使用现有配置重新创建 recognizer
MediaRecorder.AudioSource.VOICE_COMMUNICATION, if (speechConfig != null) {
sampleRate, if (isAutoDetectLanguage) {
channelConfig, val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
audioFormat, recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
bufferSize } else {
) recognizer = SpeechRecognizer(speechConfig)
}
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { return true
Log.e(TAG, "AudioRecord 初始化失败") } else {
audioRecord?.release() FileLogger.e(TAG, "语音配置未初始化")
audioRecord = null return false
throw RuntimeException("AudioRecord 初始化失败") }
} catch (e: Exception) {
FileLogger.e(TAG, "重置识别器失败: ${e.message}")
return false
} }
// 获取音频会话 ID 并设置回音消除
audioSessionId = audioRecord?.audioSessionId ?: -1
setupEchoCancellation()
} }
// 开始一次性语音识别 // 开始一次性语音识别
fun recognizeOnce(language: String? = null, callback: RecognizeCallback) { fun recognizeOnce(callback: RecognizeCallback) {
if (recognizer == null) { if (speechConfig == null) {
callback.onError("SpeechRecognizer 未初始化") callback.onError("语音服务未初始化")
return return
} }
// 如果指定了新的语言,且不是自动检测模式,需要重新初始化 // 重置 recognizer
if (language != null && language != currentLanguage && !isAutoDetectLanguage) { if (!resetRecognizer()) {
// 使用指定的语言创建新的语言列表 callback.onError("重置识别器失败")
val newLanguages = arrayOf(language) return
initialize(subscriptionKey, serviceRegion, newLanguages)
} }
try { try {
// 开始录音并推送数据 // 执行识别
startRecording()
val result = recognizer?.recognizeOnceAsync()?.get() val result = recognizer?.recognizeOnceAsync()?.get()
// 停止录音
stopRecording()
if (result != null && result.reason == ResultReason.RecognizedSpeech) { if (result != null && result.reason == ResultReason.RecognizedSpeech) {
val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(result) val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language
val detectedLanguage = autoDetectSourceLanguageResult?.language ?: ""
Log.d(TAG, "识别结果: ${result.text}, 检测到的语言: $detectedLanguage") callback.onResult(result.text, detectedLanguage ?: "")
callback.onResult(result.text, detectedLanguage)
} else { } else {
callback.onError("未能识别语音") callback.onError("未能识别语音")
} }
} catch (e: Exception) { } catch (e: Exception) {
stopRecording()
callback.onError("识别异常: ${e.message}") callback.onError("识别异常: ${e.message}")
} }
} }
// 开始连续语音识别 // 开始连续语音识别
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (recognizer == null) { if (speechConfig == null) {
Log.e(TAG, "SpeechRecognizer 未初始化") callback.onError("语音服务未初始化")
callback.onError("SpeechRecognizer 未初始化")
return false return false
} }
// 如果已经在进行连续识别,先停止 // 如果已经在进行连续识别,先停止
if (isContinuousRecognitionActive) { if (isContinuousRecognitionActive) {
Log.d(TAG, "已经在进行连续识别,先停止")
stopContinuousRecognition(callback) stopContinuousRecognition(callback)
} }
try { // 重置 recognizer
Log.d(TAG, "开始重置识别器和音频资源") if (!resetRecognizer()) {
callback.onError("重置识别器失败")
// 完全释放所有资源,确保干净的状态 return false
try { }
// 释放识别器
recognizer?.close()
recognizer = null
// 释放音频资源
releaseAudioResources()
// 确保状态标志正确
isContinuousRecognitionActive = false
isRecording = false
Log.d(TAG, "资源释放完成,准备重新创建")
} catch (e: Exception) {
Log.e(TAG, "释放资源失败: ${e.message}", e)
// 继续执行,尝试重新创建
}
// 重新创建音频配置
try {
setupAudioInput()
Log.d(TAG, "音频配置创建成功")
} catch (e: Exception) {
Log.e(TAG, "创建音频配置失败: ${e.message}", e)
callback.onError("创建音频配置失败: ${e.message}")
return false
}
// 重新创建识别器
try {
Log.d(TAG, "创建新的语音识别器")
if (isAutoDetectLanguage) {
// 创建自动语言检测配置
val autoDetectSourceLanguageConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectSourceLanguageConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} catch (e: Exception) {
Log.e(TAG, "创建识别器失败: ${e.message}", e)
callback.onError("创建识别器失败: ${e.message}")
return false
}
try {
// 设置识别事件处理 // 设置识别事件处理
recognizer?.let { recognizer -> // 最终识别结果
// 最终识别结果 recognizer?.recognized?.addEventListener(
recognizer.recognized.addEventListener( EventHandler<SpeechRecognitionEventArgs> { _, event ->
EventHandler<SpeechRecognitionEventArgs> { _, event -> if (event.result.reason == ResultReason.RecognizedSpeech) {
if (event.result.reason == ResultReason.RecognizedSpeech) { val detectedLanguage = if (isAutoDetectLanguage) {
val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result) AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
val detectedLanguage = autoDetectSourceLanguageResult?.language ?: ""
Log.d(TAG, "识别结果: ${event.result.text}, 检测到的语言: $detectedLanguage")
callback.onResult(event.result.text, detectedLanguage)
} else { } else {
Log.d(TAG, "识别结果原因: ${event.result.reason}") currentLanguage
} }
callback.onResult(event.result.text, detectedLanguage)
} }
) }
)
// 识别中事件
recognizer.recognizing.addEventListener( // 识别中事件
EventHandler<SpeechRecognitionEventArgs> { _, event -> recognizer?.recognizing?.addEventListener(
if (event.result.reason == ResultReason.RecognizingSpeech) { EventHandler<SpeechRecognitionEventArgs> { _, event ->
val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result) if (event.result.reason == ResultReason.RecognizingSpeech) {
val detectedLanguage = autoDetectSourceLanguageResult?.language ?: "" val detectedLanguage = if (isAutoDetectLanguage) {
// if (isAutoDetectLanguage) { AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
// Log.d(TAG, "正在识别: ${event.result.text}, 检测到的语言: $detectedLanguage") } else {
// } currentLanguage
callback.onRecognizing(event.result.text, detectedLanguage)
} }
callback.onRecognizing(event.result.text, detectedLanguage)
} }
) }
)
// 会话开始事件
recognizer.sessionStarted.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
Log.d(TAG, "会话开始")
isContinuousRecognitionActive = true
callback.onSessionStarted()
}
)
// 会话结束事件
recognizer.sessionStopped.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
Log.d(TAG, "会话结束")
isContinuousRecognitionActive = false
stopRecording()
callback.onSessionStopped()
}
)
// 取消事件
recognizer.canceled.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else ""
Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails")
callback.onCanceled(event.reason.toString(), errorDetails)
isContinuousRecognitionActive = false
stopRecording()
}
)
// 开始录音 // 会话事件
startRecording() recognizer?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = true
callback.onSessionStarted()
}
)
// 开始连续识别 recognizer?.sessionStopped?.addEventListener(
Log.d(TAG, "开始连续识别") EventHandler<SessionEventArgs> { _, _ ->
recognizer.startContinuousRecognitionAsync().get() isContinuousRecognitionActive = false
Log.d(TAG, "连续识别已启动") callback.onSessionStopped()
isContinuousRecognitionActive = true }
} )
// 取消事件
recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else ""
callback.onCanceled(event.reason.toString(), errorDetails)
isContinuousRecognitionActive = false
}
)
// 开始连续识别
recognizer?.startContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = true
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "开始连续识别失败: ${e.message}", e)
callback.onError("开始连续识别失败: ${e.message}") callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
return false return false
} }
} }
// 开始录音并推送数据到 PushStream
private fun startRecording() {
if (isRecording || audioRecord == null || pushStream == null) {
return
}
isRecording = true
audioRecord?.startRecording()
executorService.submit {
val bufferSize = AudioRecord.getMinBufferSize(
16000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
val buffer = ByteArray(bufferSize)
while (isRecording && audioRecord != null && pushStream != null) {
try {
val readBytes = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (readBytes > 0) {
// 修复:PushAudioInputStream.write() 只接受一个 ByteArray 参数
// 如果读取的数据长度等于缓冲区大小,直接写入
if (readBytes == buffer.size) {
pushStream?.write(buffer)
} else {
// 否则,创建一个新的数组,只包含实际读取的数据
val audioData = buffer.copyOf(readBytes)
pushStream?.write(audioData)
}
}
} catch (e: Exception) {
Log.e(TAG, "读取音频数据失败: ${e.message}", e)
break
}
}
}
}
// 停止录音
private fun stopRecording() {
isRecording = false
try {
audioRecord?.stop()
} catch (e: Exception) {
Log.e(TAG, "停止录音失败: ${e.message}", e)
}
}
// 停止连续语音识别 // 停止连续语音识别
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (!isContinuousRecognitionActive || recognizer == null) { if (!isContinuousRecognitionActive || recognizer == null) {
@ -385,13 +230,11 @@ class AzureAsrHelper {
try { try {
recognizer?.stopContinuousRecognitionAsync()?.get() recognizer?.stopContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
callback.onSessionStopped() callback.onSessionStopped()
return true return true
} catch (e: Exception) { } catch (e: Exception) {
callback.onError("停止连续识别失败: ${e.message}") callback.onError("停止连续识别失败: ${e.message}")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
return false return false
} }
} }
@ -401,96 +244,21 @@ class AzureAsrHelper {
return isContinuousRecognitionActive return isContinuousRecognitionActive
} }
// 获取当前语言 // 释放所有资源
fun getCurrentLanguage(): String {
return currentLanguage
}
// 释放资源
fun dispose() { fun dispose() {
try { try {
// 添加标志,避免重复释放 recognizer?.close()
if (recognizer == null && audioRecord == null && pushStream == null) { recognizer = null
Log.d(TAG, "资源已经释放,跳过重复释放")
return
}
Log.d(TAG, "开始释放资源")
if (isContinuousRecognitionActive && recognizer != null) {
try {
recognizer?.stopContinuousRecognitionAsync()?.get()
Log.d(TAG, "连续识别已停止")
} catch (e: Exception) {
Log.e(TAG, "停止连续识别失败: ${e.message}", e)
}
}
try {
recognizer?.close()
recognizer = null
Log.d(TAG, "识别器已关闭")
} catch (e: Exception) {
Log.e(TAG, "关闭识别器失败: ${e.message}", e)
}
releaseAudioResources() speechConfig?.close()
speechConfig = null
try {
speechConfig?.close()
speechConfig = null
Log.d(TAG, "语音配置已关闭")
} catch (e: Exception) {
Log.e(TAG, "关闭语音配置失败: ${e.message}", e)
}
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
Log.d(TAG, "所有资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源时出错: ${e.message}", e)
}
}
// 释放音频相关资源
private fun releaseAudioResources() {
// 停止录音
stopRecording()
// 释放 AudioRecord
if (audioRecord != null) {
try {
Log.d(TAG, "释放 AudioRecord")
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(TAG, "释放 AudioRecord 失败: ${e.message}", e)
}
}
// 释放 AudioConfig FileLogger.d(TAG, "语音识别资源已释放")
if (audioConfig != null) { } catch (e: Exception) {
try { FileLogger.e(TAG, "释放资源出错: ${e.message}")
Log.d(TAG, "释放 AudioConfig")
audioConfig?.close()
audioConfig = null
} catch (e: Exception) {
Log.e(TAG, "释放 AudioConfig 失败: ${e.message}", e)
}
}
// 释放 PushStream
if (pushStream != null) {
try {
Log.d(TAG, "释放 PushStream")
pushStream?.close()
pushStream = null
} catch (e: Exception) {
Log.e(TAG, "释放 PushStream 失败: ${e.message}", e)
}
} }
// 释放回音消除资源
releaseEchoCancellation()
} }
// 一次性识别回调接口 // 一次性识别回调接口
@ -509,127 +277,4 @@ class AzureAsrHelper {
fun onError(error: String) fun onError(error: String)
} }
// 设置回音消除
private fun setupEchoCancellation() {
try {
// 检查设备是否支持回音消除
if (AcousticEchoCanceler.isAvailable()) {
Log.d(TAG, "设备支持回音消除")
// 使用实际的 AudioRecord 会话 ID
if (audioSessionId != -1) {
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler != null) {
echoCanceler?.enabled = true
isEchoCancellationEnabled = echoCanceler?.enabled == true
Log.d(TAG, "回音消除已启用: $isEchoCancellationEnabled")
} else {
Log.e(TAG, "创建回音消除器失败")
}
} else {
Log.e(TAG, "无效的音频会话 ID")
}
} else {
Log.d(TAG, "设备不支持回音消除")
}
} catch (e: Exception) {
Log.e(TAG, "设置回音消除时出错: ${e.message}", e)
}
}
// 释放回音消除资源
private fun releaseEchoCancellation() {
try {
if (echoCanceler != null) {
echoCanceler?.enabled = false
echoCanceler?.release()
echoCanceler = null
isEchoCancellationEnabled = false
audioSessionId = -1
Log.d(TAG, "回音消除资源已释放")
}
} catch (e: Exception) {
Log.e(TAG, "释放回音消除资源时出错: ${e.message}", e)
}
}
// 设置回音消除开关
fun setEchoCancellationEnabled(enabled: Boolean): Boolean {
try {
if (echoCanceler != null) {
echoCanceler?.enabled = enabled
isEchoCancellationEnabled = echoCanceler?.enabled == true
Log.d(TAG, "回音消除状态已设置为: $isEchoCancellationEnabled")
return isEchoCancellationEnabled == enabled
} else if (enabled) {
// 如果当前没有回音消除器但需要启用,尝试创建
if (audioSessionId != -1) {
setupEchoCancellation()
return isEchoCancellationEnabled
}
}
return false
} catch (e: Exception) {
Log.e(TAG, "设置回音消除状态时出错: ${e.message}", e)
return false
}
}
// 获取回音消除状态
fun isEchoCancellationEnabled(): Boolean {
return isEchoCancellationEnabled
}
// 设置支持的语言列表(用于自动语言检测)
fun setSupportedLanguages(languages: Array<String>) {
if (languages.isNotEmpty()) {
Log.d(TAG, "设置支持的语言列表: ${languages.joinToString()}")
// 如果当前是自动检测模式且识别器已初始化,需要重新初始化以应用新的语言列表
if (isAutoDetectLanguage && recognizer != null) {
Log.d(TAG, "自动检测模式下更新语言列表,重新初始化识别器")
initialize(subscriptionKey, serviceRegion, languages)
} else {
// 仅更新语言列表,不重新初始化
supportedLanguages = languages
// 如果不是自动检测模式且只有一种语言,更新当前语言
if (!isAutoDetectLanguage && languages.size == 1) {
currentLanguage = languages[0]
}
}
}
}
// 获取当前是否启用自动语言检测
fun isAutoDetectLanguageEnabled(): Boolean {
return isAutoDetectLanguage
}
// 启用自动语言检测并设置支持的语言列表
fun enableAutoDetectLanguage(enable: Boolean, languages: Array<String>? = null): Boolean {
// 准备语言列表
val languagesToUse = if (languages != null && languages.isNotEmpty()) {
// 如果提供了新的语言列表,使用新的列表
languages
} else if (enable && supportedLanguages.size < 2) {
// 如果要启用自动检测但当前支持的语言少于2种,使用默认的两种语言
arrayOf("zh-CN", "en-US")
} else {
// 否则使用当前的语言列表
supportedLanguages
}
// 如果要禁用自动检测,但提供了多种语言,只使用第一种语言
val finalLanguages = if (!enable && languagesToUse.size > 1) {
arrayOf(languagesToUse[0])
} else {
languagesToUse
}
Log.d(TAG, "设置自动语言检测: enable=$enable, languages=${finalLanguages.joinToString()}")
// 重新初始化以应用新的设置
return initialize(subscriptionKey, serviceRegion, finalLanguages)
}
} }

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

@ -5,7 +5,7 @@ import android.media.AudioAttributes
import android.media.AudioDeviceInfo import android.media.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.util.Log import com.example.deep_voice.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 java.util.concurrent.Future import java.util.concurrent.Future
@ -17,12 +17,11 @@ import java.util.concurrent.Semaphore
* 该类封装了微软语音 SDK 的 TTS 功能,提供简单的接口供 Flutter 调用 * 该类封装了微软语音 SDK 的 TTS 功能,提供简单的接口供 Flutter 调用
* 基于微软官方文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis * 基于微软官方文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/ */
class AzureTtsHelper { class AzureTtsHelper(private val context: Context) {
private val TAG = "AzureTtsHelper" private val TAG = "AzureTtsHelper"
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null private var synthesizer: SpeechSynthesizer? = null
private var isInitialized = false private var isInitialized = false
private var context: Context? = null
private var audioManager: AudioManager? = null private var audioManager: AudioManager? = null
// 音频输出设备类型 // 音频输出设备类型
@ -50,14 +49,11 @@ class AzureTtsHelper {
* @param subscriptionKey Azure 语音服务订阅密钥 * @param subscriptionKey Azure 语音服务订阅密钥
* @param serviceRegion Azure 语音服务区域 * @param serviceRegion Azure 语音服务区域
* @param language 可选,默认语言,默认为 "zh-CN" * @param language 可选,默认语言,默认为 "zh-CN"
* @param context Android 上下文,用于获取音频服务
*/ */
fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN", context: Context? = null): Boolean { fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean {
try { try {
this.context = context audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (context != null) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
}
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
@ -82,7 +78,7 @@ class AzureTtsHelper {
synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() }
isInitialized = true isInitialized = true
Log.d(TAG, "TTS 引擎初始化成功") FileLogger.d(TAG, "TTS 引擎初始化成功")
// 设置默认音频输出类型为自动 // 设置默认音频输出类型为自动
if (audioManager != null) { if (audioManager != null) {
@ -91,7 +87,7 @@ class AzureTtsHelper {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "TTS 引擎初始化失败: ${e.message}") FileLogger.e(TAG, "TTS 引擎初始化失败: ${e.message}, ${serviceRegion}")
e.printStackTrace() e.printStackTrace()
return false return false
} }
@ -105,12 +101,12 @@ class AzureTtsHelper {
*/ */
fun setAudioOutputType(outputType: AudioOutputType): Boolean { fun setAudioOutputType(outputType: AudioOutputType): Boolean {
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化") FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false return false
} }
if (audioManager == null) { if (audioManager == null) {
Log.e(TAG, "AudioManager 未初始化,无法设置音频输出设备") FileLogger.e(TAG, "AudioManager 未初始化,无法设置音频输出设备")
return false return false
} }
@ -122,13 +118,13 @@ class AzureTtsHelper {
// 使用扬声器 // 使用扬声器
audioManager?.mode = AudioManager.MODE_NORMAL audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = true audioManager?.isSpeakerphoneOn = true
Log.d(TAG, "已设置音频输出设备为扬声器") FileLogger.d(TAG, "已设置音频输出设备为扬声器")
} }
AudioOutputType.EARPIECE -> { AudioOutputType.EARPIECE -> {
// 使用听筒 // 使用听筒
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager?.isSpeakerphoneOn = false audioManager?.isSpeakerphoneOn = false
Log.d(TAG, "已设置音频输出设备为听筒") FileLogger.d(TAG, "已设置音频输出设备为听筒")
} }
AudioOutputType.AUTO -> { AudioOutputType.AUTO -> {
// 检查是否有耳机连接 // 检查是否有耳机连接
@ -136,12 +132,12 @@ class AzureTtsHelper {
// 有耳机,使用耳机 // 有耳机,使用耳机
audioManager?.mode = AudioManager.MODE_NORMAL audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = false audioManager?.isSpeakerphoneOn = false
Log.d(TAG, "已设置音频输出设备为耳机") FileLogger.d(TAG, "已设置音频输出设备为耳机")
} else { } else {
// 无耳机,使用听筒 // 无耳机,使用听筒
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager?.isSpeakerphoneOn = false audioManager?.isSpeakerphoneOn = false
Log.d(TAG, "已设置音频输出设备为听筒") FileLogger.d(TAG, "已设置音频输出设备为听筒")
} }
} }
} }
@ -151,7 +147,7 @@ class AzureTtsHelper {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "设置音频输出设备失败: ${e.message}") FileLogger.e(TAG, "设置音频输出设备失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
return false return false
} }
@ -196,9 +192,9 @@ class AzureTtsHelper {
synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() }
synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() }
Log.d(TAG, "已重新创建语音合成器") FileLogger.d(TAG, "已重新创建语音合成器")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "重新创建语音合成器失败: ${e.message}") FileLogger.e(TAG, "重新创建语音合成器失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -210,10 +206,13 @@ class AzureTtsHelper {
*/ */
fun setVoice(voiceName: String): Boolean { fun setVoice(voiceName: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化") FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false return false
} }
if (voiceName == currentVoiceName) {
FileLogger.d(TAG, "已设置语音: $voiceName")
return true
}
try { try {
currentVoiceName = voiceName currentVoiceName = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName) speechConfig?.setSpeechSynthesisVoiceName(voiceName)
@ -221,10 +220,10 @@ class AzureTtsHelper {
// 重新创建合成器以应用新的语音设置 // 重新创建合成器以应用新的语音设置
recreateSynthesizer() recreateSynthesizer()
Log.d(TAG, "已设置语音: $voiceName") FileLogger.d(TAG, "已设置语音: $voiceName")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "设置语音失败: ${e.message}") FileLogger.e(TAG, "设置语音失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
return false return false
} }
@ -239,7 +238,7 @@ class AzureTtsHelper {
*/ */
fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean { fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean {
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化") FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false return false
} }
@ -248,10 +247,10 @@ class AzureTtsHelper {
currentPitch = if (pitch == 0) "0%" else "${(pitch * 0.5).toInt()}%" currentPitch = if (pitch == 0) "0%" else "${(pitch * 0.5).toInt()}%"
currentVolume = "${volume.coerceIn(0, 100)}%" currentVolume = "${volume.coerceIn(0, 100)}%"
Log.d(TAG, "已设置语音参数: 语速=$currentSpeechRate, 音调=$currentPitch, 音量=$currentVolume") FileLogger.d(TAG, "已设置语音参数: 语速=$currentSpeechRate, 音调=$currentPitch, 音量=$currentVolume")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "设置语音参数失败: ${e.message}") FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
return false return false
} }
@ -270,12 +269,8 @@ class AzureTtsHelper {
} }
try { try {
Log.d(TAG, "开始合成文本: $text") FileLogger.d(TAG, "开始合成文本: $text")
// 如果设置为自动模式,检查耳机状态并更新设置
if (currentAudioOutputType == AudioOutputType.AUTO) {
setAudioOutputType(AudioOutputType.AUTO)
}
// 生成 SSML // 生成 SSML
val ssml = generateSsml(text) val ssml = generateSsml(text)
@ -283,7 +278,7 @@ class AzureTtsHelper {
// 使用 SSML 合成语音 // 使用 SSML 合成语音
speakSsml(ssml, callback) speakSsml(ssml, callback)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "语音合成异常: ${e.message}") FileLogger.e(TAG, "语音合成异常: ${e.message}")
e.printStackTrace() e.printStackTrace()
callback.onError("语音合成异常: ${e.message}") callback.onError("语音合成异常: ${e.message}")
} }
@ -320,7 +315,7 @@ class AzureTtsHelper {
} }
try { try {
Log.d(TAG, "开始合成 SSML") FileLogger.d(TAG, "开始合成 SSML")
// 重置信号量 // 重置信号量
semaphore.drainPermits() semaphore.drainPermits()
@ -339,29 +334,29 @@ class AzureTtsHelper {
when (result.reason) { when (result.reason) {
ResultReason.SynthesizingAudioCompleted -> { ResultReason.SynthesizingAudioCompleted -> {
Log.d(TAG, "语音合成完成,音频长度: ${result.audioLength / 10000}ms,音频数据大小: ${result.audioData?.size ?: 0} 字节") FileLogger.d(TAG, "语音合成完成,音频长度: ${result.audioLength / 10000}ms,音频数据大小: ${result.audioData?.size ?: 0} 字节")
callback.onSuccess("语音合成完成") callback.onSuccess("语音合成完成")
} }
ResultReason.Canceled -> { ResultReason.Canceled -> {
val cancellation = SpeechSynthesisCancellationDetails.fromResult(result) val cancellation = SpeechSynthesisCancellationDetails.fromResult(result)
Log.e(TAG, "语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") FileLogger.e(TAG, "语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}")
callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}")
} }
else -> { else -> {
Log.e(TAG, "语音合成失败: ${result.reason}") FileLogger.e(TAG, "语音合成失败: ${result.reason}")
callback.onError("语音合成失败: ${result.reason}") callback.onError("语音合成失败: ${result.reason}")
} }
} }
result.close() result.close()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "处理语音合成结果异常: ${e.message}") FileLogger.e(TAG, "处理语音合成结果异常: ${e.message}")
e.printStackTrace() e.printStackTrace()
callback.onError("处理语音合成结果异常: ${e.message}") callback.onError("处理语音合成结果异常: ${e.message}")
} }
}.start() }.start()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "语音合成异常: ${e.message}") FileLogger.e(TAG, "语音合成异常: ${e.message}")
e.printStackTrace() e.printStackTrace()
callback.onError("语音合成异常: ${e.message}") callback.onError("语音合成异常: ${e.message}")
} }
@ -372,17 +367,17 @@ class AzureTtsHelper {
*/ */
fun stopSpeaking(): Boolean { fun stopSpeaking(): Boolean {
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化") FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false return false
} }
try { try {
synthesizer?.StopSpeakingAsync() synthesizer?.StopSpeakingAsync()
Log.d(TAG, "已停止语音合成") FileLogger.d(TAG, "已停止语音合成")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止语音合成失败: ${e.message}") FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
return false return false
} }
@ -404,9 +399,9 @@ class AzureTtsHelper {
} }
isInitialized = false isInitialized = false
Log.d(TAG, "TTS 引擎已释放") FileLogger.d(TAG, "TTS 引擎已释放")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "释放 TTS 引擎失败: ${e.message}") FileLogger.e(TAG, "释放 TTS 引擎失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
} }

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

@ -17,6 +17,9 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.example.deep_voice.core.utils.FileLogger
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
@ -140,15 +143,112 @@ class MainActivity: FlutterActivity() {
var azureSpeechKey: String = "" var azureSpeechKey: String = ""
var azureSpeechRegion: String = "" var azureSpeechRegion: String = ""
var volcanoAiApiKey: String = "" var volcanoAiApiKey: String = ""
// 安全存储相关常量
private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs"
private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key"
private const val KEY_AZURE_SPEECH_REGION = "azure_speech_region"
private const val KEY_VOLCANO_AI_API_KEY = "volcano_ai_api_key"
// 会话管理
private const val KEY_SESSION_ID = "session_id"
private var currentSessionId = ""
/**
* 将密钥安全地存储到 EncryptedSharedPreferences
*/
fun saveKeysToSecureStorage(context: Context) {
try {
// 创建或获取主密钥
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// 创建 EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
SECURE_PREFS_FILENAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 生成新的会话ID
if (currentSessionId.isEmpty()) {
currentSessionId = java.util.UUID.randomUUID().toString()
}
// 保存密钥和会话ID
sharedPreferences.edit()
.putString(KEY_AZURE_SPEECH_KEY, azureSpeechKey)
.putString(KEY_AZURE_SPEECH_REGION, azureSpeechRegion)
.putString(KEY_VOLCANO_AI_API_KEY, volcanoAiApiKey)
.putString(KEY_SESSION_ID, currentSessionId)
.apply()
FileLogger.d("MainActivity", "密钥已安全保存到加密存储中")
} catch (e: Exception) {
FileLogger.e("MainActivity", "保存密钥到加密存储失败: ${e.message}")
e.printStackTrace()
}
}
/**
* 从 EncryptedSharedPreferences 获取密钥
*/
fun loadKeysFromSecureStorage(context: Context): Boolean {
try {
// 创建或获取主密钥
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// 获取 EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
SECURE_PREFS_FILENAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 检查会话ID是否匹配
val savedSessionId = sharedPreferences.getString(KEY_SESSION_ID, "") ?: ""
// 如果会话ID为空或不匹配,说明是新会话或应用被重启
if (savedSessionId.isEmpty() || currentSessionId.isEmpty() || savedSessionId != currentSessionId) {
// 会话不匹配,但尝试加载密钥,不返回错误
FileLogger.d("MainActivity", "会话ID不匹配,但尝试加载密钥")
// 设置当前会话ID为保存的会话ID
currentSessionId = savedSessionId
}
// 加载密钥
azureSpeechKey = sharedPreferences.getString(KEY_AZURE_SPEECH_KEY, "") ?: ""
azureSpeechRegion = sharedPreferences.getString(KEY_AZURE_SPEECH_REGION, "") ?: ""
volcanoAiApiKey = sharedPreferences.getString(KEY_VOLCANO_AI_API_KEY, "") ?: ""
FileLogger.d("MainActivity", "已从加密存储加载密钥")
// 检查是否成功获取所有密钥
return azureSpeechKey.isNotEmpty() && azureSpeechRegion.isNotEmpty() && volcanoAiApiKey.isNotEmpty()
} catch (e: Exception) {
FileLogger.e("MainActivity", "从加密存储加载密钥失败: ${e.message}")
e.printStackTrace()
return false
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// 初始化本地的 Azure 语音服务实例 // 初始化 FileLogger
Log.d(TAG, "初始化本地的 Azure 语音服务实例") FileLogger.init(applicationContext)
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper() // 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(applicationContext)
azureTtsHelper = AzureTtsHelper(applicationContext)
classicBluetoothHelper = ClassicBluetoothHelper(applicationContext) classicBluetoothHelper = ClassicBluetoothHelper(applicationContext)
// 注册广播接收器 // 注册广播接收器
@ -191,6 +291,10 @@ class MainActivity: FlutterActivity() {
// 设置方法通道 // 设置方法通道
setupMethodChannels(flutterEngine) setupMethodChannels(flutterEngine)
// 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(this)
azureTtsHelper = AzureTtsHelper(this)
Log.d(TAG, "Flutter 引擎配置完成") Log.d(TAG, "Flutter 引擎配置完成")
} }
@ -268,8 +372,6 @@ class MainActivity: FlutterActivity() {
"initialize" -> { "initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") val subscriptionKey = call.argument<String>("subscriptionKey")
val region = call.argument<String>("region") val region = call.argument<String>("region")
val language = call.argument<String>("language") ?: "zh-CN"
val autoDetectLanguage = call.argument<Boolean>("autoDetectLanguage") ?: false
val supportedLanguages = call.argument<List<String>>("supportedLanguages")?.toTypedArray() ?: arrayOf("zh-CN", "en-US") val supportedLanguages = call.argument<List<String>>("supportedLanguages")?.toTypedArray() ?: arrayOf("zh-CN", "en-US")
if (subscriptionKey == null || region == null) { if (subscriptionKey == null || region == null) {
@ -285,9 +387,8 @@ class MainActivity: FlutterActivity() {
} }
} }
"recognizeOnce" -> { "recognizeOnce" -> {
val language = call.argument<String>("language")
azureAsrHelper.recognizeOnce(language, object : AzureAsrHelper.RecognizeCallback { azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) { override fun onResult(text: String, detectedLanguage: String) {
result.success(mapOf( result.success(mapOf(
"text" to text, "text" to text,
@ -375,37 +476,6 @@ class MainActivity: FlutterActivity() {
azureAsrHelper.dispose() azureAsrHelper.dispose()
result.success(true) result.success(true)
} }
"enableAutoDetectLanguage" -> {
val enable = call.argument<Boolean>("enable") ?: false
val languages = call.argument<List<String>>("languages")?.toTypedArray()
try {
val success = azureAsrHelper.enableAutoDetectLanguage(enable, languages)
result.success(success)
} catch (e: Exception) {
result.error("AUTO_DETECT_ERROR", e.message, null)
}
}
"setSupportedLanguages" -> {
val languages = call.argument<List<String>>("languages")?.toTypedArray()
if (languages == null || languages.isEmpty()) {
result.error("INVALID_ARGUMENTS", "语言列表不能为空", null)
return@setMethodCallHandler
}
try {
azureAsrHelper.setSupportedLanguages(languages)
result.success(true)
} catch (e: Exception) {
result.error("SET_LANGUAGES_ERROR", e.message, null)
}
}
"isAutoDetectLanguageEnabled" -> {
result.success(azureAsrHelper.isAutoDetectLanguageEnabled())
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
@ -416,21 +486,12 @@ class MainActivity: FlutterActivity() {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"initialize" -> { "initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("serviceRegion") val region = call.argument<String>("region") ?: ""
val language = call.argument<String>("language") ?: "zh-CN" val language = call.argument<String>("language") ?: "zh-CN"
if (subscriptionKey == null || region == null) { val success = azureTtsHelper.initialize(subscriptionKey, region, language)
result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null) result.success(success)
return@setMethodCallHandler
}
try {
val success = azureTtsHelper.initialize(subscriptionKey, region, language, this)
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
} }
"setVoice" -> { "setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "语音名称不能为空", null) val voiceName = call.argument<String>("voiceName") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
@ -601,7 +662,7 @@ class MainActivity: FlutterActivity() {
* 启动语音交互服务(带配置参数) * 启动语音交互服务(带配置参数)
*/ */
private fun startVoiceInteractionService(call: MethodCall, result: MethodChannel.Result) { private fun startVoiceInteractionService(call: MethodCall, result: MethodChannel.Result) {
Log.d(TAG, "启动语音交互服务") FileLogger.d(TAG, "启动语音交互服务")
// 获取配置参数 // 获取配置参数
val key = call.argument<String>("azure_speech_key") ?: "" val key = call.argument<String>("azure_speech_key") ?: ""
@ -613,6 +674,9 @@ class MainActivity: FlutterActivity() {
azureSpeechRegion = region azureSpeechRegion = region
volcanoAiApiKey = aiKey volcanoAiApiKey = aiKey
// 保存密钥到安全存储
saveKeysToSecureStorage(applicationContext)
// 创建 Intent 并启动服务 // 创建 Intent 并启动服务
val serviceIntent = Intent(this, VoiceInteractionService::class.java) val serviceIntent = Intent(this, VoiceInteractionService::class.java)
@ -623,6 +687,7 @@ class MainActivity: FlutterActivity() {
startService(serviceIntent) startService(serviceIntent)
} }
// 返回成功结果
result.success(true) result.success(true)
} }
@ -716,6 +781,7 @@ class MainActivity: FlutterActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
Log.d(TAG, "onDestroy")
// 注销所有广播接收器 // 注销所有广播接收器
try { try {
unregisterReceiver(voiceInteractionReceiver) unregisterReceiver(voiceInteractionReceiver)

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

@ -25,6 +25,7 @@ import org.json.JSONArray
import android.media.MediaPlayer import android.media.MediaPlayer
import android.media.AudioAttributes import android.media.AudioAttributes
import android.net.Uri import android.net.Uri
import com.example.deep_voice.core.utils.FileLogger
/** /**
* 后台语音交互 Service: * 后台语音交互 Service:
@ -71,6 +72,8 @@ class VoiceInteractionService : Service() {
// 当前用户输入 // 当前用户输入
private var currentUserInput = "" private var currentUserInput = ""
private var testKey = ""
// 服务组件 // 服务组件
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager private lateinit var audioManager: AudioManager
@ -96,12 +99,14 @@ class VoiceInteractionService : Service() {
用户用语音和你交互. 用户用语音和你交互.
""".trimIndent() """.trimIndent()
// 添加无声媒体播放器 // 添加媒体播放器
private var silentPlayer: SilentMediaPlayer? = null private var audioPlayer: MiniMediaPlayer? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(TAG, "onCreate - 语音交互服务正在创建") // 初始化文件日志系统
FileLogger.init(applicationContext)
FileLogger.d(TAG, "onCreate - 语音交互服务正在创建")
// 初始化状态 // 初始化状态
isRunning.set(true) isRunning.set(true)
@ -109,31 +114,25 @@ class VoiceInteractionService : Service() {
// 初始化组件 // 初始化组件
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.d(TAG, "AudioManager初始化完成") FileLogger.d(TAG, "AudioManager初始化完成")
initServices() initServices()
initMediaSession() initMediaSession()
registerMediaButtonReceiver() registerMediaButtonReceiver()
// 创建音频播放器
silentPlayer = SilentMediaPlayer(applicationContext) audioPlayer = MiniMediaPlayer(applicationContext)
silentPlayer?.start() audioPlayer?.play(R.raw.silence, 0f)
// Log.d(TAG, "无声播放器已启动")
// 设置为媒体播放状态 // 设置为媒体播放状态
setPlaybackState(PlaybackStateCompat.STATE_PLAYING) // setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
Log.d(TAG, "设置播放状态为PLAYING") // FileLogger.d(TAG, "设置播放状态为STATE_PAUSED")
// 启动监控和前台服务 // 启动监控和前台服务
startMonitoring() startMonitoring()
startForegroundServiceWithNotification() startForegroundServiceWithNotification()
Log.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...") FileLogger.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...")
// 延迟3秒后测试按键处理功能
// Handler(Looper.getMainLooper()).postDelayed({
// testMediaButtonEvent()
// }, 3000)
} }
/** /**
@ -152,31 +151,56 @@ class VoiceInteractionService : Service() {
*/ */
private fun initServices() { private fun initServices() {
// 创建新的Azure服务实例 // 创建新的Azure服务实例
Log.d(TAG, "创建新的Azure服务实例") FileLogger.d(TAG, "创建新的Azure服务实例")
azureAsrHelper = AzureAsrHelper() azureAsrHelper = AzureAsrHelper(this)
azureTtsHelper = AzureTtsHelper() azureTtsHelper = AzureTtsHelper(this)
// 尝试从静态变量获取配置
var subscriptionKey = MainActivity.azureSpeechKey
var serviceRegion = MainActivity.azureSpeechRegion
var volcanoKey = MainActivity.volcanoAiApiKey
// 从 MainActivity 获取 Azure 配置 // 如果静态变量中没有配置,尝试从加密存储中加载
val subscriptionKey = MainActivity.azureSpeechKey if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || volcanoKey.isEmpty()) {
val serviceRegion = MainActivity.azureSpeechRegion FileLogger.d(TAG, "静态变量中的配置信息不完整,尝试从加密存储加载")
// 从加密存储加载密钥
val keysLoaded = MainActivity.loadKeysFromSecureStorage(applicationContext)
if (keysLoaded) {
// 更新本地变量
subscriptionKey = MainActivity.azureSpeechKey
serviceRegion = MainActivity.azureSpeechRegion
volcanoKey = MainActivity.volcanoAiApiKey
FileLogger.d(TAG, "已从加密存储加载配置信息")
} else {
FileLogger.e(TAG, "无法从加密存储加载配置信息")
}
}
// 初始化语音服务
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) {
// 初始化ASR // 初始化ASR
azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN")) azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN"))
// 初始化TTS // 初始化TTS
azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
FileLogger.d(TAG, "Azure语音服务已初始化")
} else { } else {
Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务") FileLogger.e(TAG, "Azure配置信息不完整,无法初始化Azure服务")
} }
// 初始化火山AI服务 // 初始化火山AI服务
volcanoAIService = VolcanoAIService() volcanoAIService = VolcanoAIService()
// 使用 MainActivity 中的 API key 初始化
val volcanoApiKey = MainActivity.volcanoAiApiKey // 初始化火山AI服务
if (volcanoApiKey.isNotEmpty()) { if (volcanoKey.isNotEmpty()) {
volcanoAIService.initialize(volcanoApiKey) volcanoAIService.initialize(volcanoKey)
FileLogger.d(TAG, "火山AI服务已初始化")
} else { } else {
Log.e(TAG, "火山AI配置信息不完整,无法初始化火山AI服务") FileLogger.e(TAG, "火山AI配置信息不完整,无法初始化火山AI服务")
} }
} }
@ -184,7 +208,7 @@ class VoiceInteractionService : Service() {
* 初始化 MediaSession * 初始化 MediaSession
*/ */
private fun initMediaSession() { private fun initMediaSession() {
Log.d(TAG, "开始初始化MediaSession") FileLogger.d(TAG, "开始初始化MediaSession")
mediaSession = MediaSessionCompat(applicationContext, "VoiceServiceSession") mediaSession = MediaSessionCompat(applicationContext, "VoiceServiceSession")
// 设置标志和状态 // 设置标志和状态
@ -192,91 +216,24 @@ class VoiceInteractionService : Service() {
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
) )
Log.d(TAG, "MediaSession标志设置完成") FileLogger.d(TAG, "MediaSession标志设置完成")
// 设置回调 // 设置回调
mediaSession.setCallback(object : MediaSessionCompat.Callback() { mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
Log.d(TAG, "收到媒体按钮事件: $mediaButtonEvent") FileLogger.d(TAG, "initMediaSession收到媒体按钮事件: $mediaButtonEvent")
handleDoubleClick() handleDoubleClick()
return true return true
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
} else {
@Suppress("DEPRECATION")
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent
}
if (keyEvent != null) {
Log.d(TAG, "解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY -> {
Log.d(TAG, "检测到MEDIA_PLAY按键")
handleDoubleClick()
return true
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
Log.d(TAG, "检测到MEDIA_PAUSE按键")
handleDoubleClick()
return true
}
KeyEvent.KEYCODE_HEADSETHOOK -> {
Log.d(TAG, "检测到HEADSETHOOK按键")
// 检测双击
val currentTime = System.currentTimeMillis()
val timeDiff = currentTime - lastKeyEventTime
Log.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount")
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++
Log.d(TAG, "连续按键,计数增加到: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,执行处理")
handleDoubleClick()
keyEventCount = 0
return true
}
} else {
Log.d(TAG, "非连续按键,重置计数")
keyEventCount = 0
}
lastKeyEventTime = currentTime
// 单击也处理
Log.d(TAG, "单击也处理")
handleDoubleClick()
return true
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(TAG, "检测到MEDIA_PLAY_PAUSE按键")
handleDoubleClick()
return true
}
else -> {
Log.d(TAG, "检测到其他按键: ${keyEvent.keyCode}")
}
}
} else {
Log.d(TAG, "非ACTION_DOWN事件: ${keyEvent.action}")
}
} else {
Log.d(TAG, "Intent中没有KeyEvent")
}
return super.onMediaButtonEvent(mediaButtonEvent)
} }
override fun onPlay() { override fun onPlay() {
Log.d(TAG, "收到onPlay回调") FileLogger.d(TAG, "收到onPlay回调")
handleDoubleClick() handleDoubleClick()
return super.onPlay() return super.onPlay()
} }
override fun onPause() { override fun onPause() {
Log.d(TAG, "收到onPause回调") FileLogger.d(TAG, "收到onPause回调")
handleDoubleClick() handleDoubleClick()
return super.onPause() return super.onPause()
} }
@ -284,7 +241,7 @@ class VoiceInteractionService : Service() {
// 激活 MediaSession // 激活 MediaSession
mediaSession.isActive = true mediaSession.isActive = true
Log.d(TAG, "MediaSession已激活") FileLogger.d(TAG, "MediaSession已激活")
} }
/** /**
@ -294,9 +251,9 @@ class VoiceInteractionService : Service() {
try { try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
Log.d(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") FileLogger.d(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
} }
} }
@ -330,7 +287,7 @@ class VoiceInteractionService : Service() {
// 如果超过5秒没有检测到语音,且不在TTS播放中,暂停语音识别 // 如果超过5秒没有检测到语音,且不在TTS播放中,暂停语音识别
if (!hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT) { if (!hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT) {
Log.d(TAG, "超过${RECOGNITION_TIMEOUT/1000}秒未检测到语音,停止识别") FileLogger.d(TAG, "超过${RECOGNITION_TIMEOUT/1000}秒未检测到语音,停止识别")
isTimeoutPaused = true isTimeoutPaused = true
playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。") playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。")
@ -348,13 +305,13 @@ class VoiceInteractionService : Service() {
* 处理双击事件 * 处理双击事件
*/ */
private fun handleDoubleClick() { private fun handleDoubleClick() {
Log.d(TAG, "处理双击事件") FileLogger.d(TAG, "处理双击事件")
//屏蔽短时间重复响应的问题 //屏蔽短时间重复响应的问题
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
val timeDiff = currentTime - lastKeyEventTime val timeDiff = currentTime - lastKeyEventTime
if (timeDiff < 500) { if (timeDiff < 500) {
Log.d(TAG, "短时间重复响应,忽略") FileLogger.d(TAG, "短时间重复响应,忽略")
return return
} }
lastKeyEventTime = currentTime lastKeyEventTime = currentTime
@ -363,42 +320,28 @@ class VoiceInteractionService : Service() {
stopCurrentTTS() stopCurrentTTS()
// 播放提示音 // 播放提示音
playPrompt("我在!") // playPrompt("我在!")
audioPlayer?.play(R.raw.listening)
// 设置为播放状态 // 设置为播放状态
setPlaybackState(PlaybackStateCompat.STATE_PLAYING) // setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
// 重置超时暂停标志 // 重置超时暂停标志
isTimeoutPaused = false isTimeoutPaused = false
// 启动或重置语音识别 // 启动或重置语音识别
if (!isRecognitionActive) { if (!isRecognitionActive) {
Log.d(TAG, "语音识别未激活,开始启动") FileLogger.d(TAG, "语音识别未激活,开始启动")
// 如果之前是因为超时暂停,重新初始化语音识别组件 // 如果之前是因为超时暂停,重新初始化语音识别组件
if (isTimeoutPaused) { // if (isTimeoutPaused) {
Log.d(TAG, "之前因超时暂停,重新初始化Azure服务") // Log.d(TAG, "之前因超时暂停,重新初始化Azure服务")
// 释放旧的实例 // restartAsr()
try { // }
azureAsrHelper.dispose()
} catch (e: Exception) {
Log.e(TAG, "释放旧的Azure ASR实例失败: ${e.message}", e)
}
// 创建新的实例
azureAsrHelper = AzureAsrHelper()
// 重新初始化
val subscriptionKey = MainActivity.azureSpeechKey
val serviceRegion = MainActivity.azureSpeechRegion
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) {
azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN"))
}
}
startVoiceRecognition() startVoiceRecognition()
} else { } else {
Log.d(TAG, "语音识别已激活,更新活动时间") FileLogger.d(TAG, "语音识别已激活,更新活动时间")
updateLastActivityTime() updateLastActivityTime()
hasSpeechDetected = false hasSpeechDetected = false
} }
@ -415,7 +358,7 @@ class VoiceInteractionService : Service() {
playNotification("需要录音权限,请在设置中授予权限") playNotification("需要录音权限,请在设置中授予权限")
return return
} }
// 通知 Flutter 语音识别已启动 // 通知 Flutter 语音识别已启动
notifyRecognitionStarted() notifyRecognitionStarted()
isActive = true isActive = true
@ -468,6 +411,7 @@ class VoiceInteractionService : Service() {
}) })
} catch (e: Exception) { } catch (e: Exception) {
isRecognitionActive = false isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}", e)
playNotification("启动语音识别失败") playNotification("启动语音识别失败")
@ -480,7 +424,7 @@ class VoiceInteractionService : Service() {
private fun stopVoiceRecognition() { private fun stopVoiceRecognition() {
if (!isRecognitionActive) return if (!isRecognitionActive) return
Log.d(TAG, "停止语音识别") FileLogger.d(TAG, "停止语音识别")
try { try {
azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
@ -489,19 +433,19 @@ class VoiceInteractionService : Service() {
override fun onSessionStarted() {} override fun onSessionStarted() {}
override fun onSessionStopped() { override fun onSessionStopped() {
isRecognitionActive = false isRecognitionActive = false
Log.d(TAG, "语音识别会话已停止") FileLogger.d(TAG, "语音识别会话已停止")
} }
override fun onCanceled(reason: String, errorDetails: String) { override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false isRecognitionActive = false
Log.d(TAG, "语音识别已取消: $reason") FileLogger.d(TAG, "语音识别已取消: $reason")
} }
override fun onError(error: String) { override fun onError(error: String) {
isRecognitionActive = false isRecognitionActive = false
Log.e(TAG, "停止语音识别时出错: $error") FileLogger.e(TAG, "停止语音识别时出错: $error")
} }
}) })
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止语音识别异常: ${e.message}", e) FileLogger.e(TAG, "停止语音识别异常: ${e.message}", e)
// 确保状态一致性 // 确保状态一致性
isRecognitionActive = false isRecognitionActive = false
} }
@ -533,7 +477,7 @@ class VoiceInteractionService : Service() {
// 同步聊天记录到Flutter端 // 同步聊天记录到Flutter端
notifyChatHistoryUpdated("personal_assistant", text, response) notifyChatHistoryUpdated("personal_assistant", text, response)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "AI处理出错: ${e.message}") FileLogger.e(TAG, "AI处理出错: ${e.message}")
playNotification("AI处理出错") playNotification("AI处理出错")
} }
}.start() }.start()
@ -689,11 +633,11 @@ class VoiceInteractionService : Service() {
* 当 Service 被启动时调用 * 当 Service 被启动时调用
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: intent=$intent, action=${intent?.action}") FileLogger.d(TAG, "onStartCommand: intent=$intent, action=${intent?.action}")
// 处理媒体按键 Intent // 处理媒体按键 Intent
if (intent?.action == Intent.ACTION_MEDIA_BUTTON) { if (intent?.action == Intent.ACTION_MEDIA_BUTTON) {
Log.d(TAG, "收到媒体按钮Intent: ${intent.extras}") FileLogger.d(TAG, "收到媒体按钮Intent: ${intent.extras}")
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
@ -703,26 +647,26 @@ class VoiceInteractionService : Service() {
} }
if (keyEvent != null) { if (keyEvent != null) {
Log.d(TAG, "从Intent中解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") FileLogger.d(TAG, "从Intent中解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
handleKeyEvent(keyEvent) handleKeyEvent(keyEvent)
} else { } else {
Log.d(TAG, "Intent中没有按键事件,直接处理双击") FileLogger.d(TAG, "Intent中没有按键事件,直接处理双击")
handleDoubleClick() handleDoubleClick()
} }
} else if (intent?.action == ACTION_PAUSE_VOICE_INTERACTION) { } else if (intent?.action == ACTION_PAUSE_VOICE_INTERACTION) {
// 处理来自 Flutter 的暂停请求 // 处理来自 Flutter 的暂停请求
Log.d(TAG, "收到暂停请求") FileLogger.d(TAG, "收到暂停请求")
pauseVoiceInteraction() pauseVoiceInteraction()
} else { } else {
Log.d(TAG, "收到其他Intent: action=${intent?.action}") FileLogger.d(TAG, "收到其他Intent: action=${intent?.action}")
} }
// 让 MediaSession 处理 Intent // 让 MediaSession 处理 Intent
try { try {
MediaButtonReceiver.handleIntent(mediaSession, intent) MediaButtonReceiver.handleIntent(mediaSession, intent)
Log.d(TAG, "MediaButtonReceiver处理Intent完成") FileLogger.d(TAG, "MediaButtonReceiver处理Intent完成")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "MediaButtonReceiver处理Intent失败: ${e.message}", e) FileLogger.e(TAG, "MediaButtonReceiver处理Intent失败: ${e.message}", e)
} }
return START_STICKY return START_STICKY
@ -732,50 +676,50 @@ class VoiceInteractionService : Service() {
* 处理按键事件 * 处理按键事件
*/ */
private fun handleKeyEvent(keyEvent: KeyEvent) { private fun handleKeyEvent(keyEvent: KeyEvent) {
Log.d(TAG, "处理按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") FileLogger.d(TAG, "处理按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
if (keyEvent.action == KeyEvent.ACTION_DOWN) { if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) { when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY -> { KeyEvent.KEYCODE_MEDIA_PLAY -> {
Log.d(TAG, "处理MEDIA_PLAY按键") FileLogger.d(TAG, "处理MEDIA_PLAY按键")
handleDoubleClick() handleDoubleClick()
} }
KeyEvent.KEYCODE_MEDIA_PAUSE -> { KeyEvent.KEYCODE_MEDIA_PAUSE -> {
Log.d(TAG, "处理MEDIA_PAUSE按键") FileLogger.d(TAG, "处理MEDIA_PAUSE按键")
handleDoubleClick() handleDoubleClick()
} }
KeyEvent.KEYCODE_HEADSETHOOK -> { KeyEvent.KEYCODE_HEADSETHOOK -> {
Log.d(TAG, "处理HEADSETHOOK按键") FileLogger.d(TAG, "处理HEADSETHOOK按键")
// 检测双击 // 检测双击
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
val timeDiff = currentTime - lastKeyEventTime val timeDiff = currentTime - lastKeyEventTime
Log.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount") FileLogger.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount")
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 if (timeDiff < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++ keyEventCount++
Log.d(TAG, "连续按键,计数增加到: $keyEventCount") FileLogger.d(TAG, "连续按键,计数增加到: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击 if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,执行处理") FileLogger.d(TAG, "检测到双击,执行处理")
handleDoubleClick() handleDoubleClick()
keyEventCount = 0 keyEventCount = 0
} }
} else { } else {
Log.d(TAG, "非连续按键,重置计数") FileLogger.d(TAG, "非连续按键,重置计数")
keyEventCount = 0 keyEventCount = 0
} }
lastKeyEventTime = currentTime lastKeyEventTime = currentTime
} }
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(TAG, "处理MEDIA_PLAY_PAUSE按键") FileLogger.d(TAG, "处理MEDIA_PLAY_PAUSE按键")
handleDoubleClick() handleDoubleClick()
} }
else -> { else -> {
Log.d(TAG, "处理其他按键: ${keyEvent.keyCode}") FileLogger.d(TAG, "处理其他按键: ${keyEvent.keyCode}")
} }
} }
} else { } else {
Log.d(TAG, "非ACTION_DOWN事件: ${keyEvent.action}") FileLogger.d(TAG, "非ACTION_DOWN事件: ${keyEvent.action}")
} }
} }
@ -783,11 +727,11 @@ class VoiceInteractionService : Service() {
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { override fun onDestroy() {
Log.d(TAG, "onDestroy - 语音交互服务正在销毁") FileLogger.d(TAG, "onDestroy - 语音交互服务正在销毁")
// 停止无声播放器 // 释放音频播放器
silentPlayer?.release() audioPlayer?.release()
silentPlayer = null audioPlayer = null
// 停止监控 // 停止监控
stopMonitoring() stopMonitoring()
@ -804,13 +748,16 @@ class VoiceInteractionService : Service() {
mediaSession.release() mediaSession.release()
// 释放Azure服务实例 // 释放Azure服务实例
Log.d(TAG, "释放Azure服务实例") FileLogger.d(TAG, "释放Azure服务实例")
azureAsrHelper.dispose() azureAsrHelper.dispose()
azureTtsHelper.dispose() azureTtsHelper.dispose()
// 更新服务状态 // 更新服务状态
isRunning.set(false) isRunning.set(false)
// 关闭日志系统
FileLogger.shutdown()
super.onDestroy() super.onDestroy()
} }
@ -819,7 +766,7 @@ class VoiceInteractionService : Service() {
* 用于从 Flutter 端控制服务 * 用于从 Flutter 端控制服务
*/ */
private fun pauseVoiceInteraction() { private fun pauseVoiceInteraction() {
Log.d(TAG, "暂停语音交互(来自Flutter的请求)") FileLogger.d(TAG, "暂停语音交互(来自Flutter的请求)")
// 停止当前TTS播放 // 停止当前TTS播放
stopCurrentTTS() stopCurrentTTS()
@ -838,9 +785,9 @@ class VoiceInteractionService : Service() {
* 通知 Flutter 端聊天记录已更新 * 通知 Flutter 端聊天记录已更新
*/ */
private fun notifyChatHistoryUpdated(agentId: String, userMessage: String, assistantMessage: String) { private fun notifyChatHistoryUpdated(agentId: String, userMessage: String, assistantMessage: String) {
Log.d(TAG, "通知Flutter聊天记录已更新: agentId=$agentId") FileLogger.d(TAG, "通知Flutter聊天记录已更新: agentId=$agentId")
Log.d(TAG, "用户消息: ${userMessage.take(50)}...") FileLogger.d(TAG, "用户消息: ${userMessage.take(50)}...")
Log.d(TAG, "助手回复: ${assistantMessage.take(50)}...") FileLogger.d(TAG, "助手回复: ${assistantMessage.take(50)}...")
// 创建广播 Intent // 创建广播 Intent
val intent = Intent(ACTION_CHAT_HISTORY_UPDATED).apply { val intent = Intent(ACTION_CHAT_HISTORY_UPDATED).apply {
@ -852,14 +799,14 @@ class VoiceInteractionService : Service() {
// 发送广播 // 发送广播
sendBroadcast(intent) sendBroadcast(intent)
Log.d(TAG, "已发送聊天记录广播") FileLogger.d(TAG, "已发送聊天记录广播")
} }
/** /**
* 通知 Flutter 端语音识别已启动 * 通知 Flutter 端语音识别已启动
*/ */
private fun notifyRecognitionStarted() { private fun notifyRecognitionStarted() {
Log.d(TAG, "通知Flutter语音识别已启动") FileLogger.d(TAG, "通知Flutter语音识别已启动")
// 创建广播 Intent // 创建广播 Intent
val intent = Intent(ACTION_RECOGNITION_STARTED).apply { val intent = Intent(ACTION_RECOGNITION_STARTED).apply {
@ -871,162 +818,77 @@ class VoiceInteractionService : Service() {
sendBroadcast(intent) sendBroadcast(intent)
} }
/**
* 测试媒体按钮事件处理
*/
private fun testMediaButtonEvent() {
Log.d(TAG, "开始测试媒体按钮事件处理")
try {
// 创建一个模拟的HEADSETHOOK按键事件
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK)
// 创建一个包含按键事件的Intent
val intent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(Intent.EXTRA_KEY_EVENT, keyEvent)
}
// 直接调用onMediaButtonEvent方法
Log.d(TAG, "直接调用onMediaButtonEvent方法")
val result = mediaSession.controller.dispatchMediaButtonEvent(keyEvent)
Log.d(TAG, "dispatchMediaButtonEvent结果: $result")
// 通过MediaButtonReceiver发送Intent
Log.d(TAG, "通过MediaButtonReceiver发送Intent")
MediaButtonReceiver.handleIntent(mediaSession, intent)
// 直接调用handleKeyEvent方法
Log.d(TAG, "直接调用handleKeyEvent方法")
handleKeyEvent(keyEvent)
Log.d(TAG, "测试媒体按钮事件处理完成")
} catch (e: Exception) {
Log.e(TAG, "测试媒体按钮事件处理失败: ${e.message}", e)
}
}
/** /**
* 无声媒体播放器类 * 简易媒体播放器,用于播放raw中的音频资源
* 用于播放一个1秒的静音音频,确保系统认为app在实际播放声音
*/ */
inner class SilentMediaPlayer(private val context: Context) { inner class MiniMediaPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var isPlayingAudio = false
init { /**
* 播放音频资源
* @param resId 资源ID (R.raw.xxx)
* @param volume 音量 0.0-1.0
*/
fun play(resId: Int, volume: Float = 1.0f) {
try { try {
setupMediaPlayer() // 释放之前的资源
} catch (e: Exception) { release()
Log.e(TAG, "初始化无声播放器失败: ${e.message}", e)
} // 创建播放器并设置资源
} mediaPlayer = MediaPlayer().apply {
// 设置音频资源
private fun setupMediaPlayer() { context.resources.openRawResourceFd(resId)?.use { fd ->
try { setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
// 创建一个MediaPlayer }
mediaPlayer = MediaPlayer()
// 设置一个自带的静音资源
val afd = context.resources.openRawResourceFd(R.raw.silence)
if (afd != null) {
mediaPlayer?.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
afd.close()
} else {
// 如果没有资源,尝试使用一个虚拟数据源
Log.w(TAG, "未找到静音资源,尝试使用虚拟数据源")
mediaPlayer?.setDataSource(context, Uri.parse("android.resource://${context.packageName}/raw/silence"))
}
// 设置循环播放
mediaPlayer?.isLooping = false
// 设置音量为0
mediaPlayer?.setVolume(0f, 0f)
// 设置音频特性
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
mediaPlayer?.setAudioAttributes(audioAttributes)
} else {
@Suppress("DEPRECATION")
mediaPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC)
}
// 准备播放 // 设置音量
mediaPlayer?.prepare() setVolume(volume.coerceIn(0f, 1f), volume.coerceIn(0f, 1f))
// 设置完成监听器 - 虽然设置了循环播放,但以防万一 // 播放完成后自动释放
mediaPlayer?.setOnCompletionListener { setOnCompletionListener { release() }
Log.d(TAG, "无声播放完成")
isPlayingAudio = false
release()
// start()
}
// 设置错误监听器 // 准备并播放
mediaPlayer?.setOnErrorListener { _, what, extra -> prepare()
Log.e(TAG, "无声播放错误: what=$what, extra=$extra") start()
isPlayingAudio = false
// 尝试重新创建
setupMediaPlayer()
true
} }
Log.d(TAG, "无声播放器初始化成功")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "设置无声播放器失败: ${e.message}", e) release()
mediaPlayer?.release()
mediaPlayer = null
}
}
fun start() {
try {
if (mediaPlayer == null) {
setupMediaPlayer()
}
if (mediaPlayer != null && !isPlayingAudio) {
mediaPlayer?.start()
isPlayingAudio = true
Log.d(TAG, "无声播放开始")
}
} catch (e: Exception) {
Log.e(TAG, "开始无声播放失败: ${e.message}", e)
} }
} }
/**
* 停止播放
*/
fun stop() { fun stop() {
try { try {
if (mediaPlayer != null && isPlayingAudio) { mediaPlayer?.apply {
mediaPlayer?.pause() if (isPlaying) stop()
isPlayingAudio = false
Log.d(TAG, "无声播放停止")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止无声播放失败: ${e.message}", e) // 忽略异常
} }
} }
/**
* 释放资源
*/
fun release() { fun release() {
try { try {
mediaPlayer?.stop() mediaPlayer?.apply {
mediaPlayer?.release() if (isPlaying) stop()
release()
}
mediaPlayer = null mediaPlayer = null
isPlayingAudio = false
Log.d(TAG, "无声播放器已释放")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "释放无声播放器失败: ${e.message}", e) // 忽略异常
} }
} }
fun isPlaying(): Boolean { /**
return isPlayingAudio && mediaPlayer != null * 是否正在播放
} */
fun isPlaying() = mediaPlayer?.isPlaying == true
} }
} }

237
android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt

@ -0,0 +1,237 @@
package com.example.deep_voice.core.utils
import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* 文件日志工具
* 将日志同时输出到Logcat和文件系统
*/
class FileLogger private constructor(context: Context) {
private val logDir: File
private var currentLogFile: File? = null
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
private val logExecutor: ExecutorService = Executors.newSingleThreadExecutor()
init {
// 初始化日志目录
logDir = File(context.getExternalFilesDir(null), "logs")
if (!logDir.exists()) {
logDir.mkdirs()
}
// 创建当天的日志文件
updateLogFile()
}
/**
* 更新日志文件为当天日期
*/
private fun updateLogFile() {
val today = dateFormat.format(Date())
val logFile = File(logDir, "log_$today.txt")
if (currentLogFile == null || currentLogFile?.absolutePath != logFile.absolutePath) {
currentLogFile = logFile
// 如果是新文件,添加文件头
if (!logFile.exists()) {
try {
logFile.createNewFile()
FileWriter(logFile, true).use { writer ->
writer.append("===== 日志开始: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} =====\n")
}
} catch (e: IOException) {
Log.e(TAG, "创建日志文件失败: ${e.message}")
}
}
}
}
/**
* 调试级别日志
*/
fun d(tag: String, message: String) {
// 输出到Logcat
Log.d(tag, message)
// 异步写入文件
logToFile("D", tag, message)
}
/**
* 信息级别日志
*/
fun i(tag: String, message: String) {
// 输出到Logcat
Log.i(tag, message)
// 异步写入文件
logToFile("I", tag, message)
}
/**
* 警告级别日志
*/
fun w(tag: String, message: String) {
// 输出到Logcat
Log.w(tag, message)
// 异步写入文件
logToFile("W", tag, message)
}
/**
* 错误级别日志
*/
fun e(tag: String, message: String, throwable: Throwable? = null) {
// 输出到Logcat
if (throwable != null) {
Log.e(tag, message, throwable)
} else {
Log.e(tag, message)
}
// 异步写入文件
val errorMsg = if (throwable != null) {
"$message\n${throwable.message}\n${throwable.stackTraceToString()}"
} else {
message
}
logToFile("E", tag, errorMsg)
}
/**
* 将日志写入文件
*/
private fun logToFile(level: String, tag: String, message: String) {
return
logExecutor.execute {
try {
// 检查是否需要更新日志文件
updateLogFile()
// 格式化日志内容
val timestamp = timeFormat.format(Date())
val logLine = "$timestamp $level/$tag: $message\n"
// 写入文件
if (currentLogFile != null && currentLogFile!!.exists()) {
FileWriter(currentLogFile!!, true).use { writer ->
writer.append(logLine)
}
}
} catch (e: IOException) {
Log.e(TAG, "写入日志文件失败: ${e.message}")
}
}
}
/**
* 清理旧日志文件
* @param daysToKeep 保留的天数
*/
fun cleanOldLogs(daysToKeep: Int = 7) {
logExecutor.execute {
try {
val currentTime = System.currentTimeMillis()
val maxAge = daysToKeep * 24 * 60 * 60 * 1000L
logDir.listFiles()?.forEach { file ->
if (file.isFile && file.name.startsWith("log_") && file.name.endsWith(".txt")) {
val fileAge = currentTime - file.lastModified()
if (fileAge > maxAge) {
val deleted = file.delete()
if (deleted) {
Log.d(TAG, "已删除旧日志文件: ${file.name}")
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "清理旧日志失败: ${e.message}")
}
}
}
/**
* 关闭日志系统
*/
fun shutdown() {
logExecutor.shutdown()
}
companion object {
private const val TAG = "FileLogger"
private var INSTANCE: FileLogger? = null
/**
* 初始化日志系统
*/
@Synchronized
fun init(context: Context) {
if (INSTANCE == null) {
INSTANCE = FileLogger(context.applicationContext)
d(TAG, "文件日志系统已初始化")
}
}
/**
* 调试日志
*/
fun d(tag: String, message: String) {
INSTANCE?.d(tag, message) ?: Log.d(tag, message)
}
/**
* 信息日志
*/
fun i(tag: String, message: String) {
INSTANCE?.i(tag, message) ?: Log.i(tag, message)
}
/**
* 警告日志
*/
fun w(tag: String, message: String) {
INSTANCE?.w(tag, message) ?: Log.w(tag, message)
}
/**
* 错误日志
*/
fun e(tag: String, message: String, throwable: Throwable? = null) {
INSTANCE?.e(tag, message, throwable) ?: run {
if (throwable != null) {
Log.e(tag, message, throwable)
} else {
Log.e(tag, message)
}
}
}
/**
* 清理旧日志
*/
fun cleanOldLogs(daysToKeep: Int = 7) {
INSTANCE?.cleanOldLogs(daysToKeep)
}
/**
* 关闭日志系统
*/
fun shutdown() {
INSTANCE?.shutdown()
INSTANCE = null
}
}
}

2
lib/core/bindings/initial_binding.dart

@ -37,6 +37,6 @@ class InitialBinding extends Bindings {
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), fenix: true); Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), fenix: true);
// 经典蓝牙服务 // 经典蓝牙服务
Get.put<ClassicBluetoothService>(ClassicBluetoothService()..initialize(), permanent: true); Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(), fenix: true);
} }
} }

117
lib/data/services/azure_asr_service.dart

@ -15,13 +15,7 @@ class AzureAsrService {
late final String _subscriptionKey; late final String _subscriptionKey;
late final String _serviceRegion; late final String _serviceRegion;
// 当前语言
String _currentLanguage = 'zh-CN';
String get currentLanguage => _currentLanguage;
// 自动语言检测相关
bool _isAutoDetectLanguageEnabled = false;
bool get isAutoDetectLanguageEnabled => _isAutoDetectLanguageEnabled;
List<String> _supportedLanguages = ['zh-CN', 'en-US']; List<String> _supportedLanguages = ['zh-CN', 'en-US'];
List<String> get supportedLanguages => _supportedLanguages; List<String> get supportedLanguages => _supportedLanguages;
@ -102,50 +96,35 @@ class AzureAsrService {
/// ///
/// 返回 true 表示初始化成功,否则抛出 PlatformException /// 返回 true 表示初始化成功,否则抛出 PlatformException
Future<bool> initialize({ Future<bool> initialize({
String language = 'zh-CN',
bool autoDetectLanguage = false,
List<String>? supportedLanguages, List<String>? supportedLanguages,
}) async { }) async {
_currentLanguage = language; try {
_isAutoDetectLanguageEnabled = autoDetectLanguage;
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages = supportedLanguages;
}
if (_isInitialized && if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_currentLanguage == language && _supportedLanguages = supportedLanguages;
_isAutoDetectLanguageEnabled == autoDetectLanguage) { }
return true;
}
if (_isInitialized) { // 检查是否需要重新初始化
await dispose(); if (_isInitialized) {
} await dispose();
}
try {
final bool result = await _channel.invokeMethod('initialize', { final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey, 'subscriptionKey': _subscriptionKey,
'region': _serviceRegion, 'region': _serviceRegion,
'language': language,
'autoDetectLanguage': autoDetectLanguage,
'supportedLanguages': _supportedLanguages, 'supportedLanguages': _supportedLanguages,
}); });
_isInitialized = result; _isInitialized = result;
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result; return result;
} catch (e) { } catch (e) {
Logger.error('语音识别初始化失败: $e'); Logger.error('Azure 语音识别服务初始化失败: $e');
_isInitialized = false; _isInitialized = false;
throw e; rethrow;
} }
} }
/// 设置识别语言
///
/// 注意:更改语言需要重新初始化识别器
Future<bool> setRecognitionLanguage(String language) async {
return initialize(language: language);
}
/// 执行一次性语音识别 /// 执行一次性语音识别
/// ///
@ -156,9 +135,7 @@ class AzureAsrService {
} }
try { try {
final result = await _channel.invokeMethod('recognizeOnce', { final result = await _channel.invokeMethod('recognizeOnce');
'language': _currentLanguage,
});
if (result is Map) { if (result is Map) {
final String text = result['text'] as String? ?? ''; final String text = result['text'] as String? ?? '';
@ -175,7 +152,7 @@ class AzureAsrService {
throw Exception('无效的识别结果格式'); throw Exception('无效的识别结果格式');
} catch (e) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: $e');
throw e; rethrow;
} }
} }
@ -187,6 +164,11 @@ class AzureAsrService {
await initialize(); await initialize();
} }
// 确保事件通道已准备好
if (!await _waitForEventChannel()) {
throw Exception('事件通道未准备好,无法开始连续识别');
}
if (_isContinuousRecognitionActive) { if (_isContinuousRecognitionActive) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
@ -194,6 +176,7 @@ class AzureAsrService {
try { try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast(); _eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 开始连续识别
final bool result = await _channel.invokeMethod('startContinuousRecognition'); final bool result = await _channel.invokeMethod('startContinuousRecognition');
if (!result) { if (!result) {
@ -206,7 +189,7 @@ class AzureAsrService {
} catch (e) { } catch (e) {
Logger.error('开始连续语音识别失败: $e'); Logger.error('开始连续语音识别失败: $e');
_cleanupEventStream(); _cleanupEventStream();
throw e; rethrow;
} }
} }
@ -225,7 +208,7 @@ class AzureAsrService {
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: $e');
_cleanupEventStream(); _cleanupEventStream();
throw e; rethrow;
} }
} }
@ -324,8 +307,11 @@ class AzureAsrService {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
// 通知原生端释放资源
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
_isInitialized = false; _isInitialized = false;
Logger.info('Azure 语音识别资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: $e');
_cleanupEventStream(); _cleanupEventStream();
@ -333,59 +319,6 @@ class AzureAsrService {
} }
} }
/// 启用或禁用自动语言检测
Future<bool> enableAutoDetectLanguage(bool enable, {List<String>? languages}) async {
if (!_isInitialized) {
await initialize(autoDetectLanguage: enable, supportedLanguages: languages);
return _isAutoDetectLanguageEnabled;
}
try {
final bool result = await _channel.invokeMethod('enableAutoDetectLanguage', {
'enable': enable,
'languages': languages,
});
if (result) {
_isAutoDetectLanguageEnabled = enable;
if (languages != null && languages.isNotEmpty) {
_supportedLanguages = languages;
}
}
return result;
} catch (e) {
Logger.error('设置自动语言检测失败: $e');
throw e;
}
}
/// 设置支持的语言列表(用于自动语言检测)
Future<bool> setSupportedLanguages(List<String> languages) async {
if (languages.isEmpty) {
throw ArgumentError('语言列表不能为空');
}
if (!_isInitialized) {
await initialize(supportedLanguages: languages);
return true;
}
try {
final bool result = await _channel.invokeMethod('setSupportedLanguages', {
'languages': languages,
});
if (result) {
_supportedLanguages = languages;
}
return result;
} catch (e) {
Logger.error('设置支持的语言列表失败: $e');
throw e;
}
}
} }
/// 识别事件类型 /// 识别事件类型

3
lib/data/services/azure_tts_service.dart

@ -70,6 +70,7 @@ class AzureTtsService extends GetxService {
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
} }
/// 初始化TTS服务 /// 初始化TTS服务
Future<bool> initialize({String language = 'zh-CN'}) async { Future<bool> initialize({String language = 'zh-CN'}) async {
if (_isInitialized) return true; if (_isInitialized) return true;
@ -82,7 +83,7 @@ class AzureTtsService extends GetxService {
final result = await _channel.invokeMethod('initialize', { final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey, 'subscriptionKey': _subscriptionKey,
'serviceRegion': _serviceRegion, 'region': _serviceRegion,
'language': language, 'language': language,
}); });

10
lib/data/services/classic_bluetooth_service.dart

@ -162,15 +162,7 @@ class ClassicBluetoothService extends GetxService {
try { try {
Logger.info('正在初始化 ClassicBluetoothService...'); Logger.info('正在初始化 ClassicBluetoothService...');
// 检查通道是否可用
try {
// 调用一个简单方法检查通道是否已设置
await _channel.invokeMethod<bool>('isBluetoothEnabled');
Logger.info('蓝牙方法通道可用');
} catch (e) {
Logger.error('蓝牙方法通道不可用: $e');
// 这里不要立即返回false,尝试继续设置其他内容
}
// 设置事件监听器 // 设置事件监听器
_setupBluetoothStateListener(); _setupBluetoothStateListener();

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

@ -418,8 +418,6 @@ class ChatController extends GetxController {
_voiceInteractionService.pauseVoiceInteraction(); _voiceInteractionService.pauseVoiceInteraction();
try { try {
// 初始化语音识别
await _asrService.initialize(language: 'zh-CN');
// 启动连续语音识别 // 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据

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

@ -1,3 +1,4 @@
import 'package:deep_voice/data/services/voice_interaction_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
@ -6,6 +7,7 @@ import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 /// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
class PairingController extends GetxController { class PairingController extends GetxController {
static PairingController get to => Get.find(); static PairingController get to => Get.find();
@ -52,41 +54,17 @@ class PairingController extends GetxController {
_initialize(); _initialize();
} }
@override
void onReady() {
super.onReady();
// 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题
_setupDeviceListeners();
}
/// 设置设备监听器 /// 完整的初始化流程
void _setupDeviceListeners() { Future<void> _initialize() async {
try { try {
if (!_isBluetoothServiceAvailable.value) return;
// 监听设备连接事件 VoiceInteractionService.to.initialize();
_bluetoothService.onDeviceConnected((device) { VoiceInteractionService.to.startService();
Logger.info('检测到蓝牙设备连接: ${device.name}');
// 更新状态并跳转到主页
statusDescription.value = '已连接到设备: ${device.name}';
// 立即跳转到主页
_navigateToHome();
});
} catch (e) {
Logger.error('设置设备监听器失败: $e');
}
}
/// 立即跳转到主页
void _navigateToHome() {
Get.offAllNamed(Routes.home);
}
/// 完整的初始化流程
Future<void> _initialize() async {
try {
isLoading.value = true; isLoading.value = true;
statusDescription.value = '正在初始化...'; statusDescription.value = '正在初始化...';
@ -102,6 +80,9 @@ class PairingController extends GetxController {
return; return;
} }
// 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题
_setupDeviceListeners();
// 2. 检查权限 // 2. 检查权限
statusDescription.value = '正在检查权限...'; statusDescription.value = '正在检查权限...';
final hasPermission = await _bluetoothService.checkAndRequestPermissions(); final hasPermission = await _bluetoothService.checkAndRequestPermissions();
@ -134,6 +115,33 @@ class PairingController extends GetxController {
} }
} }
/// 设置设备监听器
void _setupDeviceListeners() {
try {
if (!_isBluetoothServiceAvailable.value) return;
// 监听设备连接事件
_bluetoothService.onDeviceConnected((device) {
Logger.info('检测到蓝牙设备连接: ${device.name}');
// 更新状态并跳转到主页
statusDescription.value = '已连接到设备: ${device.name}';
// 立即跳转到主页
_navigateToHome();
});
} catch (e) {
Logger.error('设置设备监听器失败: $e');
}
}
/// 立即跳转到主页
void _navigateToHome() {
Get.offAllNamed(Routes.home);
}
/// 刷新设备 /// 刷新设备
Future<void> refreshConnectedDevices() async { Future<void> refreshConnectedDevices() async {
try { try {

32
lib/modules/test/controllers/azure_asr_test_controller.dart

@ -54,9 +54,8 @@ class AzureAsrTestController extends GetxController {
// 初始化ASR服务 // 初始化ASR服务
Future<void> _initializeAsr() async { Future<void> _initializeAsr() async {
try { try {
// 根据选择的支持语言初始化服务
final success = await _azureAsrService.initialize( final success = await _azureAsrService.initialize(
language: selectedLanguage.value,
autoDetectLanguage: isAutoDetectEnabled.value,
supportedLanguages: selectedSupportedLanguages, supportedLanguages: selectedSupportedLanguages,
); );
asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error;
@ -214,16 +213,25 @@ class AzureAsrTestController extends GetxController {
selectedLanguage.value = language; selectedLanguage.value = language;
// 停止当前识别
if (_azureAsrService.isContinuousRecognitionActive()) { if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
final success = await _azureAsrService.setRecognitionLanguage(language); // 确保新语言在选中支持的语言列表中
asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; if (!selectedSupportedLanguages.contains(language)) {
// 添加新语言到支持列表
final newLanguages = [...selectedSupportedLanguages, language];
selectedSupportedLanguages.value = newLanguages;
// 重新初始化ASR服务
await _initializeAsr();
}
recognizedText.value = ''; recognizedText.value = '';
partialText.value = ''; partialText.value = '';
detectedLanguage.value = ''; detectedLanguage.value = '';
asrState.value = AzureAsrState.initialized;
} catch (e) { } catch (e) {
Logger.error('更改语言出错: $e'); Logger.error('更改语言出错: $e');
asrState.value = AzureAsrState.error; asrState.value = AzureAsrState.error;
@ -235,16 +243,13 @@ class AzureAsrTestController extends GetxController {
try { try {
isAutoDetectEnabled.value = !isAutoDetectEnabled.value; isAutoDetectEnabled.value = !isAutoDetectEnabled.value;
// 停止当前识别
if (_azureAsrService.isContinuousRecognitionActive()) { if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
final success = await _azureAsrService.enableAutoDetectLanguage( // 重新初始化ASR服务 - 自动语言检测现在通过选择多种语言实现
isAutoDetectEnabled.value, await _initializeAsr();
languages: selectedSupportedLanguages,
);
asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error;
recognizedText.value = ''; recognizedText.value = '';
partialText.value = ''; partialText.value = '';
@ -262,11 +267,16 @@ class AzureAsrTestController extends GetxController {
selectedSupportedLanguages.value = languages; selectedSupportedLanguages.value = languages;
// 停止当前识别
if (_azureAsrService.isContinuousRecognitionActive()) { if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
final success = await _azureAsrService.setSupportedLanguages(languages); // 重新初始化ASR服务,使用新的语言列表
final success = await _azureAsrService.initialize(
supportedLanguages: languages,
);
asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error;
} catch (e) { } catch (e) {
Logger.error('更新支持的语言列表出错: $e'); Logger.error('更新支持的语言列表出错: $e');

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

@ -126,13 +126,17 @@ class TranslationController extends GetxController {
Future<void> _initServices() async { Future<void> _initServices() async {
try { try {
await _translationService.initialize(); await _translationService.initialize();
await _ttsService.initialize();
// 初始化 ASR 服务时,明确指定需要支持的语言
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
await _ttsService.initialize(); Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages');
await _asrService.initialize( await _asrService.initialize(
autoDetectLanguage: true,
supportedLanguages: asrSupportedLanguages, supportedLanguages: asrSupportedLanguages,
); );
Logger.info('所有服务初始化完成');
} catch (e) { } catch (e) {
Logger.error('初始化服务失败: $e'); Logger.error('初始化服务失败: $e');
} }
@ -158,9 +162,14 @@ class TranslationController extends GetxController {
_voiceInteractionService.pauseVoiceInteraction(); _voiceInteractionService.pauseVoiceInteraction();
try { try {
// 更新ASR支持的语言 // 每次开始识别前检查 ASR 服务是否已配置为当前语言对
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; final List<String> currentLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
await _asrService.setSupportedLanguages(asrSupportedLanguages); final bool needsReinit = !_asrService.supportedLanguages.toSet().containsAll(currentLanguages);
if (needsReinit) {
Logger.info('识别开始前发现语言变更,重新初始化ASR服务');
await _reinitializeAsrService();
}
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
@ -462,6 +471,9 @@ class TranslationController extends GetxController {
// 保存语言设置 // 保存语言设置
_saveLanguageSettings(); _saveLanguageSettings();
// 更新ASR支持的语言并重新初始化
_reinitializeAsrService();
} }
// 设置源语言 // 设置源语言
@ -478,6 +490,9 @@ class TranslationController extends GetxController {
// 保存语言设置 // 保存语言设置
_saveLanguageSettings(); _saveLanguageSettings();
// 更新ASR支持的语言并重新初始化
_reinitializeAsrService();
} }
} }
@ -495,6 +510,28 @@ class TranslationController extends GetxController {
// 保存语言设置 // 保存语言设置
_saveLanguageSettings(); _saveLanguageSettings();
// 更新ASR支持的语言并重新初始化
_reinitializeAsrService();
}
}
// 重新初始化ASR服务以支持新的语言
Future<void> _reinitializeAsrService() async {
try {
// 更新ASR支持的语言
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages');
// 重新初始化ASR服务
await _asrService.initialize(
supportedLanguages: asrSupportedLanguages,
);
Logger.info('ASR服务重新初始化成功');
} catch (e) {
Logger.error('重新初始化ASR服务失败: $e');
} }
} }

Loading…
Cancel
Save