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 f7e688497..f10632901 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 @@ -1575,43 +1575,11 @@ startBytesStatistics() Log.e(TAG, failureReason.toString()) - // // 实现重发机制 - // if (retryCount < MAX_RETRY_COUNT) { - // Log.w(TAG, "准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT") - - // // 延迟后重发 - // audioSendHandler.postDelayed({ - // val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) - - // if (isSuccess == true) { - // Log.d(TAG, "重发音频数据块成功") - // } else { - // Log.e(TAG, "重发音频数据块失败") - // } - // }, RETRY_DELAY) - // } else { - // Log.e(TAG, "音频数据块发送失败,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块") - // // 可以选择将失败的数据块记录到日志或进行其他处理 - // } + } } catch (e: Exception) { Log.e(TAG, "发送音频数据块异常: ${e.message}", e) - - // // 异常情况下也进行重发 - // if (retryCount < MAX_RETRY_COUNT) { - // Log.w(TAG, "发送异常,准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT") - // audioSendHandler.postDelayed({ - // val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) - - // if (isSuccess == true) { - // Log.d(TAG, "重发音频数据块成功") - // } else { - // Log.e(TAG, "重发音频数据块失败") - // } - // }, RETRY_DELAY) - // } else { - // Log.e(TAG, "音频数据块发送异常,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块") - // } + } } } catch (e: Exception) { @@ -1981,148 +1949,4 @@ startBytesStatistics() } -class RecordingFile(private val context: Context) { - private var currentAudioFile: File? = null - private var fos: FileOutputStream? = null - - // 新增:用于异步写入的队列和线程 - private val writeQueue = LinkedBlockingQueue() - private val isWriting = AtomicBoolean(false) - private var writeThread: Thread? = null - - // 用于存储音频数据的缓冲区 - private val dataBuffer = mutableListOf() - private var totalBytesWritten = 0 - - var fileName = "" - internal fun creatingFiles() { - if (fos != null || currentAudioFile != null) { - return - } - // 创建新的音频文件 - val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) - val timestamp = dateFormat.format(Date()) - - val filePath = - File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.txt").absolutePath - currentAudioFile = File(filePath) - currentAudioFile?.createNewFile() - // 追加音频数据到文件 - fos = FileOutputStream(currentAudioFile, true) - startWriteThread() // 新增:启动写入线程 - Log.d("TAG", "创建文件: ${currentAudioFile?.absolutePath}") - } - - // 新增:启动写入线程 - private fun startWriteThread() { - if (isWriting.get()) return - isWriting.set(true) - writeThread = Thread { - try { - while (isWriting.get() || writeQueue.isNotEmpty()) { - val data = writeQueue.poll() ?: continue - fos?.write(data) - } - } catch (e: Exception) { - Log.e("", "异步写入音频数据失败: ${e.message}") - } - } - writeThread?.start() - } - - /** - * 保存音频数据到 opus 文件(异步) - * @param buffer 音频数据字节数组 - */ - internal fun saveAudioDataToWav(buffer: ByteArray) { - if (fos == null || currentAudioFile == null) return - // 放入队列,由写线程写入 - // // 1. 将字节数据转换为十六进制字符串 - // val hexData = buildString { - // buffer.forEachIndexed { index, byte -> - // append("%02X".format(byte)) - - // } - // } - - // 2. 写入十六进制字符串 - - writeQueue.offer(buffer) - totalBytesWritten += buffer.size - } - - /** - * 保存日志信息到文件(重载方法) - * 使用UTF-8编码处理字符串 - * @param logMessage 日志消息字符串 - */ - internal fun saveAudioDataToWav(logMessage: String) { - if (fos == null || currentAudioFile == null) return - try { - // 使用UTF-8编码将字符串转换为字节数组,并添加换行符 - val logBytes = (logMessage + "\n").toByteArray(Charsets.UTF_8) - writeQueue.offer(logBytes) - totalBytesWritten += logBytes.size - Log.d("RecordingFile", "保存日志信息: ${logMessage.length} 字符, ${logBytes.size} 字节 (UTF-8)") - } catch (e: Exception) { - Log.e("RecordingFile", "UTF-8编码转换失败: ${e.message}") - } - } - - /** - * 将音频数据转换为UTF-8编码的十六进制字符串格式并保存 - * @param buffer 音频数据字节数组 - * @param addTimestamp 是否添加时间戳,默认为true - */ - internal fun saveAudioDataAsUtf8Hex(buffer: ByteArray, addTimestamp: Boolean = true) { - if (fos == null || currentAudioFile == null) return - - try { - val timestamp = if (addTimestamp) { - "[${SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())}] " - } else "" - - // 将字节数组转换为十六进制字符串 - val hexString = buffer.joinToString(" ") { "%02X".format(it) } - val dataLine = "${timestamp}音频数据 (${buffer.size} 字节): $hexString\n" - - // 使用UTF-8编码转换为字节数组 - val utf8Bytes = dataLine.toByteArray(Charsets.UTF_8) - writeQueue.offer(utf8Bytes) - totalBytesWritten += utf8Bytes.size - - Log.d("RecordingFile", "保存UTF-8格式音频数据: ${buffer.size} 字节 -> ${utf8Bytes.size} UTF-8字节") - } catch (e: Exception) { - Log.e("RecordingFile", "UTF-8格式保存失败: ${e.message}") - } - } - - internal fun closeFile() { - try { - if (fos == null || currentAudioFile == null) { - return - } - isWriting.set(false) - writeThread?.join(500) // 等待写线程结束 - fos?.close() - - // 更新WAV文件头 - currentAudioFile?.let { file -> - // 如果文件小于44字节,直接删除 - if (file.length() <= 0) { - file.delete() - Log.d("TAG", "音频文件过小已删除: ${file.absolutePath}") - } else { - Log.d("TAG", "音频文件保存完成: ${file.absolutePath}") - } - } - - } catch (e: Exception) { - Log.e("TAG", "更新opus失败: ${e.message}") - } finally { - fos = null - currentAudioFile = null - } - } -} diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt new file mode 100644 index 000000000..78d73aa46 --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt @@ -0,0 +1,160 @@ +package com.yunqiinnovation.ble_service + +import android.content.Context +import android.util.Log +import java.io.File +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicBoolean + +/** + * 录音文件管理类:负责音频数据的异步写入和文件管理 + */ +class RecordingFile(private val context: Context) { + private var currentAudioFile: File? = null + private var fos: FileOutputStream? = null + + // 新增:用于异步写入的队列和线程 + private val writeQueue = LinkedBlockingQueue() + private val isWriting = AtomicBoolean(false) + private var writeThread: Thread? = null + + // 用于存储音频数据的缓冲区 + private val dataBuffer = mutableListOf() + private var totalBytesWritten = 0 + + var fileName = "" + internal fun creatingFiles() { + if (fos != null || currentAudioFile != null) { + return + } + // 创建新的音频文件 + val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + val timestamp = dateFormat.format(Date()) + + val filePath = + File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.txt").absolutePath + currentAudioFile = File(filePath) + currentAudioFile?.createNewFile() + // 追加音频数据到文件 + fos = FileOutputStream(currentAudioFile, true) + startWriteThread() // 新增:启动写入线程 + Log.d("TAG", "创建文件: ${currentAudioFile?.absolutePath}") + } + + // 新增:启动写入线程 + private fun startWriteThread() { + if (isWriting.get()) return + isWriting.set(true) + writeThread = Thread { + try { + while (isWriting.get() || writeQueue.isNotEmpty()) { + val data = writeQueue.poll() ?: continue + fos?.write(data) + } + } catch (e: Exception) { + Log.e("", "异步写入音频数据失败: ${e.message}") + } + } + writeThread?.start() + } + + /** + * 保存音频数据到 opus 文件(异步) + * @param buffer 音频数据字节数组 + */ + internal fun saveAudioDataToWav(buffer: ByteArray) { + if (fos == null || currentAudioFile == null) return + // 放入队列,由写线程写入 + // // 1. 将字节数据转换为十六进制字符串 + // val hexData = buildString { + // buffer.forEachIndexed { index, byte -> + // append("%02X".format(byte)) + + // } + // } + + // 2. 写入十六进制字符串 + + writeQueue.offer(buffer) + totalBytesWritten += buffer.size + } + + /** + * 保存日志信息到文件(重载方法) + * 使用UTF-8编码处理字符串 + * @param logMessage 日志消息字符串 + */ + internal fun saveAudioDataToWav(logMessage: String) { + if (fos == null || currentAudioFile == null) return + try { + // 使用UTF-8编码将字符串转换为字节数组,并添加换行符 + val logBytes = (logMessage + "\n").toByteArray(Charsets.UTF_8) + writeQueue.offer(logBytes) + totalBytesWritten += logBytes.size + Log.d("RecordingFile", "保存日志信息: ${logMessage.length} 字符, ${logBytes.size} 字节 (UTF-8)") + } catch (e: Exception) { + Log.e("RecordingFile", "UTF-8编码转换失败: ${e.message}") + } + } + + /** + * 将音频数据转换为UTF-8编码的十六进制字符串格式并保存 + * @param buffer 音频数据字节数组 + * @param addTimestamp 是否添加时间戳,默认为true + */ + internal fun saveAudioDataAsUtf8Hex(buffer: ByteArray, addTimestamp: Boolean = true) { + if (fos == null || currentAudioFile == null) return + + try { + val timestamp = if (addTimestamp) { + "[${SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())}] " + } else "" + + // 将字节数组转换为十六进制字符串 + val hexString = buffer.joinToString(" ") { "%02X".format(it) } + val dataLine = "${timestamp}音频数据 (${buffer.size} 字节): $hexString\n" + + // 使用UTF-8编码转换为字节数组 + val utf8Bytes = dataLine.toByteArray(Charsets.UTF_8) + writeQueue.offer(utf8Bytes) + totalBytesWritten += utf8Bytes.size + + Log.d("RecordingFile", "保存UTF-8格式音频数据: ${buffer.size} 字节 -> ${utf8Bytes.size} UTF-8字节") + } catch (e: Exception) { + Log.e("RecordingFile", "UTF-8格式保存失败: ${e.message}") + } + } + + + internal fun closeFile() { + try { + if (fos == null || currentAudioFile == null) { + return + } + isWriting.set(false) + writeThread?.join(500) // 等待写线程结束 + fos?.close() + + // 更新WAV文件头 + currentAudioFile?.let { file -> + // 如果文件小于44字节,直接删除 + if (file.length() <= 0) { + file.delete() + Log.d("TAG", "音频文件过小已删除: ${file.absolutePath}") + } else { + Log.d("TAG", "音频文件保存完成: ${file.absolutePath}") + } + } + + } catch (e: Exception) { + Log.e("TAG", "更新opus失败: ${e.message}") + } finally { + fos = null + currentAudioFile = null + } + } +}