|
|
@ -100,28 +100,58 @@ public class BleService: NSObject { |
|
|
private var replyTimeoutTimer: Timer? |
|
|
private var replyTimeoutTimer: Timer? |
|
|
private let REPLY_TIMEOUT_MS: TimeInterval = 1.0 |
|
|
private let REPLY_TIMEOUT_MS: TimeInterval = 1.0 |
|
|
|
|
|
|
|
|
|
|
|
/// 接收帧 CRC 校验开关(调试期屏蔽):false = 校验失败只告警不丢帧,改回 true 恢复强校验。 |
|
|
|
|
|
/// 仅作用于接收方向;发往设备的命令帧仍照常附带 CRC。 |
|
|
|
|
|
private let ENABLE_RX_CRC_CHECK = false |
|
|
|
|
|
|
|
|
// 新增:音频配置参数 |
|
|
// 新增:音频配置参数 |
|
|
private var sampleRate: Int32 = 16000 |
|
|
private var sampleRate: Int32 = 16000 |
|
|
private var channels: Int32 = 1 |
|
|
private var channels: Int32 = 1 |
|
|
private var frameSize: Int32 = 640 |
|
|
private var frameSize: Int32 = 640 |
|
|
|
|
|
|
|
|
// MARK: - 音频数据发送相关属性 |
|
|
// MARK: - 音频下行发送(与 Android BleService 对齐) |
|
|
private var audioBuffer = [UInt8]() // 音频数据缓冲区 |
|
|
// 左右声道各一条缓存队列 + 一条独立发送定时器,右声道错开 RIGHT_SEND_STAGGER 启动,避免两路同时抢 BLE 写。 |
|
|
private var audioSendQueue = DispatchQueue(label: "com.yunqiinnovation.ble_service.audio_send", qos: .userInitiated) |
|
|
// 队列元素是单帧 opus(40B);发送线程每拍取最多 BUNDLE_FRAME_COUNT 帧,不足用静音帧补齐后组包下发。 |
|
|
private var audioDataQueue = [Data]() // 音频数据发送队列 |
|
|
private var leftSendBuffer = [Data]() |
|
|
private var isAudioSending = false // 音频发送状态 |
|
|
private var rightSendBuffer = [Data]() |
|
|
private var audioSendTimer: Timer? // 音频发送定时器 |
|
|
private let sendBufferLock = NSLock() |
|
|
|
|
|
private var leftSendTimer: DispatchSourceTimer? |
|
|
// 音频发送配置常量 |
|
|
private var rightSendTimer: DispatchSourceTimer? |
|
|
private let AUDIO_CHUNK_SIZE = 120 // 每次发送120字节 |
|
|
private let leftSendQueue = DispatchQueue(label: "com.yunqiinnovation.ble_service.audio_send.left", qos: .userInitiated) |
|
|
private let AUDIO_SEND_INTERVAL: TimeInterval = 0.06 // 60毫秒间隔 |
|
|
private let rightSendQueue = DispatchQueue(label: "com.yunqiinnovation.ble_service.audio_send.right", qos: .userInitiated) |
|
|
// 收到 CODEC_CONTROL_CALL_Sleep 时插入的单次暂停时长(单位:秒) |
|
|
/// 两路发送线程写同一个 callWriteCharacteristic + 同一 peripheral,写入必须串行化 |
|
|
private let AUDIO_SEND_SLEEP_PAUSE: TimeInterval = 1.0 |
|
|
private let bleWriteLock = NSLock() |
|
|
// 在该时间点之前 processAudioSendQueue 不消费队列,实现"插入一次 1000ms 暂停" |
|
|
/// 以下几个状态跨「左发送队列 / 右发送队列 / 编码回调线程 / 调用线程」共享, |
|
|
private var nextSendEarliestAt: Date? |
|
|
/// 对应 Android 的 @Volatile 字段,统一用 sendStateLock 保护。 |
|
|
private let nextSendEarliestLock = NSLock() |
|
|
private let sendStateLock = NSLock() |
|
|
|
|
|
private var _isAudioSending = false |
|
|
private let audioQueueLock = NSLock() // 队列操作锁 |
|
|
/// 补齐用静音 opus 帧模板(编码器启动后异步送来);未就绪时为 nil,此时只发真实帧 |
|
|
|
|
|
private var _silenceFrame: Data? |
|
|
|
|
|
/// 下行写入连续失败(底层发送缓冲拥塞)的计数,写成功即清零 |
|
|
|
|
|
private var _writeFailRetryCount = 0 |
|
|
|
|
|
/// 上一包音频实际发送的时间戳,仅用于发送节拍日志统计间隔;-1 表示尚未发送过 |
|
|
|
|
|
private var _lastAudioSendTime: TimeInterval = -1 |
|
|
|
|
|
|
|
|
|
|
|
/// 左/右声道各自的下行包序号(每成功发一包自增),供设备侧核对连续性。 |
|
|
|
|
|
/// 各自只被对应声道的发送队列访问,无需加锁。 |
|
|
|
|
|
private var leftPacketSeq: UInt32 = 0 |
|
|
|
|
|
private var rightPacketSeq: UInt32 = 0 |
|
|
|
|
|
|
|
|
|
|
|
private var isAudioSending: Bool { |
|
|
|
|
|
get { sendStateLock.lock(); defer { sendStateLock.unlock() }; return _isAudioSending } |
|
|
|
|
|
set { sendStateLock.lock(); _isAudioSending = newValue; sendStateLock.unlock() } |
|
|
|
|
|
} |
|
|
|
|
|
private var silenceFrame: Data? { |
|
|
|
|
|
get { sendStateLock.lock(); defer { sendStateLock.unlock() }; return _silenceFrame } |
|
|
|
|
|
set { sendStateLock.lock(); _silenceFrame = newValue; sendStateLock.unlock() } |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 音频发送配置常量(与 Android 默认值对齐) |
|
|
|
|
|
private let AUDIO_SEND_INTERVAL_NORMAL: TimeInterval = 0.08 // 音频下行发送间隔 80ms/包 |
|
|
|
|
|
private let RIGHT_SEND_STAGGER: TimeInterval = 0.04 // 右声道相对左声道错开 40ms 启动 |
|
|
|
|
|
private let BUNDLE_FRAME_COUNT = 5 // 每包合并的 opus 帧数 |
|
|
|
|
|
private let BUNDLE_HEADER_SIZE = 5 // 4B 序号(小端) + 1B 声道 |
|
|
|
|
|
private let CHANNEL_HOLD_BUFFER_MAX = 10000 // 声道缓存安全上限(防 OOM 兜底) |
|
|
|
|
|
|
|
|
//private var currentCommand: Command? |
|
|
//private var currentCommand: Command? |
|
|
// 命令结构体 |
|
|
// 命令结构体 |
|
|
@ -481,16 +511,12 @@ public class BleService: NSObject { |
|
|
if centralManager != nil { |
|
|
if centralManager != nil { |
|
|
isManualDisconnect = true // 标记为主动断开 |
|
|
isManualDisconnect = true // 标记为主动断开 |
|
|
|
|
|
|
|
|
// 停止音频发送定时器 |
|
|
// 停止双声道编码与左右发送定时器(内部清空下行缓存并复位包序) |
|
|
stopAudioSendTimer() |
|
|
opusProcessor?.stopDualEncodeStream() |
|
|
|
|
|
stopAudioSendThreads() |
|
|
|
|
|
silenceFrame = nil |
|
|
stopOpusRecording() |
|
|
stopOpusRecording() |
|
|
|
|
|
|
|
|
// 清空音频缓冲区和队列 |
|
|
|
|
|
audioQueueLock.lock() |
|
|
|
|
|
audioBuffer.removeAll() |
|
|
|
|
|
audioDataQueue.removeAll() |
|
|
|
|
|
audioQueueLock.unlock() |
|
|
|
|
|
|
|
|
|
|
|
// 重置命令队列(与 Android reset 对齐) |
|
|
// 重置命令队列(与 Android reset 对齐) |
|
|
replyTimeoutTimer?.invalidate() |
|
|
replyTimeoutTimer?.invalidate() |
|
|
replyTimeoutTimer = nil |
|
|
replyTimeoutTimer = nil |
|
|
@ -684,9 +710,30 @@ public class BleService: NSObject { |
|
|
|
|
|
|
|
|
// MARK: - 编解码控制 |
|
|
// MARK: - 编解码控制 |
|
|
|
|
|
|
|
|
public func writeExternalAudioData(data: Data) { |
|
|
/// 写入左声道 PCM(本端翻译音频),走左声道独立 Opus 编码器 |
|
|
// 将外部音频数据写入编码流 |
|
|
/// - Parameter data: PCM(16kHz / 16bit / 单声道) |
|
|
opusProcessor?.encodePCMData(data) |
|
|
public func writeExternalLeftAudioData(data: Data) { |
|
|
|
|
|
guard !data.isEmpty else { return } |
|
|
|
|
|
opusProcessor?.writeLeftPcm(data) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 写入右声道 PCM(对端翻译音频),走右声道独立 Opus 编码器 |
|
|
|
|
|
/// - Parameter data: PCM(16kHz / 16bit / 单声道) |
|
|
|
|
|
public func writeExternalRightAudioData(data: Data) { |
|
|
|
|
|
guard !data.isEmpty else { return } |
|
|
|
|
|
opusProcessor?.writeRightPcm(data) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 句尾补偿静音(本端/左声道):一句翻译音频的 PCM 全部喂完后调用, |
|
|
|
|
|
/// 把编码器内滞留的不足一帧真实尾音顶出成整帧,避免耳机端句尾被截断。 |
|
|
|
|
|
/// 必须在本句最后一段真实 PCM 之后调用,保证编码顺序。 |
|
|
|
|
|
public func feedLeftTailSilence(paddingMs: Int = 400) { |
|
|
|
|
|
opusProcessor?.feedTailSilence(channel: SwiftOpusAudioProcessor.CHANNEL_LEFT, paddingMs: paddingMs) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 句尾补偿静音(对端/右声道),语义同 feedLeftTailSilence |
|
|
|
|
|
public func feedRightTailSilence(paddingMs: Int = 400) { |
|
|
|
|
|
opusProcessor?.feedTailSilence(channel: SwiftOpusAudioProcessor.CHANNEL_RIGHT, paddingMs: paddingMs) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 打开编码器 |
|
|
/// 打开编码器 |
|
|
@ -753,8 +800,9 @@ public class BleService: NSObject { |
|
|
// 确保Opus解码器处于活跃状态(如果不是则启动) |
|
|
// 确保Opus解码器处于活跃状态(如果不是则启动) |
|
|
|
|
|
|
|
|
startOpusDecoding(sampleRate: 16000, channels: 2, frameSize: 1280) |
|
|
startOpusDecoding(sampleRate: 16000, channels: 2, frameSize: 1280) |
|
|
opusProcessor?.startEncoding(sampleRate: 16000, channels: 1, frameRate: 20) |
|
|
// 通话翻译:左右声道独立编码,每帧 40B/20ms(与 Android setPacketSize(40) 对齐) |
|
|
|
|
|
_ = opusProcessor?.startDualEncodeStream(sampleRate: 16000, packetBytes: 40) |
|
|
|
|
|
startAudioSendThreads() |
|
|
|
|
|
|
|
|
os_log("[CALL_TRANS_CMD] 打开A2DP解码并开始录制(通话翻译)...", log: logger, type: .info) |
|
|
os_log("[CALL_TRANS_CMD] 打开A2DP解码并开始录制(通话翻译)...", log: logger, type: .info) |
|
|
let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_STEREO]) |
|
|
let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_STEREO]) |
|
|
@ -777,17 +825,10 @@ public class BleService: NSObject { |
|
|
public func closeCodec() -> Bool { |
|
|
public func closeCodec() -> Bool { |
|
|
os_log("关闭编解码并停止录制...", log: logger, type: .info) |
|
|
os_log("关闭编解码并停止录制...", log: logger, type: .info) |
|
|
|
|
|
|
|
|
|
|
|
// 停止双声道编码与左右发送定时器(内部清空下行缓存并复位包序) |
|
|
|
|
|
opusProcessor?.stopDualEncodeStream() |
|
|
// 停止音频发送定时器 |
|
|
stopAudioSendThreads() |
|
|
stopAudioSendTimer() |
|
|
silenceFrame = nil |
|
|
|
|
|
|
|
|
// 清空音频缓冲区和队列 |
|
|
|
|
|
audioQueueLock.lock() |
|
|
|
|
|
audioBuffer.removeAll() |
|
|
|
|
|
audioDataQueue.removeAll() |
|
|
|
|
|
audioQueueLock.unlock() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requestStopOpusRecording() |
|
|
requestStopOpusRecording() |
|
|
|
|
|
|
|
|
@ -1105,7 +1146,7 @@ public class BleService: NSObject { |
|
|
let receivedCrc = data[data.count - 1] |
|
|
let receivedCrc = data[data.count - 1] |
|
|
if calculatedCrc != receivedCrc { |
|
|
if calculatedCrc != receivedCrc { |
|
|
os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) |
|
|
os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) |
|
|
return |
|
|
if ENABLE_RX_CRC_CHECK { return } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 提取响应数据 |
|
|
// 提取响应数据 |
|
|
@ -1330,7 +1371,7 @@ public class BleService: NSObject { |
|
|
|
|
|
|
|
|
if calculatedCrc != receivedCrc { |
|
|
if calculatedCrc != receivedCrc { |
|
|
os_log("设备主动上报CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) |
|
|
os_log("设备主动上报CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) |
|
|
return |
|
|
if ENABLE_RX_CRC_CHECK { return } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 提取上报数据 |
|
|
// 提取上报数据 |
|
|
@ -1420,15 +1461,10 @@ public class BleService: NSObject { |
|
|
] |
|
|
] |
|
|
notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) |
|
|
notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) |
|
|
case BleConst.CMD_CONTROL_CODEC: |
|
|
case BleConst.CMD_CONTROL_CODEC: |
|
|
// 设备主动上报编解码控制状态,目前只关心 CODEC_CONTROL_CALL_Sleep (0xFF) |
|
|
// 设备主动上报编解码控制状态 |
|
|
// 帧格式: 0xCC 0x05 [len] [codecStatus, channelMode, ...] [crc] |
|
|
// 帧格式: 0xCC 0x05 [len] [codecStatus, channelMode, ...] [crc] |
|
|
let codecStatus: Int = notifyData.count >= 1 ? Int(notifyData[0]) : -1 |
|
|
let codecStatus: Int = notifyData.count >= 1 ? Int(notifyData[0]) : -1 |
|
|
if codecStatus == Int(BleConst.CODEC_CONTROL_CALL_Sleep) { |
|
|
os_log("收到主动上报编解码控制: codecStatus=0x%02X", log: logger, type: .debug, codecStatus) |
|
|
os_log("收到主动上报: 通话休眠 (codecStatus=0xFF)", log: logger, type: .info) |
|
|
|
|
|
handleCallSleepReceived() |
|
|
|
|
|
} else { |
|
|
|
|
|
os_log("收到主动上报编解码控制: codecStatus=0x%02X", log: logger, type: .debug, codecStatus) |
|
|
|
|
|
} |
|
|
|
|
|
let resultMap: [String: Any] = [ |
|
|
let resultMap: [String: Any] = [ |
|
|
"success": true, |
|
|
"success": true, |
|
|
"command": Int(infoType), |
|
|
"command": Int(infoType), |
|
|
@ -1514,161 +1550,210 @@ extension BleService: SwiftOpusAudioProcessor.AudioDataCallback { |
|
|
notifyAudioDataReceived(data: data, channel: channel) |
|
|
notifyAudioDataReceived(data: data, channel: channel) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 每编出一帧 opus 即回调:按声道入对应下行队列,合包由发送定时器按拍完成 |
|
|
/// 接收到编码后的音频数据 |
|
|
func onAudioFrameEncoded(channel: UInt8, frame: Data) { |
|
|
/// - Parameter data: 编码后的音频数据 |
|
|
// 调试落地:保留原有「编码下行流」录制语义(此前只有本端译音走 BLE,即左声道)。 |
|
|
func onEncodedDataReceived(data: Data) { |
|
|
// 仍回主队列写文件,与改造前一致,避免与 closeFile 抢同一个 RecordingFile。 |
|
|
// 保存编码数据到文件 |
|
|
if channel == SwiftOpusAudioProcessor.CHANNEL_LEFT { |
|
|
recordingFile?.saveAudioData(data) |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
|
|
|
self?.recordingFile?.saveAudioData(frame) |
|
|
// 根据你的业务需求处理编码数据 |
|
|
} |
|
|
// 例如:发送到蓝牙设备、保存到文件等 |
|
|
|
|
|
os_log("🎵 接收到编码音频数据: %d bytes", log: logger, type: .info, data.count) |
|
|
|
|
|
|
|
|
|
|
|
// 修复条件绑定语句 |
|
|
|
|
|
guard let characteristic = callWriteCharacteristic, |
|
|
|
|
|
let peripheral = peripheralDevice, |
|
|
|
|
|
connectionState == BleConst.STATE_CONNECTED else { |
|
|
|
|
|
os_log("通话音频特征未设置或设备未连接", log: logger, type: .error) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 将编码音频数据加入到队列,按每次发送120字节,每次间隔60毫秒 |
|
|
|
|
|
addAudioDataToSendQueue(data: data) |
|
|
|
|
|
|
|
|
|
|
|
// 启动音频发送定时器(如果尚未启动) |
|
|
|
|
|
if !isAudioSending { |
|
|
|
|
|
startAudioSendTimer() |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
enqueueOpusFrame(channel: channel, frame: frame) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/// 静音帧模板就绪,缓存起来用于合包时不足帧补齐 |
|
|
* 将音频数据添加到发送队列 |
|
|
func onSilenceFrameReady(frame: Data) { |
|
|
* 只有当缓冲区达到120字节时才发送数据 |
|
|
silenceFrame = frame |
|
|
* @param data 音频数据字节数组 |
|
|
} |
|
|
*/ |
|
|
} |
|
|
private func addAudioDataToSendQueue(data: Data) { |
|
|
|
|
|
audioQueueLock.lock() |
|
|
|
|
|
defer { audioQueueLock.unlock() } |
|
|
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
|
// 将新数据添加到缓冲区 |
|
|
|
|
|
audioBuffer.append(contentsOf: data) |
|
|
|
|
|
|
|
|
|
|
|
// 当缓冲区达到120字节时,发送数据 |
|
|
|
|
|
while audioBuffer.count >= AUDIO_CHUNK_SIZE { |
|
|
|
|
|
// 取出120字节数据 |
|
|
|
|
|
let chunk = Data(audioBuffer.prefix(AUDIO_CHUNK_SIZE)) |
|
|
|
|
|
audioBuffer.removeFirst(AUDIO_CHUNK_SIZE) |
|
|
|
|
|
|
|
|
|
|
|
// 将数据加入发送队列 |
|
|
|
|
|
|
|
|
|
|
|
audioDataQueue.append(chunk) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - 下行音频:入队 / 合包 / 按拍发送(与 Android BleService 对齐) |
|
|
|
|
|
@available(iOS 13.0, *) |
|
|
|
|
|
extension BleService { |
|
|
|
|
|
|
|
|
|
|
|
/// 将一帧 opus 按声道加入对应下行队列。合包(取 N 帧/补静音/组头)在发送定时器按拍完成。 |
|
|
|
|
|
/// 仅超 OOM 兜底上限才丢最旧帧。 |
|
|
|
|
|
private func enqueueOpusFrame(channel: UInt8, frame: Data) { |
|
|
|
|
|
guard !frame.isEmpty else { return } |
|
|
|
|
|
sendBufferLock.lock() |
|
|
|
|
|
defer { sendBufferLock.unlock() } |
|
|
|
|
|
|
|
|
|
|
|
if channel == SwiftOpusAudioProcessor.CHANNEL_RIGHT { |
|
|
|
|
|
while rightSendBuffer.count >= CHANNEL_HOLD_BUFFER_MAX { |
|
|
|
|
|
rightSendBuffer.removeFirst() |
|
|
|
|
|
os_log("声道积压缓存超上限(%d),丢弃最旧帧 ch=R", log: logger, type: .error, CHANNEL_HOLD_BUFFER_MAX) |
|
|
} |
|
|
} |
|
|
|
|
|
rightSendBuffer.append(frame) |
|
|
// os_log("音频数据已加入缓冲区,当前缓冲区大小: %d 字节", log: logger, type: .debug, audioBuffer.count) |
|
|
} else { |
|
|
} catch { |
|
|
while leftSendBuffer.count >= CHANNEL_HOLD_BUFFER_MAX { |
|
|
os_log("处理音频数据异常: %{public}@", log: logger, type: .error, error.localizedDescription) |
|
|
leftSendBuffer.removeFirst() |
|
|
|
|
|
os_log("声道积压缓存超上限(%d),丢弃最旧帧 ch=L", log: logger, type: .error, CHANNEL_HOLD_BUFFER_MAX) |
|
|
|
|
|
} |
|
|
|
|
|
leftSendBuffer.append(frame) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/// 启动左右两条独立发送定时器(各发一路声道),右声道错开 RIGHT_SEND_STAGGER 启动。 |
|
|
* 启动音频数据发送定时器 |
|
|
func startAudioSendThreads() { |
|
|
*/ |
|
|
|
|
|
private func startAudioSendTimer() { |
|
|
|
|
|
guard !isAudioSending else { |
|
|
guard !isAudioSending else { |
|
|
os_log("音频发送定时器已在运行", log: logger, type: .debug) |
|
|
os_log("音频发送定时器已在运行", log: logger, type: .debug) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
isAudioSending = true |
|
|
isAudioSending = true |
|
|
|
|
|
leftSendTimer = makeChannelSendTimer(isLeft: true) |
|
|
// 在主队列中创建定时器 |
|
|
rightSendTimer = makeChannelSendTimer(isLeft: false) |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
os_log("音频发送定时器已启动(左/右双路,右错开 %.0fms)", log: logger, type: .info, RIGHT_SEND_STAGGER * 1000) |
|
|
guard let self = self else { return } |
|
|
|
|
|
|
|
|
|
|
|
self.audioSendTimer = Timer.scheduledTimer(withTimeInterval: self.AUDIO_SEND_INTERVAL, repeats: true) { [weak self] _ in |
|
|
|
|
|
self?.processAudioSendQueue() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
os_log("音频发送定时器已启动", log: self.logger, type: .info) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/// 停止左右两条发送定时器并清空缓存 |
|
|
* 停止音频数据发送定时器 |
|
|
func stopAudioSendThreads() { |
|
|
*/ |
|
|
|
|
|
private func stopAudioSendTimer() { |
|
|
|
|
|
isAudioSending = false |
|
|
isAudioSending = false |
|
|
|
|
|
leftSendTimer?.cancel() |
|
|
|
|
|
rightSendTimer?.cancel() |
|
|
|
|
|
leftSendTimer = nil |
|
|
|
|
|
rightSendTimer = nil |
|
|
|
|
|
resetDownlinkBuffers() |
|
|
|
|
|
os_log("音频发送定时器(左/右)已停止,队列和缓冲区已清空", log: logger, type: .info) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 清空下行发送队列,并复位包序、节拍与写入拥塞计数 |
|
|
|
|
|
private func resetDownlinkBuffers() { |
|
|
|
|
|
sendBufferLock.lock() |
|
|
|
|
|
leftSendBuffer.removeAll() |
|
|
|
|
|
rightSendBuffer.removeAll() |
|
|
|
|
|
sendBufferLock.unlock() |
|
|
|
|
|
|
|
|
|
|
|
leftPacketSeq = 0 |
|
|
|
|
|
rightPacketSeq = 0 |
|
|
|
|
|
|
|
|
|
|
|
sendStateLock.lock() |
|
|
|
|
|
_lastAudioSendTime = -1 |
|
|
|
|
|
_writeFailRetryCount = 0 |
|
|
|
|
|
sendStateLock.unlock() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 构建单声道发送定时器:固定 AUDIO_SEND_INTERVAL_NORMAL 一拍,每拍取队首若干帧组包下发。 |
|
|
|
|
|
private func makeChannelSendTimer(isLeft: Bool) -> DispatchSourceTimer { |
|
|
|
|
|
let queue = isLeft ? leftSendQueue : rightSendQueue |
|
|
|
|
|
let timer = DispatchSource.makeTimerSource(queue: queue) |
|
|
|
|
|
let firstFire: DispatchTime = .now() + (isLeft ? 0 : RIGHT_SEND_STAGGER) |
|
|
|
|
|
timer.schedule(deadline: firstFire, repeating: AUDIO_SEND_INTERVAL_NORMAL, leeway: .milliseconds(2)) |
|
|
|
|
|
timer.setEventHandler { [weak self] in |
|
|
|
|
|
guard let self = self, self.isAudioSending else { return } |
|
|
|
|
|
_ = self.trySendChannelChunk(isLeft: isLeft) |
|
|
|
|
|
} |
|
|
|
|
|
timer.resume() |
|
|
|
|
|
return timer |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 从指定声道队列取最多 BUNDLE_FRAME_COUNT 帧,不足用静音帧补齐,组包后下发。 |
|
|
|
|
|
/// - 队列空:不发,返回 false |
|
|
|
|
|
/// - 失败(拥塞):取出的真实帧按原序放回队头(静音补齐帧丢弃、下拍重组),返回 false,绝不丢帧 |
|
|
|
|
|
private func trySendChannelChunk(isLeft: Bool) -> Bool { |
|
|
|
|
|
// 取最多 N 帧真实数据 |
|
|
|
|
|
sendBufferLock.lock() |
|
|
|
|
|
var realFrames = [Data]() |
|
|
|
|
|
while realFrames.count < BUNDLE_FRAME_COUNT { |
|
|
|
|
|
if isLeft { |
|
|
|
|
|
guard !leftSendBuffer.isEmpty else { break } |
|
|
|
|
|
realFrames.append(leftSendBuffer.removeFirst()) |
|
|
|
|
|
} else { |
|
|
|
|
|
guard !rightSendBuffer.isEmpty else { break } |
|
|
|
|
|
realFrames.append(rightSendBuffer.removeFirst()) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
sendBufferLock.unlock() |
|
|
|
|
|
guard !realFrames.isEmpty else { return false } // 队列空则不发 |
|
|
|
|
|
|
|
|
DispatchQueue.main.async { [weak self] in |
|
|
let channel = isLeft ? SwiftOpusAudioProcessor.CHANNEL_LEFT : SwiftOpusAudioProcessor.CHANNEL_RIGHT |
|
|
self?.audioSendTimer?.invalidate() |
|
|
let chTag = isLeft ? "L" : "R" |
|
|
self?.audioSendTimer = nil |
|
|
let seq = isLeft ? leftPacketSeq : rightPacketSeq |
|
|
|
|
|
|
|
|
guard let self = self else { return } |
|
|
// 组包帧 = 真实帧 + 静音帧补齐到 N(静音帧未就绪时只发真实帧,罕见的启动窗口) |
|
|
os_log("音频发送定时器已停止", log: self.logger, type: .info) |
|
|
var frames = realFrames |
|
|
|
|
|
var padCount = 0 |
|
|
|
|
|
if let silence = silenceFrame { |
|
|
|
|
|
while frames.count < BUNDLE_FRAME_COUNT { |
|
|
|
|
|
frames.append(silence) |
|
|
|
|
|
padCount += 1 |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
let packet = buildBundlePacket(seq: seq, channel: channel, frames: frames) |
|
|
|
|
|
|
|
|
/** |
|
|
// 同步写入,拿到真实的成功/失败:false=底层发送缓冲已满(拥塞),作为背压信号 |
|
|
* 处理音频发送队列 |
|
|
guard sendAudioPacket(packet) else { |
|
|
*/ |
|
|
// 写失败:真实帧按原顺序放回队头,下一拍重试;静音补齐帧丢弃,下次按新帧重组 |
|
|
private func processAudioSendQueue() { |
|
|
sendBufferLock.lock() |
|
|
// 若收到过 CODEC_CONTROL_CALL_Sleep,本轮直接放弃发送,等到 nextSendEarliestAt 之后再消费队列 |
|
|
if isLeft { |
|
|
nextSendEarliestLock.lock() |
|
|
leftSendBuffer.insert(contentsOf: realFrames, at: 0) |
|
|
let earliest = nextSendEarliestAt |
|
|
} else { |
|
|
nextSendEarliestLock.unlock() |
|
|
rightSendBuffer.insert(contentsOf: realFrames, at: 0) |
|
|
if let earliest = earliest, Date() < earliest { |
|
|
} |
|
|
return |
|
|
sendBufferLock.unlock() |
|
|
|
|
|
|
|
|
|
|
|
sendStateLock.lock() |
|
|
|
|
|
_writeFailRetryCount += 1 |
|
|
|
|
|
let fails = _writeFailRetryCount |
|
|
|
|
|
sendStateLock.unlock() |
|
|
|
|
|
if fails == 1 || fails % 50 == 0 { |
|
|
|
|
|
os_log("下行写入拥塞,重试中 ch=%{public}@ seq=%u 连续失败=%d", |
|
|
|
|
|
log: logger, type: .error, chTag, seq, fails) |
|
|
|
|
|
} |
|
|
|
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
audioQueueLock.lock() |
|
|
sendStateLock.lock() |
|
|
defer { audioQueueLock.unlock() } |
|
|
let prevFails = _writeFailRetryCount |
|
|
|
|
|
_writeFailRetryCount = 0 |
|
|
guard !audioDataQueue.isEmpty else { return } |
|
|
let now = Date().timeIntervalSince1970 |
|
|
|
|
|
let deltaMs = _lastAudioSendTime < 0 ? 0 : Int((now - _lastAudioSendTime) * 1000) |
|
|
|
|
|
_lastAudioSendTime = now |
|
|
|
|
|
sendStateLock.unlock() |
|
|
|
|
|
if prevFails > 0 { |
|
|
|
|
|
os_log("下行写入已恢复,之前连续失败=%d", log: logger, type: .info, prevFails) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 从队列中取出音频数据块 |
|
|
// 成功后推进该声道包序(各自只被本声道发送队列访问) |
|
|
let audioChunk = audioDataQueue.removeFirst() |
|
|
if isLeft { leftPacketSeq &+= 1 } else { rightPacketSeq &+= 1 } |
|
|
|
|
|
|
|
|
// 在后台队列中发送音频数据块 |
|
|
sendBufferLock.lock() |
|
|
audioSendQueue.async { [weak self] in |
|
|
let lBuf = leftSendBuffer.count |
|
|
self?.sendAudioChunk(audioChunk) |
|
|
let rBuf = rightSendBuffer.count |
|
|
} |
|
|
sendBufferLock.unlock() |
|
|
|
|
|
os_log("音频下行发送 ch=%{public}@ seq=%u Δ=%dms 帧=%d+静音%d size=%dB buf(L=%d,R=%d)", |
|
|
|
|
|
log: logger, type: .debug, chTag, seq, deltaMs, |
|
|
|
|
|
realFrames.count, padCount, packet.count, lBuf, rBuf) |
|
|
|
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/// 组下行包:[4B 序号(小端 uint32)] + [1B 声道(0=左/1=右)] + [N 帧 opus 顺次拼接] |
|
|
* 收到设备主动上报的 CODEC_CONTROL_CALL_Sleep 时调用, |
|
|
private func buildBundlePacket(seq: UInt32, channel: UInt8, frames: [Data]) -> Data { |
|
|
* 让音频发送定时器在 AUDIO_SEND_SLEEP_PAUSE 内跳过发送,相当于插入一次较长暂停, |
|
|
var packet = Data() |
|
|
* 之后自动回到 60ms 节拍。期间重复触发会"刷新"暂停截止时间。 |
|
|
packet.reserveCapacity(BUNDLE_HEADER_SIZE + frames.reduce(0) { $0 + $1.count }) |
|
|
*/ |
|
|
packet.append(UInt8(seq & 0xFF)) |
|
|
private func handleCallSleepReceived() { |
|
|
packet.append(UInt8((seq >> 8) & 0xFF)) |
|
|
nextSendEarliestLock.lock() |
|
|
packet.append(UInt8((seq >> 16) & 0xFF)) |
|
|
nextSendEarliestAt = Date().addingTimeInterval(AUDIO_SEND_SLEEP_PAUSE) |
|
|
packet.append(UInt8((seq >> 24) & 0xFF)) |
|
|
nextSendEarliestLock.unlock() |
|
|
packet.append(channel) |
|
|
os_log("收到通话休眠指令,音频发送将暂停 %.0f ms", log: logger, type: .info, AUDIO_SEND_SLEEP_PAUSE * 1000) |
|
|
for frame in frames { packet.append(frame) } |
|
|
|
|
|
return packet |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/// 写一包到通话音频特征,返回是否成功塞入底层发送缓冲。 |
|
|
* 发送音频数据块到蓝牙设备 |
|
|
/// canSendWriteWithoutResponse == false 即背压信号:底层缓冲已满,调用方应放回队头下一拍重试。 |
|
|
* @param data 要发送的音频数据 |
|
|
private func sendAudioPacket(_ packet: Data) -> Bool { |
|
|
*/ |
|
|
|
|
|
private func sendAudioChunk(_ data: Data) { |
|
|
|
|
|
guard let characteristic = callWriteCharacteristic, |
|
|
guard let characteristic = callWriteCharacteristic, |
|
|
let peripheral = peripheralDevice, |
|
|
let peripheral = peripheralDevice, |
|
|
connectionState == BleConst.STATE_CONNECTED else { |
|
|
connectionState == BleConst.STATE_CONNECTED else { |
|
|
os_log("通话音频特征未设置或设备未连接,无法发送音频数据", log: logger, type: .error) |
|
|
return false |
|
|
return |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
bleWriteLock.lock() |
|
|
|
|
|
defer { bleWriteLock.unlock() } |
|
|
|
|
|
|
|
|
// 发送音频数据块 |
|
|
guard peripheral.canSendWriteWithoutResponse else { return false } |
|
|
peripheral.writeValue(data, for: characteristic, type: .withoutResponse) |
|
|
peripheral.writeValue(packet, for: characteristic, type: .withoutResponse) |
|
|
//判断是否发送成功 |
|
|
return true |
|
|
if peripheral.state == .connected { |
|
|
|
|
|
os_log("🎵 发送音频数据块: %d bytes", log: logger, type: .debug, data.count) |
|
|
|
|
|
} else { |
|
|
|
|
|
os_log("🎵 发送音频数据块失败: %d bytes", log: logger, type: .debug, data.count) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -2120,6 +2205,11 @@ extension BleService: CBPeripheralDelegate { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 底层发送缓冲从「满」恢复为可写。发送定时器下一拍会自然重试队头帧,这里只记录一次恢复。 |
|
|
|
|
|
public func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { |
|
|
|
|
|
os_log("下行写入缓冲已就绪(canSendWriteWithoutResponse 恢复)", log: logger, type: .debug) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { |
|
|
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { |
|
|
if let error = error { |
|
|
if let error = error { |
|
|
return |
|
|
return |
|
|
|