Browse Source

添加刘总的上行修改调试逻辑

newdev_chengguofeng
liwei1dao 3 months ago
parent
commit
d0ccdf7226
  1. 31
      lib/modules/opus_test/controllers/opus_test_controller.dart
  2. 17
      lib/modules/pairing/controllers/pairing_controller.dart
  3. 14
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  4. 44
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt

31
lib/modules/opus_test/controllers/opus_test_controller.dart

@ -164,7 +164,10 @@ class OpusTestController extends GetxController {
for (var entity in files) {
if (entity is File) {
final path = entity.path.toLowerCase();
if (path.endsWith('.opus') || path.endsWith('.txt')) {
// .wav:已解码 PCM(如上行「翻译-上行」音频),直接播放;.opus 需解码
if (path.endsWith('.opus') ||
path.endsWith('.txt') ||
path.endsWith('.wav')) {
final fileSize = await entity.length();
externalFiles.add(PlatformFile(
name: entity.path.split('/').last,
@ -206,7 +209,12 @@ class OpusTestController extends GetxController {
void selectExternalFile(PlatformFile file) {
selectedFiles.clear();
selectedFiles.add(file);
// 「翻译-*」文件自动套用手机端下行 opus 解码参数,套上后提示里带说明;否则只提示已选择
// .wav 是已解码 PCM(如上行「翻译-上行」音频),点播放直接播,无需 opus 解码参数
if (file.name.toLowerCase().endsWith('.wav')) {
statusMessage.value = '已选择WAV(已解码PCM),点播放直接播: ${file.name}';
return;
}
// 「翻译-*」opus 文件自动套用手机端下行 opus 解码参数,套上后提示里带说明;否则只提示已选择
if (!_applyAutoDecodeParams(file.name)) {
statusMessage.value = '已选择文件: ${file.name}';
}
@ -261,11 +269,28 @@ class OpusTestController extends GetxController {
}
try {
final inPath = selectedFiles.first.path!;
// .wav 已是解码后 PCM(如上行「翻译-上行」音频),直接播放,跳过 opus 解码
if (inPath.toLowerCase().endsWith('.wav')) {
statusMessage.value = '正在播放WAV...';
await player.setFilePath(inPath);
await player.play();
isPlaying.value = true;
statusMessage.value = '正在播放WAV音频';
player.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
isPlaying.value = false;
statusMessage.value = '播放完成';
}
});
return;
}
isDecoding.value = true;
statusMessage.value = '正在解码...';
// 使用杰理OPUS库进行解码
final inPath = selectedFiles.first.path!;
// 创建WAV输出文件路径
final tempDir = await getTemporaryDirectory();

17
lib/modules/pairing/controllers/pairing_controller.dart

@ -889,7 +889,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
// 找到匹配的设备,提取授权码
if (Platform.isIOS) {
// iOS设备,提取13-22字节的授权码
code = extractAndReverseBytes(hexData, 13, 22);
code = extractBytesBigEndian(hexData, 13, 22);
_verifiedDevicePid =
extractAndReverseBytesToInt(hexData, 4, 5);
final pidHexIos =
@ -930,7 +930,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
}
} else if (Platform.isAndroid) {
// Android设备,提取15-24字节的授权码
code = extractAndReverseBytes(hexData, 15, 24);
code = extractBytesBigEndian(hexData, 15, 24);
_verifiedDevicePid =
extractAndReverseBytesToInt(hexData, 6, 7);
final pidHexAndroid =
@ -1165,6 +1165,19 @@ class PairingController extends GetxController with WidgetsBindingObserver {
.toUpperCase();
}
/// 大端模式:按原始字节顺序提取 [start, end](含 end),不反转
String extractBytesBigEndian(String hex, int start, int end) {
List<int> bytes = [];
for (int i = 0; i < hex.length; i += 2) {
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
}
List<int> slice = bytes.sublist(start, end + 1);
return slice
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
}
int extractAndReverseBytesToInt(String hex, int start, int end) {
List<int> bytes = [];
// 将十六进制字符串解析为字节列表

14
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -1435,7 +1435,9 @@ object BleService {
}
/**
* 上行音频收包长度保护:opus 帧长为 40B(立体声一帧=80B=2×40B),合法包应是 40 的整数倍且非空。
* 上行音频收包长度保护:普通流 opus 帧=40B;通话流(8kbps/60ms)立体声一帧=120B=2×60B。
* 两条流共用此校验,故按其公约数 40 判:40 与 120 均为 40 的整数倍,合法包应是 40 的整数倍且非空。
* 注意不要改成 % 60——那会把普通单声道流(40B)误判为坏包。
* 非 40 整数倍或空包视为异常(坏包/字节流错位),返回 false —— 调用方据此直接丢弃、
* 且不计入接收统计、不喂解码器;避免坏包喂进定长流式解码器把后续整段字节流搞错位、全部解错。
* @return true=长度合法可继续处理;false=异常包(已打印告警),应丢弃
@ -2111,6 +2113,9 @@ object BleService {
recordfile?.closeFile()
recordfile1Left?.closeFile()
recordfile1Right?.closeFile()
// 会话结束即停解码流:既停设备上行解码,也让上行「翻译-上行」PCM WAV 补齐文件头,
// opus_test 立即可直接播放听辨(否则要等 BleService 整体 cleanup 才 finalize)
stopOpusStreamDecoding()
val cmdData = byteArrayOf(
BleConst.CODEC_CONTROL_CLOSE.toByte(),
BleConst.AUDIO_CHANNEL_STEREO.toByte()
@ -2151,7 +2156,9 @@ object BleService {
startOpusEncodeStream()
recordfile1Left?.creatingFiles();
recordfile1Right?.creatingFiles();
startOpusStreamDecoding(false, 2, 16000, 80)
// 设备端通话上行 opus 参数:8kbps/60ms/8kHz 立体声。
// packetSize=每帧字节数=码率×帧长÷8=8000×0.06÷8=60B(单声道),立体声打包 2×60=120B。
startOpusStreamDecoding(false, 2, 16000, 240)
val cmdData = byteArrayOf(
BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
channelMode.toByte()
@ -2168,7 +2175,8 @@ object BleService {
*/
fun openCallRecordDecoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean {
Log.i(TAG, "打开通话录音解码器...")
startOpusStreamDecoding(false, 2, 16000, 80)
// 通话录音与通话翻译走同一条设备上行流:8kbps/60ms/8kHz 立体声,packetSize=2×60=120B。
startOpusStreamDecoding(false, 2, 16000, 240)
val cmdData = byteArrayOf(
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY.toByte(),
channelMode.toByte()

44
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt

@ -121,6 +121,8 @@ class OpusAudioManager {
private var appContext: Context? = null
private var leftPcmWavWriter: WavFileWriter? = null
private var rightPcmWavWriter: WavFileWriter? = null
// 上行(设备→手机)opus 解码后 PCM 的 WAV 调试文件:命名「翻译-上行」,opus_test 可直接播放听辨
private var decodePcmWavWriter: WavFileWriter? = null
private val mainHandler = Handler(Looper.getMainLooper())
@ -280,11 +282,16 @@ class OpusAudioManager {
}
Log.d(TAG, "准备开始Opus数据流解码, 参数: hasHeader=$hasHeader, channel=$channel, sampleRate=$sampleRate, packetSize=$packetSize")
// 落地上行解码后 PCM 为 WAV,供 opus_test 直接播放听辨(参数与解码配置一致)
openDecodePcmWavWriter(channel, sampleRate)
try {
decodeOpusManager?.startDecodeStream(decodeOption, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
// 解码后 PCM 落地一份(每帧异步入队写盘,不阻塞解码线程)
decodePcmWavWriter?.writePcmData(data)
val channelCount = decodeOption?.getChannel() ?: 1
if (channelCount >= 1) {
callback?.onAudioDataDecoded(data, channelCount)
@ -314,11 +321,13 @@ class OpusAudioManager {
return true
} catch (e: Exception) {
Log.e(TAG, "启动Opus解码流失败: ${e.message}", e)
// 启动失败:关闭刚创建的上行 PCM WAV,避免留下空文件
closeDecodePcmWavWriter()
callback?.onError("decode", -1, e.message)
return false
}
}
/**
* 停止Opus数据流解码
* @return 是否成功停止解码
@ -327,6 +336,9 @@ class OpusAudioManager {
if (decodeOpusManager?.isDecodeStream == true) {
decodeOpusManager?.stopDecodeStream()
// 关闭上行解码 PCM 的 WAV 落地文件(补齐 WAV 头、无数据则删除)
closeDecodePcmWavWriter()
// 清理音频数据缓存
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
@ -337,6 +349,8 @@ class OpusAudioManager {
callback?.onDecodeStreamStateChanged(false)
return true
}
// 即便未在解码(如启动失败后重复停止),也兜底关闭可能残留的写入器
closeDecodePcmWavWriter()
Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化")
return false
}
@ -607,6 +621,32 @@ class OpusAudioManager {
rightPcmWavWriter = null
}
/**
* 打开上行(设备→手机)解码后 PCM 的 WAV 落地文件。
* 命名「翻译-上行」前缀,写到 getExternalFilesDir,opus_test 扫描到 .wav 直接播放听辨;
* 采样率/声道与解码配置一致(通话翻译为 16k 立体声),16bit PCM。
*/
private fun openDecodePcmWavWriter(channel: Int, sampleRate: Int) {
val ctx = appContext ?: return
// 复用前先关掉可能残留的旧写入器
closeDecodePcmWavWriter()
try {
// createWavFile 内部会追加 _时间戳,这里只给前缀,最终名如「翻译-上行_20260707_153000.wav」
decodePcmWavWriter = WavFileWriter(ctx).also {
val ok = it.createWavFile("翻译-上行", sampleRate, if (channel >= 1) channel else 1, 16)
if (!ok) decodePcmWavWriter = null
}
} catch (e: Exception) {
Log.e(TAG, "创建上行解码 PCM WAV 调试文件失败: ${e.message}", e)
decodePcmWavWriter = null
}
}
private fun closeDecodePcmWavWriter() {
try { decodePcmWavWriter?.closeWavFile() } catch (e: Exception) { Log.e(TAG, "关闭上行解码 PCM WAV 失败", e) }
decodePcmWavWriter = null
}
/**
* 写入外部音频数据到Opus编码流
* 该函数用于将外部音频数据(如麦克风录音)写入到Opus编码器中

Loading…
Cancel
Save