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