37 changed files with 4693 additions and 737 deletions
@ -0,0 +1,113 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'dart:typed_data'; |
||||
|
|
||||
|
/// 语音识别服务接口 |
||||
|
abstract class AstService { |
||||
|
/// 支持的语言 |
||||
|
List<String> get supportedLanguages; |
||||
|
|
||||
|
/// 初始化语音识别服务 |
||||
|
Future<bool> initialize({required List<String> supportedLanguages}); |
||||
|
|
||||
|
/// 开始录音 |
||||
|
Future<bool> enableRecord(String filePath); |
||||
|
|
||||
|
/// |
||||
|
Future<bool> startContinuousTranslation(); |
||||
|
|
||||
|
/// |
||||
|
Future<bool> stopContinuousTranslation(); |
||||
|
|
||||
|
/// 返回一个包含识别事件的流 |
||||
|
Future<Stream<RecognitionEvent1>> recognizeCallback(); |
||||
|
|
||||
|
/// 开始录音 |
||||
|
Future<bool> path(String filePath); |
||||
|
|
||||
|
/// 释放资源 |
||||
|
Future<void> dispose(); |
||||
|
} |
||||
|
|
||||
|
// 识别事件类型 |
||||
|
enum RecognitionEventType1 { |
||||
|
/// 最终识别结果 |
||||
|
finalResult, |
||||
|
|
||||
|
/// 中间识别结果(实时反馈) |
||||
|
intermediateResult, |
||||
|
|
||||
|
/// 会话开始 |
||||
|
sessionStarted, |
||||
|
|
||||
|
/// 会话结束 |
||||
|
sessionStopped, |
||||
|
|
||||
|
/// 识别取消 |
||||
|
canceled, |
||||
|
|
||||
|
/// 识别错误 |
||||
|
error, |
||||
|
} |
||||
|
|
||||
|
// 识别事件 |
||||
|
class RecognitionEvent1 { |
||||
|
/// 事件类型 |
||||
|
final RecognitionEventType1 type; |
||||
|
|
||||
|
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) |
||||
|
final String text; |
||||
|
|
||||
|
/// 检测到的语言 |
||||
|
final String detectedLanguage; |
||||
|
|
||||
|
/// 角色 |
||||
|
final String role; |
||||
|
|
||||
|
/// 原始音频 |
||||
|
final Uint8List? audio; |
||||
|
|
||||
|
/// 错误信息(仅在 error 和 canceled 类型中有效) |
||||
|
final String error; |
||||
|
|
||||
|
RecognitionEvent1({ |
||||
|
required this.type, |
||||
|
this.text = '', |
||||
|
this.detectedLanguage = '', |
||||
|
this.role = '', |
||||
|
this.audio, |
||||
|
this.error = '', |
||||
|
}); |
||||
|
|
||||
|
/// 创建最终结果事件的快捷构造函数 |
||||
|
factory RecognitionEvent1.finalResult({ |
||||
|
required String text, |
||||
|
String detectedLanguage = '', |
||||
|
}) { |
||||
|
return RecognitionEvent1( |
||||
|
type: RecognitionEventType1.finalResult, |
||||
|
text: text, |
||||
|
detectedLanguage: detectedLanguage, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 创建错误事件的快捷构造函数 |
||||
|
factory RecognitionEvent1.error(String errorMessage) { |
||||
|
return RecognitionEvent1( |
||||
|
type: RecognitionEventType1.error, |
||||
|
error: errorMessage, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 检查是否为最终结果 |
||||
|
bool get isFinalResult => type == RecognitionEventType1.finalResult; |
||||
|
|
||||
|
/// 检查是否为错误 |
||||
|
bool get isError => |
||||
|
type == RecognitionEventType1.error || |
||||
|
type == RecognitionEventType1.canceled; |
||||
|
|
||||
|
@override |
||||
|
String toString() { |
||||
|
return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'dart:typed_data'; |
||||
|
|
||||
|
/// 语音识别服务接口 |
||||
|
abstract class AudioService { |
||||
|
/// 开始录音 |
||||
|
Future<bool> enableRecord(String filePath); |
||||
|
|
||||
|
/// 暂停录音 |
||||
|
Future<bool> pauseRecord(); |
||||
|
|
||||
|
/// 继续录音 |
||||
|
Future<bool> resumeRecord(); |
||||
|
|
||||
|
/// 设置音频配置 |
||||
|
Future<bool> setAudioConfig({ |
||||
|
int sampleRate = 16000, |
||||
|
int channels = 1, |
||||
|
}); |
||||
|
|
||||
|
// /// 移动文件到新路径 |
||||
|
Future<bool> moveFile(String sourcePath, String destPath); |
||||
|
|
||||
|
// /// 重命名指定路径的音频文件 |
||||
|
Future<bool> renameFile(String filePath, String newName); |
||||
|
|
||||
|
/// 结束录音 |
||||
|
Future<bool> stopRecord(bool isSave); |
||||
|
} |
||||
@ -0,0 +1,305 @@ |
|||||
|
import 'dart:async'; |
||||
|
import '../../../data/models/appconfig.dart'; |
||||
|
import 'package:flutter/services.dart'; |
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import '../ast_service.dart'; |
||||
|
|
||||
|
/// 音频源类型 |
||||
|
enum AudioSourceType { |
||||
|
microphone, // 使用设备麦克风 |
||||
|
external // 使用外部提供的音频数据 |
||||
|
} |
||||
|
|
||||
|
/// Azure 语音识别服务 |
||||
|
/// |
||||
|
/// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口 |
||||
|
class AzureAstService extends GetxService implements AstService { |
||||
|
static final AzureAstService to = Get.put(AzureAstService()); |
||||
|
static const MethodChannel _channel = MethodChannel('azure_speech/ast'); |
||||
|
static const EventChannel _eventChannel = |
||||
|
EventChannel('azure_speech/ast_events'); |
||||
|
// final GetStorage _storage = GetStorage(); |
||||
|
bool _isInitialized = false; |
||||
|
late final String _subscriptionKey; |
||||
|
late final String _serviceRegion; |
||||
|
late String _baseUrl; |
||||
|
final String _endpoint = '/'; // 修改为根路径 |
||||
|
late final String _accessKey; |
||||
|
late final String _secretKey; |
||||
|
late final String _region; |
||||
|
late final String _service; |
||||
|
|
||||
|
final List<String> _defaultSupportedLanguages = ['zh-CN', 'en-US']; |
||||
|
@override |
||||
|
List<String> get supportedLanguages => _defaultSupportedLanguages; |
||||
|
|
||||
|
//连续识别相关 |
||||
|
bool _isContinuousRecognitionActive = false; |
||||
|
StreamController<RecognitionEvent1>? _eventStreamController; |
||||
|
StreamSubscription? _eventSubscription; |
||||
|
|
||||
|
// 最新的识别结果 |
||||
|
String _latestRecognizedText = ''; |
||||
|
String get latestRecognizedText => _latestRecognizedText; |
||||
|
|
||||
|
// 最新检测到的语言 |
||||
|
String _latestDetectedLanguage = ''; |
||||
|
String get latestDetectedLanguage => _latestDetectedLanguage; |
||||
|
|
||||
|
// 当前音频源类型 |
||||
|
AudioSourceType _audioSourceType = AudioSourceType.microphone; |
||||
|
|
||||
|
AzureAstService() { |
||||
|
_loadConfig(); |
||||
|
} |
||||
|
|
||||
|
/// 设置事件通道 |
||||
|
void _setupEventChannel() { |
||||
|
_eventSubscription?.cancel(); |
||||
|
_eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) { |
||||
|
if (event is Map) { |
||||
|
_handleRecognitionEvent(event); |
||||
|
} |
||||
|
}, onError: _handleRecognitionError); |
||||
|
} |
||||
|
|
||||
|
/// 处理来自原生端的识别事件 |
||||
|
void _handleRecognitionEvent(dynamic event) { |
||||
|
if (event is! Map || _eventStreamController == null) return; |
||||
|
|
||||
|
final Map<dynamic, dynamic> eventMap = event; |
||||
|
final String eventType = eventMap['type'] as String? ?? ''; |
||||
|
|
||||
|
// 添加日志帮助调试 |
||||
|
|
||||
|
switch (eventType) { |
||||
|
case 'result': |
||||
|
final String text = eventMap['text'] as String? ?? ''; |
||||
|
final String detectedLanguage = |
||||
|
eventMap['detectedLanguage'] as String? ?? ''; |
||||
|
_latestRecognizedText = text; |
||||
|
_latestDetectedLanguage = detectedLanguage; |
||||
|
_eventStreamController?.add(RecognitionEvent1( |
||||
|
type: RecognitionEventType1.finalResult, |
||||
|
text: text, |
||||
|
detectedLanguage: detectedLanguage, |
||||
|
)); |
||||
|
break; |
||||
|
|
||||
|
case 'recognizing': |
||||
|
final String text = eventMap['text'] as String? ?? ''; |
||||
|
final String detectedLanguage = |
||||
|
eventMap['detectedLanguage'] as String? ?? ''; |
||||
|
_eventStreamController?.add(RecognitionEvent1( |
||||
|
type: RecognitionEventType1.intermediateResult, |
||||
|
text: text, |
||||
|
detectedLanguage: detectedLanguage, |
||||
|
)); |
||||
|
break; |
||||
|
case 'sessionStarted': |
||||
|
_eventStreamController?.add(RecognitionEvent1( |
||||
|
type: RecognitionEventType1.sessionStarted, |
||||
|
)); |
||||
|
break; |
||||
|
|
||||
|
case 'sessionStopped': |
||||
|
_isContinuousRecognitionActive = false; |
||||
|
_eventStreamController?.add(RecognitionEvent1( |
||||
|
type: RecognitionEventType1.sessionStopped, |
||||
|
)); |
||||
|
break; |
||||
|
|
||||
|
case 'canceled': |
||||
|
_isContinuousRecognitionActive = false; |
||||
|
final String reason = eventMap['reason'] as String? ?? ''; |
||||
|
final String errorDetails = eventMap['errorDetails'] as String? ?? ''; |
||||
|
|
||||
|
if (reason.isNotEmpty || errorDetails.isNotEmpty) { |
||||
|
Logger.error('识别取消: $reason - ${errorDetails.toString()}'); |
||||
|
} |
||||
|
|
||||
|
_eventStreamController?.add(RecognitionEvent1( |
||||
|
type: RecognitionEventType1.canceled, |
||||
|
error: '$reason: $errorDetails', |
||||
|
)); |
||||
|
break; |
||||
|
|
||||
|
case 'error': |
||||
|
final String error = eventMap['message'] as String? ?? ''; |
||||
|
Logger.error('识别错误: ${error.toString()}'); |
||||
|
_eventStreamController?.add(RecognitionEvent1( |
||||
|
type: RecognitionEventType1.error, |
||||
|
error: error, |
||||
|
)); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理识别事件流错误 |
||||
|
void _handleRecognitionError(Object error) { |
||||
|
Logger.error('识别事件流错误: ${error.toString()}'); |
||||
|
_eventStreamController?.addError(error); |
||||
|
_cleanupEventStream(); |
||||
|
} |
||||
|
|
||||
|
/// 清理事件流资源 |
||||
|
void _cleanupEventStream() { |
||||
|
_eventStreamController?.close(); |
||||
|
_eventStreamController = null; |
||||
|
_isContinuousRecognitionActive = false; |
||||
|
} |
||||
|
|
||||
|
/// 从环境变量加载配置 |
||||
|
void _loadConfig() { |
||||
|
// final _env = _storage.read("ENV") as Map<String, String>; |
||||
|
_subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; |
||||
|
_serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? ''; |
||||
|
_accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? ''; |
||||
|
_secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? ''; |
||||
|
_region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? 'cn-north-1'; |
||||
|
_service = 'translate'; |
||||
|
_baseUrl = 'https://translate.volcengineapi.com'; |
||||
|
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { |
||||
|
throw Exception( |
||||
|
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<Stream<RecognitionEvent1>> recognizeCallback() async { |
||||
|
if (!_isInitialized) { |
||||
|
await initialize(); |
||||
|
} |
||||
|
try { |
||||
|
_eventStreamController = StreamController<RecognitionEvent1>.broadcast(); |
||||
|
|
||||
|
// 开始连续识别 |
||||
|
final bool result = await _channel.invokeMethod('recognizeCallback'); |
||||
|
if (!result) { |
||||
|
_cleanupEventStream(); |
||||
|
} |
||||
|
|
||||
|
return _eventStreamController!.stream; |
||||
|
} catch (e) { |
||||
|
Logger.error('开始连续语音识别失败: ${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<bool> enableRecord(String filePath) async { |
||||
|
try { |
||||
|
final bool result = await _channel.invokeMethod('enableRecord', { |
||||
|
'filePath': filePath, |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('开始录音: ${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<bool> path(String filePath) async { |
||||
|
try { |
||||
|
final bool result = await _channel.invokeMethod('path', { |
||||
|
'filePath': filePath, |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('开始录音: ${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<bool> startContinuousTranslation() async { |
||||
|
try { |
||||
|
final bool result = |
||||
|
await _channel.invokeMethod('startContinuousTranslation'); |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('停止录音: ${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<bool> stopContinuousTranslation() async { |
||||
|
try { |
||||
|
final bool result = |
||||
|
await _channel.invokeMethod('stopContinuousTranslation'); |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('停止录音: ${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<void> dispose() async { |
||||
|
try { |
||||
|
final bool result = await _channel.invokeMethod('dispose'); |
||||
|
|
||||
|
return; |
||||
|
} catch (e) { |
||||
|
Logger.error('停止录音: ${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<bool> initialize({ |
||||
|
List<String>? supportedLanguages, |
||||
|
bool useExternalAudio = false, |
||||
|
bool useEchoCancellation = false, |
||||
|
}) async { |
||||
|
try { |
||||
|
final List<String> languages = |
||||
|
supportedLanguages ?? _defaultSupportedLanguages; |
||||
|
//底层会初始化前释放 |
||||
|
// // 检查是否需要重新初始化 |
||||
|
// if (_isInitialized) { |
||||
|
// await dispose(); |
||||
|
// } |
||||
|
|
||||
|
// 设置音频源类型 |
||||
|
_audioSourceType = useExternalAudio |
||||
|
? AudioSourceType.external |
||||
|
: AudioSourceType.microphone; |
||||
|
// Future<bool> initialize({ |
||||
|
// required String subscriptionKey, |
||||
|
// required String region, |
||||
|
// required List<String> supportedLanguages, |
||||
|
// required String audioSourceType, |
||||
|
// required String translationAccessKey, |
||||
|
// required String translationSecretKey, |
||||
|
// String translationRegion = 'cn-north-1', |
||||
|
// }); |
||||
|
|
||||
|
final bool result = await _channel.invokeMethod('initialize', { |
||||
|
'subscriptionKey': _subscriptionKey, |
||||
|
'region': _serviceRegion, |
||||
|
'supportedLanguages': languages, |
||||
|
'audioSourceType': _audioSourceType.toString().split('.').last, |
||||
|
'translationAccessKey': _accessKey, |
||||
|
'translationSecretKey': _secretKey, |
||||
|
'translationRegion': _region, |
||||
|
}); |
||||
|
|
||||
|
_isInitialized = result; |
||||
|
_setupEventChannel(); |
||||
|
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); |
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); |
||||
|
_isInitialized = false; |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,426 @@ |
|||||
|
package com.yunqiinnovation.azure_speech.tools |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.media.AudioFormat |
||||
|
import android.media.AudioRecord |
||||
|
import android.media.MediaRecorder |
||||
|
import android.util.Log |
||||
|
import com.microsoft.cognitiveservices.speech.* |
||||
|
import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat |
||||
|
import java.util.concurrent.LinkedBlockingQueue |
||||
|
import com.microsoft.cognitiveservices.speech.audio.AudioInputStream |
||||
|
import com.microsoft.cognitiveservices.speech.audio.PushAudioInputStream |
||||
|
import java.util.concurrent.atomic.AtomicBoolean |
||||
|
import android.media.AudioManager |
||||
|
// 添加缺失的导入 |
||||
|
import android.media.AudioFocusRequest |
||||
|
import android.media.AudioAttributes |
||||
|
import com.yunqiinnovation.azure_speech.tools.RecordFile |
||||
|
|
||||
|
/** |
||||
|
* 简单音频接收器类,用于处理音频录制和流传输 |
||||
|
* @param azureAsrHelper AzureAsrHelper实例的引用,用于访问共享状态 |
||||
|
*/ |
||||
|
class SimpleAudioReceiver(private val context: Context) { |
||||
|
|
||||
|
companion object { |
||||
|
private const val TAG = "SimpleAudioReceiver" |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 音频来源类型(使用 AzureAsrHelper 中定义的枚举) |
||||
|
*/ |
||||
|
enum class AudioSourceType { |
||||
|
/** 使用设备麦克风 */ |
||||
|
MICROPHONE, |
||||
|
/** 使用外部提供的音频数据 */ |
||||
|
EXTERNAL |
||||
|
} |
||||
|
|
||||
|
private val bufferSize = 4096 // 可根据需要调整 |
||||
|
private var audioDataCallback: AudioDataCallback? = null |
||||
|
var audioRecord: AudioRecord? = null |
||||
|
var pushAudioStream: PushAudioInputStream? = null |
||||
|
// 音频源配置 |
||||
|
var audioSourceType = AudioSourceType.MICROPHONE |
||||
|
// 新增:用于异步写入的队列和线程 |
||||
|
private val writeQueue = LinkedBlockingQueue<ByteArray>() |
||||
|
private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在 |
||||
|
private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据 |
||||
|
private var writeThread: Thread? = null |
||||
|
|
||||
|
// 音频配置 |
||||
|
private val channelConfig = AudioFormat.CHANNEL_IN_MONO |
||||
|
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT |
||||
|
// 音频模式管理 |
||||
|
private var audioManager: AudioManager? = null |
||||
|
private var originalAudioMode = AudioManager.MODE_NORMAL |
||||
|
|
||||
|
// 录音文件处理 |
||||
|
var recordfile: RecordFile? = null |
||||
|
/** |
||||
|
* 获取最优音频格式 |
||||
|
*/ |
||||
|
private fun getOptimalAudioFormat(): AudioStreamFormat { |
||||
|
// 支持的采样率列表(按优先级排序) |
||||
|
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) |
||||
|
|
||||
|
// 查找设备支持的最佳采样率 |
||||
|
val sampleRate = supportedSampleRates.firstOrNull { rate -> |
||||
|
val bufferSize = AudioRecord.getMinBufferSize( |
||||
|
rate, |
||||
|
AudioFormat.CHANNEL_IN_MONO, |
||||
|
AudioFormat.ENCODING_PCM_16BIT |
||||
|
) |
||||
|
bufferSize > 0 // 返回正值表示支持 |
||||
|
} ?: 16000 // 默认回退值 |
||||
|
|
||||
|
Log.i(TAG, "使用采样率: ${sampleRate}Hz") |
||||
|
|
||||
|
// 创建对应的音频格式 |
||||
|
return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化音频录制 |
||||
|
*/ |
||||
|
fun initAudioRecord() { |
||||
|
val format = getOptimalAudioFormat() |
||||
|
pushAudioStream = AudioInputStream.createPushStream(format) |
||||
|
isRunning.set(true) |
||||
|
Log.d(TAG, "initAudioRecord: ${isRunning}") |
||||
|
startWriteThread() // 再启动数据读取线程 |
||||
|
// 初始化音频管理器 |
||||
|
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
||||
|
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 启动写入线程 |
||||
|
*/ |
||||
|
private fun startWriteThread() { |
||||
|
|
||||
|
writeThread = Thread { |
||||
|
try { |
||||
|
while (isRunning.get()) { |
||||
|
// Log.d(TAG, "startWriteThread:isWriting= ${isWriting}") |
||||
|
if (!isWriting.get()) { |
||||
|
Thread.sleep(10) // 短暂休眠避免空转 |
||||
|
continue |
||||
|
} |
||||
|
var data: ByteArray? = null |
||||
|
var bytesToWrite = 0 |
||||
|
//Log.d(TAG, "startWriteThread: ${audioRecord?.recordingState}") |
||||
|
// 情况1:正在录制中 -> 直接从AudioRecord读取 |
||||
|
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { |
||||
|
data = ByteArray(bufferSize) |
||||
|
val bytesRead = audioRecord?.read(data, 0, bufferSize) ?: -1 |
||||
|
|
||||
|
when { |
||||
|
bytesRead < 0 -> { |
||||
|
Log.e("tag", "读取音频失败,错误码: $bytesRead") |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
bytesRead == 0 -> continue // 无数据可读 |
||||
|
else -> bytesToWrite = bytesRead // 有效数据 |
||||
|
} |
||||
|
} |
||||
|
// 情况2:不在录制但队列有数据 -> 从队列获取 |
||||
|
else if (writeQueue.isNotEmpty()) { |
||||
|
data = writeQueue.poll() |
||||
|
Log.d("tag", "写入数据: ${data?.size}") |
||||
|
bytesToWrite = data?.size ?: 0 |
||||
|
} |
||||
|
// 确保有有效数据再写入 |
||||
|
if (data != null && bytesToWrite > 0) { |
||||
|
// 处理实际读取长度 < bufferSize 的情况 |
||||
|
val finalData = |
||||
|
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data |
||||
|
|
||||
|
try { |
||||
|
Log.d(TAG, "写入数据大小: ${finalData.size}") |
||||
|
if(audioDataCallback!=null){ |
||||
|
audioDataCallback?.onAudio(finalData) |
||||
|
} |
||||
|
pushAudioStream?.write(finalData) |
||||
|
|
||||
|
|
||||
|
recordfile?.saveAudioDataToWav(finalData) |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "写入失败: ${e.message}") |
||||
|
} |
||||
|
} else { |
||||
|
Thread.yield() // 避免空转消耗CPU |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "写入线程异常: ${e.stackTraceToString()}") |
||||
|
} finally { |
||||
|
Log.d(TAG, "音频写入线程退出") |
||||
|
writeQueue.clear() |
||||
|
} |
||||
|
}.apply { |
||||
|
name = "AudioWriteThread" |
||||
|
start() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 外部音频输入 |
||||
|
*/ |
||||
|
fun saveAudioDataTo(buffer: ByteArray) { |
||||
|
if (audioSourceType == AudioSourceType.MICROPHONE) return |
||||
|
// 放入队列,由写线程写入 |
||||
|
writeQueue.offer(buffer.copyOf()) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 开始音频输入 |
||||
|
* @param audioSourceType 音频源类型 |
||||
|
* @param callback 音频数据回调,可为空 |
||||
|
*/ |
||||
|
fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) { |
||||
|
this.audioSourceType = audioSourceType |
||||
|
this.audioDataCallback = callback |
||||
|
writeQueue.clear() |
||||
|
isWriting.set(true) |
||||
|
|
||||
|
when (audioSourceType) { |
||||
|
AudioSourceType.MICROPHONE -> runMicrophoneCapture() |
||||
|
AudioSourceType.EXTERNAL -> runExternalCapture() |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 配置通话音频模式以优化回声消除 |
||||
|
*/ |
||||
|
private fun setupCommunicationAudioMode() { |
||||
|
try { |
||||
|
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
||||
|
|
||||
|
// 保存原始音频模式 |
||||
|
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL |
||||
|
|
||||
|
// 设置通话模式 - 这是关键! |
||||
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION |
||||
|
|
||||
|
// 启用扬声器(如果需要外放) |
||||
|
audioManager?.isSpeakerphoneOn = true |
||||
|
|
||||
|
// 请求音频焦点 |
||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { |
||||
|
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) |
||||
|
.setAudioAttributes( |
||||
|
AudioAttributes.Builder() |
||||
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) |
||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) |
||||
|
.build() |
||||
|
) |
||||
|
.build() |
||||
|
|
||||
|
audioManager?.requestAudioFocus(focusRequest) |
||||
|
} |
||||
|
|
||||
|
Log.d("AudioMode", "通话音频模式已设置: MODE_IN_COMMUNICATION") |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioMode", "设置通话音频模式失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 运行麦克风捕获 |
||||
|
*/ |
||||
|
private fun runMicrophoneCapture() { |
||||
|
try { |
||||
|
// 首先设置通话音频模式 |
||||
|
setupCommunicationAudioMode() |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
// 使用VOICE_COMMUNICATION音频源(专为VoIP优化) |
||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { |
||||
|
val format = AudioFormat.Builder() |
||||
|
.setSampleRate(sampleRate) |
||||
|
.setEncoding(audioFormat) |
||||
|
.setChannelMask(AudioFormat.CHANNEL_IN_MONO) |
||||
|
.build() |
||||
|
|
||||
|
audioRecord = AudioRecord.Builder() |
||||
|
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) |
||||
|
.setAudioFormat(format) |
||||
|
.setBufferSizeInBytes(minBufferSize * 2) |
||||
|
.build() |
||||
|
} else { |
||||
|
audioRecord = AudioRecord( |
||||
|
MediaRecorder.AudioSource.VOICE_COMMUNICATION, |
||||
|
sampleRate, |
||||
|
channelConfig, |
||||
|
audioFormat, |
||||
|
minBufferSize * 2 |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { |
||||
|
throw IllegalStateException("AudioRecord初始化失败") |
||||
|
} |
||||
|
|
||||
|
audioRecord?.startRecording() |
||||
|
|
||||
|
Log.d("TAG", "通话模式录音开始,采样率: $sampleRate Hz") |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("TAG", "麦克风捕获失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 运行外部捕获 |
||||
|
*/ |
||||
|
private fun runExternalCapture() { |
||||
|
Log.d("TAG", "外部音频捕获启动") |
||||
|
try { // TODO: 实现外部音频源捕获逻辑 |
||||
|
// 停止录音 |
||||
|
if (audioRecord?.recordingState == AudioRecord.STATE_INITIALIZED) { |
||||
|
Log.d("TAG", "外部音频捕获启动 释放audioRecord") |
||||
|
audioRecord?.stop() |
||||
|
// 释放录音实例 |
||||
|
audioRecord?.release() |
||||
|
audioRecord = null |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("TAG", "外部音频捕获异常: ${e.message}") |
||||
|
} finally { |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 继续麦克风捕获 |
||||
|
*/ |
||||
|
fun resumeRecord() { |
||||
|
try { |
||||
|
isWriting.set(true) |
||||
|
// 首先设置通话音频模式 |
||||
|
setupCommunicationAudioMode() |
||||
|
// 停止录音 |
||||
|
audioRecord?.startRecording() |
||||
|
|
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "继续录音失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止麦克风捕获 |
||||
|
*/ |
||||
|
fun stopMicrophoneCapture() { |
||||
|
try { |
||||
|
isWriting.set(false) |
||||
|
|
||||
|
// 停止录音 |
||||
|
audioRecord?.stop() |
||||
|
|
||||
|
// 恢复音频模式 |
||||
|
restoreCommunicationAudioMode() |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "停止麦克风捕获失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 释放音频资源 |
||||
|
*/ |
||||
|
fun releaseAudioResources() { |
||||
|
try { |
||||
|
isRunning.set(false) |
||||
|
isWriting.set(false) |
||||
|
|
||||
|
// 中断并等待捕获线程结束 |
||||
|
writeThread?.interrupt() |
||||
|
writeThread?.join(300) // 最多等待300ms |
||||
|
|
||||
|
// 释放录音实例 |
||||
|
audioRecord?.release() |
||||
|
audioRecord = null |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "释放音频资源失败: ${e.message}") |
||||
|
e.printStackTrace() |
||||
|
} finally { |
||||
|
writeQueue.clear() |
||||
|
writeThread = null |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 禁用蓝牙音频功能,切换回正常音频模式 |
||||
|
*/ |
||||
|
fun disableBluetoothAudio() { |
||||
|
try { |
||||
|
// 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由 |
||||
|
audioManager?.isBluetoothScoOn = false |
||||
|
// 停止蓝牙SCO连接 |
||||
|
audioManager?.stopBluetoothSco() |
||||
|
// 切换回正常音频模式(非通话模式) |
||||
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 恢复原始音频设备状态(通常是重新启用蓝牙) |
||||
|
*/ |
||||
|
fun restoreOriginalAudioState() { |
||||
|
try { |
||||
|
// 恢复应用启动时的原始音频模式 |
||||
|
audioManager?.mode = AudioManager.MODE_NORMAL |
||||
|
|
||||
|
// 重新启用蓝牙SCO |
||||
|
audioManager?.isBluetoothScoOn = true |
||||
|
// 启动蓝牙SCO连接(通常在需要蓝牙通话时使用) |
||||
|
audioManager?.startBluetoothSco() |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioConfig", "Failed to restore audio state: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 恢复原始音频模式 |
||||
|
*/ |
||||
|
private fun restoreCommunicationAudioMode() { |
||||
|
try { |
||||
|
audioManager?.mode = originalAudioMode |
||||
|
audioManager?.isSpeakerphoneOn = false |
||||
|
// 释放音频焦点 |
||||
|
Log.d("AudioMode", "音频模式已恢复") |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioMode", "恢复音频模式失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fun isWriting(): Boolean { |
||||
|
return isWriting.get() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 连续识别回调接口 |
||||
|
*/ |
||||
|
interface AudioDataCallback { |
||||
|
/** |
||||
|
* 音频数据回调 |
||||
|
* @param data 音频数据字节数组 |
||||
|
*/ |
||||
|
fun onAudio(data: ByteArray) |
||||
|
} |
||||
|
} |
||||
|
|
||||
Loading…
Reference in new issue