|
|
|
@ -71,6 +71,9 @@ object BleService { |
|
|
|
|
|
|
|
// 升级唤醒词失败相关回调 |
|
|
|
fun onWakeupVoiceUpgradeFailed() |
|
|
|
|
|
|
|
// 声道通话休眠相关回调 |
|
|
|
fun onCallSleepReceived(channel: Int) |
|
|
|
} |
|
|
|
|
|
|
|
// ====================================================================================================== |
|
|
|
@ -141,10 +144,23 @@ object BleService { |
|
|
|
// 音频数据分块发送的常量 |
|
|
|
private val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节 |
|
|
|
private const val AUDIO_SEND_INTERVAL_NORMAL = 60L // 正常发送间隔60ms |
|
|
|
private const val AUDIO_SEND_INTERVAL_SLEEP = 1000L // 收到 CODEC_CONTROL_CALL_Sleep 后插入的单次暂停 |
|
|
|
// 收到 CODEC_CONTROL_CALL_Sleep 时置 true,下一次发包前会消费此标记并 sleep 一次更长的间隔 |
|
|
|
private const val CHANNEL_SLEEP_DURATION = 400L // 收到 F0/F1 后该声道固定休眠 1s,到期自动恢复 |
|
|
|
// 左/右声道休眠到期时间戳(SystemClock.elapsedRealtime 基准,0 表示未休眠)。 |
|
|
|
// 收到 F0(左)/F1(右) 时置为 now + CHANNEL_SLEEP_DURATION, |
|
|
|
// 休眠窗口内该声道的包暂不发送(转入积压缓存),仅传输另一声道,到期后按序补发,不丢包。 |
|
|
|
@Volatile |
|
|
|
private var leftChannelSleepUntil: Long = 0L |
|
|
|
@Volatile |
|
|
|
private var pendingCallSleep: Boolean = false |
|
|
|
private var rightChannelSleepUntil: Long = 0L |
|
|
|
// 声道休眠期间该声道的包不丢弃,转入对应积压缓存,待声道唤醒后按 FIFO 顺序补发。 |
|
|
|
// 仅在音频发送线程内访问,无需额外加锁。 |
|
|
|
private val leftHoldBuffer = ArrayDeque<ByteArray>() |
|
|
|
private val rightHoldBuffer = ArrayDeque<ByteArray>() |
|
|
|
// 积压缓存安全上限(防止设备持续上报休眠导致内存无限增长)。 |
|
|
|
// 1s 休眠约积压 16 包,留足余量;超限时丢弃最旧包并告警(仅在异常场景触发)。 |
|
|
|
private const val CHANNEL_HOLD_BUFFER_MAX = 200 |
|
|
|
// 左右积压同时可补发时的轮转标记,保证两声道公平补发、避免一方饿死。 |
|
|
|
private var holdDrainPreferLeft = true |
|
|
|
// 音频数据缓冲区,用于累积数据到80字节再发送 |
|
|
|
private val audioBuffer = mutableListOf<Byte>() |
|
|
|
// 重发机制相关常量 |
|
|
|
@ -349,11 +365,11 @@ object BleService { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 通知通话休眠指令接收 |
|
|
|
* 通知声道通话休眠指令接收 |
|
|
|
*/ |
|
|
|
override fun notifyCallSleepReceived() { |
|
|
|
// 使用外部类的方法来分发通话休眠指令,避免无限递归 |
|
|
|
this@BleService.notifyCallSleepReceived() |
|
|
|
override fun notifyCallSleepReceived(channel: Int) { |
|
|
|
// 使用外部类的方法来处理声道通话休眠指令,避免无限递归 |
|
|
|
this@BleService.notifyCallSleepReceived(channel) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@ -1350,11 +1366,34 @@ object BleService { |
|
|
|
|
|
|
|
while (isAudioSending.get() && !Thread.currentThread().isInterrupted) { |
|
|
|
try { |
|
|
|
// 从队列中取出音频数据块 |
|
|
|
val audioChunk = audioSendQueue.poll(20, TimeUnit.MILLISECONDS) |
|
|
|
// 1) 从主队列取新包,按声道分流到积压缓存。 |
|
|
|
// 合包布局:[4B 序号(大端)] + [1B 声道(0=左/1=右)] + [N × opus] |
|
|
|
// 阻塞至多 20ms 等待新数据,避免空转;随后把队列里已到达的剩余包一次性分流。 |
|
|
|
audioSendQueue.poll(20, TimeUnit.MILLISECONDS)?.let { first -> |
|
|
|
routeToHoldBuffer(first) |
|
|
|
while (true) { |
|
|
|
val more = audioSendQueue.poll() ?: break |
|
|
|
routeToHoldBuffer(more) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 2) 选出一包可发送的数据:声道已唤醒(不在休眠窗口)且缓存非空。 |
|
|
|
// 左右都可发时按 holdDrainPreferLeft 轮转,保证两声道公平补发、避免一方饿死。 |
|
|
|
val now = SystemClock.elapsedRealtime() |
|
|
|
val leftReady = leftHoldBuffer.isNotEmpty() && now >= leftChannelSleepUntil |
|
|
|
val rightReady = rightHoldBuffer.isNotEmpty() && now >= rightChannelSleepUntil |
|
|
|
val audioChunk: ByteArray? = when { |
|
|
|
leftReady && rightReady -> { |
|
|
|
holdDrainPreferLeft = !holdDrainPreferLeft |
|
|
|
if (holdDrainPreferLeft) leftHoldBuffer.removeFirst() |
|
|
|
else rightHoldBuffer.removeFirst() |
|
|
|
} |
|
|
|
leftReady -> leftHoldBuffer.removeFirst() |
|
|
|
rightReady -> rightHoldBuffer.removeFirst() |
|
|
|
else -> null // 两声道都在休眠或都无积压,本轮不发送 |
|
|
|
} |
|
|
|
|
|
|
|
if (audioChunk != null) { |
|
|
|
// 合包布局:[4B 序号(大端)] + [1B 声道(0=左/1=右)] + [N × opus] |
|
|
|
// 调试录音文件只保留拼接后的纯 opus 数据,剥离 5 字节包头再写入 |
|
|
|
if (audioChunk.size > 5) { |
|
|
|
val payload = audioChunk.copyOfRange(5, audioChunk.size) |
|
|
|
@ -1371,16 +1410,11 @@ object BleService { |
|
|
|
OpusAudioManager.CHANNEL_RIGHT -> "R" |
|
|
|
else -> "?(${audioChunk[4]})" |
|
|
|
} |
|
|
|
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq size=${audioChunk.size}B queue=${audioSendQueue.size}") |
|
|
|
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq size=${audioChunk.size}B " + |
|
|
|
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size}) queue=${audioSendQueue.size}") |
|
|
|
} |
|
|
|
sendAudioChunkWithCallback(audioChunk) |
|
|
|
// 若刚收到通话休眠主动上报,则把下一次间隔替换为 SLEEP 间隔, |
|
|
|
// 消费后立即清标记,确保只插入一次较长暂停 |
|
|
|
val intervalToUse = if (pendingCallSleep) { |
|
|
|
pendingCallSleep = false |
|
|
|
AUDIO_SEND_INTERVAL_SLEEP |
|
|
|
} else AUDIO_SEND_INTERVAL_NORMAL |
|
|
|
Thread.sleep(intervalToUse) |
|
|
|
Thread.sleep(AUDIO_SEND_INTERVAL_NORMAL) |
|
|
|
} |
|
|
|
} catch (e: InterruptedException) { |
|
|
|
Log.d(TAG, "音频发送线程被中断") |
|
|
|
@ -1396,6 +1430,25 @@ object BleService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 将一包数据按其声道标记分流到对应的积压缓存。 |
|
|
|
* 休眠声道的包不会丢弃,先入缓存等唤醒后补发;非休眠声道的包同样入缓存,由发送线程统一按节拍取出。 |
|
|
|
* 仅作为安全阀:当某声道缓存超过 CHANNEL_HOLD_BUFFER_MAX 时丢弃最旧包并告警(正常 1s 休眠不会触发)。 |
|
|
|
*/ |
|
|
|
private fun routeToHoldBuffer(chunk: ByteArray) { |
|
|
|
val channelTag = if (chunk.size >= 5) chunk[4] else null |
|
|
|
val buffer = when (channelTag) { |
|
|
|
OpusAudioManager.CHANNEL_RIGHT -> rightHoldBuffer |
|
|
|
// 左声道及无有效声道标记(异常包)统一归入左缓存处理 |
|
|
|
else -> leftHoldBuffer |
|
|
|
} |
|
|
|
if (buffer.size >= CHANNEL_HOLD_BUFFER_MAX) { |
|
|
|
buffer.removeFirst() |
|
|
|
Log.w(TAG, "声道积压缓存超上限($CHANNEL_HOLD_BUFFER_MAX),丢弃最旧包 ch=${channelTag}") |
|
|
|
} |
|
|
|
buffer.addLast(chunk) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 停止音频数据发送线程 |
|
|
|
*/ |
|
|
|
@ -1404,6 +1457,10 @@ object BleService { |
|
|
|
audioSendThread?.interrupt() |
|
|
|
audioSendQueue.clear() |
|
|
|
audioBuffer.clear() // 清空音频缓冲区 |
|
|
|
leftHoldBuffer.clear() // 清空左声道积压缓存 |
|
|
|
rightHoldBuffer.clear() // 清空右声道积压缓存 |
|
|
|
leftChannelSleepUntil = 0L |
|
|
|
rightChannelSleepUntil = 0L |
|
|
|
Log.i(TAG, "音频发送线程已停止,队列和缓冲区已清空") |
|
|
|
} |
|
|
|
|
|
|
|
@ -1939,13 +1996,32 @@ object BleService { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 收到设备主动上报的 CODEC_CONTROL_CALL_Sleep 时调用。 |
|
|
|
* 仅设置一个一次性标记,由音频发送线程在下一次发包时消费并执行一次更长的暂停, |
|
|
|
* 之后自动回到正常 60ms 节拍。多次连续触发会折叠为单次暂停(满足"已在休眠,无需叠加")。 |
|
|
|
* 收到设备主动上报的左/右声道通话休眠 (F0/F1) 时调用。 |
|
|
|
* 将对应声道的休眠到期时间置为 now + CHANNEL_SLEEP_DURATION(1s), |
|
|
|
* 休眠窗口内该声道的包转入积压缓存暂不发送、仅传输另一声道,到期后按序补发,不丢包。 |
|
|
|
* 重复触发只是刷新到期时间,不会叠加。 |
|
|
|
* @param channel BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右) |
|
|
|
*/ |
|
|
|
private fun notifyCallSleepReceived() { |
|
|
|
pendingCallSleep = true |
|
|
|
Log.i(TAG, "收到通话休眠指令,下一帧发送间隔将切到 ${AUDIO_SEND_INTERVAL_SLEEP}ms") |
|
|
|
private fun notifyCallSleepReceived(channel: Int) { |
|
|
|
val until = SystemClock.elapsedRealtime() + CHANNEL_SLEEP_DURATION |
|
|
|
when (channel) { |
|
|
|
BleConst.AUDIO_CHANNEL_LEFT -> { |
|
|
|
leftChannelSleepUntil = until |
|
|
|
Log.i(TAG, "收到左声道休眠指令,暂停左声道下发 ${CHANNEL_SLEEP_DURATION}ms") |
|
|
|
} |
|
|
|
BleConst.AUDIO_CHANNEL_RIGHT -> { |
|
|
|
rightChannelSleepUntil = until |
|
|
|
Log.i(TAG, "收到右声道休眠指令,暂停右声道下发 ${CHANNEL_SLEEP_DURATION}ms") |
|
|
|
} |
|
|
|
else -> Log.w(TAG, "收到未知声道休眠指令: channel=$channel") |
|
|
|
} |
|
|
|
for (callback in callbacks) { |
|
|
|
try { |
|
|
|
callback.onCallSleepReceived(channel) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "分发声道通话休眠回调异常", e) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
|