From e773ad44a3b8fa32eee6e1d52aa53fc7e1f51194 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 11 Jul 2026 11:22:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=20=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/ble_manager.dart | 11 +- .../call_translation_debug_controller.dart | 78 +++++++++++++- .../views/call_translation_debug_view.dart | 102 ++++++++++++++++++ .../yunqiinnovation/ble_service/BleService.kt | 46 ++++++-- .../ble_service/BleServicePlugin.kt | 6 +- .../ble_service/lib/ble_service.dart | 13 ++- 6 files changed, 241 insertions(+), 15 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 790bd82a6..e1050da4a 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -1247,16 +1247,22 @@ class BleManager extends GetxService { } } - /// 设置通话翻译调试参数(运行时生效) + /// 设置通话翻译调试参数 /// [sendIntervalMs] 音频下行发送间隔(ms);[bundleFrameCount] 下行合包帧数 + /// [uplinkPacketSize] 上行编码 packetSize(字节);[downlinkPacketSize] 下行解码 packetSize(字节) + /// 说明:interval/frames 运行时立即生效;packetSize 在编解码器创建时确定,需重新启动通话翻译才生效。 Future setCallTranslationDebugParams({ int? sendIntervalMs, int? bundleFrameCount, + int? uplinkPacketSize, + int? downlinkPacketSize, }) async { try { return await _bleService.setCallTranslationDebugParams( sendIntervalMs: sendIntervalMs, bundleFrameCount: bundleFrameCount, + uplinkPacketSize: uplinkPacketSize, + downlinkPacketSize: downlinkPacketSize, ); } catch (e) { Logger.e('CALL_TRANS_DEBUG', '设置通话翻译调试参数失败: ${e.toString()}'); @@ -1264,7 +1270,8 @@ class BleManager extends GetxService { } } - /// 获取当前通话翻译调试参数 {'sendIntervalMs': int, 'bundleFrameCount': int} + /// 获取当前通话翻译调试参数 + /// {'sendIntervalMs', 'bundleFrameCount', 'uplinkPacketSize', 'downlinkPacketSize'} Future> getCallTranslationDebugParams() async { try { return await _bleService.getCallTranslationDebugParams(); diff --git a/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart b/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart index 703fd5af3..b0a696b89 100644 --- a/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart +++ b/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart @@ -14,10 +14,15 @@ class CallTranslationDebugController extends GetxController { // 持久化键 static const String _kSendInterval = 'call_trans_debug_send_interval'; static const String _kBundleFrameCount = 'call_trans_debug_bundle_frames'; + static const String _kUplinkBitrate = 'call_trans_debug_uplink_bitrate'; + static const String _kDownlinkBitrate = 'call_trans_debug_downlink_bitrate'; // 默认值(与原生默认保持一致) static const int defaultSendIntervalMs = 100; static const int defaultBundleFrameCount = 4; + // 上行/下行默认每声道码率(kbps):对应上行 packetSize=40、下行 packetSize=80 + static const int defaultUplinkBitrateKbps = 16; + static const int defaultDownlinkBitrateKbps = 16; // 取值范围 static const int minSendIntervalMs = 20; @@ -26,12 +31,44 @@ class CallTranslationDebugController extends GetxController { static const int minBundleFrameCount = 1; static const int maxBundleFrameCount = 20; + // 可选码率档位(每声道 kbps)。取偶数以保证换算出的 packetSize 为整数字节。 + static const List bitrateOptionsKbps = [8, 12, 16, 20, 24, 32]; + // 设备端 Opus 单帧固定时长(ms),packetSize 随码率线性变化。 + static const int frameDurationMs = 20; + /// 音频下行发送间隔(ms) final RxInt sendIntervalMs = defaultSendIntervalMs.obs; /// 下行合包帧数 final RxInt bundleFrameCount = defaultBundleFrameCount.obs; + /// 上行编码码率(每声道 kbps)——麦克风→设备 + final RxInt uplinkBitrateKbps = defaultUplinkBitrateKbps.obs; + + /// 下行解码码率(每声道 kbps)——设备→本机播放,需与设备固件编码码率一致 + final RxInt downlinkBitrateKbps = defaultDownlinkBitrateKbps.obs; + + /// 上行 packetSize:每声道独立 mono 流,每帧字节数 = 码率 × 帧时长 ÷ 8 + int get uplinkPacketSize => uplinkBitrateKbps.value * frameDurationMs ~/ 8; + + /// 下行 packetSize:立体声合并流(左右各一声道),每帧字节数 = 2 × 码率 × 帧时长 ÷ 8 + int get downlinkPacketSize => + 2 * downlinkBitrateKbps.value * frameDurationMs ~/ 8; + + /// 由 packetSize 反推每声道码率(kbps),用于同步原生返回值 + int _uplinkBitrateFromPacket(int packet) => packet * 8 ~/ frameDurationMs; + int _downlinkBitrateFromPacket(int packet) => + packet * 8 ~/ (2 * frameDurationMs); + + /// 就近吸附到最接近的合法档位 + int _snapBitrate(int kbps) { + var best = bitrateOptionsKbps.first; + for (final o in bitrateOptionsKbps) { + if ((o - kbps).abs() < (best - kbps).abs()) best = o; + } + return best; + } + /// 通话翻译编解码是否已开启 RxBool get isCodecActive => _bleManager.isCodecActiveRx; @@ -50,6 +87,8 @@ class CallTranslationDebugController extends GetxController { void _loadFromStorage() { final savedInterval = _storage.read(_kSendInterval); final savedFrames = _storage.read(_kBundleFrameCount); + final savedUplink = _storage.read(_kUplinkBitrate); + final savedDownlink = _storage.read(_kDownlinkBitrate); if (savedInterval != null) { sendIntervalMs.value = savedInterval.clamp(minSendIntervalMs, maxSendIntervalMs); @@ -58,6 +97,12 @@ class CallTranslationDebugController extends GetxController { bundleFrameCount.value = savedFrames.clamp(minBundleFrameCount, maxBundleFrameCount); } + if (savedUplink != null) { + uplinkBitrateKbps.value = _snapBitrate(savedUplink); + } + if (savedDownlink != null) { + downlinkBitrateKbps.value = _snapBitrate(savedDownlink); + } } /// 用原生当前实际值同步显示(不覆盖用户本地保存的偏好,仅在无本地保存时参考) @@ -79,6 +124,19 @@ class CallTranslationDebugController extends GetxController { nf.clamp(minBundleFrameCount, maxBundleFrameCount); } } + if (_storage.read(_kUplinkBitrate) == null) { + final up = params['uplinkPacketSize']; + if (up != null && up > 0) { + uplinkBitrateKbps.value = _snapBitrate(_uplinkBitrateFromPacket(up)); + } + } + if (_storage.read(_kDownlinkBitrate) == null) { + final dp = params['downlinkPacketSize']; + if (dp != null && dp > 0) { + downlinkBitrateKbps.value = + _snapBitrate(_downlinkBitrateFromPacket(dp)); + } + } } catch (e) { Logger.e('CALL_TRANS_DEBUG', '同步原生参数失败: $e'); } @@ -96,22 +154,38 @@ class CallTranslationDebugController extends GetxController { value.clamp(minBundleFrameCount, maxBundleFrameCount); } + void setUplinkBitrate(int kbps) { + uplinkBitrateKbps.value = _snapBitrate(kbps); + } + + void setDownlinkBitrate(int kbps) { + downlinkBitrateKbps.value = _snapBitrate(kbps); + } + /// 恢复默认值 void resetDefaults() { sendIntervalMs.value = defaultSendIntervalMs; bundleFrameCount.value = defaultBundleFrameCount; + uplinkBitrateKbps.value = defaultUplinkBitrateKbps; + downlinkBitrateKbps.value = defaultDownlinkBitrateKbps; } - /// 保存并下发参数到原生(运行时立即生效) + /// 保存并下发参数到原生 + /// 注意:packetSize(码率)在编解码器创建时确定,运行中修改需停止后重新「应用并启动」才生效。 Future applyParams() async { _storage.write(_kSendInterval, sendIntervalMs.value); _storage.write(_kBundleFrameCount, bundleFrameCount.value); + _storage.write(_kUplinkBitrate, uplinkBitrateKbps.value); + _storage.write(_kDownlinkBitrate, downlinkBitrateKbps.value); final ok = await _bleManager.setCallTranslationDebugParams( sendIntervalMs: sendIntervalMs.value, bundleFrameCount: bundleFrameCount.value, + uplinkPacketSize: uplinkPacketSize, + downlinkPacketSize: downlinkPacketSize, ); Logger.i('CALL_TRANS_DEBUG', - '下发调试参数 interval=${sendIntervalMs.value}ms frames=${bundleFrameCount.value} result=$ok'); + '下发调试参数 interval=${sendIntervalMs.value}ms frames=${bundleFrameCount.value} ' + 'up=${uplinkBitrateKbps.value}kbps/${uplinkPacketSize}B down=${downlinkBitrateKbps.value}kbps/${downlinkPacketSize}B result=$ok'); Get.snackbar( '通话翻译调试', ok ? '参数已应用' : '参数下发失败(请确认设备已连接)', diff --git a/lib/modules/call_translation_debug/views/call_translation_debug_view.dart b/lib/modules/call_translation_debug/views/call_translation_debug_view.dart index 7221bf98b..684ab14d6 100644 --- a/lib/modules/call_translation_debug/views/call_translation_debug_view.dart +++ b/lib/modules/call_translation_debug/views/call_translation_debug_view.dart @@ -36,6 +36,10 @@ class CallTranslationDebugView _buildSendIntervalCard(isDark), SizedBox(height: 16.h), _buildBundleFrameCard(isDark), + SizedBox(height: 16.h), + _buildUplinkBitrateCard(isDark), + SizedBox(height: 16.h), + _buildDownlinkBitrateCard(isDark), SizedBox(height: 24.h), _buildActionButtons(isDark), ], @@ -193,6 +197,104 @@ class CallTranslationDebugView ); } + // 上行编码码率(麦克风→设备) + Widget _buildUplinkBitrateCard(bool isDark) { + return _card( + isDark, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _title(isDark, '上行编码码率', + '麦克风→设备方向每声道 Opus 码率,值越高音质越好、带宽越大。packetSize = 码率×${CallTranslationDebugController.frameDurationMs}ms÷8'), + SizedBox(height: 8.h), + Obx( + () => Wrap( + spacing: 8.w, + runSpacing: 8.h, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...CallTranslationDebugController.bitrateOptionsKbps.map( + (kbps) => _bitrateChip( + isDark, + kbps, + controller.uplinkBitrateKbps.value == kbps, + () => controller.setUplinkBitrate(kbps), + ), + ), + _valueChip(isDark, '包 ${controller.uplinkPacketSize} B'), + ], + ), + ), + ], + ), + ); + } + + // 下行解码码率(设备→本机播放) + Widget _buildDownlinkBitrateCard(bool isDark) { + return _card( + isDark, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _title(isDark, '下行解码码率', + '设备→本机方向每声道 Opus 码率。⚠️ 需与设备固件编码码率一致,否则解码错位。立体声合包 packetSize = 2×码率×${CallTranslationDebugController.frameDurationMs}ms÷8'), + SizedBox(height: 8.h), + Obx( + () => Wrap( + spacing: 8.w, + runSpacing: 8.h, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...CallTranslationDebugController.bitrateOptionsKbps.map( + (kbps) => _bitrateChip( + isDark, + kbps, + controller.downlinkBitrateKbps.value == kbps, + () => controller.setDownlinkBitrate(kbps), + ), + ), + _valueChip(isDark, '包 ${controller.downlinkPacketSize} B'), + ], + ), + ), + ], + ), + ); + } + + Widget _bitrateChip( + bool isDark, int kbps, bool selected, VoidCallback onTap) { + final selBg = isDark ? Colors.blue[600] : Colors.blue[600]; + final unselBg = isDark ? const Color(0xFF2A2A2A) : Colors.grey[100]; + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 7.h), + decoration: BoxDecoration( + color: selected ? selBg : unselBg, + borderRadius: BorderRadius.circular(10.r), + border: Border.all( + color: selected + ? Colors.blue[600]! + : (isDark ? const Color(0xFF3A3A3A) : Colors.grey[300]!), + width: 1, + ), + ), + child: Text( + '$kbps kbps', + style: TextStyle( + fontSize: 13.sp, + fontWeight: selected ? FontWeight.w600 : FontWeight.w400, + color: selected + ? Colors.white + : (isDark ? Colors.white70 : Colors.black87), + ), + ), + ), + ); + } + Widget _title(bool isDark, String title, String subtitle) { return Column( crossAxisAlignment: CrossAxisAlignment.start, 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 9b1a95e03..7a25dba49 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 @@ -159,6 +159,16 @@ object BleService { // 音频下行发送间隔(ms),调试界面可在运行时调整;通过 setCallTranslationDebugParams() 修改 @Volatile private var audioSendIntervalNormal = DEFAULT_AUDIO_SEND_INTERVAL_NORMAL + + // 上行编码 packetSize(字节):每声道独立 mono 流每帧字节数,默认 40 = 每声道 16kbps@20ms。 + // 调试界面可修改;因编码器创建时才读取,需重新启动通话翻译才生效。 + @Volatile + private var uplinkPacketSize = 40 + + // 下行解码 packetSize(字节):立体声合并流每帧字节数,默认 80 = 每声道 16kbps@20ms。 + // 需与设备固件编码 packetSize 一致,否则解码错位;同样需重新启动通话翻译才生效。 + @Volatile + private var downlinkPacketSize = 80 // 左/右声道休眠标记(true 表示该声道处于休眠,暂停下发)。 // 收到 F0(左)/F1(右) 时置 true,收到 F2(左)/F3(右) 恢复指令时置 false。 // 休眠期间该声道的包暂不发送(转入积压缓存),仅传输另一声道,恢复后按序补发,不丢包。 @@ -1356,7 +1366,8 @@ object BleService { */ private fun startOpusEncodeStream(): Boolean { startAudioSendThread() - return opusAudioManager.startDualEncodeStream(false, 16000, 40) + Log.i(TAG, "[CALL_TRANS_DEBUG] 启动上行编码 packetSize=$uplinkPacketSize") + return opusAudioManager.startDualEncodeStream(false, 16000, uplinkPacketSize) } /** @@ -1723,11 +1734,20 @@ object BleService { // ====================================================================================================== /** - * 设置通话翻译调试参数(运行时生效,无需重连)。 + * 设置通话翻译调试参数。 + * sendIntervalMs / bundleFrameCount 运行时立即生效; + * uplinkPacketSize / downlinkPacketSize 在编解码器创建时读取,需重新启动通话翻译才生效。 * @param sendIntervalMs 音频下行发送间隔(ms),null 或 <=0 表示不修改 * @param bundleFrameCount 下行合包帧数,null 或 <=0 表示不修改 - */ - fun setCallTranslationDebugParams(sendIntervalMs: Long?, bundleFrameCount: Int?) { + * @param uplinkPacketSize 上行编码 packetSize(字节),null 或 <=0 表示不修改 + * @param downlinkPacketSize 下行解码 packetSize(字节),null 或 <=0 表示不修改 + */ + fun setCallTranslationDebugParams( + sendIntervalMs: Long?, + bundleFrameCount: Int?, + uplinkPacketSize: Int? = null, + downlinkPacketSize: Int? = null + ) { if (sendIntervalMs != null && sendIntervalMs > 0) { audioSendIntervalNormal = sendIntervalMs.coerceIn(1L, 1000L) Log.i(TAG, "[CALL_TRANS_DEBUG] 设置音频下行发送间隔: ${audioSendIntervalNormal}ms") @@ -1736,16 +1756,26 @@ object BleService { opusAudioManager.setBundleFrameCount(bundleFrameCount) Log.i(TAG, "[CALL_TRANS_DEBUG] 设置下行合包帧数: ${opusAudioManager.getBundleFrameCount()}") } + if (uplinkPacketSize != null && uplinkPacketSize > 0) { + this.uplinkPacketSize = uplinkPacketSize.coerceIn(1, 1024) + Log.i(TAG, "[CALL_TRANS_DEBUG] 设置上行编码 packetSize: ${this.uplinkPacketSize}(重启通话翻译后生效)") + } + if (downlinkPacketSize != null && downlinkPacketSize > 0) { + this.downlinkPacketSize = downlinkPacketSize.coerceIn(1, 1024) + Log.i(TAG, "[CALL_TRANS_DEBUG] 设置下行解码 packetSize: ${this.downlinkPacketSize}(重启通话翻译后生效)") + } } /** * 获取当前通话翻译调试参数。 - * @return Map: sendIntervalMs(Long) / bundleFrameCount(Int) + * @return Map: sendIntervalMs(Long) / bundleFrameCount(Int) / uplinkPacketSize(Int) / downlinkPacketSize(Int) */ fun getCallTranslationDebugParams(): Map { return mapOf( "sendIntervalMs" to audioSendIntervalNormal, - "bundleFrameCount" to opusAudioManager.getBundleFrameCount() + "bundleFrameCount" to opusAudioManager.getBundleFrameCount(), + "uplinkPacketSize" to uplinkPacketSize, + "downlinkPacketSize" to downlinkPacketSize ) } @@ -1826,10 +1856,10 @@ object BleService { * @return 是否成功发送命令 */ fun openA2DPDecoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { - Log.i(TAG, "[CALL_TRANS_CMD] 打开A2DP解码器(通话翻译) channelMode=$channelMode") + Log.i(TAG, "[CALL_TRANS_CMD] 打开A2DP解码器(通话翻译) channelMode=$channelMode downlinkPacketSize=$downlinkPacketSize") startOpusEncodeStream() recordfile1?.creatingFiles(); - startOpusStreamDecoding(false, 2, 16000, 80) + startOpusStreamDecoding(false, 2, 16000, downlinkPacketSize) val cmdData = byteArrayOf( BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), channelMode.toByte() diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 7bb7d2563..40bd526a3 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -233,7 +233,11 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, "setCallTranslationDebugParams" -> { val sendIntervalMs = (call.argument("sendIntervalMs"))?.toLong() val bundleFrameCount = (call.argument("bundleFrameCount"))?.toInt() - BleService.setCallTranslationDebugParams(sendIntervalMs, bundleFrameCount) + val uplinkPacketSize = (call.argument("uplinkPacketSize"))?.toInt() + val downlinkPacketSize = (call.argument("downlinkPacketSize"))?.toInt() + BleService.setCallTranslationDebugParams( + sendIntervalMs, bundleFrameCount, uplinkPacketSize, downlinkPacketSize + ) result.success(true) } "getCallTranslationDebugParams" -> { diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index 2fbe25f60..f869dda0f 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -172,18 +172,24 @@ class BleService { } } - /// 设置通话翻译调试参数(运行时生效) + /// 设置通话翻译调试参数 /// [sendIntervalMs] 音频下行发送间隔(ms) /// [bundleFrameCount] 下行合包帧数 + /// [uplinkPacketSize] 上行编码 packetSize(字节),下次启动通话翻译时生效 + /// [downlinkPacketSize] 下行解码 packetSize(字节),下次启动通话翻译时生效 Future setCallTranslationDebugParams({ int? sendIntervalMs, int? bundleFrameCount, + int? uplinkPacketSize, + int? downlinkPacketSize, }) async { try { final result = await _methodChannel .invokeMethod('setCallTranslationDebugParams', { if (sendIntervalMs != null) 'sendIntervalMs': sendIntervalMs, if (bundleFrameCount != null) 'bundleFrameCount': bundleFrameCount, + if (uplinkPacketSize != null) 'uplinkPacketSize': uplinkPacketSize, + if (downlinkPacketSize != null) 'downlinkPacketSize': downlinkPacketSize, }); return result ?? false; } catch (e) { @@ -193,7 +199,7 @@ class BleService { } /// 获取当前通话翻译调试参数 - /// 返回 {'sendIntervalMs': int, 'bundleFrameCount': int},失败返回空 Map + /// 返回 {'sendIntervalMs', 'bundleFrameCount', 'uplinkPacketSize', 'downlinkPacketSize'},失败返回空 Map Future> getCallTranslationDebugParams() async { try { final result = await _methodChannel @@ -202,6 +208,9 @@ class BleService { return { 'sendIntervalMs': (result['sendIntervalMs'] as num?)?.toInt() ?? 0, 'bundleFrameCount': (result['bundleFrameCount'] as num?)?.toInt() ?? 0, + 'uplinkPacketSize': (result['uplinkPacketSize'] as num?)?.toInt() ?? 0, + 'downlinkPacketSize': + (result['downlinkPacketSize'] as num?)?.toInt() ?? 0, }; } catch (e) { print('获取通话翻译调试参数失败: $e');