Browse Source

优化ai按住对话&唤醒冲突问题

newdev_shunjiawei
fdp 1 year ago
parent
commit
3bd503caf6
  1. 2
      lib/data/services/network/nw_api.dart
  2. 91
      lib/modules/agent/controllers/agent_controller.dart
  3. 55
      lib/modules/agent/views/agent_view.dart
  4. 1
      lib/modules/meeting/controllers/meeting_record_controller.dart
  5. 2
      lib/modules/translation/controllers/translation_controller.dart
  6. 80
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  7. 18
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  8. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  9. 26
      local_plugins/agent_service/lib/agent_service.dart
  10. 701
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  11. 6
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  12. 9
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

2
lib/data/services/network/nw_api.dart

@ -1,5 +1,5 @@
class NWApi {
// static const baseApi = "https://www.deapsound.com";
// static const baseApi = "https://api.deapsound.com";
//static const baseApi = "https://api.deapsound.com";
static const baseApi = "https://api.ideapsound.com";
}

91
lib/modules/agent/controllers/agent_controller.dart

@ -93,7 +93,7 @@ class AgentController extends GetxController {
final isPushToTalkActive = false.obs; // 是否正在按住说话
// 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住)
final isPushToTalkMode = false.obs; // 是否处于按住说话模式
//final isPushToTalkMode = false.obs; // 是否处于按住说话模式
// 蓝牙唤醒状态
final isBleWakeupActive = false.obs; // 是否正在进行蓝牙唤醒对话
@ -143,6 +143,7 @@ class AgentController extends GetxController {
_initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
startVoiceInput(); //开启语音输入
}
// 初始化音乐播放状态
@ -355,6 +356,7 @@ class AgentController extends GetxController {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
isTextInputMode.value = false;
print("订阅代理服务事件识别开始");
// isTextInputMode.refresh();
// 只标记识别开始,不添加消息
Logger.i(TAG, '识别开始');
@ -371,7 +373,7 @@ class AgentController extends GetxController {
} else {
// 只有在非按住说话模式且非打电话模式下才自动切换到文字模式
// 按住说话模式下应该保持在语音模式,等待下次按住
if (!isPushToTalkMode.value && !isPhoneCallMode.value) {
if (!isPhoneCallMode.value) {
isTextInputMode.value = true;
}
}
@ -890,19 +892,18 @@ class AgentController extends GetxController {
// 切换到文本模式时,禁用按住说话模式并停止任何正在进行的语音输入
if (isTextInputMode.value) {
// 切换到文本模式
isPushToTalkMode.value = false;
stopVoiceInput();
isTextInputMode.value = true;
// stopVoiceInput();
} else {
// 切换到语音模式,启用按住说话模式
isPushToTalkMode.value = true;
isTextInputMode.value = false;
}
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
// 检查麦克风权限
print("开始语音输入"); // 检查麦克风权限
final hasPermission = await PermissionUtil.instance.requestPermission(
// context: Get.context!,
permissionType: Permission.microphone,
@ -1129,13 +1130,14 @@ class AgentController extends GetxController {
// 保存当前输入模式
void _saveCurrentInputMode() {
if (isPushToTalkMode.value) {
if (!isTextInputMode.value) {
_previousInputMode = VoiceInputMode.pushToTalk;
} else if (isTextInputMode.value) {
_previousInputMode = VoiceInputMode.text;
} else {
_previousInputMode = null;
_previousInputMode = VoiceInputMode.text;
}
// else {
// _previousInputMode = null;
// }
}
// 恢复之前的输入模式
@ -1143,16 +1145,16 @@ class AgentController extends GetxController {
switch (_previousInputMode) {
case VoiceInputMode.pushToTalk:
isTextInputMode.value = false;
isPushToTalkMode.value = true;
//isPushToTalkMode.value = true;
break;
case VoiceInputMode.text:
isTextInputMode.value = true;
isPushToTalkMode.value = false;
// isPushToTalkMode.value = false;
break;
default:
// 默认恢复到文字模式
isTextInputMode.value = true;
isPushToTalkMode.value = false;
//isPushToTalkMode.value = false;
break;
}
_previousInputMode = null; // 清除历史记录
@ -1171,8 +1173,8 @@ class AgentController extends GetxController {
// 进入打电话模式
isPhoneCallMode.value = true;
// 如果当前是文本模式,切换到语音模式
if (isTextInputMode.value) {
// 如果当前是文本模式,切换到语音模式????
if (isListening.value) {
// 检查麦克风权限
final hasPermission = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone,
@ -1193,8 +1195,10 @@ class AgentController extends GetxController {
// 切换到语音模式并开始语音输入
isTextInputMode.value = false;
// 打电话模式不使用按住说话模式,而是持续语音输入
isPushToTalkMode.value = false;
await startVoiceInput();
//isPushToTalkMode.value = false;
// 开始语音识别
print("开始打电话模式,开始语音输入");
await AgentService.audioContinue();
}
}
@ -1206,8 +1210,9 @@ class AgentController extends GetxController {
isPhoneCallMode.value = false;
// 停止语音输入
await stopVoiceInput();
//await stopVoiceInput();
// 开始语音识别
await AgentService.audioPause();
// 恢复之前的输入模式,而不是强制切换到文字模式
_restorePreviousInputMode();
}
@ -1240,23 +1245,23 @@ class AgentController extends GetxController {
// 验证语音输入状态一致性
void _validateVoiceInputStates() {
// 检查状态冲突
if (isPhoneCallMode.value && isPushToTalkMode.value) {
Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式');
isPushToTalkMode.value = false;
}
// if (isPhoneCallMode.value && isPushToTalkMode.value) {
// Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式');
// isPushToTalkMode.value = false;
// }
// 检查文字模式与语音模式的一致性
if (isTextInputMode.value &&
(isPushToTalkMode.value || isPhoneCallMode.value)) {
Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式');
isTextInputMode.value = false;
}
// // 检查文字模式与语音模式的一致性
// if (isTextInputMode.value &&
// (isPushToTalkMode.value || isPhoneCallMode.value)) {
// Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式');
// isTextInputMode.value = false;
// }
// 检查按住说话状态的合理性
if (isPushToTalkActive.value && !isPushToTalkMode.value) {
Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式');
isPushToTalkActive.value = false;
}
// // 检查按住说话状态的合理性
// if (isPushToTalkActive.value && !isPushToTalkMode.value) {
// Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式');
// isPushToTalkActive.value = false;
// }
}
// 安全的状态切换方法
@ -1266,21 +1271,21 @@ class AgentController extends GetxController {
switch (targetMode) {
case VoiceInputMode.text:
isTextInputMode.value = true;
isPushToTalkMode.value = false;
// isPushToTalkMode.value = false;
isPhoneCallMode.value = false;
isPushToTalkActive.value = false;
break;
case VoiceInputMode.pushToTalk:
isTextInputMode.value = false;
isPushToTalkMode.value = true;
// isPushToTalkMode.value = true;
isPhoneCallMode.value = false;
isPushToTalkActive.value = false;
break;
case VoiceInputMode.phoneCall:
isTextInputMode.value = false;
isPushToTalkMode.value = false;
// isPushToTalkMode.value = false;
isPhoneCallMode.value = true;
isPushToTalkActive.value = false;
break;
@ -1295,7 +1300,7 @@ class AgentController extends GetxController {
Logger.i(TAG, '🎤 开始按住说话');
// 只有在语音模式且按住说话模式下才能按住说话
if (isTextInputMode.value || !isPushToTalkMode.value) return;
if (isTextInputMode.value) return;
// 如果已经在按住说话,不重复开始
if (isPushToTalkActive.value) return;
@ -1303,8 +1308,10 @@ class AgentController extends GetxController {
// 设置按住说话状态
isPushToTalkActive.value = true;
print("开始按住说话,状态已设置为true");
// 开始语音识别
await startVoiceInput();
await AgentService.audioContinue();
// await startVoiceInput();
}
// 结束按住说话
@ -1316,9 +1323,9 @@ class AgentController extends GetxController {
// 清除按住说话状态(但保持按住说话模式)
isPushToTalkActive.value = false;
await AgentService.audioPause();
// 停止语音识别
await stopVoiceInput();
// await stopVoiceInput();
}
// 获取音乐播放状态

55
lib/modules/agent/views/agent_view.dart

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@ -374,7 +375,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
final isTextMode =
controller.isTextInputMode.value;
if (isTextMode) {
if (controller.isTextInputMode.value) {
// 文本输入模式 - 限制最大高度并只向上扩展
return Container(
constraints: const BoxConstraints(
@ -466,16 +467,44 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
),
);
} else {
Timer? _holdTimer; // 增加延迟触发的计时器
// 语音输入模式 - 按住说话模式
return Listener(
onPointerDown: (_) {
// 开始按住说话
controller.startPushToTalk();
// 设置300ms延迟防止误触
_holdTimer = Timer(
const Duration(milliseconds: 200),
() {
controller.startPushToTalk();
// if (activeSpeaker == 0) {
// if (speakerId == 1) {
// controller
// .restoreOriginalAudioState();
// } else {
// controller
// .disableBluetoothAudio();
// }
// controller
// .setActiveSpeaker(speakerId);
// }
});
},
onPointerUp: (_) {
// 结束按住说话
_holdTimer?.cancel();
controller.endPushToTalk();
},
onPointerCancel: (_) {
_holdTimer?.cancel();
controller.endPushToTalk();
},
// onLongPressStart: (_) {
// // 开始按住说话
// controller.startPushToTalk();
// },
// onLongPressEnd: (_) {
// // 结束按住说话
// controller.endPushToTalk();
// },
// onLongPressCancel: () {
// // 取消按住说话
// controller.endPushToTalk();
@ -517,7 +546,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
// 语音波形图 - 只在按住时显示
if (isPushToTalkActive) ...[
const AgentSoundBarsWidget(),
//const Spacer(),
const Spacer(),
],
// 状态文字和图标
@ -526,7 +555,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
mainAxisAlignment:
isPushToTalkActive
? MainAxisAlignment
.center
.end
: MainAxisAlignment
.center,
children: [
@ -1020,13 +1049,13 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
Obx(() {
final isActive = controller.isListening.value;
return GestureDetector(
onTap: () {
if (isActive) {
controller.stopVoiceInput();
} else {
controller.startVoiceInput();
}
},
// onTap: () {
// if (isActive) {
// controller.stopVoiceInput();
// } else {
// controller.startVoiceInput();
// }
// },
child: Container(
width: 80,
height: 80,

1
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -138,7 +138,6 @@ class MeetingRecordController extends GetxController
_bleManager.closeCodec();
}
_asrService.stopRecord(true);
_asrService.setPause();
_asrService.stopContinuousRecognition();
isRecording.value = false;
}

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

@ -430,7 +430,7 @@ class TranslationController extends GetxController {
try {
await _asrService.stopContinuousRecognition();
_asrService.setPause();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
isRecognizing.value = false;

80
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -58,7 +58,7 @@ object AgentService : CoroutineScope {
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
var azureAsrHelper: AzureAsrHelper? = null
// 使用BytedanceTTS作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null
@ -111,6 +111,7 @@ object AgentService : CoroutineScope {
private var lastInterruptTime = 0L
private val INTERRUPT_DEBOUNCE_MS = 300L
private var audioSourceType= AzureAsrHelper.AudioSourceType.MICROPHONE
// 语气词过滤列表(中英文)
private val FILLER_WORDS = setOf(
// 中文语气词
@ -351,15 +352,16 @@ object AgentService : CoroutineScope {
*/
private fun startIdleCheck() {
stopIdleCheck() // 先停止现有的检查
Log.d(TAG, "启动空闲检测: ${isRecognitionActive}")
if (!isRecognitionActive) return
idleCheckJob = launch {
try {
delay(maxIdleSeconds * 1000L)
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) {
stopRecognition()
Log.d(TAG, "启动空闲检测: stopRecognition")
//stopRecognition()
sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds))
}
} catch (e: CancellationException) {
@ -393,10 +395,17 @@ object AgentService : CoroutineScope {
*/
fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) {
Log.e(TAG, "服务未初始化")
Log.d(TAG, "服务未初始化")
return false
}
Log.d(TAG, "麦克风音频未暂停,无法设置${azureAsrHelper!!.isPaused},$isExternalActive")
if(!(azureAsrHelper!!.isPaused)&&isExternalActive)
{
Log.d(TAG, "麦克风音频未暂停,无法设置")
BleService.closeCodec()
return false
}
Log.d(TAG, "开始语音识别")
if (isRecognitionActive) return true
// 检查录音权限
@ -410,7 +419,7 @@ object AgentService : CoroutineScope {
_hasSpeechDetected.set(false)
try {
val audioSourceType = if (isExternalActive) {
audioSourceType = if (isExternalActive) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
@ -480,6 +489,7 @@ object AgentService : CoroutineScope {
override fun onSessionStarted() {
sendEvent("recognition_started", mapOf("status" to "started"))
startIdleCheck()
Log.d(TAG, "onSessionStarted")
audioPlayer?.playAudio(R.raw.start)
}
@ -489,12 +499,14 @@ object AgentService : CoroutineScope {
_isRecognitionActive.set(false)
stopIdleCheck()
Log.d(TAG, "onSessionStopped")
audioPlayer?.playAudio(R.raw.stop)
}
override fun onCanceled(reason: String, errorDetails: String) {
_isRecognitionActive.set(false)
stopIdleCheck()
Log.d(TAG, "recognition_canceled")
BleService.closeCodec()
sendEvent("recognition_canceled", mapOf(
"reason" to reason,
@ -505,6 +517,7 @@ object AgentService : CoroutineScope {
override fun onError(error: String) {
_isRecognitionActive.set(false)
stopIdleCheck()
Log.d(TAG, "error")
BleService.closeCodec()
Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf(
@ -536,6 +549,7 @@ object AgentService : CoroutineScope {
try {
azureAsrHelper?.stopContinuousRecognition()
Log.d(TAG, "停止语音识别")
BleService.closeCodec()
_isRecognitionActive.set(false)
stopIdleCheck()
@ -545,7 +559,55 @@ object AgentService : CoroutineScope {
stopIdleCheck()
}
}
/**
* 继续音频输入
*/
fun audioContinue() {
try {
if(audioSourceType== AzureAsrHelper.AudioSourceType.EXTERNAL)
{
Log.d(TAG, "当前是什么输入:$audioSourceType")
BleService.closeCodec()
stopRecognition()
startRecognition(false)
}
// if (!isRecognitionActive) {
// }
azureAsrHelper?.audioContinue()
//BleService.closeCodec()
//_isRecognitionActive.set(false)
//stopIdleCheck()
} catch (e: Exception) {
Log.e(TAG, "暂停音频输入异常: ${e.message}")
// _isRecognitionActive.set(false)
//stopIdleCheck()
}
}
/**
* 暂停音频输入
*/
fun audioPause() {
if (!isRecognitionActive) return
try {
azureAsrHelper?.audioPause()
Log.d(TAG, "暂停音频输入")
BleService.closeCodec()
//_isRecognitionActive.set(false)
//stopIdleCheck()
} catch (e: Exception) {
Log.e(TAG, "暂停音频输入异常: ${e.message}")
// _isRecognitionActive.set(false)
//stopIdleCheck()
}
}
/**
* 打断当前响应
* 停止TTS播放和AI流输出
@ -676,6 +738,7 @@ object AgentService : CoroutineScope {
hasImage: Boolean = false,
imagePath: String? = null
) {
Log.d(TAG, "内部方法:通用的ChatAPI处理逻辑")
// 如果有正在进行的AI流式输出,先停止它
stopAiStream()
stopTts()
@ -685,6 +748,7 @@ object AgentService : CoroutineScope {
currsessionId = UUID.randomUUID().toString()
// 设置状态为正在流式输出
_isAiStreaming.set(true)
Log.d(TAG, "processWithChatApiInternal")
audioPlayer?.playAudio(R.raw.await, true,0.3f)
// 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder()
@ -1190,6 +1254,7 @@ object AgentService : CoroutineScope {
fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) {
synchronized(playbackLock) {
try {
Log.d(TAG, "playAudio${isLooping}")
// 强制停止并释放之前的资源
forceStop()
@ -1233,6 +1298,7 @@ object AgentService : CoroutineScope {
fun stopAudio() {
synchronized(playbackLock) {
try {
Log.d(TAG, "stopAudio")
mediaPlayer?.apply {
if (isMediaPlayerPlaying()) {
stop()

18
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -103,6 +103,24 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
Log.e(TAG, "停止对话失败", e)
result.error("STOP_ERROR", "停止对话失败: ${e.message}", null)
}
}
"audioContinue" -> {
try {
AgentService.audioContinue()
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "audioContinue失败", e)
result.error("STOP_ERROR", "audioContinue失败: ${e.message}", null)
}
}
"audioPause" -> {
try {
AgentService.audioPause()
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "audioContinue失败", e)
result.error("STOP_ERROR", "audioContinue失败: ${e.message}", null)
}
}
"processTextInput" -> {
val text = call.argument<String>("text")

2
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -142,7 +142,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
AgentService.interruptCurrentResponse()
// AgentService.speakText("我在!")
// 启动语音识别
if (AgentService.isRecognitionActive) {
if (AgentService.isRecognitionActive&&AgentService.azureAsrHelper!!.isPaused) {
// 如果已经在识别,则重新启动
stopListening()
}

26
local_plugins/agent_service/lib/agent_service.dart

@ -261,6 +261,32 @@ class AgentService {
}
}
/// 停止对话
///
/// 停止语音识别
/// 返回是否成功停止对话
static Future<bool> audioContinue() async {
try {
final bool result = await _channel.invokeMethod('audioContinue');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
}
}
/// 停止对话
///
/// 停止语音识别
/// 返回是否成功停止对话
static Future<bool> audioPause() async {
try {
final bool result = await _channel.invokeMethod('audioPause');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
}
}
/// 处理文本输入
///
/// [text] 文本内容

701
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -34,9 +34,6 @@ class AzureAsrHelper(private val context: Context) {
private val tag = "AzureAsrHelper"
// 核心组件
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
private var speechConfig: SpeechConfig? = null
private var recognizer: SpeechRecognizer? = null
private var audioConfig: AudioConfig? = null
@ -55,7 +52,7 @@ class AzureAsrHelper(private val context: Context) {
var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理
// private var microphoneStream: MicrophoneStream? = null
private var audioStream: AudioStream? = null
private var externalAudioStream: ExternalAudioPullStream? = null
// 音频模式管理
@ -64,11 +61,7 @@ class AzureAsrHelper(private val context: Context) {
// 录音文件处理
var recordfile: RecordFile? = null
private var isPaused = false
private var audioRecord: AudioRecord? = null
private var captureThread: Thread? = null
private var audioStream: PushAudioInputStream? = null
var isPaused = true
/**
* 音频来源类型
@ -129,9 +122,7 @@ class AzureAsrHelper(private val context: Context) {
// 释放之前的资源
dispose()
// 1. 创建可控制的音频流
val format = getOptimalAudioFormat()
audioStream = AudioInputStream.createPushStream(format)
// 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
@ -197,6 +188,7 @@ class AzureAsrHelper(private val context: Context) {
*/
private fun setupRecognizer(): Boolean {
try {
// 清理旧的识别器
recognizer?.close()
recognizer = null
@ -211,7 +203,11 @@ class AzureAsrHelper(private val context: Context) {
AudioSourceType.EXTERNAL -> {
// 改用拉流方式处理外部音频
Log.d(tag, "改用拉流方式处理外部音频")
setupExternalAudioStream()
}
}
@ -237,13 +233,25 @@ class AzureAsrHelper(private val context: Context) {
*/
private fun setupMicrophoneStream() {
try {
Log.d(tag, "设置麦克风流 - 使用拉流方式: }")
restoreOriginalAudioState()
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = AudioStream()
audioStream!!.initAudioRecord()
}
// else
// {
// audioStream.stopCaptureThread()
// audioStream.startCaptureThread()
// }
// 创建麦克风流
// microphoneStream = MicrophoneStream()
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
// audioConfig = AudioConfig.fromStreamInput(microphoneStream)
audioConfig = AudioConfig.fromStreamInput(audioStream)
audioConfig = AudioConfig.fromStreamInput(audioStream!!.pushAudioStream)
} catch (e: Exception) {
Log.e(tag, "设置麦克风流失败: ${e.message}")
@ -278,6 +286,8 @@ class AzureAsrHelper(private val context: Context) {
*/
private fun setupExternalAudioStream() {
try {
Log.d(tag, "设置外部音频流 - 使用拉流方式: }")
//disableBluetoothAudio()
// 创建外部音频拉流对象
externalAudioStream = ExternalAudioPullStream()
@ -299,7 +309,6 @@ class AzureAsrHelper(private val context: Context) {
if (audioSourceType != AudioSourceType.EXTERNAL) {
return
}
// 使用拉流模式,将数据推入队列
externalAudioStream?.pushAudio(data)
@ -379,8 +388,7 @@ class AzureAsrHelper(private val context: Context) {
}
try {
// 5. 初始化麦克风
initAudioRecord()
// 设置各种事件监听
setupEventListeners(callback)
@ -389,9 +397,7 @@ class AzureAsrHelper(private val context: Context) {
// 开始连续识别
recognizer?.startContinuousRecognitionAsync()
// 开始录音
audioRecord?.startRecording()
startCaptureThread() // 再启动数据读取线程
audioStream?.startCaptureThread()
isContinuousRecognitionActive = true
@ -442,6 +448,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
// 直接在当前线程调用回调
Log.d(tag, "会话开始事件")
callback.onSessionStarted()
}
)
@ -450,6 +457,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.sessionStopped?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
// 直接在当前线程调用回调
Log.d(tag, "会话结束事件")
callback.onSessionStopped()
isContinuousRecognitionActive = false
stopAudioProcessing()
@ -497,8 +505,6 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理
stopAudioProcessing()
// 停止录音
stopMicrophoneCapture()
// 会话结束事件会设置isContinuousRecognitionActive = false
return true
} catch (e: Exception) {
@ -508,8 +514,6 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理
stopAudioProcessing()
// 停止录音
stopMicrophoneCapture()
// 尝试强制关闭识别器
try {
recognizer?.close()
@ -538,7 +542,6 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
}
releaseAudioResources()
// 停止音频处理
stopAudioProcessing()
@ -552,9 +555,6 @@ class AzureAsrHelper(private val context: Context) {
speechConfig?.close()
speechConfig = null
// 关闭音频流(根据实际实现可能需要)
audioStream?.close()
audioStream = null
// 释放音频配置
audioConfig?.close()
@ -601,16 +601,19 @@ class AzureAsrHelper(private val context: Context) {
* 停止音频处理
*/
private fun stopAudioProcessing() {
// microphoneStream?.let {
// try {
// it.close()
// microphoneStream = null
// } catch (e: Exception) {
// Log.e(tag, "关闭麦克风流失败: ${e.message}")
// e.printStackTrace()
// }
// }
audioStream?.let {
try {
it.stopMicrophoneCapture()
it.releaseAudioResources()
it.pushAudioStream?.close()
audioStream = null
} catch (e: Exception) {
Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
} // 关闭音频流(根据实际实现可能需要)
externalAudioStream?.let {
try {
it.close()
@ -622,70 +625,22 @@ class AzureAsrHelper(private val context: Context) {
}
}
/**
* 开启录音
*/
fun enableRecord(filePath: String) {
Log.i(tag, "开启录音:")
recordfile!!.closeFile(true)
recordfile!!.creatingFiles(filePath)
}
/**
* 移动文件到新路径
*/
fun moveFile(sourcePath: String, destPath: String): Boolean {
Log.i(tag, "移动文件到新路径:")
recordfile!!.moveFile(sourcePath, destPath)
return true
}
/**
* 重命名指定路径的音频文件
*/
fun renameFile(filePath: String, newName: String): Boolean {
Log.i(tag, "重命名指定路径的音频文件:")
recordfile!!.renameFile(filePath, newName)
return true
}
/**
* 停止录音
*/
fun pauseRecord() {
Log.i(tag, "停止连续录音:")
recordfile!!.isPause = true
}
/**
* 关闭录音
*/
fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:")
recordfile!!.isPause = false
recordfile!!.closeFile(isSave)
}
fun stopAudioPause() {
/*
暂停音频输入
*/
fun audioPause() {
Log.d(tag, "关闭麦克风stopAudioPause: ")
// 清理旧的识别器
isPaused = true
isPaused = true
}
fun setAudiocontinue() {
/*
继续音频输入
*/
fun audioContinue() {
Log.d(tag, "继续麦克风stopAudioPause: ")
// 开始连续识别
@ -727,403 +682,251 @@ class AzureAsrHelper(private val context: Context) {
}
}
//录音音频文件
/**
* 开始麦克风捕获
* 开启录音
*/
private fun initAudioRecord() {
// 获取已确定的采样率
val format = getOptimalAudioFormat()
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
val channelConfig = AudioFormat.CHANNEL_IN_MONO // 单声道输入
val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(
rate,
channelConfig,
audioFormat
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
// 计算最小缓冲区大小(避免出现音频欠载)
val minBufferSize = AudioRecord.getMinBufferSize(
sampleRate,
channelConfig,
audioFormat
)
// 创建录音对象
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Log.d("AzureASR", "android.os.Build.VERSION.SDK_INT")
val format = AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
.setBufferSizeInBytes(minBufferSize * 2)
.build()
} else {
// 创建 AudioRecord 对象
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除)
sampleRate,
channelConfig,
audioFormat,
minBufferSize * 2 // 使用两倍缓冲区防止溢出
)
}
fun enableRecord(filePath: String) {
Log.i(tag, "开启录音:")
recordfile!!.closeFile(true)
// 检查初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
recordfile!!.creatingFiles(filePath)
}
// 3. 单独封装线程启动逻辑
private fun startCaptureThread() {
captureThread = Thread {
// 根据实际采样率计算缓冲区大小
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
var bufferSize = 0
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
bufferSize = AudioRecord.getMinBufferSize(
rate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
/**
* 移动文件到新路径
*/
fun moveFile(sourcePath: String, destPath: String): Boolean {
Log.i(tag, "移动文件到新路径:")
val buffer = ByteArray(bufferSize)
recordfile!!.moveFile(sourcePath, destPath)
return true
}
while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
/**
* 重命名指定路径的音频文件
*/
fun renameFile(filePath: String, newName: String): Boolean {
Log.i(tag, "重命名指定路径的音频文件:")
if (!isPaused) {
// 读取音频
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (bytesRead > 0) {
// 写入Azure流
audioStream?.write(buffer)
// 保存录音
recordfile?.saveAudioDataToWav(buffer)
}
}
}
}.apply { start() }
recordfile!!.renameFile(filePath, newName)
return true
}
/**
* 停止麦克风捕获并释放所有相关资源
* 停止录音
*/
private fun stopMicrophoneCapture() {
try {
// 中断并等待捕获线程结束
captureThread?.interrupt()
captureThread?.join(300) // 最多等待300ms
fun pauseRecord() {
Log.i(tag, "停止连续录音:")
// 停止录音并释放AudioRecord资源
audioRecord?.stop() // 必须先停止再释放
recordfile!!.isPause = true
}
} catch (e: Exception) {
Log.e("AudioConfig", "Error releasing resources: ${e.message}")
} finally {
// 确保恢复原始音频状态
restoreOriginalAudioState()
}
/**
* 关闭录音
*/
fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:")
recordfile!!.isPause = false
recordfile!!.closeFile(isSave)
}
/**
* 应用音频效果 - 总是启用所有音频增强功能
* 麦克风流 - 拉流模式
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
*/
private fun applyAudioEffects() {
val sessionId = audioRecord?.audioSessionId ?: return
inner class AudioStream {
try {
// 设置音频模式为通信模式
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
// 启用声学回音消除
if (AcousticEchoCanceler.isAvailable()) {
echoCanceler = AcousticEchoCanceler.create(sessionId)
echoCanceler?.enabled = true
Log.i(tag, "声学回音消除已启用")
} else {
Log.w(tag, "设备不支持声学回音消除")
}
// 启用噪声抑制
if (NoiseSuppressor.isAvailable()) {
noiseSuppressor = NoiseSuppressor.create(sessionId)
noiseSuppressor?.enabled = true
Log.i(tag, "噪声抑制已启用")
var audioRecord: AudioRecord? = null
private var captureThread: Thread? = null
var pushAudioStream: PushAudioInputStream? = null
// 音频配置
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
/**
* 开始麦克风捕获
*/
fun initAudioRecord() {
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(
rate,
channelConfig,
audioFormat
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
// 计算最小缓冲区大小(避免出现音频欠载)
val minBufferSize = AudioRecord.getMinBufferSize(
sampleRate,
channelConfig,
audioFormat
)
// 创建录音对象
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Log.d("AzureASR", "android.os.Build.VERSION.SDK_INT")
val format = AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
.setBufferSizeInBytes(minBufferSize * 2)
.build()
} else {
Log.w(tag, "设备不支持噪声抑制")
// 创建 AudioRecord 对象
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除)
sampleRate,
channelConfig,
audioFormat,
minBufferSize * 2 // 使用两倍缓冲区防止溢出
)
}
// 启用自动增益控制
if (AutomaticGainControl.isAvailable()) {
automaticGainControl = AutomaticGainControl.create(sessionId)
automaticGainControl?.enabled = true
Log.i(tag, "自动增益控制已启用")
} else {
Log.w(tag, "设备不支持自动增益控制")
// 检查初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
Log.i(tag, "回音消除和音频增强功能已全部启用")
} catch (e: Exception) {
Log.e(tag, "设置音频效果失败: ${e.message}")
startCaptureThread() // 再启动数据读取线程
}
}
/**
* 释放音频资源
*/
private fun releaseAudioResources() {
try {
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 3. 单独封装线程启动逻辑
fun startCaptureThread() {
// 开始录音
audioRecord?.startRecording()
captureThread = Thread {
// 根据实际采样率计算缓冲区大小
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
var bufferSize = 0
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
bufferSize = AudioRecord.getMinBufferSize(
rate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
val buffer = ByteArray(bufferSize)
while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
if (!isPaused) {
// 读取音频
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (bytesRead > 0) {
// 写入Azure流
pushAudioStream?.write(buffer)
// 保存录音
recordfile?.saveAudioDataToWav(buffer)
}
}
}
}.apply { start() }
}
// 释放回音消除器
echoCanceler?.let {
it.enabled = false
it.release()
echoCanceler = null
}
/**
* 停止麦克风捕获并释放所有相关资源
*/
fun stopMicrophoneCapture() {
try {
if (captureThread != null) {
// 中断并等待捕获线程结束
captureThread?.interrupt()
captureThread?.join(300) // 最多等待300ms
// 停止录音并释放AudioRecord资源
audioRecord?.stop() // 必须先停止再释放
isPaused= true
}
// 释放噪声抑制器
noiseSuppressor?.let {
it.enabled = false
it.release()
noiseSuppressor = null
}
// 释放自动增益控制
automaticGainControl?.let {
it.enabled = false
it.release()
automaticGainControl = null
} catch (e: Exception) {
Log.e("AudioConfig", "Error releasing resources: ${e.message}")
} finally {
// 确保恢复原始音频状态
// restoreOriginalAudioState()
}
}
// 恢复音频模式
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
/**
* 释放音频资源
*/
fun releaseAudioResources() {
try {
isPaused= true
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(tag, "释放音频资源失败: ${e.message}")
e.printStackTrace()
// // 释放回音消除器
// echoCanceler?.let {
// it.enabled = false
// it.release()
// echoCanceler = null
// }
// // 释放噪声抑制器
// noiseSuppressor?.let {
// it.enabled = false
// it.release()
// noiseSuppressor = null
// }
// // 释放自动增益控制
// automaticGainControl?.let {
// it.enabled = false
// it.release()
// automaticGainControl = null
// }
// 恢复音频模式
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(tag, "释放音频资源失败: ${e.message}")
e.printStackTrace()
}
}
}
// /**
// * 麦克风流 - 拉流模式
// * 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
// */
// private inner class MicrophoneStream : PullAudioInputStreamCallback() {
// private var audioRecord: AudioRecord? = null
// private var echoCanceler: AcousticEchoCanceler? = null
// private var noiseSuppressor: NoiseSuppressor? = null
// private var automaticGainControl: AutomaticGainControl? = null
// // 音频配置
// private val sampleRate = 16000
// private val channelConfig = AudioFormat.CHANNEL_IN_MONO
// private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// // 优化:减少缓冲区大小以降低延迟
// private val bufferSize = AudioRecord.getMinBufferSize(
// sampleRate, channelConfig, audioFormat
// ).let { if (it < 0) 2048 else it }
// init {
// initMicrophone()
// }
// /**
// * 初始化麦克风
// */
// private fun initMicrophone() {
// try {
// // 创建录音对象
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// val format = AudioFormat.Builder()
// .setSampleRate(sampleRate)
// .setEncoding(audioFormat)
// .setChannelMask(channelConfig)
// .build()
// audioRecord = AudioRecord.Builder()
// .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
// .setAudioFormat(format)
// .setBufferSizeInBytes(bufferSize)
// .build()
// } else {
// audioRecord = AudioRecord(
// MediaRecorder.AudioSource.VOICE_COMMUNICATION,
// sampleRate,
// channelConfig,
// audioFormat,
// bufferSize
// )
// }
// // 检查初始化状态
// if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
// throw IllegalStateException("AudioRecord初始化失败")
// }
// // 应用音频效果
// applyAudioEffects()
// // 开始录音
// audioRecord?.startRecording()
// // 检查录音状态
// if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
// throw IllegalStateException("录音启动失败")
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// /**
// * 应用音频效果 - 总是启用所有音频增强功能
// */
// private fun applyAudioEffects() {
// val sessionId = audioRecord?.audioSessionId ?: return
// try {
// // 设置音频模式为通信模式
// this@AzureAsrHelper.audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
// // 启用声学回音消除
// if (AcousticEchoCanceler.isAvailable()) {
// echoCanceler = AcousticEchoCanceler.create(sessionId)
// echoCanceler?.enabled = true
// Log.i(tag, "声学回音消除已启用")
// } else {
// Log.w(tag, "设备不支持声学回音消除")
// }
// // 启用噪声抑制
// if (NoiseSuppressor.isAvailable()) {
// noiseSuppressor = NoiseSuppressor.create(sessionId)
// noiseSuppressor?.enabled = true
// Log.i(tag, "噪声抑制已启用")
// } else {
// Log.w(tag, "设备不支持噪声抑制")
// }
// // 启用自动增益控制
// if (AutomaticGainControl.isAvailable()) {
// automaticGainControl = AutomaticGainControl.create(sessionId)
// automaticGainControl?.enabled = true
// Log.i(tag, "自动增益控制已启用")
// } else {
// Log.w(tag, "设备不支持自动增益控制")
// }
// Log.i(tag, "回音消除和音频增强功能已全部启用")
// } catch (e: Exception) {
// Log.e(tag, "设置音频效果失败: ${e.message}")
// }
// }
// /**
// * 获取音频格式
// */
// fun getFormat(): AudioStreamFormat =
// AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort())
// /**
// * Azure SDK 调用此方法获取音频数据
// * 这是拉流模式的核心方法,由SDK调用以获取音频数据
// */
// /**
// * 优化:简化音频数据读取(移除文件保存)
// */
// override fun read(buffer: ByteArray): Int {
// try {
// val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1
// if (result < 0) {
// Log.e(tag, "读取音频数据失败: $result")
// return 0
// }
// recordfile?.saveAudioDataToWav(buffer)
// return result
// } catch (e: Exception) {
// Log.e(tag, "读取音频异常: ${e.message}")
// return 0
// }
// }
// /**
// * 优化:简化资源关闭
// */
// override fun close() {
// releaseAudioResources()
// }
// /**
// * 释放音频资源
// */
// private fun releaseAudioResources() {
// try {
// // 停止录音
// if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
// audioRecord?.stop()
// }
// // 释放回音消除器
// echoCanceler?.let {
// it.enabled = false
// it.release()
// echoCanceler = null
// }
// // 释放噪声抑制器
// noiseSuppressor?.let {
// it.enabled = false
// it.release()
// noiseSuppressor = null
// }
// // 释放自动增益控制
// automaticGainControl?.let {
// it.enabled = false
// it.release()
// automaticGainControl = null
// }
// // 恢复音频模式
// this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
// // 释放录音实例
// audioRecord?.release()
// audioRecord = null
// } catch (e: Exception) {
// Log.e(tag, "释放音频资源失败: ${e.message}")
// e.printStackTrace()
// }
// }
// }
}
/**
* 外部音频拉流

6
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -256,13 +256,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
"setPause" -> {
azureAsrHelper.stopAudioPause();
azureAsrHelper.audioPause();
return
}
"setcontinue" -> {
// azureAsrHelper.stopAudioPause();
azureAsrHelper.setAudiocontinue();
azureAsrHelper.audioContinue();
return
}
@ -272,7 +271,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
"restoreOriginalAudioState" -> {
// azureAsrHelper.stopAudioPause();
azureAsrHelper.restoreOriginalAudioState();
return
}

9
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -578,15 +578,14 @@ object BleService {
// 根据特征UUID区分处理
when (c.uuid) {
// 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> {
// BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> {
Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1")
processAudioData(data)
}
// Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1")
// processAudioData(data)
// }
// 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID")
processAudioData(data)
}
// 通知特征数据(命令和控制)

Loading…
Cancel
Save