From eab05419d630ea809d8e49dd58e817b35c8446c8 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Thu, 8 May 2025 19:31:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9ble=E9=80=9A=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentService.kt | 20 +- .../agent_service/AgentServicePlugin.kt | 2 +- .../azure_speech/AzureAsrHelper.kt | 8 +- .../azure_speech/AzureSpeechPlugin.kt | 21 +- .../ble_service/android/build.gradle.kts | 2 + .../yunqiinnovation/ble_service/BleConst.kt | 2 +- .../yunqiinnovation/ble_service/BleService.kt | 395 ++++++---- .../yunqiinnovation/ble_service/ChatAgent.kt | 49 +- .../com/yunqiinnovation/jieli/BleService.kt | 18 +- .../yunqiinnovation/jl_opus/JlOpusPlugin.kt | 695 ++++++++++-------- 10 files changed, 693 insertions(+), 519 deletions(-) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 3b486cdcf..36817caf0 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -50,7 +50,7 @@ object AgentService : CoroutineScope { private var openaiModel: String = "gpt-3.5-turbo" // Azure服务 - private var azureAsrHelper: AzureAsrHelper? = null + var azureAsrHelper: AzureAsrHelper? = null private var azureTtsHelper: AzureTtsHelper? = null // OpenAI服务 @@ -112,7 +112,7 @@ object AgentService : CoroutineScope { * @param config 配置参数,包含所需的所有API密钥和设置 * @return 是否初始化成功 */ - fun initialize(appContext: Context, config: Map): Boolean { + fun initialize(appContext: Context, config: Map,isExternalActive:Boolean): Boolean { if (isInitialized) return true try { @@ -149,7 +149,8 @@ object AgentService : CoroutineScope { initialize( subscriptionKey = azureSpeechKey, region = azureSpeechRegion, - audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE, + audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL + else AzureAsrHelper.AudioSourceType.MICROPHONE ) } @@ -277,7 +278,7 @@ object AgentService : CoroutineScope { /** * 开始语音识别 */ - fun startRecognition(): Boolean { + fun startRecognition(isExternalActive:Boolean): Boolean { if (!isInitialized) { FileLogger.e(TAG, "服务未初始化") return false @@ -295,7 +296,14 @@ object AgentService : CoroutineScope { isRecognitionActive = true hasSpeechDetected = false - try { + try { + FileLogger.d(TAG, "选择音频源类型: ${isExternalActive}") // + // 选择音频源类型 + val audioSourceType = if (isExternalActive) { + AzureAsrHelper.AudioSourceType.EXTERNAL + } else { + AzureAsrHelper.AudioSourceType.MICROPHONE + } azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onRecognizing(recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()) { @@ -370,7 +378,7 @@ object AgentService : CoroutineScope { "message" to error )) } - }) + },audioSourceType) return true } catch (e: Exception) { isRecognitionActive = false diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 50d189240..2505a6318 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -39,7 +39,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream "startConversation" -> { try { - val isStarted = AgentService.startRecognition() + val isStarted = AgentService.startRecognition(false) result.success(isStarted) } catch (e: Exception) { FileLogger.e(TAG, "启动对话失败", e) 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 1c722d140..dbd520dc8 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 @@ -254,12 +254,12 @@ class AzureAsrHelper(private val context: Context) { * * @param callback 识别结果回调 */ - fun recognizeOnce(callback: RecognizeCallback) { + fun recognizeOnce(callback: RecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE) { if (speechConfig == null) { callback.onError("语音服务未初始化") return } - + this.audioSourceType = audioSourceType // 确保不在连续识别中 if (isContinuousRecognitionActive) { stopContinuousRecognition() @@ -299,7 +299,7 @@ class AzureAsrHelper(private val context: Context) { * @param callback 连续识别结果回调 * @return 是否成功开始识别 */ - fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { + fun startContinuousRecognition(callback: ContinuousRecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE): Boolean { if (speechConfig == null) { callback.onError("语音服务未初始化") return false @@ -308,7 +308,7 @@ class AzureAsrHelper(private val context: Context) { if (isContinuousRecognitionActive) { return true } - + this.audioSourceType = audioSourceType // 重置识别器 if (!setupRecognizer()) { callback.onError("重置识别器失败") 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 2f0fc4bce..662d627ad 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 @@ -123,6 +123,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { val useExternalAudio = call.argument("useExternalAudio") ?: false try { + FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") // // 选择音频源类型 val audioSourceType = if (useExternalAudio) { AzureAsrHelper.AudioSourceType.EXTERNAL @@ -156,7 +157,15 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { } } "recognizeOnce" -> { + val useExternalAudio = call.argument("useExternalAudio") ?: false try { + FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") // + // 选择音频源类型 + val audioSourceType = if (useExternalAudio) { + AzureAsrHelper.AudioSourceType.EXTERNAL + } else { + AzureAsrHelper.AudioSourceType.MICROPHONE + } // 直接使用同步方式进行一次性识别 azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback { override fun onResult(text: String, detectedLanguage: String) { @@ -170,12 +179,13 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { override fun onError(error: String) { result.error("RECOGNITION_ERROR", error, null) } - }) + },audioSourceType) } catch (e: Exception) { result.error("RECOGNITION_ERROR", "识别过程异常: ${e.message}", null) } } "startContinuousRecognition" -> { + val useExternalAudio = call.argument("useExternalAudio") ?: false // 确保事件通道已准备好 if (asrEventSink == null) { result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null) @@ -183,6 +193,13 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { } try { + FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") // + // 选择音频源类型 + val audioSourceType = if (useExternalAudio) { + AzureAsrHelper.AudioSourceType.EXTERNAL + } else { + AzureAsrHelper.AudioSourceType.MICROPHONE + } val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onResult(text: String, detectedLanguage: String) { sendAsrEvent(mapOf( @@ -222,7 +239,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { "message" to error )) } - }) + },audioSourceType) result.success(success) } catch (e: Exception) { result.error("START_RECOGNITION_ERROR", e.message, null) diff --git a/local_plugins/ble_service/android/build.gradle.kts b/local_plugins/ble_service/android/build.gradle.kts index b7633199c..e6a8a984f 100644 --- a/local_plugins/ble_service/android/build.gradle.kts +++ b/local_plugins/ble_service/android/build.gradle.kts @@ -35,4 +35,6 @@ dependencies { implementation("androidx.security:security-crypto:1.1.0-alpha03") implementation(project(":deepvoice_utils")) implementation(project(":agent_service")) + implementation(project(":azure_speech")) + implementation(project(":jl_opus")) } \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index e6aa4710b..74f8c35d0 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -23,7 +23,7 @@ object BleConst { val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc") /** 客户端特征配置描述符UUID */ - val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("0000fd7b-0000-1000-8000-00805f9b34fb") + val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") /** BLE扫描Action */ const val ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" 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 dc615d51e..e029783b1 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 @@ -23,6 +23,7 @@ import androidx.core.content.ContextCompat import java.util.concurrent.ConcurrentHashMap import android.content.pm.ServiceInfo import java.util.concurrent.CopyOnWriteArrayList +import com.yunqiinnovation.jl_opus.JlOpusPlugin /** * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 @@ -44,14 +45,14 @@ object BleService { interface Callback { // 扫描相关回调 fun onScanResult(devices: List>) - + // 连接状态相关回调 fun onConnectionStateChanged(state: Int) - + // 数据相关回调 fun onAudioDataReceived(data: ByteArray) fun onWakeupSignalReceived() - + // 设备信息相关回调 - 统一回调接口 fun onDeviceInfoReceived(infoType: Int, infoData: Map) } @@ -59,7 +60,7 @@ object BleService { // ====================================================================================================== // 成员变量 // ====================================================================================================== - + // 上下文 private lateinit var context: Context @@ -84,8 +85,10 @@ object BleService { // 回调集合 - 使用线程安全的集合存储所有回调 private val callbacks = CopyOnWriteArrayList() - - + + // 获取插件实例 + private val opusPlugin = JlOpusPlugin() + // 初始化状态 private var isInitialized = false @@ -96,18 +99,19 @@ object BleService { */ fun initialize(appContext: Context): Boolean { if (isInitialized) return true - + try { this.context = appContext.applicationContext - + // 初始化蓝牙管理器和适配器 - bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + bluetoothManager = + context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothAdapter = bluetoothManager.adapter bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner - + // 初始化Handler scanHandler = Handler(Looper.getMainLooper()) - + isInitialized = true FileLogger.d(TAG, "BLE服务初始化成功") return true @@ -116,7 +120,7 @@ object BleService { return false } } - + /** * 添加回调监听器 * @param callback 要添加的回调监听器 @@ -130,7 +134,7 @@ object BleService { } return false } - + /** * 移除回调监听器 * @param callback 要移除的回调监听器 @@ -143,7 +147,7 @@ object BleService { } return removed } - + /** * 移除所有回调监听器 */ @@ -165,7 +169,7 @@ object BleService { FileLogger.e(TAG, "BLE服务未初始化") return false } - + // 获取一个局部引用,避免并发访问问题 val scanner = bluetoothLeScanner ?: return false if (isScanning) return false @@ -211,17 +215,17 @@ object BleService { // 获取一个局部引用,避免并发访问问题 val scanner = bluetoothLeScanner ?: return false if (!isScanning) return false - + FileLogger.i(TAG, "停止主动扫描") try { scanner.stopScan(scanCallback) scanHandler.removeCallbacksAndMessages(null) isScanning = false - + // 重新注册后台扫描,保证唤醒能力 registerBackgroundScan() - + return true } catch (e: Exception) { FileLogger.e(TAG, "停止扫描失败: ${e.message}", e) @@ -235,24 +239,25 @@ object BleService { private val scanCallback = object : ScanCallback() { override fun onScanResult(type: Int, result: ScanResult) { val dev = result.device ?: return - + // 打印设备信息和服务UUID printDeviceInfo(result) // 直接上报每个扫描结果 try { // 将单个扫描结果转换为需要的格式 - val uuids = result.scanRecord?.serviceUuids?.map { it.uuid.toString() } ?: emptyList() + val uuids = + result.scanRecord?.serviceUuids?.map { it.uuid.toString() } ?: emptyList() val deviceMap = mapOf( "id" to dev.address, "name" to (dev.name ?: "未知"), "rssi" to result.rssi, "uuids" to uuids ) - + // 直接回调单个设备结果 notifyScanResult(listOf(deviceMap)) - + // 更新缓存 val idx = scanResults.indexOfFirst { it.device.address == dev.address } if (idx >= 0) scanResults[idx] = result else scanResults.add(result) @@ -273,17 +278,20 @@ object BleService { private fun printDeviceInfo(result: ScanResult) { val device = result.device val scanRecord = result.scanRecord - val serviceUuids = scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无" - - FileLogger.d(TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " + - "RSSI=${result.rssi}, 服务UUID=[$serviceUuids]") - + val serviceUuids = + scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无" + + FileLogger.d( + TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " + + "RSSI=${result.rssi}, 服务UUID=[$serviceUuids]" + ) + // 打印广播数据的详细信息 val scanData = scanRecord?.bytes if (scanData != null) { val hexString = scanData.joinToString("") { String.format("%02X", it) } FileLogger.d(TAG, "广播数据: $hexString") - + // 解析服务数据 scanRecord.serviceData?.forEach { (uuid, data) -> val dataHex = data.joinToString("") { String.format("%02X", it) } @@ -304,13 +312,13 @@ object BleService { FileLogger.w(TAG, "已经连接中,忽略连接请求") return } - + // 获取一个局部引用,避免并发访问问题 val adapter = bluetoothAdapter ?: run { updateConnectionState(BleConst.STATE_ERROR) return } - + // 关闭现有连接 disconnectGatt() @@ -372,10 +380,12 @@ object BleService { updateConnectionState(BleConst.STATE_CONNECTED) g.discoverServices() } + newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) disconnectGatt() } + else -> { updateConnectionState(BleConst.STATE_ERROR) disconnectGatt() @@ -391,13 +401,14 @@ object BleService { // 获取主服务特征 val primarySvc = g.getService(BleConst.PRIMARY_SERVICE_UUID) + notifyChar = primarySvc?.getCharacteristic(BleConst.NOTIFY_CHAR_UUID) writeChar = primarySvc?.getCharacteristic(BleConst.WRITE_CHAR_UUID) // 获取音频服务特征 val audioSvc = g.getService(BleConst.AUDIO_SERVICE_UUID) audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) - + if (notifyChar == null || writeChar == null) { FileLogger.e(TAG, "未找到主服务所需特征") updateConnectionState(BleConst.STATE_ERROR) @@ -405,10 +416,7 @@ object BleService { } writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - - // 设置主服务的通知 - setupNotifications(g, notifyChar) - + //要先设置音频服务的通知,否则接收不到 // 设置音频服务的通知(如果存在) if (audioChar != null) { setupNotifications(g, audioChar) @@ -416,13 +424,22 @@ object BleService { } else { FileLogger.w(TAG, "音频服务特征未找到") } - + // 设置主服务的通知 + setupNotifications(g, notifyChar) +//开启解码 + opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object : + JlOpusPlugin.CallbackOpus { + override fun onAudioDataReceivedOpus(pcmData: ByteArray) { + // 回调给上层 + notifyAudioDataReceived(pcmData) + } + }) } override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) { - FileLogger.d(TAG, "收到特征数据: ${c.uuid}, 数据=${c.value?.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}") + val data = c.value ?: return - + // 根据特征UUID区分处理 when (c.uuid) { // 音频特征数据 @@ -443,13 +460,23 @@ object BleService { } // 其他未知数据 else -> { - FileLogger.d(TAG, "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}") + FileLogger.d( + TAG, + "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}" + ) } } } // 其他未知特征 else -> { - FileLogger.d(TAG, "收到未知特征数据: ${c.uuid}, 数据=${data.joinToString(", ") { "0x%02X".format(it) }}") + FileLogger.d( + TAG, + "收到未知特征数据: ${c.uuid}, 数据=${ + data.joinToString(", ") { + "0x%02X".format(it) + } + }" + ) } } } @@ -457,7 +484,10 @@ object BleService { override fun onCharacteristicWrite( g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int ) { - FileLogger.i(TAG, "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}") + FileLogger.i( + TAG, + "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" + ) } override fun onDescriptorWrite( @@ -476,7 +506,7 @@ object BleService { FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") return } - + g.setCharacteristicNotification(characteristic, true) val desc = characteristic.getDescriptor(BleConst.CLIENT_CHAR_CONFIG_UUID) desc?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE @@ -489,11 +519,9 @@ object BleService { */ private fun processAudioData(data: ByteArray) { try { - // 记录日志 - FileLogger.d(TAG, "收到音频数据: ${data.size}字节") - - // 回调给上层 - notifyAudioDataReceived(data) + + opusPlugin?.handleDecodeRequest(data) + } catch (e: Exception) { FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e) } @@ -503,15 +531,18 @@ object BleService { * 处理设备响应数据 - 帧头为0xBB */ private fun processDeviceResponse(data: ByteArray) { - FileLogger.d(TAG, "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}") + FileLogger.d( + TAG, + "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" + ) if (data.size < 3) { FileLogger.e(TAG, "设备响应数据格式错误:数据长度过短") return } - + val header = data[0] // 0xBB val cmdType = data[1].toInt() and 0xFF - + // 判断命令失败响应(长度为0) if (data.size == 4 && data[2].toInt() and 0xFF == 0) { FileLogger.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") @@ -524,34 +555,46 @@ object BleService { notifyDeviceInfoReceived(cmdType, resultMap) return } - + // 正常响应处理 val length = data[2].toInt() and 0xFF - + // 检查数据长度 if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC - FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length+4}, 实际=${data.size}") + FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") return } - + // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 val payload = data.sliceArray(0 until data.size - 1) val calculatedCrc = crc8Maxim(payload, payload.size) val receivedCrc = data[data.size - 1].toInt() and 0xFF - + if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - FileLogger.e(TAG, "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}") + FileLogger.e( + TAG, + "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" + ) return } - + // 提取响应数据 val responseData = if (length > 0) ByteArray(length) else ByteArray(0) if (length > 0) { System.arraycopy(data, 3, responseData, 0, length) } - - FileLogger.d(TAG, "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${responseData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}") - + + FileLogger.d( + TAG, + "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ + responseData.joinToString(", ") { + "0x${ + (it.toInt() and 0xFF).toString(16) + }" + } + }" + ) + // 根据命令类型处理不同的响应 when (cmdType) { BleConst.CMD_GET_VERSION -> { @@ -560,64 +603,74 @@ object BleService { "command" to "versionInfo", "rawData" to responseData ) - + if (responseData.isNotEmpty()) { // 转换为字符串前,确认是否是有效的ASCII字符 - val version = responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } + val version = + responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } resultMap["version"] = version FileLogger.i(TAG, "接收到目标版本号: $version") } - + notifyDeviceInfoReceived(cmdType, resultMap) } + BleConst.CMD_GET_PRODUCT_ID -> { val resultMap = mutableMapOf( "success" to true, "command" to "productInfo", "rawData" to responseData ) - + if (responseData.isNotEmpty() && responseData.size >= 2) { - val productId = "0x${(responseData[0].toInt() and 0xFF).toString(16)}${(responseData[1].toInt() and 0xFF).toString(16)}" + val productId = "0x${(responseData[0].toInt() and 0xFF).toString(16)}${ + (responseData[1].toInt() and 0xFF).toString(16) + }" resultMap["productId"] = productId FileLogger.i(TAG, "接收到产品ID: $productId") } - + notifyDeviceInfoReceived(cmdType, resultMap) } + BleConst.CMD_GET_COLOR_ID -> { val resultMap = mutableMapOf( "success" to true, "command" to "colorInfo", "rawData" to responseData ) - + if (responseData.isNotEmpty()) { val colorId = (responseData[0].toInt() and 0xFF).toString() resultMap["colorId"] = colorId FileLogger.i(TAG, "接收到耳机颜色ID: $colorId") } - + notifyDeviceInfoReceived(cmdType, resultMap) } + BleConst.CMD_GET_BATTERY_INFO -> { val resultMap = mutableMapOf( "success" to true, "command" to "batteryInfo", "rawData" to responseData ) - + if (responseData.isNotEmpty() && responseData.size >= 3) { // 处理电池信息 - 左耳、右耳、充电仓 val leftEarBattery = responseData[0].toInt() and 0x7F val leftEarCharging = (responseData[0].toInt() and 0x80) != 0 - - val rightEarBattery = if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0 - val rightEarCharging = if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false - - val caseBattery = if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0 - val caseCharging = if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false - + + val rightEarBattery = + if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0 + val rightEarCharging = + if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false + + val caseBattery = + if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0 + val caseCharging = + if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false + // 添加到结果Map resultMap["leftEarBattery"] = leftEarBattery resultMap["leftEarCharging"] = leftEarCharging @@ -626,26 +679,29 @@ object BleService { resultMap["caseBattery"] = caseBattery resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - - FileLogger.i(TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + - "(${if(leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + - "(${if(rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + - "(${if(caseCharging) "充电中" else "未充电"})") + + FileLogger.i( + TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + + "(${if (caseCharging) "充电中" else "未充电"})" + ) } - + notifyDeviceInfoReceived(cmdType, resultMap) } + BleConst.CMD_CONTROL_CODEC -> { val resultMap = mutableMapOf( "success" to true, "command" to cmdType, "rawData" to responseData ) - + if (responseData.isNotEmpty() && responseData.size >= 2) { val codecStatus = responseData[0].toInt() and 0xFF val channelMode = responseData[1].toInt() and 0xFF - + // 解码状态描述 val statusDesc = when (codecStatus) { BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码" @@ -654,7 +710,7 @@ object BleService { BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码" else -> "未知状态($codecStatus)" } - + // 声道模式描述 val channelDesc = when (channelMode) { BleConst.AUDIO_CHANNEL_LEFT -> "左声道" @@ -662,17 +718,18 @@ object BleService { BleConst.AUDIO_CHANNEL_STEREO -> "立体声" else -> "未知声道($channelMode)" } - + resultMap["codecStatus"] = codecStatus resultMap["channelMode"] = channelMode resultMap["statusDesc"] = statusDesc resultMap["channelDesc"] = channelDesc - + FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") } - + notifyDeviceInfoReceived(cmdType, resultMap) } + else -> { // 对于未知命令,返回原始数据 val resultMap = mapOf( @@ -686,45 +743,60 @@ object BleService { } } } - + /** * 处理设备主动上报数据 - 帧头为0xCC */ private fun processDeviceNotification(data: ByteArray) { - FileLogger.d(TAG, "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}") + FileLogger.d( + TAG, + "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" + ) if (data.size < 3) { FileLogger.e(TAG, "设备主动上报数据格式错误:数据长度过短") return } - + val header = data[0] // 0xCC val infoType = data[1].toInt() and 0xFF val length = data[2].toInt() and 0xFF - + // 检查数据长度 if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC - FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length+4}, 实际=${data.size}") + FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") return } - + // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 val payload = data.sliceArray(0 until data.size - 1) val calculatedCrc = crc8Maxim(payload, payload.size) val receivedCrc = data[data.size - 1].toInt() and 0xFF - + if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - FileLogger.e(TAG, "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}") + FileLogger.e( + TAG, + "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" + ) return } - + // 提取上报数据 val notifyData = if (length > 0) ByteArray(length) else ByteArray(0) if (length > 0) { System.arraycopy(data, 3, notifyData, 0, length) } - - FileLogger.d(TAG, "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${notifyData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}") - + + FileLogger.d( + TAG, + "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ + notifyData.joinToString(", ") { + "0x${ + (it.toInt() and 0xFF).toString(16) + }" + } + }" + ) + // 根据上报类型处理 when (infoType) { BleConst.CMD_GET_BATTERY_INFO -> { // 电量信息 @@ -734,19 +806,22 @@ object BleService { "rawData" to notifyData, "isNotification" to true // 标记为主动上报 ) - + if (notifyData.isNotEmpty() && notifyData.size >= 3) { // 处理电池信息 - 左耳、右耳、充电仓 val leftEarBattery = notifyData[0].toInt() and 0x7F val leftEarCharging = (notifyData[0].toInt() and 0x80) != 0 - - val rightEarBattery = if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0 - val rightEarCharging = if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false - + + val rightEarBattery = + if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0 + val rightEarCharging = + if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false + val caseBattery = if (notifyData.size > 2) notifyData[2].toInt() and 0x7F else 0 - val caseCharging = if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false - - + val caseCharging = + if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false + + // 添加到结果Map resultMap["leftEarBattery"] = leftEarBattery resultMap["leftEarCharging"] = leftEarCharging @@ -755,15 +830,18 @@ object BleService { resultMap["caseBattery"] = caseBattery resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - - FileLogger.i(TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + - "(${if(leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + - "(${if(rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + - "(${if(caseCharging) "充电中" else "未充电"})") + + FileLogger.i( + TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + + "(${if (caseCharging) "充电中" else "未充电"})" + ) } - + notifyDeviceInfoReceived(infoType, resultMap) } + BleConst.CMD_WAKE_UP -> { FileLogger.i(TAG, "收到唤醒指令") // 接收到唤醒信号,打开编码器 @@ -797,7 +875,7 @@ object BleService { FileLogger.i(TAG, "获取耳机版本号...") return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte())) } - + /** * 获取产品ID */ @@ -805,7 +883,7 @@ object BleService { FileLogger.i(TAG, "获取耳机产品ID...") return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte())) } - + /** * 获取耳机颜色ID */ @@ -813,7 +891,7 @@ object BleService { FileLogger.i(TAG, "获取耳机颜色ID...") return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte())) } - + /** * 获取耳机电量信息 */ @@ -821,23 +899,29 @@ object BleService { FileLogger.i(TAG, "获取耳机电量信息...") return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte())) } - + /** * 控制编解码 - 关闭编解码 */ fun closeCodec(): Boolean { FileLogger.i(TAG, "关闭编解码...") - return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_CLOSE.toByte())) + return sendCommand( + BleConst.CMD_CONTROL_CODEC.toByte(), + byteArrayOf(BleConst.CODEC_CONTROL_CLOSE.toByte()) + ) } - + /** * 控制编解码 - 打开解码 */ fun openDecoder(): Boolean { FileLogger.i(TAG, "打开解码...") - return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte())) + return sendCommand( + BleConst.CMD_CONTROL_CODEC.toByte(), + byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte()) + ) } - + /** * 控制编解码 - A2DP播放 */ @@ -845,15 +929,21 @@ object BleService { FileLogger.i(TAG, "切换至A2DP播放模式...") return sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) } - + /** * 控制编解码 - 打开编码 */ fun openEncoder(): Boolean { FileLogger.i(TAG, "打开编码...") - return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_ENCODE_ON.toByte())) + return sendCommand( + BleConst.CMD_CONTROL_CODEC.toByte(), + byteArrayOf( + BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), + BleConst.AUDIO_CHANNEL_RIGHT.toByte() + ) + ) } - + /** * 设置声道模式 * @param channelMode 声道模式:AUDIO_CHANNEL_LEFT/AUDIO_CHANNEL_RIGHT/AUDIO_CHANNEL_STEREO @@ -868,14 +958,16 @@ object BleService { return false } } - + // 高级控制:设置命令类型、长度和两个参数 - return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( - 0xA1.toByte(), // 状态码 - 使用解码状态 - channelMode.toByte() // 声道模式 - )) + return sendCommand( + BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( + 0xA1.toByte(), // 状态码 - 使用解码状态 + channelMode.toByte() // 声道模式 + ) + ) } - + /** * 音量增大 */ @@ -883,7 +975,7 @@ object BleService { FileLogger.i(TAG, "设备音量增加...") return sendCommand(BleConst.CMD_VOLUME_UP.toByte()) } - + /** * 音量减小 */ @@ -901,21 +993,21 @@ object BleService { fun isConnected(): Boolean { return connectionState.value == BleConst.STATE_CONNECTED } - + /** * 连接状态检查 */ private fun checkConn(): Boolean = bluetoothGatt != null && writeChar != null && connectionState.value == BleConst.STATE_CONNECTED - + /** * 获取已连接设备地址 */ fun getConnectedDeviceAddress(): String? { return bluetoothGatt?.device?.address } - + /** * 获取蓝牙适配器 */ @@ -976,7 +1068,7 @@ object BleService { val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) .setClass(context, BleWakeReceiver::class.java) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - + val pi = PendingIntent.getBroadcast( context, 0, scanIntent, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -1022,34 +1114,37 @@ object BleService { try { // 计算命令长度 val cmdLength = cmdData.size - + // 创建命令包 val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC - + // 帧头 cmdPacket[0] = BleConst.APP_REQUEST_HEADER // 命令类型 cmdPacket[1] = cmdType // 命令长度 cmdPacket[2] = cmdLength.toByte() - + // 填充命令数据(如果有) if (cmdLength > 0) { System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength) } - + // 计算CRC - 对帧头到数据的所有字节进行校验 (不包括CRC自身) val crcPayload = cmdPacket.sliceArray(0 until cmdPacket.size - 1) val crc = crc8Maxim(crcPayload, crcPayload.size) - + // 设置CRC cmdPacket[3 + cmdLength] = crc - + // 记录命令详情 - FileLogger.d(TAG, "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + - "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + - "CRC=0x${(crc.toInt() and 0xFF).toString(16)}") - + FileLogger.d( + TAG, + "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + + "CRC=0x${(crc.toInt() and 0xFF).toString(16)}" + ) + // 发送数据 writeChar!!.value = cmdPacket return bluetoothGatt?.writeCharacteristic(writeChar) ?: false @@ -1091,7 +1186,7 @@ object BleService { } } } - + /** * 向所有回调监听器分发连接状态变化 */ @@ -1104,7 +1199,7 @@ object BleService { } } } - + /** * 向所有回调监听器分发音频数据 */ @@ -1117,7 +1212,7 @@ object BleService { } } } - + /** * 向所有回调监听器分发唤醒信号 */ @@ -1130,7 +1225,7 @@ object BleService { } } } - + /** * 向所有回调监听器分发设备信息 */ diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt index 64e82778f..4fcca2ac6 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt @@ -12,13 +12,13 @@ import com.yunqiinnovation.deepvoice_utils.FileLogger */ object ChatAgent : BleService.Callback, AgentServiceListener { private const val TAG = "ChatAgent" - + // 是否已初始化 private var isInitialized = false - + // 是否已注册BLE回调 private var isRegisteredToBle = false - + /** * 初始化语音代理 * @param context 上下文 @@ -26,16 +26,16 @@ object ChatAgent : BleService.Callback, AgentServiceListener { */ fun initialize(context: Context, config: Map): Boolean { if (isInitialized) return true - + try { FileLogger.d(TAG, "初始化语音代理..., $config") - + // 1. 初始化AgentService - if (!AgentService.initialize(context, config)) { + if (!AgentService.initialize(context, config, true)) { FileLogger.e(TAG, "语音服务初始化失败") return false } - + // 2. 初始化BleService并注册回调 if (BleService.initialize(context)) { if (BleService.addCallback(this)) { @@ -48,7 +48,7 @@ object ChatAgent : BleService.Callback, AgentServiceListener { FileLogger.e(TAG, "BLE服务初始化失败") return false } - + isInitialized = true FileLogger.d(TAG, "语音代理初始化成功") return true @@ -57,7 +57,7 @@ object ChatAgent : BleService.Callback, AgentServiceListener { return false } } - + /** * 手动启动语音识别 */ @@ -66,31 +66,31 @@ object ChatAgent : BleService.Callback, AgentServiceListener { FileLogger.e(TAG, "语音代理未初始化") return false } - - return AgentService.startRecognition() + + return AgentService.startRecognition(true) } - + /** * 停止语音识别 */ fun stopListening() { AgentService.stopRecognition() } - + /** * 处理文本输入 */ fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { return AgentService.processTextInput(text, speakResponse) } - + /** * 停止TTS */ fun stopTts() { AgentService.stopTts() } - + /** * 清理资源 */ @@ -100,21 +100,21 @@ object ChatAgent : BleService.Callback, AgentServiceListener { BleService.removeCallback(this) isRegisteredToBle = false } - + // 释放AgentService资源 AgentService.dispose() - + isInitialized = false FileLogger.d(TAG, "语音代理资源已释放") } - + //============================================================================================ // BleService.Callback 接口实现 //============================================================================================ override fun onScanResult(devices: List>) { // 不处理扫描结果 } - + override fun onConnectionStateChanged(state: Int) { // 可选:处理连接状态变化 if (state == BleConst.STATE_CONNECTED) { @@ -125,18 +125,19 @@ object ChatAgent : BleService.Callback, AgentServiceListener { // stopListening() } } - + override fun onAudioDataReceived(data: ByteArray) { + AgentService.azureAsrHelper?.pushAudioData(data) // 可选:处理音频数据 } - + /** * 处理唤醒信号 * 在收到唤醒信号时启动语音识别 */ override fun onWakeupSignalReceived() { FileLogger.d(TAG, "收到唤醒信号,启动语音识别") - + // 停止当前TTS,避免冲突 AgentService.interruptCurrentResponse() AgentService.speakText("我在!") @@ -148,11 +149,11 @@ object ChatAgent : BleService.Callback, AgentServiceListener { startListening() } - + override fun onDeviceInfoReceived(infoType: Int, infoData: Map) { // 不处理设备信息 } - + //============================================================================================ // AgentServiceListener 接口实现 //============================================================================================ diff --git a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt b/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt index 30ad93caa..5921ba603 100644 --- a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt +++ b/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt @@ -70,6 +70,7 @@ class BleService(private val context: Context) { // 获取插件实例 private val opusPlugin = JlOpusPlugin() + // 蓝牙管理器和适配器 private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager @@ -325,13 +326,14 @@ class BleService(private val context: Context) { // 注册特征通知 setupNotifications(gatt) - opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object : - JlOpusPlugin.Callback1 { - override fun onAudioDataReceived1(pcmData: ByteArray) { - // Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}") + opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object : + JlOpusPlugin.CallbackOpus { + override fun onAudioDataReceivedOpus(pcmData: ByteArray) { + // Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}") callback?.onAudioDataReceived(pcmData) // 处理解码后的 PCM 数据(如播放或写入文件) - }}) + } + }) } else { Log.e(TAG, "未找到所需特征") updateConnectionState(STATE_ERROR) @@ -356,7 +358,7 @@ class BleService(private val context: Context) { characteristic: BluetoothGattCharacteristic ) { val characteristicUuid = characteristic.uuid.toString() - // Log.d(TAG, "收到特征变化通知: $characteristicUuid") + // Log.d(TAG, "收到特征变化通知: $characteristicUuid") when (characteristicUuid) { // NOTIFY_CHAR_UUID -> { @@ -369,7 +371,7 @@ class BleService(private val context: Context) { NOTIFY_CHAR_UUID -> { // 处理控制消息 val value = characteristic.value - // Log.i(TAG, "收到控制通知: ${value.size}") + // Log.i(TAG, "收到控制通知: ${value.size}") // 判断是否收到音频数据 if (value.isNotEmpty() && (value[0].toInt() and 0xFF == 0x4b || value[0].toInt() and 0xFF == 0x48)) { @@ -378,7 +380,7 @@ class BleService(private val context: Context) { opusPlugin?.handleDecodeRequest(value) - + } // 判断是否是协议格式响应 if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) { diff --git a/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt b/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt index e8c6db4c9..6e2bb30d5 100644 --- a/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt +++ b/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt @@ -19,359 +19,408 @@ import io.flutter.plugin.common.MethodChannel.Result import java.io.File /** JlOpusPlugin */ -class JlOpusPlugin: FlutterPlugin, MethodCallHandler { - private lateinit var channel: MethodChannel - private lateinit var eventChannel: EventChannel - private lateinit var context: Context - private var opusManager: OpusManager? = null - private var streamSink: EventChannel.EventSink? = null - // 修正1:正确的主构造函数写法 - constructor() { +class JlOpusPlugin : FlutterPlugin, MethodCallHandler { + private lateinit var channel: MethodChannel + private lateinit var eventChannel: EventChannel + private lateinit var context: Context + private var opusManager: OpusManager? = null + private var streamSink: EventChannel.EventSink? = null + + // 修正1:正确的主构造函数写法 + constructor() { opusManager = OpusManager() Log.d(TAG, "OpusManager初始化成功") } - // 添加主线程Handler用于确保在主线程回调 - private val mainHandler = Handler(Looper.getMainLooper()) - interface Callback1 { - - fun onAudioDataReceived1(data: ByteArray) - - } + // 添加主线程Handler用于确保在主线程回调 + private val mainHandler = Handler(Looper.getMainLooper()) + + interface CallbackOpus { + + fun onAudioDataReceivedOpus(data: ByteArray) - companion object { - private const val TAG = "JlOpusPlugin" - private const val CHANNEL_NAME = "com.yunqiinnovation.jl_opus" - private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_opus/events" - - } - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) - channel.setMethodCallHandler(this) - - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) - - eventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - streamSink = events - } - - override fun onCancel(arguments: Any?) { - streamSink = null - } - }) - - // 初始化OpusManager - try { - opusManager = OpusManager() - Log.d(TAG, "OpusManager初始化成功") - } catch (e: OpusException) { - Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) - } - } - - // 在主线程上安全发送事件 - private fun sendEventOnMainThread(event: Map) { - if (streamSink == null) return - - if (Looper.myLooper() == Looper.getMainLooper()) { - // 如果已经在主线程,直接发送 - streamSink?.success(event) - } else { - // 否则切换到主线程 - mainHandler.post { - streamSink?.success(event) - } - } - } - - // 在主线程上安全返回结果 - private fun sendResultOnMainThread(result: Result, value: Any?) { - if (Looper.myLooper() == Looper.getMainLooper()) { - // 如果已经在主线程,直接返回 - result.success(value) - } else { - // 否则切换到主线程 - mainHandler.post { - result.success(value) - } - } - } - - // 在主线程上安全返回错误 - private fun sendErrorOnMainThread(result: Result, errorCode: String, errorMessage: String, errorDetails: Any?) { - if (Looper.myLooper() == Looper.getMainLooper()) { - // 如果已经在主线程,直接返回 - result.error(errorCode, errorMessage, errorDetails) - } else { - // 否则切换到主线程 - mainHandler.post { - result.error(errorCode, errorMessage, errorDetails) - } } - } - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - if (opusManager == null) { - result.error("OPUS_MANAGER_NULL", "OpusManager未初始化", null) - return + companion object { + private const val TAG = "JlOpusPlugin" + private const val CHANNEL_NAME = "com.yunqiinnovation.jl_opus" + private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_opus/events" + } - - when (call.method) { - "initOpusDecoder" -> { - result.success(opusManager != null) - } - "decodeOpusFile" -> { - val inPath = call.argument("inPath") - val outPath = call.argument("outPath") - val hasHeader = call.argument("hasHeader") ?: false - val channel = call.argument("channel") ?: 1 - val sampleRate = call.argument("sampleRate") ?: 16000 - val packetSize = call.argument("packetSize") ?: 40 - - if (inPath == null || outPath == null) { - result.error("INVALID_ARGS", "输入或输出路径不能为空", null) - return - } - - // 确保输入文件存在 - val inFile = File(inPath) - if (!inFile.exists()) { - result.error("FILE_NOT_FOUND", "输入文件不存在: $inPath", null) - return - } - - // 创建解码参数 - val option = OpusOption() - .setHasHead(hasHeader) - .setChannel(channel) - .setSampleRate(sampleRate) - .setPacketSize(packetSize) - - Log.d(TAG, "开始解码文件, 参数: $option, 输入: $inPath, 输出: $outPath") - - opusManager?.decodeFile(inPath, outPath, option, object : OnStateCallback { - override fun onStart() { - Log.d(TAG, "文件解码开始") - sendEventOnMainThread(mapOf( - "event" to "onStart", - "type" to "file" - )) - } - - override fun onComplete(outFilePath: String?) { - Log.d(TAG, "文件解码完成: $outFilePath") - sendEventOnMainThread(mapOf( - "event" to "onComplete", - "type" to "file", - "filePath" to outFilePath - )) - sendResultOnMainThread(result, outFilePath) - } - - override fun onError(code: Int, message: String?) { - Log.e(TAG, "文件解码错误: [$code] $message") - sendEventOnMainThread(mapOf( - "event" to "onError", - "type" to "file", - "code" to code, - "message" to message - )) - sendErrorOnMainThread(result, "DECODE_ERROR", "解码出错: [$code] $message", null) - } - }) - } - "startDecodeStream" -> { - val hasHeader = call.argument("hasHeader") ?: false - val channel = call.argument("channel") ?: 1 - val sampleRate = call.argument("sampleRate") ?: 16000 - val packetSize = call.argument("packetSize") ?: 40 - - // 如果已经在解码流,先停止 - if (opusManager?.isDecodeStream == true) { - opusManager?.stopDecodeStream() - } - - // 创建解码参数 - val option = OpusOption() - .setHasHead(hasHeader) - .setChannel(channel) - .setSampleRate(sampleRate) - .setPacketSize(packetSize) - - Log.d(TAG, "开始解码数据流, 参数: $option") - - opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { - override fun onDecodeStream(data: ByteArray?) { - if (data != null) { - sendEventOnMainThread(mapOf( - "event" to "onDecodeStream", - "data" to data - )) + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + + context = flutterPluginBinding.applicationContext + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) + + eventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + streamSink = events + } + + override fun onCancel(arguments: Any?) { + streamSink = null } - } - - override fun onStart() { - Log.d(TAG, "数据流解码开始") - sendEventOnMainThread(mapOf( - "event" to "onStart", - "type" to "stream" - )) - sendResultOnMainThread(result, true) - } - - override fun onComplete(outPath: String?) { - Log.d(TAG, "数据流解码完成: $outPath") - sendEventOnMainThread(mapOf( - "event" to "onComplete", - "type" to "stream" - )) - } - - override fun onError(code: Int, message: String?) { - Log.e(TAG, "数据流解码错误: [$code] $message") - sendEventOnMainThread(mapOf( - "event" to "onError", - "type" to "stream", - "code" to code, - "message" to message - )) - sendErrorOnMainThread(result, "DECODE_STREAM_ERROR", "数据流解码出错: [$code] $message", null) - } }) - } - "stopDecodeStream" -> { - if (opusManager?.isDecodeStream == true) { - opusManager?.stopDecodeStream() - result.success(true) + + // 初始化OpusManager + try { + opusManager = OpusManager() + Log.d(TAG, "OpusManager初始化成功") + } catch (e: OpusException) { + Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) + } + } + + // 在主线程上安全发送事件 + private fun sendEventOnMainThread(event: Map) { + if (streamSink == null) return + + if (Looper.myLooper() == Looper.getMainLooper()) { + // 如果已经在主线程,直接发送 + streamSink?.success(event) } else { - result.success(false) + // 否则切换到主线程 + mainHandler.post { + streamSink?.success(event) + } } - } - "writeAudioStream" -> { - val data = call.argument("data") - if (data == null) { - result.error("INVALID_ARGS", "音频数据不能为空", null) - return + } + + // 在主线程上安全返回结果 + private fun sendResultOnMainThread(result: Result, value: Any?) { + if (Looper.myLooper() == Looper.getMainLooper()) { + // 如果已经在主线程,直接返回 + result.success(value) + } else { + // 否则切换到主线程 + mainHandler.post { + result.success(value) + } } - - if (opusManager?.isDecodeStream == true) { - opusManager?.writeAudioStream(data) - result.success(true) + } + + // 在主线程上安全返回错误 + private fun sendErrorOnMainThread( + result: Result, + errorCode: String, + errorMessage: String, + errorDetails: Any? + ) { + if (Looper.myLooper() == Looper.getMainLooper()) { + // 如果已经在主线程,直接返回 + result.error(errorCode, errorMessage, errorDetails) } else { - result.error("NOT_DECODING", "当前没有处于解码状态", null) + // 否则切换到主线程 + mainHandler.post { + result.error(errorCode, errorMessage, errorDetails) + } } - } - "isDecoding" -> { - result.success(opusManager?.isDecodeStream == true) - } - "dispose" -> { - opusManager?.let { - if (it.isDecodeStream) { - it.stopDecodeStream() - } - it.release() + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (opusManager == null) { + result.error("OPUS_MANAGER_NULL", "OpusManager未初始化", null) + return + } + + when (call.method) { + "initOpusDecoder" -> { + result.success(opusManager != null) + } + + "decodeOpusFile" -> { + val inPath = call.argument("inPath") + val outPath = call.argument("outPath") + val hasHeader = call.argument("hasHeader") ?: false + val channel = call.argument("channel") ?: 1 + val sampleRate = call.argument("sampleRate") ?: 16000 + val packetSize = call.argument("packetSize") ?: 40 + + if (inPath == null || outPath == null) { + result.error("INVALID_ARGS", "输入或输出路径不能为空", null) + return + } + + // 确保输入文件存在 + val inFile = File(inPath) + if (!inFile.exists()) { + result.error("FILE_NOT_FOUND", "输入文件不存在: $inPath", null) + return + } + + // 创建解码参数 + val option = OpusOption() + .setHasHead(hasHeader) + .setChannel(channel) + .setSampleRate(sampleRate) + .setPacketSize(packetSize) + + Log.d(TAG, "开始解码文件, 参数: $option, 输入: $inPath, 输出: $outPath") + + opusManager?.decodeFile(inPath, outPath, option, object : OnStateCallback { + override fun onStart() { + Log.d(TAG, "文件解码开始") + sendEventOnMainThread( + mapOf( + "event" to "onStart", + "type" to "file" + ) + ) + } + + override fun onComplete(outFilePath: String?) { + Log.d(TAG, "文件解码完成: $outFilePath") + sendEventOnMainThread( + mapOf( + "event" to "onComplete", + "type" to "file", + "filePath" to outFilePath + ) + ) + sendResultOnMainThread(result, outFilePath) + } + + override fun onError(code: Int, message: String?) { + Log.e(TAG, "文件解码错误: [$code] $message") + sendEventOnMainThread( + mapOf( + "event" to "onError", + "type" to "file", + "code" to code, + "message" to message + ) + ) + sendErrorOnMainThread( + result, + "DECODE_ERROR", + "解码出错: [$code] $message", + null + ) + } + }) + } + + "startDecodeStream" -> { + val hasHeader = call.argument("hasHeader") ?: false + val channel = call.argument("channel") ?: 1 + val sampleRate = call.argument("sampleRate") ?: 16000 + val packetSize = call.argument("packetSize") ?: 40 + + // 如果已经在解码流,先停止 + if (opusManager?.isDecodeStream == true) { + opusManager?.stopDecodeStream() + } + + // 创建解码参数 + val option = OpusOption() + .setHasHead(hasHeader) + .setChannel(channel) + .setSampleRate(sampleRate) + .setPacketSize(packetSize) + + Log.d(TAG, "开始解码数据流, 参数: $option") + + opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { + override fun onDecodeStream(data: ByteArray?) { + if (data != null) { + sendEventOnMainThread( + mapOf( + "event" to "onDecodeStream", + "data" to data + ) + ) + } + } + + override fun onStart() { + Log.d(TAG, "数据流解码开始") + sendEventOnMainThread( + mapOf( + "event" to "onStart", + "type" to "stream" + ) + ) + sendResultOnMainThread(result, true) + } + + override fun onComplete(outPath: String?) { + Log.d(TAG, "数据流解码完成: $outPath") + sendEventOnMainThread( + mapOf( + "event" to "onComplete", + "type" to "stream" + ) + ) + } + + override fun onError(code: Int, message: String?) { + Log.e(TAG, "数据流解码错误: [$code] $message") + sendEventOnMainThread( + mapOf( + "event" to "onError", + "type" to "stream", + "code" to code, + "message" to message + ) + ) + sendErrorOnMainThread( + result, + "DECODE_STREAM_ERROR", + "数据流解码出错: [$code] $message", + null + ) + } + }) + } + + "stopDecodeStream" -> { + if (opusManager?.isDecodeStream == true) { + opusManager?.stopDecodeStream() + result.success(true) + } else { + result.success(false) + } + } + + "writeAudioStream" -> { + val data = call.argument("data") + if (data == null) { + result.error("INVALID_ARGS", "音频数据不能为空", null) + return + } + + if (opusManager?.isDecodeStream == true) { + opusManager?.writeAudioStream(data) + result.success(true) + } else { + result.error("NOT_DECODING", "当前没有处于解码状态", null) + } + } + + "isDecoding" -> { + result.success(opusManager?.isDecodeStream == true) + } + + "dispose" -> { + opusManager?.let { + if (it.isDecodeStream) { + it.stopDecodeStream() + } + it.release() + } + opusManager = null + result.success(true) + } + + else -> { + result.notImplemented() + } } - opusManager = null - result.success(true) - } - else -> { - result.notImplemented() - } } - } - fun handleDecodeRequest(opusdata: ByteArray) { - + + fun handleDecodeRequest(opusdata: ByteArray) { + // Log.d(TAG, "handleDecodeRequest${opusdata}") if (opusdata == null) { - - return + + return } - if (opusManager?.isDecodeStream == true) { - opusManager?.writeAudioStream(opusdata) - + if (opusManager?.isDecodeStream == true) { + opusManager?.writeAudioStream(opusdata) + } else { } -} - fun startOpusDecodeStream( - hasHeader: Boolean, - channel: Int, - sampleRate: Int, - packetSize: Int, - callback: Callback1 - -) { - Log.d(TAG, "开始解码数据流,startOpusDecodeStream: $opusManager") - // 如果已经在解码流,先停止 + } + + fun startOpusDecodeStream( + hasHeader: Boolean, + channel: Int, + sampleRate: Int, + packetSize: Int, + callback: CallbackOpus + + ) { + Log.d(TAG, "开始解码数据流,startOpusDecodeStream: $opusManager") + // 如果已经在解码流,先停止 if (opusManager?.isDecodeStream == true) { - opusManager?.stopDecodeStream() + opusManager?.stopDecodeStream() } - + // 创建解码参数 val option = OpusOption() - .setHasHead(hasHeader) - .setChannel(channel) - .setSampleRate(sampleRate) - .setPacketSize(packetSize) - + .setHasHead(hasHeader) + .setChannel(channel) + .setSampleRate(sampleRate) + .setPacketSize(packetSize) + Log.d(TAG, "开始解码数据流, 参数: $option") - + opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { - override fun onDecodeStream(data: ByteArray?) { - if (data != null) { - //Log.d(TAG, "onDecodeStream${data}") - callback.onAudioDataReceived1(data) - sendEventOnMainThread(mapOf( - "event" to "onDecodeStream", - "data" to data - )) + override fun onDecodeStream(data: ByteArray?) { + if (data != null) { + //Log.d(TAG, "onDecodeStream${data}") + callback.onAudioDataReceivedOpus(data) + sendEventOnMainThread( + mapOf( + "event" to "onDecodeStream", + "data" to data + ) + ) + } + } + + override fun onStart() { + Log.d(TAG, "数据流解码开始") + sendEventOnMainThread( + mapOf( + "event" to "onStart", + "type" to "stream" + ) + ) + + } + + override fun onComplete(outPath: String?) { + Log.d(TAG, "数据流解码完成: $outPath") + sendEventOnMainThread( + mapOf( + "event" to "onComplete", + "type" to "stream" + ) + ) + } + + override fun onError(code: Int, message: String?) { + Log.e(TAG, "数据流解码错误: [$code] $message") + sendEventOnMainThread( + mapOf( + "event" to "onError", + "type" to "stream", + "code" to code, + "message" to message + ) + ) + } - } - - override fun onStart() { - Log.d(TAG, "数据流解码开始") - sendEventOnMainThread(mapOf( - "event" to "onStart", - "type" to "stream" - )) - - } - - override fun onComplete(outPath: String?) { - Log.d(TAG, "数据流解码完成: $outPath") - sendEventOnMainThread(mapOf( - "event" to "onComplete", - "type" to "stream" - )) - } - - override fun onError(code: Int, message: String?) { - Log.e(TAG, "数据流解码错误: [$code] $message") - sendEventOnMainThread(mapOf( - "event" to "onError", - "type" to "stream", - "code" to code, - "message" to message - )) - - } }) -} - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - - channel.setMethodCallHandler(null) - eventChannel.setStreamHandler(null) - opusManager?.let { - if (it.isDecodeStream) { - it.stopDecodeStream() - } - it.release() } - opusManager = null - } -} \ No newline at end of file + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + + channel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + opusManager?.let { + if (it.isDecodeStream) { + it.stopDecodeStream() + } + it.release() + } + opusManager = null + } +} \ No newline at end of file