|
|
@ -21,7 +21,6 @@ import android.os.Handler |
|
|
import java.util.concurrent.atomic.AtomicBoolean |
|
|
import java.util.concurrent.atomic.AtomicBoolean |
|
|
import android.util.Log |
|
|
import android.util.Log |
|
|
import androidx.annotation.RequiresPermission |
|
|
import androidx.annotation.RequiresPermission |
|
|
import java.util.concurrent.TimeUnit |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 |
|
|
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 |
|
|
@ -71,12 +70,6 @@ object BleService { |
|
|
|
|
|
|
|
|
// 升级唤醒词失败相关回调 |
|
|
// 升级唤醒词失败相关回调 |
|
|
fun onWakeupVoiceUpgradeFailed() |
|
|
fun onWakeupVoiceUpgradeFailed() |
|
|
|
|
|
|
|
|
// 声道通话休眠相关回调 |
|
|
|
|
|
fun onCallSleepReceived(channel: Int) |
|
|
|
|
|
|
|
|
|
|
|
// 声道通话恢复相关回调 |
|
|
|
|
|
fun onCallResumeReceived(channel: Int) |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// ====================================================================================================== |
|
|
// ====================================================================================================== |
|
|
@ -153,8 +146,6 @@ object BleService { |
|
|
// 延迟追踪:右声道(对方译音)每句首包写到 BLE 的时刻,配合 azure_speech 侧 [LAT-TRACE] 点1/点2 |
|
|
// 延迟追踪:右声道(对方译音)每句首包写到 BLE 的时刻,配合 azure_speech 侧 [LAT-TRACE] 点1/点2 |
|
|
// 相减即得 app 内部"收到译音→写到耳机"耗时;间隔>700ms 视为新一句,避免逐包刷屏 |
|
|
// 相减即得 app 内部"收到译音→写到耳机"耗时;间隔>700ms 视为新一句,避免逐包刷屏 |
|
|
private var latLastBleRightTs = 0L |
|
|
private var latLastBleRightTs = 0L |
|
|
// 写失败(拥塞)后的退避时长(ms):给底层发送缓冲腾空的时间,再重试同一包,期间绝不丢包 |
|
|
|
|
|
private const val WRITE_RETRY_BACKOFF_MS = 5L |
|
|
|
|
|
|
|
|
|
|
|
// 音频数据分块发送的常量 |
|
|
// 音频数据分块发送的常量 |
|
|
private val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节 |
|
|
private val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节 |
|
|
@ -162,23 +153,33 @@ object BleService { |
|
|
// 音频下行发送间隔(ms),调试界面可在运行时调整;通过 setCallTranslationDebugParams() 修改 |
|
|
// 音频下行发送间隔(ms),调试界面可在运行时调整;通过 setCallTranslationDebugParams() 修改 |
|
|
@Volatile |
|
|
@Volatile |
|
|
private var audioSendIntervalNormal = DEFAULT_AUDIO_SEND_INTERVAL_NORMAL |
|
|
private var audioSendIntervalNormal = DEFAULT_AUDIO_SEND_INTERVAL_NORMAL |
|
|
// 左/右声道休眠标记(true 表示该声道处于休眠,暂停下发)。 |
|
|
// 左/右声道下发节拍倍数(以一个 audioSendIntervalNormal=40ms 拍为单位): |
|
|
// 收到 F0(左)/F1(右) 时置 true,收到 F2(左)/F3(右) 恢复指令时置 false。 |
|
|
// 1 拍=40ms/包(全速)、4 拍=160ms/包、8 拍=320ms/包(三档)。 |
|
|
// 休眠期间该声道的包暂不发送(转入积压缓存),仅传输另一声道,恢复后按序补发,不丢包。 |
|
|
// 由设备 F4(左)/F5(右) 上报的解码缓存空余字节数动态切换:空余越少越降速,缓解设备解码溢出丢包。 |
|
|
@Volatile |
|
|
@Volatile |
|
|
private var leftChannelSleeping: Boolean = false |
|
|
private var leftPaceTicks: Int = 1 |
|
|
@Volatile |
|
|
@Volatile |
|
|
private var rightChannelSleeping: Boolean = false |
|
|
private var rightPaceTicks: Int = 1 |
|
|
// 声道休眠期间该声道的包不丢弃,转入对应积压缓存,待声道唤醒后按 FIFO 顺序补发。 |
|
|
// 各声道上次成功发送所处的拍号;-1 表示尚未发送过(立即可发)。用于按 paceTicks 控制每声道下发频率。 |
|
|
// 仅在音频发送线程内访问,无需额外加锁。 |
|
|
private var lastLeftSendTick = -1L |
|
|
|
|
|
private var lastRightSendTick = -1L |
|
|
|
|
|
// 左右声道各自的下行缓存:主队列取出的合包先按声道分流到这里,再由发送线程按各自档位取出下发。 |
|
|
|
|
|
// 降速档位会在此积压;仅在音频发送线程内访问,无需额外加锁。 |
|
|
private val leftHoldBuffer = ArrayDeque<ByteArray>() |
|
|
private val leftHoldBuffer = ArrayDeque<ByteArray>() |
|
|
private val rightHoldBuffer = ArrayDeque<ByteArray>() |
|
|
private val rightHoldBuffer = ArrayDeque<ByteArray>() |
|
|
// 积压缓存安全上限(防止设备持续上报休眠导致内存无限增长)。 |
|
|
// 缓存安全上限(纯防 OOM 兜底):超限时丢弃最旧包并告警;单包约 165B,10000 包≈1.6MB。 |
|
|
// 1s 休眠约积压 16 包,留足余量;超限时丢弃最旧包并告警(仅在异常场景触发)。 |
|
|
|
|
|
// 要求绝对不丢包,故大幅抬高上限作为纯防 OOM 的兜底;单包约 165B,10000 包≈1.6MB。 |
|
|
|
|
|
private const val CHANNEL_HOLD_BUFFER_MAX = 10000 |
|
|
private const val CHANNEL_HOLD_BUFFER_MAX = 10000 |
|
|
// 左右积压同时可补发时的轮转标记,保证两声道公平补发、避免一方饿死。 |
|
|
// 左右同一拍都满足发送条件时的轮转标记,保证两声道公平、避免一方饿死。 |
|
|
private var holdDrainPreferLeft = true |
|
|
private var holdDrainPreferLeft = true |
|
|
|
|
|
// ---- F4/F5 解码空余字节数 → 下发档位阈值(带迟滞)---- |
|
|
|
|
|
// 设备解码缓存总量≈1840B、单包160B(≈11.5包),按包步进上报。边界取 160(单包)整数倍, |
|
|
|
|
|
// 正好落在相邻上报值中间,避免临界抖动。降档敏感(及早减速防溢出)、升档保守(回升够多才提速), |
|
|
|
|
|
// 中间留 2 包迟滞带,消除边界反复横跳。 |
|
|
|
|
|
// 阈值整体上调(补偿空余上报的网络延迟、留提前量):相对初始已上调 4 档(+640B≈4包),更早降速、更晚升速。 |
|
|
|
|
|
private const val DECODE_FREE_CRIT_DOWN = 1120 // 空余 ≤7包:紧急降到 320ms(8x) |
|
|
|
|
|
private const val DECODE_FREE_CRIT_UP = 1440 // 处于 320ms 时,回升到 ≥9包 才升回 160ms(4x) |
|
|
|
|
|
private const val DECODE_FREE_NORMAL_DOWN = 1920 // 处于 40ms 时,跌到 ≤12包 才降到 160ms(4x) |
|
|
|
|
|
private const val DECODE_FREE_NORMAL_UP = 2880 // 升到 ≥18包 才回 40ms(1x) 全速 |
|
|
// 音频数据缓冲区,用于累积数据到80字节再发送 |
|
|
// 音频数据缓冲区,用于累积数据到80字节再发送 |
|
|
private val audioBuffer = mutableListOf<Byte>() |
|
|
private val audioBuffer = mutableListOf<Byte>() |
|
|
// 重发机制相关常量 |
|
|
// 重发机制相关常量 |
|
|
@ -383,19 +384,11 @@ object BleService { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 通知声道通话休眠指令接收 |
|
|
* 通知设备上报某声道解码缓存空余字节数(F4=左 / F5=右),用于下行流控调速 |
|
|
*/ |
|
|
|
|
|
override fun notifyCallSleepReceived(channel: Int) { |
|
|
|
|
|
// 使用外部类的方法来处理声道通话休眠指令,避免无限递归 |
|
|
|
|
|
this@BleService.notifyCallSleepReceived(channel) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 通知声道通话恢复指令接收 |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
override fun notifyCallResumeReceived(channel: Int) { |
|
|
override fun notifyCallDecodeFreeReported(channel: Int, freeBytes: Int) { |
|
|
// 使用外部类的方法来处理声道通话恢复指令,避免无限递归 |
|
|
// 使用外部类的方法处理,避免无限递归 |
|
|
this@BleService.notifyCallResumeReceived(channel) |
|
|
this@BleService.onDecodeFreeReported(channel, freeBytes) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
@ -1038,9 +1031,9 @@ object BleService { |
|
|
} else { |
|
|
} else { |
|
|
Log.w(TAG, "通话音频服务特征未完整找到") |
|
|
Log.w(TAG, "通话音频服务特征未完整找到") |
|
|
} |
|
|
} |
|
|
// 在服务发现完成后,请求最大MTU |
|
|
// 服务发现完成后,请求高优先级连接参数(最短连接间隔),提升 BLE 双向吞吐、 |
|
|
//requestMaxMtu(g) |
|
|
// 缓解上下行互相挤占导致的收发失败。连接断开后系统自动复位,无需手动还原。 |
|
|
|
|
|
requestHighConnectionPriority(g) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) { |
|
|
override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) { |
|
|
@ -1306,6 +1299,22 @@ object BleService { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 请求高优先级连接参数(CONNECTION_PRIORITY_HIGH,最短连接间隔)。 |
|
|
|
|
|
* BLE 是半双工,上下行共享同一连接事件;下行音频写入过多会挤占设备上行 notify 的时隙, |
|
|
|
|
|
* 表现为"设备端发数据失败"。提高连接优先级可缩短连接间隔、增大双向带宽,缓解互相挤占。 |
|
|
|
|
|
* 代价是更耗电,适合实时音频通话场景;连接断开后系统自动复位,无需手动还原。 |
|
|
|
|
|
* 注意:此调用不占用 GATT 操作队列(走 HCI 连接参数更新),不会干扰正在进行的 CCCD 串行注册。 |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun requestHighConnectionPriority(gatt: BluetoothGatt) { |
|
|
|
|
|
try { |
|
|
|
|
|
val ok = gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH) |
|
|
|
|
|
Log.i(TAG, "请求高优先级连接(CONNECTION_PRIORITY_HIGH): ${if (ok) "已发起" else "失败"}") |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(TAG, "请求高优先级连接异常: ${e.message}", e) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 请求最大MTU值 |
|
|
* 请求最大MTU值 |
|
|
* @param gatt BluetoothGatt实例 |
|
|
* @param gatt BluetoothGatt实例 |
|
|
@ -1419,40 +1428,71 @@ object BleService { |
|
|
audioSendThread = Thread { |
|
|
audioSendThread = Thread { |
|
|
Log.i(TAG, "音频发送线程已启动") |
|
|
Log.i(TAG, "音频发送线程已启动") |
|
|
|
|
|
|
|
|
// 固定节拍的下一拍绝对时刻(ms)。每发完一包就 += 间隔,按这个时刻补偿性 sleep, |
|
|
// 固定 40ms 一拍的发送节拍。每一拍做一次"发不发/发哪路"的决策;左右各按自身档位(paceTicks) |
|
|
// 把 poll 等待和发送耗时一起抵消掉,保证实际发送周期稳定贴近 audioSendIntervalNormal。 |
|
|
// 控制频率(40/80/160ms),定时器始终稳定在 audioSendIntervalNormal 一拍,下发间隔不随 poll/发送耗时抖动。 |
|
|
// 0 表示尚未起拍,由首包发送时初始化。 |
|
|
var nextTickAt = System.currentTimeMillis() |
|
|
var nextSendAt = 0L |
|
|
var tick = 0L |
|
|
|
|
|
|
|
|
while (isAudioSending.get() && !Thread.currentThread().isInterrupted) { |
|
|
while (isAudioSending.get() && !Thread.currentThread().isInterrupted) { |
|
|
try { |
|
|
try { |
|
|
// 1) 从主队列取新包,按声道分流到积压缓存。 |
|
|
// 1) 非阻塞抽干主队列,按声道分流到左右缓存(不在热路径阻塞,保证节拍稳定)。 |
|
|
// 合包布局:[4B 序号(大端)] + [1B 声道(0=左/1=右)] + [N × opus] |
|
|
// 合包布局:[4B 序号(小端)] + [1B 声道(0=左/1=右)] + [N × opus] |
|
|
// 阻塞至多 20ms 等待新数据,避免空转;随后把队列里已到达的剩余包一次性分流。 |
|
|
|
|
|
audioSendQueue.poll(20, TimeUnit.MILLISECONDS)?.let { first -> |
|
|
|
|
|
routeToHoldBuffer(first) |
|
|
|
|
|
while (true) { |
|
|
while (true) { |
|
|
val more = audioSendQueue.poll() ?: break |
|
|
val more = audioSendQueue.poll() ?: break |
|
|
routeToHoldBuffer(more) |
|
|
routeToHoldBuffer(more) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 2) 选出一包可发送的数据:声道未休眠且缓存非空。 |
|
|
// 2) 本拍最多发一包:左右各按自身档位(paceTicks)判断是否到点;两路都到点时按 holdDrainPreferLeft |
|
|
// 左右都可发时按 holdDrainPreferLeft 轮转,保证两声道公平补发、避免一方饿死。 |
|
|
// 轮转二选一,另一路顺延到下一拍(即"一拍一包、左右交替"的下发方式)。 |
|
|
val leftReady = leftHoldBuffer.isNotEmpty() && !leftChannelSleeping |
|
|
val leftDue = leftHoldBuffer.isNotEmpty() && |
|
|
val rightReady = rightHoldBuffer.isNotEmpty() && !rightChannelSleeping |
|
|
(lastLeftSendTick < 0 || tick - lastLeftSendTick >= leftPaceTicks) |
|
|
val audioChunk: ByteArray? = when { |
|
|
val rightDue = rightHoldBuffer.isNotEmpty() && |
|
|
leftReady && rightReady -> { |
|
|
(lastRightSendTick < 0 || tick - lastRightSendTick >= rightPaceTicks) |
|
|
holdDrainPreferLeft = !holdDrainPreferLeft |
|
|
val pickLeft: Boolean? = when { |
|
|
if (holdDrainPreferLeft) leftHoldBuffer.removeFirst() |
|
|
leftDue && rightDue -> { holdDrainPreferLeft = !holdDrainPreferLeft; holdDrainPreferLeft } |
|
|
else rightHoldBuffer.removeFirst() |
|
|
leftDue -> true |
|
|
|
|
|
rightDue -> false |
|
|
|
|
|
else -> null |
|
|
|
|
|
} |
|
|
|
|
|
when (pickLeft) { |
|
|
|
|
|
true -> if (trySendChannelChunk(true)) lastLeftSendTick = tick |
|
|
|
|
|
false -> if (trySendChannelChunk(false)) lastRightSendTick = tick |
|
|
|
|
|
null -> {} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 3) 固定 40ms 节拍:无论本拍是否发送都睡到下一拍绝对时刻,保证下发节拍稳定、不随耗时抖动。 |
|
|
|
|
|
nextTickAt += audioSendIntervalNormal |
|
|
|
|
|
tick++ |
|
|
|
|
|
val sleepMs = nextTickAt - System.currentTimeMillis() |
|
|
|
|
|
if (sleepMs > 0) { |
|
|
|
|
|
Thread.sleep(sleepMs) |
|
|
|
|
|
} else { |
|
|
|
|
|
// 落后于节拍(发送/分流超时),重新对齐,避免之后疯狂追发 |
|
|
|
|
|
nextTickAt = System.currentTimeMillis() |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e: InterruptedException) { |
|
|
|
|
|
Log.d(TAG, "音频发送线程被中断") |
|
|
|
|
|
break |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(TAG, "音频发送线程异常: ${e.message}", e) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
Log.i(TAG, "音频发送线程已停止") |
|
|
|
|
|
}.apply { |
|
|
|
|
|
name = "AudioSendThread" |
|
|
|
|
|
start() |
|
|
} |
|
|
} |
|
|
leftReady -> leftHoldBuffer.removeFirst() |
|
|
|
|
|
rightReady -> rightHoldBuffer.removeFirst() |
|
|
|
|
|
else -> null // 两声道都在休眠或都无积压,本轮不发送 |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (audioChunk != null) { |
|
|
/** |
|
|
|
|
|
* 从指定声道缓存取队首一包下发,返回是否成功写入。仅在音频发送线程内调用。 |
|
|
|
|
|
* - 成功:已塞入底层发送缓冲,写调试录音并打印发送日志(Δ/包序/档位),返回 true; |
|
|
|
|
|
* - 失败(拥塞):原包放回队头、记一次拥塞告警,返回 false,调用方据此不更新该声道发送拍号、下一拍重试,绝不丢包。 |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun trySendChannelChunk(isLeft: Boolean): Boolean { |
|
|
|
|
|
val buffer = if (isLeft) leftHoldBuffer else rightHoldBuffer |
|
|
|
|
|
if (buffer.isEmpty()) return false |
|
|
|
|
|
val audioChunk = buffer.removeFirst() |
|
|
// 解析包头:声道(byte[4]) 与 序号(byte[0..3] 小端),供日志与重试归位使用 |
|
|
// 解析包头:声道(byte[4]) 与 序号(byte[0..3] 小端),供日志与重试归位使用 |
|
|
val channelByte = if (audioChunk.size >= 5) audioChunk[4] else null |
|
|
val channelByte = if (audioChunk.size >= 5) audioChunk[4] else null |
|
|
val seq = if (audioChunk.size >= 5) |
|
|
val seq = if (audioChunk.size >= 5) |
|
|
@ -1469,22 +1509,16 @@ object BleService { |
|
|
|
|
|
|
|
|
// 同步写入,拿到真实的成功/失败:false=底层发送缓冲已满(拥塞),作为背压信号 |
|
|
// 同步写入,拿到真实的成功/失败:false=底层发送缓冲已满(拥塞),作为背压信号 |
|
|
val sent = sendAudioChunkBlocking(audioChunk) |
|
|
val sent = sendAudioChunkBlocking(audioChunk) |
|
|
|
|
|
|
|
|
if (!sent) { |
|
|
if (!sent) { |
|
|
// 写失败:把这一包放回原声道队头,下轮重试,绝不丢弃(满足“绝对不丢包”)。 |
|
|
// 写失败:放回原声道队头,下一拍重试,绝不丢弃、声道内顺序不变 |
|
|
// 包仍在队头,声道内顺序不变。 |
|
|
buffer.addFirst(audioChunk) |
|
|
if (channelByte == OpusAudioManager.CHANNEL_RIGHT) rightHoldBuffer.addFirst(audioChunk) |
|
|
|
|
|
else leftHoldBuffer.addFirst(audioChunk) |
|
|
|
|
|
writeFailRetryCount++ |
|
|
writeFailRetryCount++ |
|
|
// 节流告警,避免拥塞期刷屏 |
|
|
|
|
|
if (writeFailRetryCount == 1 || writeFailRetryCount % 50 == 0) { |
|
|
if (writeFailRetryCount == 1 || writeFailRetryCount % 50 == 0) { |
|
|
Log.w(TAG, "下行写入拥塞,重试中 ch=$chTag seq=$seq 连续失败=$writeFailRetryCount " + |
|
|
Log.w(TAG, "下行写入拥塞,重试中 ch=$chTag seq=$seq 连续失败=$writeFailRetryCount " + |
|
|
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size})") |
|
|
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size})") |
|
|
} |
|
|
} |
|
|
// 退避,给底层缓冲腾空时间;重置节拍,待恢复后重新起拍 |
|
|
return false |
|
|
nextSendAt = 0L |
|
|
} |
|
|
Thread.sleep(WRITE_RETRY_BACKOFF_MS) |
|
|
|
|
|
} else { |
|
|
|
|
|
if (writeFailRetryCount > 0) { |
|
|
if (writeFailRetryCount > 0) { |
|
|
Log.i(TAG, "下行写入已恢复,之前连续失败=$writeFailRetryCount") |
|
|
Log.i(TAG, "下行写入已恢复,之前连续失败=$writeFailRetryCount") |
|
|
writeFailRetryCount = 0 |
|
|
writeFailRetryCount = 0 |
|
|
@ -1514,7 +1548,7 @@ object BleService { |
|
|
} |
|
|
} |
|
|
val gapTag = if (seqGap == 1L) "" else " !gap=$seqGap" |
|
|
val gapTag = if (seqGap == 1L) "" else " !gap=$seqGap" |
|
|
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq Δ=${deltaMs}ms size=${audioChunk.size}B " + |
|
|
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq Δ=${deltaMs}ms size=${audioChunk.size}B " + |
|
|
"lastSeq(L=$lastLeftSeq,R=$lastRightSeq)$gapTag " + |
|
|
"lastSeq(L=$lastLeftSeq,R=$lastRightSeq)$gapTag pace(L=${leftPaceTicks}x,R=${rightPaceTicks}x) " + |
|
|
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size}) queue=${audioSendQueue.size}") |
|
|
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size}) queue=${audioSendQueue.size}") |
|
|
|
|
|
|
|
|
// [LAT-TRACE] 点3:右声道(对方译音)首包写到 BLE 耳机(每句首包)。 |
|
|
// [LAT-TRACE] 点3:右声道(对方译音)首包写到 BLE 耳机(每句首包)。 |
|
|
@ -1526,38 +1560,12 @@ object BleService { |
|
|
latLastBleRightTs = now |
|
|
latLastBleRightTs = now |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return true |
|
|
// 固定节拍:睡到 nextSendAt 这个绝对时刻,把 poll/发送耗时算进同一周期,间隔不随它们抖动。 |
|
|
|
|
|
val nowMs = System.currentTimeMillis() |
|
|
|
|
|
if (nextSendAt == 0L) nextSendAt = nowMs |
|
|
|
|
|
nextSendAt += audioSendIntervalNormal |
|
|
|
|
|
val sleepMs = nextSendAt - nowMs |
|
|
|
|
|
if (sleepMs > 0) { |
|
|
|
|
|
Thread.sleep(sleepMs) |
|
|
|
|
|
} else { |
|
|
|
|
|
// 落后于节拍(发送/积压处理超时),重新对齐,避免之后疯狂追发 |
|
|
|
|
|
nextSendAt = System.currentTimeMillis() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e: InterruptedException) { |
|
|
|
|
|
Log.d(TAG, "音频发送线程被中断") |
|
|
|
|
|
break |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(TAG, "音频发送线程异常: ${e.message}", e) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
Log.i(TAG, "音频发送线程已停止") |
|
|
|
|
|
}.apply { |
|
|
|
|
|
name = "AudioSendThread" |
|
|
|
|
|
start() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 将一包数据按其声道标记分流到对应的积压缓存。 |
|
|
* 将一包数据按其声道标记分流到对应的下行缓存,由发送线程按各自档位取出下发。 |
|
|
* 休眠声道的包不会丢弃,先入缓存等唤醒后补发;非休眠声道的包同样入缓存,由发送线程统一按节拍取出。 |
|
|
* 仅作为安全阀:当某声道缓存超过 CHANNEL_HOLD_BUFFER_MAX 时丢弃最旧包并告警(正常流控不会触发)。 |
|
|
* 仅作为安全阀:当某声道缓存超过 CHANNEL_HOLD_BUFFER_MAX 时丢弃最旧包并告警(正常 1s 休眠不会触发)。 |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
private fun routeToHoldBuffer(chunk: ByteArray) { |
|
|
private fun routeToHoldBuffer(chunk: ByteArray) { |
|
|
val channelTag = if (chunk.size >= 5) chunk[4] else null |
|
|
val channelTag = if (chunk.size >= 5) chunk[4] else null |
|
|
@ -1581,10 +1589,13 @@ object BleService { |
|
|
audioSendThread?.interrupt() |
|
|
audioSendThread?.interrupt() |
|
|
audioSendQueue.clear() |
|
|
audioSendQueue.clear() |
|
|
audioBuffer.clear() // 清空音频缓冲区 |
|
|
audioBuffer.clear() // 清空音频缓冲区 |
|
|
leftHoldBuffer.clear() // 清空左声道积压缓存 |
|
|
leftHoldBuffer.clear() // 清空左声道下行缓存 |
|
|
rightHoldBuffer.clear() // 清空右声道积压缓存 |
|
|
rightHoldBuffer.clear() // 清空右声道下行缓存 |
|
|
leftChannelSleeping = false |
|
|
leftPaceTicks = 1 // 档位复位为全速 40ms |
|
|
rightChannelSleeping = false |
|
|
rightPaceTicks = 1 |
|
|
|
|
|
lastLeftSendTick = -1L // 重置左右声道发送拍号 |
|
|
|
|
|
lastRightSendTick = -1L |
|
|
|
|
|
holdDrainPreferLeft = true |
|
|
lastAudioSendTime = -1L // 重置发送节拍计时,下次首包 Δ 从 0 开始 |
|
|
lastAudioSendTime = -1L // 重置发送节拍计时,下次首包 Δ 从 0 开始 |
|
|
lastLeftSeq = -1L // 重置左右声道包序追踪 |
|
|
lastLeftSeq = -1L // 重置左右声道包序追踪 |
|
|
lastRightSeq = -1L |
|
|
lastRightSeq = -1L |
|
|
@ -2184,58 +2195,44 @@ object BleService { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 收到设备主动上报的左/右声道通话休眠 (F0/F1) 时调用。 |
|
|
* 收到设备主动上报的某声道解码缓存空余字节数 (F4=左 / F5=右) 时调用,用于下行流控。 |
|
|
* 将对应声道置为休眠:该声道的包转入积压缓存暂不发送、仅传输另一声道, |
|
|
* 空余越少说明设备解码越来不及,则把该声道下发节拍降速(40→80→160ms),缓解解码缓存溢出丢包; |
|
|
* 直到收到对应的恢复指令 (F2/F3) 才恢复并按序补发,不丢包。 |
|
|
* 空余恢复后再升回全速。仅更新对应声道档位,由发送线程在下一拍按新档位执行。 |
|
|
* 重复触发幂等,不会叠加。 |
|
|
|
|
|
* @param channel BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右) |
|
|
* @param channel BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右) |
|
|
|
|
|
* @param freeBytes 该声道解码缓存空余字节数 |
|
|
*/ |
|
|
*/ |
|
|
private fun notifyCallSleepReceived(channel: Int) { |
|
|
private fun onDecodeFreeReported(channel: Int, freeBytes: Int) { |
|
|
when (channel) { |
|
|
when (channel) { |
|
|
BleConst.AUDIO_CHANNEL_LEFT -> { |
|
|
BleConst.AUDIO_CHANNEL_LEFT -> { |
|
|
leftChannelSleeping = true |
|
|
val pace = nextPaceTicks(leftPaceTicks, freeBytes) |
|
|
Log.i(TAG, "收到左声道休眠指令(F0),暂停左声道下发,等待恢复指令(F2)") |
|
|
if (leftPaceTicks != pace) { |
|
|
|
|
|
Log.i(TAG, "左声道解码空余=$freeBytes 字节,下发档位 ${leftPaceTicks}x→${pace}x (${pace * audioSendIntervalNormal}ms/包)") |
|
|
|
|
|
} |
|
|
|
|
|
leftPaceTicks = pace |
|
|
} |
|
|
} |
|
|
BleConst.AUDIO_CHANNEL_RIGHT -> { |
|
|
BleConst.AUDIO_CHANNEL_RIGHT -> { |
|
|
rightChannelSleeping = true |
|
|
val pace = nextPaceTicks(rightPaceTicks, freeBytes) |
|
|
Log.i(TAG, "收到右声道休眠指令(F1),暂停右声道下发,等待恢复指令(F3)") |
|
|
if (rightPaceTicks != pace) { |
|
|
|
|
|
Log.i(TAG, "右声道解码空余=$freeBytes 字节,下发档位 ${rightPaceTicks}x→${pace}x (${pace * audioSendIntervalNormal}ms/包)") |
|
|
} |
|
|
} |
|
|
else -> Log.w(TAG, "收到未知声道休眠指令: channel=$channel") |
|
|
rightPaceTicks = pace |
|
|
} |
|
|
|
|
|
for (callback in callbacks) { |
|
|
|
|
|
try { |
|
|
|
|
|
callback.onCallSleepReceived(channel) |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(TAG, "分发声道通话休眠回调异常", e) |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
else -> Log.w(TAG, "收到未知声道解码空余上报: channel=$channel") |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 收到设备主动上报的左/右声道通话恢复 (F2/F3) 时调用。 |
|
|
* 带迟滞的三档流控:由当前档位 currentPace 与最新空余 freeBytes 决定新档位(拍)。三档=拍数 1/4/8 → 40/160/320ms/包。 |
|
|
* 解除对应声道的休眠标记,发送线程随即按 FIFO 顺序补发休眠期间积压的该声道包,不丢包。 |
|
|
* 降档敏感、升档保守,中间区按当前档位保持,避免边界反复横跳: |
|
|
* 未处于休眠时收到恢复指令为幂等操作。 |
|
|
* 空余 ≤CRIT_DOWN → 8(320ms);≥NORMAL_UP → 1(40ms); |
|
|
* @param channel BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右) |
|
|
* 中间区:40ms 档跌破 NORMAL_DOWN 才降 160ms;320ms 档升过 CRIT_UP 才回 160ms;160ms 档维持。 |
|
|
*/ |
|
|
*/ |
|
|
private fun notifyCallResumeReceived(channel: Int) { |
|
|
private fun nextPaceTicks(currentPace: Int, freeBytes: Int): Int = when { |
|
|
when (channel) { |
|
|
freeBytes <= DECODE_FREE_CRIT_DOWN -> 8 |
|
|
BleConst.AUDIO_CHANNEL_LEFT -> { |
|
|
freeBytes >= DECODE_FREE_NORMAL_UP -> 1 |
|
|
leftChannelSleeping = false |
|
|
currentPace == 8 -> if (freeBytes >= DECODE_FREE_CRIT_UP) 4 else 8 |
|
|
Log.i(TAG, "收到左声道恢复指令(F2),恢复左声道下发,补发积压 ${leftHoldBuffer.size} 包") |
|
|
currentPace == 1 -> if (freeBytes <= DECODE_FREE_NORMAL_DOWN) 4 else 1 |
|
|
} |
|
|
else -> 4 |
|
|
BleConst.AUDIO_CHANNEL_RIGHT -> { |
|
|
|
|
|
rightChannelSleeping = false |
|
|
|
|
|
Log.i(TAG, "收到右声道恢复指令(F3),恢复右声道下发,补发积压 ${rightHoldBuffer.size} 包") |
|
|
|
|
|
} |
|
|
|
|
|
else -> Log.w(TAG, "收到未知声道恢复指令: channel=$channel") |
|
|
|
|
|
} |
|
|
|
|
|
for (callback in callbacks) { |
|
|
|
|
|
try { |
|
|
|
|
|
callback.onCallResumeReceived(channel) |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(TAG, "分发声道通话恢复回调异常", e) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
|