Browse Source

录音解偶代码

weicu
fdp 1 year ago
parent
commit
b2905ea259
  1. 3
      lib/data/services/asr_service.dart
  2. 29
      lib/data/services/audio_service.dart
  3. 12
      lib/data/services/speech_impl/azure_asr_service.dart
  4. 6
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  5. 6
      lib/data/services/speech_impl/volcano_asr_service.dart
  6. 6
      lib/data/services/speech_impl/xunfei_asr_service.dart
  7. 4
      lib/modules/meeting/controllers/meeting_record_controller.dart
  8. 482
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  9. 10
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  10. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt
  11. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt
  12. 403
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  13. 5
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

3
lib/data/services/asr_service.dart

@ -46,6 +46,9 @@ abstract class AsrService {
/// 暂停录音
Future<bool> pauseRecord();
/// 继续录音
Future<bool> resumeRecord();
/// 设置音频配置
Future<bool> setAudioConfig({
int sampleRate = 16000,

29
lib/data/services/audio_service.dart

@ -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);
}

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

@ -494,4 +494,16 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
Future<bool> resumeRecord() async {
try {
final bool result = await _channel.invokeMethod('resumeRecord');
return result;
} catch (e) {
Logger.error('继续录音: ${e.toString()}');
rethrow;
}
}
}

6
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -871,4 +871,10 @@ class VolcanoAsrApiService implements AsrService {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
}

6
lib/data/services/speech_impl/volcano_asr_service.dart

@ -406,4 +406,10 @@ class VolcanoAsrService extends GetxService implements AsrService {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
}

6
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -330,4 +330,10 @@ class XunfeiAsrService extends GetxService implements AsrService {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
}

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

@ -123,7 +123,7 @@ class MeetingRecordController extends GetxController
/// Initializes UI controllers and focus nodes
void _initializeComponents() {
tabController = TabController(length: 2, vsync: this);
tabController = TabController(length: 1, vsync: this);
titleEditingController = TextEditingController(text: fileName.value);
titleFocusNode = FocusNode();
titleFocusNode.addListener(_handleTitleFocusChange);
@ -294,7 +294,7 @@ class MeetingRecordController extends GetxController
/// Resumes paused recording session
Future<void> _resumeRecording() async {
await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.resumeRecord();
startTimer();
}

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

@ -58,14 +58,11 @@ class AzureAsrHelper(private val context: Context) {
var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理
var audioStream: AudioStream? = null
var audioStream: SimpleAudioReceiver? = null
// 音频模式管理
private var audioManager: AudioManager? = null
private var originalAudioMode = AudioManager.MODE_NORMAL
// 录音文件处理
var recordfile: RecordFile? = null
/**
* 音频来源类型
@ -103,10 +100,6 @@ class AzureAsrHelper(private val context: Context) {
// 释放之前的资源
dispose()
// 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
// 保存配置
this.subscriptionKey = subscriptionKey
@ -147,9 +140,7 @@ class AzureAsrHelper(private val context: Context) {
// 设置分段策略为时间模式
setProperty("Speech_SegmentationStrategy", "Time")
}
// 录音文件类
recordfile = RecordFile();
return true
} catch (e: Exception) {
@ -158,20 +149,20 @@ class AzureAsrHelper(private val context: Context) {
}
}
/**
* 设置音频配置
* @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000)
* @param channels 声道数 (1=单声道, 2=立体声)
*/
fun setAudioConfig(sampleRate: Int, channels: Int) {
try {
// 设置RecordFile的音频配置
recordfile?.setAudioConfig(sampleRate, channels)
Log.d(tag, "音频配置已设置: 采样率=${sampleRate}Hz, 声道数=${channels}")
} catch (e: Exception) {
Log.e(tag, "设置音频配置失败: ${e.message}")
}
}
// /**
// * 设置音频配置
// * @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000)
// * @param channels 声道数 (1=单声道, 2=立体声)
// */
// fun setAudioConfig(sampleRate: Int, channels: Int) {
// try {
// // 设置RecordFile的音频配置
// recordfile?.setAudioConfig(sampleRate, channels)
// Log.d(tag, "音频配置已设置: 采样率=${sampleRate}Hz, 声道数=${channels}")
// } catch (e: Exception) {
// Log.e(tag, "设置音频配置失败: ${e.message}")
// }
// }
private fun isRecognizerValid(): Boolean {
return recognizer != null
@ -218,7 +209,7 @@ class AzureAsrHelper(private val context: Context) {
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = AudioStream()
audioStream = SimpleAudioReceiver(context)
audioStream!!.initAudioRecord()
}
@ -256,7 +247,10 @@ class AzureAsrHelper(private val context: Context) {
try {
// 启动音频处理
audioStream!!.startAudioRecord()
audioStream!!.startAudioRecord( when(audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
})
// 执行同步识别
val result = recognizer?.recognizeOnceAsync()?.get()
@ -311,7 +305,12 @@ class AzureAsrHelper(private val context: Context) {
// 开始连续识别
// 启动音频处理
audioStream!!.startAudioRecord()
audioStream!!.startAudioRecord(
when(audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
}
)
recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true
@ -483,7 +482,9 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理
stopAudioProcessing()
// 停止录音
recordfile!!.closeFile(true)
if (audioStream!!.recordfile != null) {
audioStream!!.recordfile!!.closeFile(true)
}
// 释放recognizer
recognizer?.close()
recognizer = null
@ -538,40 +539,7 @@ class AzureAsrHelper(private val context: Context) {
}
/**
* 禁用蓝牙音频功能,切换回正常音频模式
*/
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}")
}
}
//录音音频文件
@ -580,11 +548,24 @@ class AzureAsrHelper(private val context: Context) {
*/
fun enableRecord(filePath: String) {
Log.i(tag, "开启录音:")
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = SimpleAudioReceiver(context)
audioStream!!.initAudioRecord()
}
if (audioStream!!.recordfile == null) {
audioStream!!.recordfile = RecordFile()
}
recordfile!!.closeFile(true)
// 启动音频处理
audioStream!!.startAudioRecord( when(audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
})
audioStream!!.recordfile!!.closeFile(true)
recordfile!!.creatingFiles(filePath)
audioStream!!.recordfile!!.creatingFiles(filePath)
}
@ -593,8 +574,8 @@ class AzureAsrHelper(private val context: Context) {
*/
fun moveFile(sourcePath: String, destPath: String): Boolean {
Log.i(tag, "移动文件到新路径:")
recordfile!!.moveFile(sourcePath, destPath)
if (audioStream!!.recordfile == null)return false
audioStream!!.recordfile!!.moveFile(sourcePath, destPath)
return true
}
@ -605,8 +586,8 @@ class AzureAsrHelper(private val context: Context) {
fun renameFile(filePath: String, newName: String): Boolean {
Log.i(tag, "重命名指定路径的音频文件:")
recordfile!!.renameFile(filePath, newName)
if (audioStream!!.recordfile == null)return false
audioStream!!.recordfile!!.renameFile(filePath, newName)
return true
}
@ -615,308 +596,35 @@ class AzureAsrHelper(private val context: Context) {
*/
fun pauseRecord() {
Log.i(tag, "停止连续录音:")
if (audioStream!!.recordfile == null)return
audioStream!!.stopMicrophoneCapture()
recordfile!!.isPause = true
}
/**
* 继续录音
*/
fun resumeRecord() {
Log.i(tag, "继续录音:")
if (audioStream!!.recordfile == null)return
audioStream!!.resumeRecord()
}
/**
* 关闭录音
*/
fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:")
recordfile!!.isPause = false
recordfile!!.closeFile(isSave)
if (audioStream!!.recordfile == null)return
audioStream!!.stopMicrophoneCapture()
audioStream!!.recordfile!!.closeFile(isSave)
}
/**
* 麦克风流 - 拉流模式
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
*/
inner class AudioStream {
private val bufferSize = 4096 // 可根据需要调整
var audioRecord: AudioRecord? = null
var pushAudioStream: PushAudioInputStream? = null
// 新增:用于异步写入的队列和线程
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
/**
* 获取设备支持的最佳音频格式
* 优先选择16000Hz,若不支持则降级到8000Hz
*/
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)
startWriteThread() // 再启动数据读取线程
}
/**
* 开启音频写入线程
*/
private fun startWriteThread() {
writeThread = Thread {
try {
while (isRunning.get()) {
// 等待录音信号
if (!isWriting.get()) {
Thread.sleep(10) // 短暂休眠避免空转
continue
}
var data: ByteArray? = null
var bytesToWrite = 0
// 情况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 {
//continuousCallback?.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||!isContinuousRecognitionActive) return
// 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf())
}
/**
* 开始音频输入
*/
fun startAudioRecord() {
isWriting.set(true)
when (audioSourceType) {
AudioSourceType.MICROPHONE -> runMicrophoneCapture()
AudioSourceType.EXTERNAL -> runExternalCapture()
}
}
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}")
// 出错时恢复音频模式
restoreCommunicationAudioMode()
}
}
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 stopMicrophoneCapture() {
try {
if (!isWriting.get()) return
isWriting.set(false)
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 停止录音并释放AudioRecord资源
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e("AudioConfig", "Error releasing resources: ${e.message}")
} finally {
writeQueue.clear()
// 确保恢复原始音频状态
// restoreOriginalAudioState()
}
}
/**
* 释放音频资源
*/
fun releaseAudioResources() {
try {
if (!isWriting.get()) return
isWriting.set(false)
isRunning.set(false)
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 中断并等待捕获线程结束
writeThread?.interrupt()
writeThread?.join(300) // 最多等待300ms
// 恢复音频模式
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(tag, "释放音频资源失败: ${e.message}")
e.printStackTrace()
} finally {
writeQueue.clear()
writeThread = null
// 确保恢复原始音频状态
// restoreOriginalAudioState()
}
}
}
/**
* 一次性识别回调接口
*/
@ -990,55 +698,7 @@ class AzureAsrHelper(private val context: Context) {
fun onError(error: String)
}
/**
* 配置通话音频模式以优化回声消除
*/
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 restoreCommunicationAudioMode() {
try {
audioManager?.mode = originalAudioMode
audioManager?.isSpeakerphoneOn = false
// 释放音频焦点
Log.d("AudioMode", "音频模式已恢复")
} catch (e: Exception) {
Log.e("AudioMode", "恢复音频模式失败: ${e.message}")
}
}
}

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

@ -372,12 +372,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
"disableBluetoothAudio" -> {
azureAsrHelper.disableBluetoothAudio();
azureAsrHelper.audioStream?.disableBluetoothAudio();
return
}
"restoreOriginalAudioState" -> {
azureAsrHelper.restoreOriginalAudioState();
azureAsrHelper.audioStream?.restoreOriginalAudioState();
return
}
@ -487,6 +487,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
azureAsrHelper.pauseRecord()
result.success(true)
}
"resumeRecord" -> {
azureAsrHelper.resumeRecord()
result.success(true)
}
"stopRecord" -> {
val isSave = call.argument<Boolean>("isSave") ?: false
@ -502,7 +506,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val channels = call.argument<Int>("channels") ?: 1
try {
azureAsrHelper.setAudioConfig(sampleRate, channels)
azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels)
result.success(true)
} catch (e: Exception) {
result.error("SET_AUDIO_CONFIG_ERROR", e.message, null)

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt

@ -33,7 +33,7 @@ class RecordFile {
// 用于存储最后成功保存的文件
private var lastSavedFile: File? = null
private var lastFileTimestamp: String = ""
var isPause = false
var fileName = ""
// 双声道分离相关变量

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt → local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt

@ -49,7 +49,7 @@ class SimpleAudioPlayer(private val context: Context? = null) {
// 推送流
private var pushOutputStream: PushAudioOutputStream? = null
// 自定义推送流回调
private inner class AudioOutputCallback : PushAudioOutputStreamCallback() {
override fun write(dataBuffer: ByteArray): Int {

403
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt

@ -0,0 +1,403 @@
package com.yunqiinnovation.azure_speech
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 // 可根据需要调整
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}")
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())
}
/**
* 开始音频输入
*/
fun startAudioRecord(audioSourceType: AudioSourceType) {
this.audioSourceType = audioSourceType
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}")
}
}
}

5
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -262,7 +262,7 @@ import os.log
return
}
do {
print("音频文件名称为: (filePath)")
print("音频文件名称为: \(filePath)")
try azureAsrHelper.enableRecord(filePath: filePath)
result(true)
} catch {
@ -272,6 +272,9 @@ import os.log
case "pauseRecord":
azureAsrHelper.pauseRecord()
result(true)
case "resumeRecord":
azureAsrHelper.resumeRecord()
result(true)
case "stopRecord":
guard let args = call.arguments as? [String: Any],

Loading…
Cancel
Save