From 21b7077a4fbdf6ace9e65c63c499304e6cc998d9 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Tue, 17 Jun 2025 11:21:24 +0800 Subject: [PATCH] add --- .../azure_speech/AzureAsrHelper.kt | 43 ++-- .../azure_speech/AzureSpeechPlugin.kt | 55 ++--- .../azure_speech/tools/RecordFile.kt | 215 +++++++++--------- .../yunqiinnovation/ble_service/BleService.kt | 4 - .../main/kotlin/com/example/ota/OtaPlugin.kt | 57 +++-- 5 files changed, 188 insertions(+), 186 deletions(-) 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 24e9f6e82..9b21bd39a 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 @@ -57,7 +57,7 @@ class AzureAsrHelper(private val context: Context) { // 音频处理 private var microphoneStream: MicrophoneStream? = null private var externalAudioStream: ExternalAudioPullStream? = null - + // 音频模式管理 private var audioManager: AudioManager? = null private var originalAudioMode = AudioManager.MODE_NORMAL @@ -101,7 +101,7 @@ class AzureAsrHelper(private val context: Context) { // 释放之前的资源 dispose() - + // 初始化音频管理器 audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL @@ -353,7 +353,7 @@ class AzureAsrHelper(private val context: Context) { // 开始连续识别 recognizer?.startContinuousRecognitionAsync() isContinuousRecognitionActive = true - + return true } catch (e: Exception) { stopAudioProcessing() @@ -584,17 +584,18 @@ class AzureAsrHelper(private val context: Context) { recordfile!!.closeFile(true) - recordfile!!.creatingFiles(filePath) + recordfile!!.creatingFiles(filePath) } - /** + + /** * 移动文件到新路径 */ fun moveFile(sourcePath: String, destPath: String): Boolean { Log.i(tag, "移动文件到新路径:") - recordfile!!.moveFile(sourcePath,destPath) - return true + recordfile!!.moveFile(sourcePath, destPath) + return true } @@ -605,27 +606,27 @@ class AzureAsrHelper(private val context: Context) { Log.i(tag, "重命名指定路径的音频文件:") - recordfile!!.renameFile(filePath,newName) - return true + recordfile!!.renameFile(filePath, newName) + return true } - /** + /** * 停止录音 */ fun pauseRecord() { Log.i(tag, "停止连续录音:") - recordfile!!.isPause=true + recordfile!!.isPause = true } /** * 关闭录音 */ - fun stopRecord(isSave:Boolean) { + fun stopRecord(isSave: Boolean) { Log.i(tag, "关闭录音:") - recordfile!!.isPause=false + recordfile!!.isPause = false recordfile!!.closeFile(isSave) } @@ -706,11 +707,11 @@ class AzureAsrHelper(private val context: Context) { */ private fun applyAudioEffects() { val sessionId = audioRecord?.audioSessionId ?: return - + try { // 设置音频模式为通信模式 this@AzureAsrHelper.audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION - + // 启用声学回音消除 if (AcousticEchoCanceler.isAvailable()) { echoCanceler = AcousticEchoCanceler.create(sessionId) @@ -719,7 +720,7 @@ class AzureAsrHelper(private val context: Context) { } else { Log.w(tag, "设备不支持声学回音消除") } - + // 启用噪声抑制 if (NoiseSuppressor.isAvailable()) { noiseSuppressor = NoiseSuppressor.create(sessionId) @@ -728,7 +729,7 @@ class AzureAsrHelper(private val context: Context) { } else { Log.w(tag, "设备不支持噪声抑制") } - + // 启用自动增益控制 if (AutomaticGainControl.isAvailable()) { automaticGainControl = AutomaticGainControl.create(sessionId) @@ -737,7 +738,7 @@ class AzureAsrHelper(private val context: Context) { } else { Log.w(tag, "设备不支持自动增益控制") } - + Log.i(tag, "回音消除和音频增强功能已全部启用") } catch (e: Exception) { Log.e(tag, "设置音频效果失败: ${e.message}") @@ -795,21 +796,21 @@ class AzureAsrHelper(private val context: Context) { it.release() echoCanceler = null } - + // 释放噪声抑制器 noiseSuppressor?.let { it.enabled = false it.release() noiseSuppressor = null } - + // 释放自动增益控制 automaticGainControl?.let { it.enabled = false it.release() automaticGainControl = null } - + // 恢复音频模式 this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode 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 38346fd28..e19579cc3 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 @@ -11,7 +11,6 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel -import com.microsoft.cognitiveservices.speech.audio.* import com.deep_voice.speech.tts.TtsEvent import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType @@ -355,50 +354,54 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } "renameFile" -> { - - val filePath = call.argument("filePath")?: "" - val newName = call.argument("newName")?: "" + + val filePath = call.argument("filePath") ?: "" + val newName = call.argument("newName") ?: "" try { - FileLogger.d(tag, "音频文件名称为: ${filePath}") // - azureAsrHelper.renameFile(filePath,newName) - result.success(true) - } catch (e: Exception) { + FileLogger.d(tag, "音频文件名称为: ${filePath}") // + azureAsrHelper.renameFile(filePath, newName) + result.success(true) + } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) } } - "moveFile" -> { - val sourcePath = call.argument("sourcePath")?: "" - val destPath = call.argument("destPath")?: "" + + "moveFile" -> { + val sourcePath = call.argument("sourcePath") ?: "" + val destPath = call.argument("destPath") ?: "" try { - FileLogger.d(tag, "音频文件名称为: ${sourcePath}") // + FileLogger.d(tag, "音频文件名称为: ${sourcePath}") // - azureAsrHelper.moveFile(sourcePath,destPath) - result.success(true) - } catch (e: Exception) { + azureAsrHelper.moveFile(sourcePath, destPath) + result.success(true) + } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) } } - "enableRecord" -> { - val filePath = call.argument("filePath")?: "" + + "enableRecord" -> { + val filePath = call.argument("filePath") ?: "" try { - FileLogger.d(tag, "音频文件名称为: ${filePath}") // + FileLogger.d(tag, "音频文件名称为: ${filePath}") // - azureAsrHelper.enableRecord(filePath) - result.success(true) - } catch (e: Exception) { + azureAsrHelper.enableRecord(filePath) + result.success(true) + } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) } } - "pauseRecord" -> { + + "pauseRecord" -> { azureAsrHelper.pauseRecord() result.success(true) } + "stopRecord" -> { - val isSave = call.argument("isSave")?:false + val isSave = call.argument("isSave") ?: false try { - azureAsrHelper.stopRecord(isSave) - result.success(true) - } catch (e: Exception) { + azureAsrHelper.stopRecord(isSave) + result.success(true) + } catch (e: Exception) { result.error("ENABLERECORD_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 e23c6cb34..7a32f21da 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 @@ -1,4 +1,5 @@ package com.yunqiinnovation.azure_speech.tools + import java.io.File import java.io.FileOutputStream import java.io.RandomAccessFile @@ -8,41 +9,41 @@ import java.io.FileInputStream import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.atomic.AtomicBoolean - object RecordFile { - private var currentAudioFile: File? = null - private var fos: FileOutputStream? = null +object RecordFile { + private var currentAudioFile: File? = null + private var fos: FileOutputStream? = null - // 新增:用于异步写入的队列和线程 - private val writeQueue = LinkedBlockingQueue() - private val isWriting = AtomicBoolean(false) - private var writeThread: Thread? = null + // 新增:用于异步写入的队列和线程 + private val writeQueue = LinkedBlockingQueue() + private val isWriting = AtomicBoolean(false) + private var writeThread: Thread? = null - // 用于存储音频数据的缓冲区 - private val dataBuffer = mutableListOf() - private var totalBytesWritten = 0 + // 用于存储音频数据的缓冲区 + private val dataBuffer = mutableListOf() + private var totalBytesWritten = 0 - // 音频配置 - private val sampleRate = 16000 + // 音频配置 + private val sampleRate = 16000 - // 新增:用于存储最后成功保存的文件 - private var lastSavedFile: File? = null - private var lastFileTimestamp: String = "" - var isPause = false - var fileName = "" + // 新增:用于存储最后成功保存的文件 + private var lastSavedFile: File? = null + private var lastFileTimestamp: String = "" + var isPause = false + var fileName = "" - // 修改:添加 filePath 参数 + // 修改:添加 filePath 参数 internal fun creatingFiles(filePath: String) { if (fos != null || currentAudioFile != null) { return } - + val file = File(filePath) val parentDir = file.parentFile if (!parentDir.exists()) { parentDir.mkdirs() } - + currentAudioFile = file currentAudioFile?.createNewFile() @@ -56,59 +57,59 @@ import java.util.concurrent.atomic.AtomicBoolean } - // 新增:启动写入线程 - private fun startWriteThread() { - if (isWriting.get()) return - isWriting.set(true) - writeThread = Thread { - try { - while (isWriting.get() || writeQueue.isNotEmpty()) { - val data = writeQueue.poll() ?: continue - fos?.write(data) - } - } catch (e: Exception) { - Log.e("tag", "异步写入音频数据失败: ${e.message}") + // 新增:启动写入线程 + private fun startWriteThread() { + if (isWriting.get()) return + isWriting.set(true) + writeThread = Thread { + try { + while (isWriting.get() || writeQueue.isNotEmpty()) { + val data = writeQueue.poll() ?: continue + fos?.write(data) } + } catch (e: Exception) { + Log.e("tag", "异步写入音频数据失败: ${e.message}") } - writeThread?.start() } + writeThread?.start() + } - /** - * 保存音频数据到 WAV 文件(异步) - */ - internal fun saveAudioDataToWav(buffer: ByteArray) { - if (fos == null || currentAudioFile == null) return - // 放入队列,由写线程写入 - writeQueue.offer(buffer.copyOf()) - totalBytesWritten += buffer.size - } + /** + * 保存音频数据到 WAV 文件(异步) + */ + internal fun saveAudioDataToWav(buffer: ByteArray) { + if (fos == null || currentAudioFile == null) return + // 放入队列,由写线程写入 + writeQueue.offer(buffer.copyOf()) + totalBytesWritten += buffer.size + } - // 更新文件头生成(修正RIFF长度计算) - private fun generateWavHeader(dataLength: Int): ByteArray { - val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 - val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 - - return byteArrayOf( - 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), - (totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), - ((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), - 'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), - 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), - 16, 0, 0, 0, // PCM头长度 - 1, 0, // PCM格式 - 1, 0, // 单声道 - (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 - (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 - 2, 0, // 块对齐 (通道数 * 样本位数/8) - 16, 0, // 样本位数 - 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), - (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), - ((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() - ) - } + // 更新文件头生成(修正RIFF长度计算) + private fun generateWavHeader(dataLength: Int): ByteArray { + val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 + val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 + + return byteArrayOf( + 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), + (totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), + ((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), + 'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), + 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), + 16, 0, 0, 0, // PCM头长度 + 1, 0, // PCM格式 + 1, 0, // 单声道 + (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 + (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 + 2, 0, // 块对齐 (通道数 * 样本位数/8) + 16, 0, // 样本位数 + 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), + (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), + ((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() + ) + } - /** + /** * 重命名指定路径的音频文件 * @param filePath 要重命名的文件完整路径 * @param newName 新的文件名(不含扩展名) @@ -116,28 +117,28 @@ import java.util.concurrent.atomic.AtomicBoolean */ internal fun renameFile(filePath: String, newName: String): Boolean { val sourceFile = File(filePath) - + // 验证源文件存在 if (!sourceFile.exists()) { Log.w("tag", "重命名失败: 文件不存在 $filePath") return false } - + // 防止重命名正在录制的文件 if (sourceFile == currentAudioFile) { Log.w("tag", "重命名失败: 不能重命名正在录制的文件") return false } - + val parentDir = sourceFile.parentFile val newFile = File(parentDir, "$newName.wav") // 保持.wav扩展名 - + // 处理目标文件已存在的情况 if (newFile.exists()) { Log.w("tag", "重命名失败: 目标文件已存在 ${newFile.absolutePath}") return false } - + // 执行重命名 return try { val success = sourceFile.renameTo(newFile) @@ -156,7 +157,8 @@ import java.util.concurrent.atomic.AtomicBoolean false } } -/** + + /** * 移动文件到新路径 * @param sourcePath 原始文件完整路径 * @param destPath 目标文件完整路径 @@ -165,25 +167,25 @@ import java.util.concurrent.atomic.AtomicBoolean internal fun moveFile(sourcePath: String, destPath: String): Boolean { val sourceFile = File(sourcePath) val destFile = File(destPath) - + if (!sourceFile.exists()) { Log.w("tag", "移动失败: 源文件不存在 ${sourceFile.absolutePath}") return false } - + // 确保目标目录存在 val destDir = destFile.parentFile if (!destDir.exists() && !destDir.mkdirs()) { Log.w("tag", "移动失败: 无法创建目标目录 ${destDir.absolutePath}") return false } - + // 删除已存在的目标文件 if (destFile.exists() && !destFile.delete()) { Log.w("tag", "移动失败: 无法覆盖已存在文件 ${destFile.absolutePath}") return false } - + return try { // 使用renameTo尝试移动(同分区最快) if (sourceFile.renameTo(destFile)) { @@ -213,39 +215,40 @@ import java.util.concurrent.atomic.AtomicBoolean false } } - internal fun closeFile( isSave:Boolean): Boolean { - try { - if (fos == null || currentAudioFile == null) { + + internal fun closeFile(isSave: Boolean): Boolean { + try { + if (fos == null || currentAudioFile == null) { + return false + } + isWriting.set(false) + writeThread?.join(500) + fos?.close() + + currentAudioFile?.let { file -> + if (file.length() <= 44 || isSave == false) { + file.delete() + Log.d("tag", "音频文件过小已删除: ${file.absolutePath}") return false - } - isWriting.set(false) - writeThread?.join(500) - fos?.close() - - currentAudioFile?.let { file -> - if (file.length() <= 44||isSave==false) { - file.delete() - Log.d("tag", "音频文件过小已删除: ${file.absolutePath}") - return false - } else { - RandomAccessFile(file, "rw").use { raf -> - raf.seek(0) - raf.write(generateWavHeader(totalBytesWritten)) - } - Log.d("tag", "音频文件保存完成: ${file.absolutePath}") - - // 保存成功文件引用 - lastSavedFile = file - return true + } else { + RandomAccessFile(file, "rw").use { raf -> + raf.seek(0) + raf.write(generateWavHeader(totalBytesWritten)) } + Log.d("tag", "音频文件保存完成: ${file.absolutePath}") + + // 保存成功文件引用 + lastSavedFile = file + return true } - } catch (e: Exception) { - Log.e("tag", "更新WAV文件头失败: ${e.message}") - return false - } finally { - fos = null - currentAudioFile = null } + } catch (e: Exception) { + Log.e("tag", "更新WAV文件头失败: ${e.message}") return false + } finally { + fos = null + currentAudioFile = null } - } \ No newline at end of file + return false + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index d81842a90..7e6141db4 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -16,14 +16,10 @@ import com.jieli.jl_audio_decode.exceptions.OpusException import java.io.File import java.io.FileOutputStream -import java.io.RandomAccessFile import java.text.SimpleDateFormat import java.util.Date -import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit import android.os.Handler -import android.os.HandlerThread import java.util.concurrent.atomic.AtomicBoolean import android.util.Log /** diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index 3b34cf9cd..c2ddcfe1f 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -268,37 +268,36 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { streamSink = null } }) - + } override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "init") { - - // 初始化 otaManager 和 bluetoothHelper - ConfigHelper.initialize(context) - otaManager = OTAManager(context) - bluetoothHelper = BluetoothHelper(context) - // 如果需要额外的初始化逻辑,请在这里添加 - otaManager.registerBluetoothCallback(bluetoothCallback) - // 初始化BleService - // 2. 初始化BleService并注册回调 - if (BleService.initialize(context)) { - if (BleService.addCallback(this)) { - isRegisteredToBle = true - Log.d(TAG, "已注册BLE回调") + if (call.method == "init") { + + // 初始化 otaManager 和 bluetoothHelper + ConfigHelper.initialize(context) + otaManager = OTAManager(context) + bluetoothHelper = BluetoothHelper(context) + // 如果需要额外的初始化逻辑,请在这里添加 + otaManager.registerBluetoothCallback(bluetoothCallback) + // 初始化BleService + // 2. 初始化BleService并注册回调 + if (BleService.initialize(context)) { + if (BleService.addCallback(this)) { + isRegisteredToBle = true + Log.d(TAG, "已注册BLE回调") + } else { + Log.w(TAG, "BLE回调注册失败") + } } else { - Log.w(TAG, "BLE回调注册失败") + Log.e(TAG, "BLE服务初始化失败") + } - } else { - Log.e(TAG, "BLE服务初始化失败") - - } - Log.i("TAG", "onAttachedToEngine") - result.success("") - } - else if (call.method == "startUpgrading") { + Log.i("TAG", "onAttachedToEngine") + result.success("") + } else if (call.method == "startUpgrading") { Log.d("TAG", "startUpgrading") // 接收传入的参数 val arguments = call.arguments as? Map @@ -331,16 +330,16 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { BleService.isEnterOta = false } - result.success("") + result.success("") } else if (call.method == "onOtaClose") { BleService.isEnterOta = false isDisconnect = false - otaManager?.bluetoothHelper?.stopScan() - + otaManager?.bluetoothHelper?.stopScan() + otaManager?.bluetoothHelper?.disconnectDevice(otaDevice) - - result.success("") + + result.success("") } else { result.notImplemented() }