From fbf15009e293c6838745924d80cf4cef0cedcc31 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sat, 9 Aug 2025 21:08:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=95=E9=9F=B3=E8=A7=A3=E5=81=B6=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/asr_service.dart | 3 + lib/data/services/audio_service.dart | 29 ++ .../speech_impl/azure_asr_service.dart | 12 + .../speech_impl/volcano_asr_api_service.dart | 6 + .../speech_impl/volcano_asr_service.dart | 6 + .../speech_impl/xunfei_asr_service.dart | 6 + .../meeting_record_controller.dart | 4 +- .../azure_speech/AzureAsrHelper.kt | 482 +++--------------- .../azure_speech/AzureSpeechPlugin.kt | 10 +- .../azure_speech/tools/RecordFile.kt | 2 +- .../{ => tools}/SimpleAudioPlayer.kt | 2 +- .../azure_speech/tools/SimpleAudioReceiver.kt | 403 +++++++++++++++ .../azure_speech/AzureSpeechPlugin.swift | 5 +- 13 files changed, 551 insertions(+), 419 deletions(-) create mode 100644 lib/data/services/audio_service.dart rename local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/{ => tools}/SimpleAudioPlayer.kt (99%) create mode 100644 local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index 54b507005..1fba23f52 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -46,6 +46,9 @@ abstract class AsrService { /// 暂停录音 Future pauseRecord(); + /// 继续录音 + Future resumeRecord(); + /// 设置音频配置 Future setAudioConfig({ int sampleRate = 16000, diff --git a/lib/data/services/audio_service.dart b/lib/data/services/audio_service.dart new file mode 100644 index 000000000..6dbdd781f --- /dev/null +++ b/lib/data/services/audio_service.dart @@ -0,0 +1,29 @@ +import 'dart:async'; +import 'dart:typed_data'; + +/// 语音识别服务接口 +abstract class AudioService { + /// 开始录音 + Future enableRecord(String filePath); + + /// 暂停录音 + Future pauseRecord(); + + /// 继续录音 + Future resumeRecord(); + + /// 设置音频配置 + Future setAudioConfig({ + int sampleRate = 16000, + int channels = 1, + }); + + // /// 移动文件到新路径 + Future moveFile(String sourcePath, String destPath); + + // /// 重命名指定路径的音频文件 + Future renameFile(String filePath, String newName); + + /// 结束录音 + Future stopRecord(bool isSave); +} diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 832b83b5b..0009cd758 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -494,4 +494,16 @@ class AzureAsrService extends GetxService implements AsrService { rethrow; } } + + @override + Future resumeRecord() async { + try { + final bool result = await _channel.invokeMethod('resumeRecord'); + + return result; + } catch (e) { + Logger.error('继续录音: ${e.toString()}'); + rethrow; + } + } } diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 32844d1bb..126aea878 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/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 resumeRecord() { + // TODO: implement resumeRecord + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index 412656ddd..95d9c1afc 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/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 resumeRecord() { + // TODO: implement resumeRecord + throw UnimplementedError(); + } } diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index cc94063e8..ea8a52251 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/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 resumeRecord() { + // TODO: implement resumeRecord + throw UnimplementedError(); + } } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index a5b57087b..71ce8796e 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/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 _resumeRecording() async { - await _asrService.startContinuousRecognition(_audioSourceType); + await _asrService.resumeRecord(); startTimer(); } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 8ccfc4206..373c2a5d7 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/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() - 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}") - } - } + + } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 4263376e1..41eb3b938 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/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("isSave") ?: false @@ -502,7 +506,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val sampleRate = call.argument("sampleRate") ?: 16000 val channels = call.argument("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) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt index d0b69ccb1..66a24e058 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt +++ b/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 = "" // 双声道分离相关变量 diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt similarity index 99% rename from local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt rename to local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt index 84f846967..61870debb 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt +++ b/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 { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt new file mode 100644 index 000000000..75f76d53a --- /dev/null +++ b/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() + 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}") + } + } +} diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index 8167155ad..4d18c9c71 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/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],