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. 460
      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. 125
      lib/data/services/azure_asr_service.dart
  10. 5
      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. 72
      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依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
// 添加 AndroidX Security 加密 SharedPreferences 依赖
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// // 添加火山语音合成SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")

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

@ -17,8 +17,10 @@
<!-- Android 13+ 通知权限 -->
<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+的存储权限 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />

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

@ -1,20 +1,15 @@
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.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
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 speechConfig: SpeechConfig? = null
private var audioConfig: AudioConfig? = null
private val TAG = "AzureAsrHelper"
private var isContinuousRecognitionActive = false
private var currentLanguage = "zh-CN"
@ -22,34 +17,26 @@ class AzureAsrHelper {
private var serviceRegion = ""
private var isAutoDetectLanguage = false
private var supportedLanguages = arrayOf("zh-CN", "en-US")
// 回音消除相关
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
// 初始化SDK并创建recognizer
fun initialize(subscriptionKey: String, serviceRegion: String,
supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US")): Boolean {
try {
Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, supportedLanguages=${supportedLanguages.joinToString()}")
FileLogger.d(TAG, "初始化 Azure 语音服务")
// 检查配置是否为空
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) {
Log.e(TAG, "Azure 配置信息不完整: subscriptionKey=${subscriptionKey.isEmpty()}, serviceRegion=${serviceRegion.isEmpty()}")
FileLogger.e(TAG, "Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
this.subscriptionKey = subscriptionKey
this.serviceRegion = serviceRegion
// 设置支持的语言列表
// 设置语言
if (supportedLanguages.isNotEmpty()) {
this.supportedLanguages = supportedLanguages
}
@ -62,320 +49,178 @@ class AzureAsrHelper {
this.currentLanguage = supportedLanguages[0]
}
// 释放之前的资源
dispose()
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
// 设置为 CONVERSATION 模式
speechConfig?.setProperty(PropertyId.SpeechServiceConnection_RecoMode, "CONVERSATION")
// 设置语言配置
if (isAutoDetectLanguage) {
// 设置自动语言检测模式为连续检测
// 设置自动语言检测
speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous")
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
}
// 创建 PushAudioInputStream 和 AudioConfig
// 创建识别器
try {
setupAudioInput()
Log.d(TAG, "成功创建音频配置")
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
FileLogger.d(TAG, "Azure 语音服务初始化成功")
return true
} catch (e: Exception) {
Log.e(TAG, "创建音频配置失败: ${e.message}", e)
FileLogger.e(TAG, "创建识别器失败: ${e.message}")
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) {
Log.e(TAG, "初始化失败: ${e.message}", e)
FileLogger.e(TAG, "初始化失败: ${e.message}")
return false
}
}
// 设置音频输入
private fun setupAudioInput() {
// 释放之前的资源
releaseAudioResources()
// 创建 PushAudioInputStream
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(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
)
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "AudioRecord 初始化失败")
audioRecord?.release()
audioRecord = null
throw RuntimeException("AudioRecord 初始化失败")
// 重置 recognizer
private fun resetRecognizer(): Boolean {
try {
// 释放之前的 recognizer
recognizer?.close()
recognizer = null
// 使用现有配置重新创建 recognizer
if (speechConfig != null) {
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
return true
} else {
FileLogger.e(TAG, "语音配置未初始化")
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "重置识别器失败: ${e.message}")
return false
}
// 获取音频会话 ID 并设置回音消除
audioSessionId = audioRecord?.audioSessionId ?: -1
setupEchoCancellation()
}
// 开始一次性语音识别
fun recognizeOnce(language: String? = null, callback: RecognizeCallback) {
if (recognizer == null) {
callback.onError("SpeechRecognizer 未初始化")
fun recognizeOnce(callback: RecognizeCallback) {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return
}
// 如果指定了新的语言,且不是自动检测模式,需要重新初始化
if (language != null && language != currentLanguage && !isAutoDetectLanguage) {
// 使用指定的语言创建新的语言列表
val newLanguages = arrayOf(language)
initialize(subscriptionKey, serviceRegion, newLanguages)
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return
}
try {
// 开始录音并推送数据
startRecording()
// 执行识别
val result = recognizer?.recognizeOnceAsync()?.get()
// 停止录音
stopRecording()
if (result != null && result.reason == ResultReason.RecognizedSpeech) {
val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(result)
val detectedLanguage = autoDetectSourceLanguageResult?.language ?: ""
Log.d(TAG, "识别结果: ${result.text}, 检测到的语言: $detectedLanguage")
callback.onResult(result.text, detectedLanguage)
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language
callback.onResult(result.text, detectedLanguage ?: "")
} else {
callback.onError("未能识别语音")
}
} catch (e: Exception) {
stopRecording()
callback.onError("识别异常: ${e.message}")
}
}
// 开始连续语音识别
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (recognizer == null) {
Log.e(TAG, "SpeechRecognizer 未初始化")
callback.onError("SpeechRecognizer 未初始化")
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return false
}
// 如果已经在进行连续识别,先停止
if (isContinuousRecognitionActive) {
Log.d(TAG, "已经在进行连续识别,先停止")
stopContinuousRecognition(callback)
}
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return false
}
try {
Log.d(TAG, "开始重置识别器和音频资源")
// 完全释放所有资源,确保干净的状态
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
}
// 设置识别事件处理
recognizer?.let { recognizer ->
// 最终识别结果
recognizer.recognized.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result)
val detectedLanguage = autoDetectSourceLanguageResult?.language ?: ""
Log.d(TAG, "识别结果: ${event.result.text}, 检测到的语言: $detectedLanguage")
callback.onResult(event.result.text, detectedLanguage)
// 最终识别结果
recognizer?.recognized?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
} else {
Log.d(TAG, "识别结果原因: ${event.result.reason}")
currentLanguage
}
callback.onResult(event.result.text, detectedLanguage)
}
)
}
)
// 识别中事件
recognizer.recognizing.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) {
val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result)
val detectedLanguage = autoDetectSourceLanguageResult?.language ?: ""
// if (isAutoDetectLanguage) {
// Log.d(TAG, "正在识别: ${event.result.text}, 检测到的语言: $detectedLanguage")
// }
callback.onRecognizing(event.result.text, detectedLanguage)
// 识别中事件
recognizer?.recognizing?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
} else {
currentLanguage
}
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?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = true
callback.onSessionStarted()
}
)
// 取消事件
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()
}
)
recognizer?.sessionStopped?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = false
callback.onSessionStopped()
}
)
// 开始录音
startRecording()
// 开始连续识别
Log.d(TAG, "开始连续识别")
recognizer.startContinuousRecognitionAsync().get()
Log.d(TAG, "连续识别已启动")
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
} catch (e: Exception) {
Log.e(TAG, "开始连续识别失败: ${e.message}", e)
callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false
stopRecording()
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 {
if (!isContinuousRecognitionActive || recognizer == null) {
@ -385,13 +230,11 @@ class AzureAsrHelper {
try {
recognizer?.stopContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = false
stopRecording()
callback.onSessionStopped()
return true
} catch (e: Exception) {
callback.onError("停止连续识别失败: ${e.message}")
isContinuousRecognitionActive = false
stopRecording()
return false
}
}
@ -400,98 +243,23 @@ class AzureAsrHelper {
fun isContinuousRecognitionActive(): Boolean {
return isContinuousRecognitionActive
}
// 获取当前语言
fun getCurrentLanguage(): String {
return currentLanguage
}
// 释放资源
// 释放所有资源
fun dispose() {
try {
// 添加标志,避免重复释放
if (recognizer == null && audioRecord == null && pushStream == 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)
}
recognizer?.close()
recognizer = null
releaseAudioResources()
try {
speechConfig?.close()
speechConfig = null
Log.d(TAG, "语音配置已关闭")
} catch (e: Exception) {
Log.e(TAG, "关闭语音配置失败: ${e.message}", e)
}
speechConfig?.close()
speechConfig = null
isContinuousRecognitionActive = false
Log.d(TAG, "所有资源已释放")
FileLogger.d(TAG, "语音识别资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源时出错: ${e.message}", e)
FileLogger.e(TAG, "释放资源出错: ${e.message}")
}
}
// 释放音频相关资源
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
if (audioConfig != null) {
try {
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()
}
// 一次性识别回调接口
interface RecognizeCallback {
@ -509,127 +277,4 @@ class AzureAsrHelper {
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.AudioManager
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.audio.*
import java.util.concurrent.Future
@ -17,12 +17,11 @@ import java.util.concurrent.Semaphore
* 该类封装了微软语音 SDK 的 TTS 功能,提供简单的接口供 Flutter 调用
* 基于微软官方文档: 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 var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null
private var isInitialized = false
private var context: Context? = null
private var audioManager: AudioManager? = null
// 音频输出设备类型
@ -50,14 +49,11 @@ class AzureTtsHelper {
* @param subscriptionKey Azure 语音服务订阅密钥
* @param serviceRegion Azure 语音服务区域
* @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 {
this.context = context
if (context != null) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
}
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
@ -82,7 +78,7 @@ class AzureTtsHelper {
synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() }
isInitialized = true
Log.d(TAG, "TTS 引擎初始化成功")
FileLogger.d(TAG, "TTS 引擎初始化成功")
// 设置默认音频输出类型为自动
if (audioManager != null) {
@ -91,7 +87,7 @@ class AzureTtsHelper {
return true
} catch (e: Exception) {
Log.e(TAG, "TTS 引擎初始化失败: ${e.message}")
FileLogger.e(TAG, "TTS 引擎初始化失败: ${e.message}, ${serviceRegion}")
e.printStackTrace()
return false
}
@ -105,12 +101,12 @@ class AzureTtsHelper {
*/
fun setAudioOutputType(outputType: AudioOutputType): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化")
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
if (audioManager == null) {
Log.e(TAG, "AudioManager 未初始化,无法设置音频输出设备")
FileLogger.e(TAG, "AudioManager 未初始化,无法设置音频输出设备")
return false
}
@ -122,13 +118,13 @@ class AzureTtsHelper {
// 使用扬声器
audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = true
Log.d(TAG, "已设置音频输出设备为扬声器")
FileLogger.d(TAG, "已设置音频输出设备为扬声器")
}
AudioOutputType.EARPIECE -> {
// 使用听筒
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager?.isSpeakerphoneOn = false
Log.d(TAG, "已设置音频输出设备为听筒")
FileLogger.d(TAG, "已设置音频输出设备为听筒")
}
AudioOutputType.AUTO -> {
// 检查是否有耳机连接
@ -136,12 +132,12 @@ class AzureTtsHelper {
// 有耳机,使用耳机
audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = false
Log.d(TAG, "已设置音频输出设备为耳机")
FileLogger.d(TAG, "已设置音频输出设备为耳机")
} else {
// 无耳机,使用听筒
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager?.isSpeakerphoneOn = false
Log.d(TAG, "已设置音频输出设备为听筒")
FileLogger.d(TAG, "已设置音频输出设备为听筒")
}
}
}
@ -151,7 +147,7 @@ class AzureTtsHelper {
return true
} catch (e: Exception) {
Log.e(TAG, "设置音频输出设备失败: ${e.message}")
FileLogger.e(TAG, "设置音频输出设备失败: ${e.message}")
e.printStackTrace()
return false
}
@ -196,9 +192,9 @@ class AzureTtsHelper {
synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() }
synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() }
Log.d(TAG, "已重新创建语音合成器")
FileLogger.d(TAG, "已重新创建语音合成器")
} catch (e: Exception) {
Log.e(TAG, "重新创建语音合成器失败: ${e.message}")
FileLogger.e(TAG, "重新创建语音合成器失败: ${e.message}")
e.printStackTrace()
}
}
@ -210,10 +206,13 @@ class AzureTtsHelper {
*/
fun setVoice(voiceName: String): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化")
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
if (voiceName == currentVoiceName) {
FileLogger.d(TAG, "已设置语音: $voiceName")
return true
}
try {
currentVoiceName = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName)
@ -221,10 +220,10 @@ class AzureTtsHelper {
// 重新创建合成器以应用新的语音设置
recreateSynthesizer()
Log.d(TAG, "已设置语音: $voiceName")
FileLogger.d(TAG, "已设置语音: $voiceName")
return true
} catch (e: Exception) {
Log.e(TAG, "设置语音失败: ${e.message}")
FileLogger.e(TAG, "设置语音失败: ${e.message}")
e.printStackTrace()
return false
}
@ -239,7 +238,7 @@ class AzureTtsHelper {
*/
fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化")
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
@ -248,10 +247,10 @@ class AzureTtsHelper {
currentPitch = if (pitch == 0) "0%" else "${(pitch * 0.5).toInt()}%"
currentVolume = "${volume.coerceIn(0, 100)}%"
Log.d(TAG, "已设置语音参数: 语速=$currentSpeechRate, 音调=$currentPitch, 音量=$currentVolume")
FileLogger.d(TAG, "已设置语音参数: 语速=$currentSpeechRate, 音调=$currentPitch, 音量=$currentVolume")
return true
} catch (e: Exception) {
Log.e(TAG, "设置语音参数失败: ${e.message}")
FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
e.printStackTrace()
return false
}
@ -270,12 +269,8 @@ class AzureTtsHelper {
}
try {
Log.d(TAG, "开始合成文本: $text")
FileLogger.d(TAG, "开始合成文本: $text")
// 如果设置为自动模式,检查耳机状态并更新设置
if (currentAudioOutputType == AudioOutputType.AUTO) {
setAudioOutputType(AudioOutputType.AUTO)
}
// 生成 SSML
val ssml = generateSsml(text)
@ -283,7 +278,7 @@ class AzureTtsHelper {
// 使用 SSML 合成语音
speakSsml(ssml, callback)
} catch (e: Exception) {
Log.e(TAG, "语音合成异常: ${e.message}")
FileLogger.e(TAG, "语音合成异常: ${e.message}")
e.printStackTrace()
callback.onError("语音合成异常: ${e.message}")
}
@ -320,7 +315,7 @@ class AzureTtsHelper {
}
try {
Log.d(TAG, "开始合成 SSML")
FileLogger.d(TAG, "开始合成 SSML")
// 重置信号量
semaphore.drainPermits()
@ -339,29 +334,29 @@ class AzureTtsHelper {
when (result.reason) {
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("语音合成完成")
}
ResultReason.Canceled -> {
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}")
}
else -> {
Log.e(TAG, "语音合成失败: ${result.reason}")
FileLogger.e(TAG, "语音合成失败: ${result.reason}")
callback.onError("语音合成失败: ${result.reason}")
}
}
result.close()
} catch (e: Exception) {
Log.e(TAG, "处理语音合成结果异常: ${e.message}")
FileLogger.e(TAG, "处理语音合成结果异常: ${e.message}")
e.printStackTrace()
callback.onError("处理语音合成结果异常: ${e.message}")
}
}.start()
} catch (e: Exception) {
Log.e(TAG, "语音合成异常: ${e.message}")
FileLogger.e(TAG, "语音合成异常: ${e.message}")
e.printStackTrace()
callback.onError("语音合成异常: ${e.message}")
}
@ -372,17 +367,17 @@ class AzureTtsHelper {
*/
fun stopSpeaking(): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化")
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
try {
synthesizer?.StopSpeakingAsync()
Log.d(TAG, "已停止语音合成")
FileLogger.d(TAG, "已停止语音合成")
return true
} catch (e: Exception) {
Log.e(TAG, "停止语音合成失败: ${e.message}")
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
e.printStackTrace()
return false
}
@ -404,9 +399,9 @@ class AzureTtsHelper {
}
isInitialized = false
Log.d(TAG, "TTS 引擎已释放")
FileLogger.d(TAG, "TTS 引擎已释放")
} catch (e: Exception) {
Log.e(TAG, "释放 TTS 引擎失败: ${e.message}")
FileLogger.e(TAG, "释放 TTS 引擎失败: ${e.message}")
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.bluetooth.BluetoothAdapter
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() {
@ -140,15 +143,112 @@ class MainActivity: FlutterActivity() {
var azureSpeechKey: String = ""
var azureSpeechRegion: 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?) {
super.onCreate(savedInstanceState)
// 初始化本地的 Azure 语音服务实例
Log.d(TAG, "初始化本地的 Azure 语音服务实例")
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
// 初始化 FileLogger
FileLogger.init(applicationContext)
// 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(applicationContext)
azureTtsHelper = AzureTtsHelper(applicationContext)
classicBluetoothHelper = ClassicBluetoothHelper(applicationContext)
// 注册广播接收器
@ -191,6 +291,10 @@ class MainActivity: FlutterActivity() {
// 设置方法通道
setupMethodChannels(flutterEngine)
// 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(this)
azureTtsHelper = AzureTtsHelper(this)
Log.d(TAG, "Flutter 引擎配置完成")
}
@ -268,8 +372,6 @@ class MainActivity: FlutterActivity() {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey")
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")
if (subscriptionKey == null || region == null) {
@ -285,9 +387,8 @@ class MainActivity: FlutterActivity() {
}
}
"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) {
result.success(mapOf(
"text" to text,
@ -375,37 +476,6 @@ class MainActivity: FlutterActivity() {
azureAsrHelper.dispose()
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 -> {
result.notImplemented()
}
@ -416,21 +486,12 @@ class MainActivity: FlutterActivity() {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey")
val region = call.argument<String>("serviceRegion")
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val language = call.argument<String>("language") ?: "zh-CN"
if (subscriptionKey == null || region == null) {
result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null)
return@setMethodCallHandler
}
try {
val success = azureTtsHelper.initialize(subscriptionKey, region, language, this)
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
val success = azureTtsHelper.initialize(subscriptionKey, region, language)
result.success(success)
}
"setVoice" -> {
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) {
Log.d(TAG, "启动语音交互服务")
FileLogger.d(TAG, "启动语音交互服务")
// 获取配置参数
val key = call.argument<String>("azure_speech_key") ?: ""
@ -613,6 +674,9 @@ class MainActivity: FlutterActivity() {
azureSpeechRegion = region
volcanoAiApiKey = aiKey
// 保存密钥到安全存储
saveKeysToSecureStorage(applicationContext)
// 创建 Intent 并启动服务
val serviceIntent = Intent(this, VoiceInteractionService::class.java)
@ -623,6 +687,7 @@ class MainActivity: FlutterActivity() {
startService(serviceIntent)
}
// 返回成功结果
result.success(true)
}
@ -716,6 +781,7 @@ class MainActivity: FlutterActivity() {
}
override fun onDestroy() {
Log.d(TAG, "onDestroy")
// 注销所有广播接收器
try {
unregisterReceiver(voiceInteractionReceiver)

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

125
lib/data/services/azure_asr_service.dart

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

5
lib/data/services/azure_tts_service.dart

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

10
lib/data/services/classic_bluetooth_service.dart

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

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

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

72
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 'dart:async';
import '../../../routes/app_routes.dart';
@ -6,6 +7,7 @@ import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
@ -52,41 +54,17 @@ class PairingController extends GetxController {
_initialize();
}
@override
void onReady() {
super.onReady();
// 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题
_setupDeviceListeners();
}
/// 设置设备监听器
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> _initialize() async {
try {
VoiceInteractionService.to.initialize();
VoiceInteractionService.to.startService();
isLoading.value = true;
statusDescription.value = '正在初始化...';
@ -102,6 +80,9 @@ class PairingController extends GetxController {
return;
}
// 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题
_setupDeviceListeners();
// 2. 检查权限
statusDescription.value = '正在检查权限...';
final hasPermission = await _bluetoothService.checkAndRequestPermissions();
@ -133,6 +114,33 @@ class PairingController extends GetxController {
isLoading.value = false;
}
}
/// 设置设备监听器
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 {

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

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

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

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