Browse Source

加入音频放回flutter,加入异常重置识别器机制

weicu
fdp 1 year ago
parent
commit
ddad5c07c8
  1. 3
      lib/data/services/asr_service.dart
  2. 8
      lib/data/services/speech_impl/azure_asr_service.dart
  3. 90
      lib/modules/meeting/controllers/meeting_record_controller.dart
  4. 111
      lib/modules/meeting/views/meeting_record_view.dart
  5. 3
      lib/modules/speech_test/controllers/speech_test_controller.dart
  6. 73
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  7. 70
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  8. 133
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  9. 89
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

3
lib/data/services/asr_service.dart

@ -67,6 +67,9 @@ enum RecognitionEventType {
/// 中间识别结果(实时反馈) /// 中间识别结果(实时反馈)
intermediateResult, intermediateResult,
/// 音频
onAudio,
/// 会话开始 /// 会话开始
sessionStarted, sessionStarted,

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

@ -253,7 +253,13 @@ class AzureAsrService extends GetxService implements AsrService {
detectedLanguage: detectedLanguage, detectedLanguage: detectedLanguage,
)); ));
break; break;
case 'onAudio':
final Uint8List data = eventMap['data'] as Uint8List? ?? Uint8List(0);
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.onAudio,
audio: data,
));
break;
case 'sessionStarted': case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted, type: RecognitionEventType.sessionStarted,

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

@ -2,6 +2,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'package:deep_voice/core/utils/logger.dart'; import 'package:deep_voice/core/utils/logger.dart';
import 'package:deep_voice/core/utils/permission_util.dart'; import 'package:deep_voice/core/utils/permission_util.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -56,10 +57,20 @@ class MeetingRecordController extends GetxController
Timer? _timer; Timer? _timer;
int _elapsedMilliseconds = 0; int _elapsedMilliseconds = 0;
int? _lastStartTime; int? _lastStartTime;
int _currentPosition = 0;
bool _audioSourceType = false; bool _audioSourceType = false;
late TabController tabController; late TabController tabController;
// 添加波形放大倍数变量
final double waveAmplifyFactor = 100.0; // 增加波动幅度
final RxDouble ursorPosition = 0.0.obs; // 当前播放位置
final RxInt _currentPosition = 0.obs; // 原始位置索引
int maxPoints = 500; // 最大显示点数(根据屏幕宽度优化)
int sampleRate = 16000; // 假设音频采样率(根据实际情况调整)
// 设置最大点数(根据屏幕宽度)
void setMaxPoints(double screenWidth) {
maxPoints = screenWidth ~/ 2; // 每2像素一个数据点
}
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -291,6 +302,10 @@ class MeetingRecordController extends GetxController
case RecognitionEventType.intermediateResult: case RecognitionEventType.intermediateResult:
intermediateContent.value = event.text; intermediateContent.value = event.text;
break; break;
case RecognitionEventType.onAudio:
// 添加新音频数据到波形
_processAudioData(event.audio!);
break;
case RecognitionEventType.finalResult: case RecognitionEventType.finalResult:
finalContent.value += _formatTranscript(event); finalContent.value += _formatTranscript(event);
intermediateContent.value = ''; intermediateContent.value = '';
@ -300,6 +315,79 @@ class MeetingRecordController extends GetxController
} }
} }
/// 处理原始Uint8List音频数据
void _processAudioData(Uint8List audioData) {
// 将Uint8List转换为Float32List(PCM16 -> 浮点)
final pcmData = _convertToPCM(audioData);
// 降采样和添加数据
_addAudioData(pcmData);
}
/// 转换Uint8List为PCM浮点数组
Float32List _convertToPCM(Uint8List uint8List) {
// 假设音频为16位PCM小端格式
final byteData = ByteData.sublistView(uint8List);
final length = uint8List.length ~/ 2;
final pcmData = Float32List(length);
for (int i = 0; i < length; i++) {
// 读取16位有符号整数 (-32768 到 32767)
final int intValue = byteData.getInt16(i * 2, Endian.little);
// 转换为浮点数 (-1.0 到 1.0)
pcmData[i] = intValue / 32768.0;
}
return pcmData;
}
/// 降采样音频数据
List<double> _downsampleAudio(Float32List audio) {
if (audio.isEmpty) return [];
final result = <double>[];
final step = (audio.length / maxPoints).ceil();
// 计算每个区间的峰值(保留更多细节)
for (int i = 0; i < audio.length; i += step) {
double maxVal = 0.0;
final end = min(i + step, audio.length);
for (int j = i; j < end; j++) {
final absVal = audio[j].abs();
if (absVal > maxVal) maxVal = absVal;
}
// 应用放大倍数增强波动效果
result.add(maxVal * waveAmplifyFactor);
}
return result;
}
// 获取当前光标位置(0.0 - 1.0)
double get cursorPositionValue {
if (waveData.isEmpty) return 0.0;
return _currentPosition.value / waveData.length;
}
/// 添加处理后的音频数据
void _addAudioData(Float32List newAudio) {
// 1. 降采样:只保留部分数据点
final downsampled = _downsampleAudio(newAudio);
waveData.addAll(downsampled);
// 3. 更新光标位置到最新点
_currentPosition.value = waveData.length;
// 4. 限制数据长度,移除旧数据
if (waveData.length > maxPoints * 2) {
waveData.removeRange(0, waveData.length - maxPoints);
_currentPosition.value = waveData.length;
}
}
/// Formats transcript with speaker identification /// Formats transcript with speaker identification
String _formatTranscript(RecognitionEvent event) { String _formatTranscript(RecognitionEvent event) {
final buffer = StringBuffer(); final buffer = StringBuffer();

111
lib/modules/meeting/views/meeting_record_view.dart

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -112,43 +114,53 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
} }
Widget _buildAudioTab(BuildContext context, bool isDarkMode) { Widget _buildAudioTab(BuildContext context, bool isDarkMode) {
return Obx(() => Column( return Obx(() {
children: [ final screenWidth = MediaQuery.of(context).size.width;
const SizedBox(height: 24), // 初始化最大点数
SizedBox( if (controller.maxPoints == 0) {
height: 120, controller.setMaxPoints(screenWidth);
child: Stack( }
alignment: Alignment.center, final cursorPos = controller.cursorPositionValue;
children: [ return Column(
CustomPaint( children: [
painter: WaveformPainter( const SizedBox(height: 24),
controller.waveData, SizedBox(
cursorPosition: controller.cursorPosition, height: 120,
), child: Stack(
size: Size.infinite, alignment: Alignment.center,
children: [
// 波形绘制
CustomPaint(
painter: WaveformPainter(
controller.waveData,
cursorPosition: cursorPos,
// color: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
), ),
Positioned( size: Size(screenWidth, 120),
left: MediaQuery.of(context).size.width * ),
controller.cursorPosition, // 光标指示器
child: Container( Positioned(
width: 2, left: MediaQuery.of(context).size.width * cursorPos,
height: 120, child: Container(
color: Colors.red, width: 2,
), height: 120,
color: Colors.red,
), ),
], ),
), ],
),
const SizedBox(height: 16),
Text(
controller.formatTime(controller.seconds.value),
style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
), ),
const Spacer(), ),
_buildBottomBar(context, isDarkMode), const SizedBox(height: 16),
const SizedBox(height: 32), Text(
], controller.formatTime(controller.seconds.value),
)); style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
const Spacer(),
_buildBottomBar(context, isDarkMode),
const SizedBox(height: 32),
],
);
});
} }
Widget _buildTextTab(BuildContext context, bool isDarkMode) { Widget _buildTextTab(BuildContext context, bool isDarkMode) {
@ -394,16 +406,33 @@ class WaveformPainter extends CustomPainter {
..color = Colors.red ..color = Colors.red
..strokeWidth = 2; ..strokeWidth = 2;
final double barWidth = size.width / data.length; final double barWidth = size.width / (data.length * 1.2); // 增加条宽
final centerY = size.height / 2; final centerY = size.height / 2;
// 改为绘制矩形条,增加视觉冲击力
for (int i = 0; i < data.length; i++) { for (int i = 0; i < data.length; i++) {
final x = i * barWidth + barWidth / 2; final x = i * barWidth;
final height = data[i]; final height = min(data[i], 50); // 限制最大高度
canvas.drawLine( final rect = Rect.fromLTWH(
Offset(x, centerY - height), x,
Offset(x, centerY + height), centerY - height / 2,
paint, barWidth * 0.8, // 条宽稍小于间隔
height.toDouble(),
);
// 添加渐变效果增强视觉
final gradient = LinearGradient(
colors: [Colors.blue[400]!, Colors.blue[200]!],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
final rectPaint = Paint()
..shader = gradient.createShader(rect)
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(4)),
rectPaint,
); );
} }

3
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -176,6 +176,9 @@ class SpeechTestController extends GetxController {
// 清空中间结果 // 清空中间结果
intermediateResult.value = ''; intermediateResult.value = '';
break; break;
case RecognitionEventType.onAudio:
// TODO: Handle this case.
break;
} }
}, onError: (error) { }, onError: (error) {
isListening.value = false; isListening.value = false;

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

@ -112,7 +112,7 @@ object AgentService : CoroutineScope {
private var lastInterruptTime = 0L private var lastInterruptTime = 0L
private val INTERRUPT_DEBOUNCE_MS = 300L private val INTERRUPT_DEBOUNCE_MS = 300L
var audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE var audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE
// 语气词过滤列表(中英文) // 语气词过滤列表(中英文)
private val FILLER_WORDS = setOf( private val FILLER_WORDS = setOf(
@ -177,24 +177,24 @@ object AgentService : CoroutineScope {
} }
// 初始化Azure ASR // 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply { azureAsrHelper = AzureAsrHelper(context).apply {
// 使用协程处理异步初始化 // 使用协程处理异步初始化
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val asrInitSuccess = initialize( val asrInitSuccess = initialize(
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "", region = config["azureSpeechRegion"]?.toString() ?: "",
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE else AzureAsrHelper.AudioSourceType.MICROPHONE
) )
if (asrInitSuccess) { if (asrInitSuccess) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
recognizeCallback() recognizeCallback()
}
} else {
Log.w(TAG, "Azure ASR初始化失败")
}
}
} }
} else {
Log.w(TAG, "Azure ASR初始化失败")
}
}
}
// 初始化TTS服务 // 初始化TTS服务
initializeTtsEngine(config) initializeTtsEngine(config)
@ -207,9 +207,8 @@ object AgentService : CoroutineScope {
config["openaivisionModel"]?.toString() ?: "", config["openaivisionModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: "" config["mcpServer"]?.toString() ?: ""
) )
// 加载最近的聊天记录 // 加载最近的聊天记录
loadChatHistory() loadChatHistory()
@ -430,14 +429,14 @@ object AgentService : CoroutineScope {
Log.d(TAG, "服务未初始化") Log.d(TAG, "服务未初始化")
return false return false
} }
Log.d("TAG", "开始语音识别startRecognition?") Log.d("TAG", "开始语音识别startRecognition?")
stopRecognition() stopRecognition()
BleService.openEncoder() BleService.openEncoder()
// if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){ // if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){
// Log.d(TAG, "开始语音识别stopRecognition") // Log.d(TAG, "开始语音识别stopRecognition")
// // BleService.closeCodec() // // BleService.closeCodec()
// stopRecognition() // stopRecognition()
// } // }
// if((isExternalActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE)||!isRecognitionActive) // if((isExternalActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE)||!isRecognitionActive)
// { // {
@ -466,9 +465,9 @@ object AgentService : CoroutineScope {
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
Log.d("TAG", "azureAsrHelper?.startContinuousRecognition") Log.d("TAG", "azureAsrHelper?.startContinuousRecognition")
azureAsrHelper?.startContinuousRecognition(audioSourceType) azureAsrHelper?.startContinuousRecognition(audioSourceType)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
@ -485,7 +484,7 @@ object AgentService : CoroutineScope {
fun recognizeCallback(): Boolean { fun recognizeCallback(): Boolean {
try { try {
Log.d(TAG, "recognizeCallback") Log.d(TAG, "recognizeCallback")
val success = azureAsrHelper?.setupEventListeners(object : val success = azureAsrHelper?.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback { AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
@ -560,6 +559,10 @@ object AgentService : CoroutineScope {
} }
override fun onAudio(data: ByteArray) {
}
override fun onSessionStopped() { override fun onSessionStopped() {
sendEvent("recognition_stopped", mapOf("status" to "stopped")) sendEvent("recognition_stopped", mapOf("status" to "stopped"))
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
@ -570,8 +573,7 @@ object AgentService : CoroutineScope {
override fun onCanceled(reason: String, errorDetails: String) { override fun onCanceled(reason: String, errorDetails: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "recognition_canceled:") Log.d(TAG, "recognition_canceled:")
recognizeCallback()
BleService.closeCodec() BleService.closeCodec()
sendEvent( sendEvent(
"recognition_canceled", mapOf( "recognition_canceled", mapOf(
@ -579,12 +581,12 @@ object AgentService : CoroutineScope {
"details" to errorDetails "details" to errorDetails
) )
) )
recognizeCallback()
} }
override fun onError(error: String) { override fun onError(error: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
recognizeCallback()
BleService.closeCodec() BleService.closeCodec()
Log.d(TAG, "语音识别错误: $error") Log.d(TAG, "语音识别错误: $error")
sendEvent( sendEvent(
@ -593,6 +595,7 @@ recognizeCallback()
"message" to error "message" to error
) )
) )
recognizeCallback()
} }
}) })
return true return true
@ -609,19 +612,19 @@ recognizeCallback()
* 停止语音识别 * 停止语音识别
*/ */
fun stopRecognition() { fun stopRecognition() {
if (!isRecognitionActive) return if (!isRecognitionActive) return
try { try {
azureAsrHelper?.stopContinuousRecognition() azureAsrHelper?.stopContinuousRecognition()
BleService.closeCodec() BleService.closeCodec()
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
return return
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止语音识别异常: ${e.message}") Log.e(TAG, "停止语音识别异常: ${e.message}")
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
return return
} }
} }
@ -857,7 +860,7 @@ recognizeCallback()
override fun onComplete(sessionid: String) { override fun onComplete(sessionid: String) {
try { try {
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse && reply && broadcast && sessionid == currsessionId){ if (speakResponse && reply && broadcast && sessionid == currsessionId) {
ttsService?.flushStream() ttsService?.flushStream()
audioPlayer?.stopAudio() audioPlayer?.stopAudio()
} }
@ -870,7 +873,7 @@ recognizeCallback()
if (hasImage) { if (hasImage) {
responseData["hasImage"] = true responseData["hasImage"] = true
} }
if (reply){ if (reply) {
sendEvent("assistant_response", responseData) sendEvent("assistant_response", responseData)
} }

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

@ -38,6 +38,7 @@ class AzureAsrHelper(private val context: Context) {
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var recognizer: SpeechRecognizer? = null private var recognizer: SpeechRecognizer? = null
private var audioConfig: AudioConfig? = null private var audioConfig: AudioConfig? = null
private var continuousCallback: ContinuousRecognizeCallback? = null
// 状态管理 // 状态管理
private var isContinuousRecognitionActive = false private var isContinuousRecognitionActive = false
@ -145,22 +146,24 @@ class AzureAsrHelper(private val context: Context) {
// 录音文件类 // 录音文件类
recordfile = RecordFile; recordfile = RecordFile;
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "初始化失败: ${e.message}") Log.e(tag, "初始化失败: ${e.message}")
return false return false
} }
} }
private fun isRecognizerValid(): Boolean {
return recognizer != null private fun isRecognizerValid(): Boolean {
} return recognizer != null
}
/** /**
* 设置识别器 * 设置识别器
*/ */
private fun setupRecognizer(): Boolean { private fun setupRecognizer(): Boolean {
try { try {
// 如果正在进行连续识别,先停止 // 如果正在进行连续识别,先停止
if (isContinuousRecognitionActive) { if (isContinuousRecognitionActive) {
// 直接停止,不等待结果 // 直接停止,不等待结果
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
@ -192,7 +195,7 @@ private fun isRecognizerValid(): Boolean {
private fun setupMicrophoneStream() { private fun setupMicrophoneStream() {
try { try {
Log.d(tag, "设置麦克风流 - 使用拉流方式: }") Log.d(tag, "设置麦克风流 - 使用拉流方式: }")
if (audioStream == null) { if (audioStream == null) {
// 创建外部音频拉流对象 // 创建外部音频拉流对象
audioStream = AudioStream() audioStream = AudioStream()
@ -222,7 +225,7 @@ private fun isRecognizerValid(): Boolean {
callback.onError("语音服务未初始化") callback.onError("语音服务未初始化")
return return
} }
this.audioSourceType = audioSourceType this.audioSourceType = audioSourceType
// 确保不在连续识别中 // 确保不在连续识别中
if (isContinuousRecognitionActive) { if (isContinuousRecognitionActive) {
@ -270,26 +273,26 @@ private fun isRecognizerValid(): Boolean {
return true return true
} }
if (!isRecognizerValid()) { if (!isRecognizerValid()) {
Log.w(tag, "识别器已失效,正在重新创建...") Log.w(tag, "识别器已失效,正在重新创建...")
if (!setupRecognizer()) { if (!setupRecognizer()) {
Log.e(tag, "重新创建识别器失败") Log.e(tag, "重新创建识别器失败")
return false return false
}
} }
}
this.audioSourceType = audioSourceType this.audioSourceType = audioSourceType
try { try {
Log.d(tag, "startContinuousRecognition: ") Log.d(tag, "startContinuousRecognition: ")
// 启动音频处理 // 启动音频处理
//startAudioProcessing() //startAudioProcessing()
// 开始连续识别 // 开始连续识别
// 启动音频处理 // 启动音频处理
audioStream!!.startAudioRecord() audioStream!!.startAudioRecord()
recognizer?.startContinuousRecognitionAsync() recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true isContinuousRecognitionActive = true
@ -306,19 +309,20 @@ private fun isRecognizerValid(): Boolean {
* 设置事件监听器 * 设置事件监听器
*/ */
fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean {
Log.d(tag, "设置ssssss监听器:${speechConfig} ") Log.d(tag, "设置ssssss监听器:${speechConfig} ")
if (speechConfig == null) { if (speechConfig == null) {
callback.onError("语音服务未初始化") callback.onError("语音服务未初始化")
return false return false
} }
// 重设识别器 // 重设识别器
if (!setupRecognizer()) { if (!setupRecognizer()) {
Log.d(tag, "初始化失败: ") Log.d(tag, "初始化失败: ")
return false return false
} }
try { try {
Log.d(tag, "设置ssssss监听器: ") continuousCallback = callback;
Log.d(tag, "设置ssssss监听器: ")
// 优化:识别中事件 - 添加文本长度检查 // 优化:识别中事件 - 添加文本长度检查
recognizer?.recognizing?.addEventListener( recognizer?.recognizing?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
@ -367,7 +371,7 @@ private fun isRecognizerValid(): Boolean {
callback.onSessionStopped() callback.onSessionStopped()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
// stopAudioProcessing() // stopAudioProcessing()
} }
} }
) )
@ -378,10 +382,10 @@ private fun isRecognizerValid(): Boolean {
val errorDetails = event.errorDetails ?: "未知错误" val errorDetails = event.errorDetails ?: "未知错误"
val reason = event.reason.toString() val reason = event.reason.toString()
callback.onCanceled(reason, errorDetails) callback.onCanceled(reason, errorDetails)
} }
) )
} catch (e: Exception) { } catch (e: Exception) {
callback.onError("启动连续识别失败: ${e.message}") callback.onError("启动连续识别失败: ${e.message}")
return false return false
@ -414,7 +418,7 @@ private fun isRecognizerValid(): Boolean {
} }
Log.d(tag, "停止连续语音识别: ") Log.d(tag, "停止连续语音识别: ")
// 直接停止连续识别(SDK内部已是异步操作) // 直接停止连续识别(SDK内部已是异步操作)
recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS)
// 停止音频处理 // 停止音频处理
audioStream!!.stopMicrophoneCapture() audioStream!!.stopMicrophoneCapture()
@ -424,7 +428,7 @@ recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS)
// 强制重置状态 // 强制重置状态
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
Log.e(tag, "停止连续识别失败: ${e.message}") Log.e(tag, "停止连续识别失败: ${e.message}")
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
// 停止音频处理 // 停止音频处理
audioStream!!.stopMicrophoneCapture() audioStream!!.stopMicrophoneCapture()
// 尝试强制关闭识别器 // 尝试强制关闭识别器
@ -455,7 +459,7 @@ recognizer?.stopContinuousRecognitionAsync()
recognizer?.stopContinuousRecognitionAsync() recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
} }
// recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) // recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS)
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 停止录音 // 停止录音
@ -499,7 +503,7 @@ recognizer?.stopContinuousRecognitionAsync()
private fun stopAudioProcessing() { private fun stopAudioProcessing() {
audioStream?.let { audioStream?.let {
try { try {
Log.e(tag, "停止音频处理: }") Log.e(tag, "停止音频处理: }")
it.releaseAudioResources() it.releaseAudioResources()
it.pushAudioStream?.close() it.pushAudioStream?.close()
@ -695,7 +699,7 @@ recognizer?.stopContinuousRecognitionAsync()
// 情况2:不在录制但队列有数据 -> 从队列获取 // 情况2:不在录制但队列有数据 -> 从队列获取
else if (writeQueue.isNotEmpty()) { else if (writeQueue.isNotEmpty()) {
data = writeQueue.poll() data = writeQueue.poll()
Log.d("tag", "写入数据: ${ data?.size}") Log.d("tag", "写入数据: ${data?.size}")
bytesToWrite = data?.size ?: 0 bytesToWrite = data?.size ?: 0
} }
@ -706,6 +710,7 @@ recognizer?.stopContinuousRecognitionAsync()
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try { try {
continuousCallback?.onAudio(finalData)
pushAudioStream?.write(finalData) pushAudioStream?.write(finalData)
recordfile?.saveAudioDataToWav(finalData) recordfile?.saveAudioDataToWav(finalData)
} catch (e: Exception) { } catch (e: Exception) {
@ -732,7 +737,7 @@ recognizer?.stopContinuousRecognitionAsync()
* 外部音频输入 * 外部音频输入
*/ */
fun saveAudioDataTo(buffer: ByteArray) { fun saveAudioDataTo(buffer: ByteArray) {
if (audioSourceType == AudioSourceType.MICROPHONE) return if (audioSourceType == AudioSourceType.MICROPHONE) return
// 放入队列,由写线程写入 // 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf()) writeQueue.offer(buffer.copyOf())
@ -817,7 +822,7 @@ recognizer?.stopContinuousRecognitionAsync()
try { // TODO: 实现外部音频源捕获逻辑 try { // TODO: 实现外部音频源捕获逻辑
// 停止录音 // 停止录音
if (audioRecord?.recordingState == AudioRecord.STATE_INITIALIZED) { if (audioRecord?.recordingState == AudioRecord.STATE_INITIALIZED) {
Log.d("TAG", "外部音频捕获启动 释放audioRecord") Log.d("TAG", "外部音频捕获启动 释放audioRecord")
audioRecord?.stop() audioRecord?.stop()
// 释放录音实例 // 释放录音实例
audioRecord?.release() audioRecord?.release()
@ -826,7 +831,7 @@ recognizer?.stopContinuousRecognitionAsync()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("TAG", "外部音频捕获异常: ${e.message}") Log.e("TAG", "外部音频捕获异常: ${e.message}")
} finally { } finally {
} }
} }
@ -947,6 +952,13 @@ recognizer?.stopContinuousRecognitionAsync()
*/ */
fun onSessionStopped() fun onSessionStopped()
/**
* 返回音频
*
* @param data 识别的音频
*/
fun onAudio(data: ByteArray)
/** /**
* 识别取消时调用 * 识别取消时调用
* *

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

@ -132,6 +132,83 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
} }
//设置识别器
private fun recognizeCallback(): Boolean {
try {
azureAsrHelper.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(
mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
)
)
}
override fun onRecognizing(
recognizing: String,
detectedLanguage: String
) {
sendAsrEvent(
mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
)
)
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onAudio(data: ByteArray) {
sendAsrEvent(
mapOf(
"type" to "onAudio",
"data" to data
)
)
}
override fun onCanceled(reason: String, errorDetails: String) {
sendAsrEvent(
mapOf(
"type" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
)
)
recognizeCallback()
}
override fun onError(error: String) {
sendAsrEvent(
mapOf(
"type" to "error",
"message" to error
)
)
recognizeCallback()
}
})
return true
} catch (e: Exception) {
return false
}
}
/** /**
* 设置TTS事件监听器 * 设置TTS事件监听器
*/ */
@ -296,60 +373,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
"recognizeCallback" -> { "recognizeCallback" -> {
val success = azureAsrHelper.setupEventListeners(object : var success = recognizeCallback()
AzureAsrHelper.ContinuousRecognizeCallback { FileLogger.d(tag, "recognizeCallback: ${success}") //
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(
mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
)
)
}
override fun onRecognizing(
recognizing: String,
detectedLanguage: String
) {
sendAsrEvent(
mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
)
)
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendAsrEvent(
mapOf(
"type" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
)
)
}
override fun onError(error: String) {
sendAsrEvent(
mapOf(
"type" to "error",
"message" to error
)
)
}
})
FileLogger.d(tag, "recognizeCallback: ${success}") //
result.success(success) result.success(success)
} }

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

@ -64,7 +64,8 @@ object BleService {
// 上下文 // 上下文
private lateinit var context: Context private lateinit var context: Context
private val commandQueue: Queue<ByteArray> = LinkedList() private val commandQueue: Queue<ByteArray> = LinkedList()
// 蓝牙相关组件 // 蓝牙相关组件
private lateinit var bluetoothManager: BluetoothManager private lateinit var bluetoothManager: BluetoothManager
private var bluetoothAdapter: BluetoothAdapter? = null private var bluetoothAdapter: BluetoothAdapter? = null
@ -75,7 +76,7 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
private var notifyChar: BluetoothGattCharacteristic? = null private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null
private var audioChar: BluetoothGattCharacteristic? = null private var audioChar: BluetoothGattCharacteristic? = null
private var audioChar1: BluetoothGattCharacteristic? = null private var audioChar1: BluetoothGattCharacteristic? = null
var recordfile: RecordingFile? = null var recordfile: RecordingFile? = null
// 扫描相关 // 扫描相关
@ -88,8 +89,10 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
// 是否回复指令 // 是否回复指令
var isReply = true var isReply = true
// 是否回复指令码 // 是否回复指令码
var cmdReplyType =0xff var cmdReplyType = 0xff
// 连接状态 // 连接状态
val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED) val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED)
@ -198,12 +201,13 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
bluetoothAdapter = manager.adapter // 重新获取新引用 bluetoothAdapter = manager.adapter // 重新获取新引用
bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
} }
/** /**
* 开始主动扫描BLE设备 * 开始主动扫描BLE设备
* @param timeout 扫描超时时间(毫秒) * @param timeout 扫描超时时间(毫秒)
* @return 是否成功开始扫描 * @return 是否成功开始扫描
*/ */
fun startScan(deviceName:String,timeout: Long = 15000): Boolean { fun startScan(deviceName: String, timeout: Long = 15000): Boolean {
// Log.i(TAG, "liwei---------startScan 1 $deviceName") // Log.i(TAG, "liwei---------startScan 1 $deviceName")
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "BLE服务未初始化") Log.e(TAG, "BLE服务未初始化")
@ -332,12 +336,12 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
} }
} }
fun getDeviceInfo(address: String): Map<String,Any>? { fun getDeviceInfo(address: String): Map<String, Any>? {
// 更新缓存 // 更新缓存
val idx = scanResults.indexOfFirst { it.device.address == address } val idx = scanResults.indexOfFirst { it.device.address == address }
Log.d(TAG, "获取设备信息:${address} idx:$idx") Log.d(TAG, "获取设备信息:${address} idx:$idx")
if (idx >= 0){ if (idx >= 0) {
stopScan() stopScan()
val result = scanResults[idx]; val result = scanResults[idx];
return printDeviceInfo(result) return printDeviceInfo(result)
@ -348,7 +352,7 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
/** /**
* 打印设备信息和服务UUID * 打印设备信息和服务UUID
*/ */
private fun printDeviceInfo(result: ScanResult):Map<String,Any>?{ private fun printDeviceInfo(result: ScanResult): Map<String, Any>? {
val device = result.device val device = result.device
val scanRecord = result.scanRecord val scanRecord = result.scanRecord
val serviceUuids = val serviceUuids =
@ -545,7 +549,7 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
// 获取音频服务1特征 // 获取音频服务1特征
val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1) val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1)
audioChar1 = audioSvc1?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID1) audioChar1 = audioSvc1?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID1)
if (notifyChar == null || writeChar == null) { if (notifyChar == null || writeChar == null) {
@ -563,7 +567,7 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
} else { } else {
Log.w(TAG, "音频服务特征未找到") Log.w(TAG, "音频服务特征未找到")
} }
if (audioChar1 != null) { if (audioChar1 != null) {
setupNotifications(g, audioChar1) setupNotifications(g, audioChar1)
Log.i(TAG, "音频服务特征找到并设置通知") Log.i(TAG, "音频服务特征找到并设置通知")
} else { } else {
@ -581,7 +585,7 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
// 根据特征UUID区分处理 // 根据特征UUID区分处理
when (c.uuid) { when (c.uuid) {
// 音频特征数据 // 音频特征数据
// BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> { // BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> {
// Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1") // Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1")
@ -667,7 +671,7 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
private fun processAudioData(data: ByteArray) { private fun processAudioData(data: ByteArray) {
try { try {
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
recordfile?.saveAudioDataToWav(data) recordfile?.saveAudioDataToWav(data)
opusManager?.writeAudioStream(data) opusManager?.writeAudioStream(data)
} else { } else {
@ -748,21 +752,19 @@ private val commandQueue: Queue<ByteArray> = LinkedList()
} }
}" }"
) )
isReply=true; isReply = true;
cmdReplyType=cmdType; cmdReplyType = cmdType;
if(!commandQueue.isEmpty()) if (!commandQueue.isEmpty()) {
{ val nextCmd = commandQueue.poll()
val nextCmd = commandQueue.poll() writeChar!!.value = nextCmd
writeChar!!.value=nextCmd var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar)
var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) if (isSeccess == true) {
if(isSeccess==true) Log.d(
{ TAG,
Log.d( "收到设备响应:"
TAG, )
"收到设备响应:" }
) }
}
}
// 根据命令类型处理不同的响应 // 根据命令类型处理不同的响应
when (cmdType) { when (cmdType) {
BleConst.CMD_GET_VERSION -> { BleConst.CMD_GET_VERSION -> {
@ -1026,7 +1028,7 @@ if(!commandQueue.isEmpty())
BleConst.CMD_WAKE_UP -> { BleConst.CMD_WAKE_UP -> {
Log.i(TAG, "收到唤醒指令") Log.i(TAG, "收到唤醒指令")
// 接收到唤醒信号,打开编码器 (设备侧) // 接收到唤醒信号,打开编码器 (设备侧)
notifyWakeupSignalReceived() notifyWakeupSignalReceived()
} }
@ -1380,26 +1382,21 @@ if(!commandQueue.isEmpty())
// 发送数据 // 发送数据
writeChar!!.value = cmdPacket writeChar!!.value = cmdPacket
// 加入队列并触发发送 // 加入队列并触发发送
if(isReply) if (isReply) {
{ var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar)
var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) if (isSeccess == true) {
if(isSeccess == true) isReply = false;
{ cmdType;
isReply=false; return true
cmdType; } else
return true return false
} } else {
else
return false
}
else
{
commandQueue.offer(cmdPacket) commandQueue.offer(cmdPacket)
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送命令异常: ${e.message}", e) Log.e(TAG, "发送命令异常: ${e.message}", e)
return false return false

Loading…
Cancel
Save