Browse Source

上出新版本的通话翻译

newdev_shunjiawei
liwei1dao 5 months ago
parent
commit
d0ce420e9d
  1. 27
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  2. 94
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  3. 246
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt
  4. 243
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/WavFileWriter.kt

27
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -85,6 +85,13 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
astStereoRecorder.enqueueRightChannel(data)
}
if (broadcastPeerTranslate) callBPcmPlayer.feed(data)
// 通话翻译:将对端翻译 PCM 也下发到耳机右声道
// 仅当 BleService 中的双声道编码已启动时 writeRightPcm 才真正落到编码器,否则会被静默忽略
bleWriteScope.launch {
bleRightMutex.withLock {
BleService.writeExternalRightAudioData(data)
}
}
}
// 翻译(Volcano)相关
@ -1338,9 +1345,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
"A", "$lang0->$lang1",
{ sendAstEvent(it) },
{ data ->
// 本端翻译音频走左声道
bleWriteScope.launch {
bleRightMutex.withLock {
BleService.writeExternalAudioData(data)
bleLeftMutex.withLock {
BleService.writeExternalLeftAudioData(data)
}
}
}
@ -1407,9 +1415,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
"A", "$lang0->$lang1",
{ sendAstEvent(it) },
{ data ->
// 本端翻译音频走左声道
bleWriteScope.launch {
bleRightMutex.withLock {
BleService.writeExternalAudioData(data)
bleLeftMutex.withLock {
BleService.writeExternalLeftAudioData(data)
}
}
}
@ -1458,9 +1467,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
"A", "$translationLang0->$translationLang1", translationLang1,
{ sendAstEvent(it) },
{ data ->
// 本端翻译音频走左声道
bleWriteScope.launch {
bleRightMutex.withLock {
BleService.writeExternalAudioData(data)
bleLeftMutex.withLock {
BleService.writeExternalLeftAudioData(data)
}
}
}
@ -1517,9 +1527,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
"A", "$translationLang0->$translationLang1", translationLang1,
{ sendAstEvent(it) },
{ data ->
// 本端翻译音频走左声道
bleWriteScope.launch {
bleRightMutex.withLock {
BleService.writeExternalAudioData(data)
bleLeftMutex.withLock {
BleService.writeExternalLeftAudioData(data)
}
}
}

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

@ -236,8 +236,8 @@ object BleService {
recordfile1 = RecordingFile(this.context)
recordfile1!!.fileName = "手机端"
// 初始化Opus音频管理器
if (!opusAudioManager.initialize()) {
// 初始化Opus音频管理器(传入 context 以支持双声道编码时分别写 PCM WAV 调试文件)
if (!opusAudioManager.initialize(this.context)) {
Log.e(TAG, "Opus音频管理器初始化失败")
// 根据需要决定是否因为Opus初始化失败而返回false
} else {
@ -435,8 +435,7 @@ object BleService {
}
/**
* 写入外部音频数据到Opus编码流
* 该函数用于将外部音频数据(如麦克风录音)写入到Opus编码器中
* 写入外部音频数据到Opus编码流(单通道,老接口,仅用于非通话翻译场景)
*
* @param data 音频数据字节数组,通常为PCM格式
*/
@ -444,6 +443,28 @@ object BleService {
opusAudioManager.writeExternalAudioData(data)
}
/**
* 写入外部左声道 PCM 音频数据(本端翻译音频)
* 通话翻译模式下,左声道走独立 Opus 编码器,累满 5 帧后打包下发到耳机
*
* @param data 音频数据字节数组,PCM格式(16kHz / 16bit / 单声道)
*/
fun writeExternalLeftAudioData(data: ByteArray) {
if (data.isEmpty()) return
opusAudioManager.writeLeftPcm(data)
}
/**
* 写入外部右声道 PCM 音频数据(对端翻译音频)
* 通话翻译模式下,右声道走独立 Opus 编码器,累满 5 帧后打包下发到耳机
*
* @param data 音频数据字节数组,PCM格式(16kHz / 16bit / 单声道)
*/
fun writeExternalRightAudioData(data: ByteArray) {
if (data.isEmpty()) return
opusAudioManager.writeRightPcm(data)
}
// ======================================================================================================
// 扫描功能
// ======================================================================================================
@ -1267,12 +1288,13 @@ object BleService {
// ======================================================================================================
/**
* 开始Opus数据流编码
* 开始Opus数据流编码(通话翻译模式:左右声道独立编码)
* 输出合包格式:[4B 序号(大端 uint32)] + [1B 声道(0=左/1=右)] + [5 × opus(40B)] = 205B
* @return 是否成功开始编码
*/
private fun startOpusEncodeStream(): Boolean {
startAudioSendThread()
return opusAudioManager.startEncodeStream()
startAudioSendThread()
return opusAudioManager.startDualEncodeStream(false, 16000, 40)
}
/**
@ -1281,37 +1303,22 @@ object BleService {
*/
private fun stopOpusEncodeStream(): Boolean {
stopAudioSendThread()
return opusAudioManager.stopEncodeStream()
// 停止双声道独立编码;如果有单通道遗留也一并停止
val dualStopped = opusAudioManager.stopDualEncodeStream()
val singleStopped = opusAudioManager.stopEncodeStream()
return dualStopped || singleStopped
}
/**
* 将音频数据添加到发送队列
* 只有当缓冲区达到80字节时才发送数据
* @param data 音频数据字节数组
* 将一包已成型的音频合帧加入下行发送队列
* 包格式:[4B 序号(大端 uint32)] + [1B 声道(0=左/1=右)] + [5 × opus(40B)]
* 双声道独立编码模式下,每次回调送来的就是一整包,直接入队,不再二次切片
* @param data 待发送的合帧包
*/
private fun addAudioDataToSendQueue(data: ByteArray) {
try {
// 将新数据添加到缓冲区
audioBuffer.addAll(data.toList())
// 当缓冲区达到120字节时,发送数据
while (audioBuffer.size >= AUDIO_CHUNK_SIZE) {
// 取出120字节数据
val chunk = ByteArray(AUDIO_CHUNK_SIZE)
for (i in 0 until AUDIO_CHUNK_SIZE) {
chunk[i] = audioBuffer.removeAt(0)
}
// 将数据加入发送队列
if (!audioSendQueue.offer(chunk)) {
Log.w(TAG, "音频发送队列已满,丢弃数据块")
break
}
}
// Log.d(TAG, "音频数据已加入缓冲区,当前缓冲区大小: ${audioBuffer.size} 字节")
} catch (e: Exception) {
Log.e(TAG, "处理音频数据异常: ${e.message}", e)
if (data.isEmpty()) return
if (!audioSendQueue.offer(data)) {
Log.w(TAG, "音频发送队列已满,丢弃当前包 size=${data.size}")
}
}
@ -1335,8 +1342,25 @@ object BleService {
val audioChunk = audioSendQueue.poll(100, TimeUnit.MILLISECONDS)
if (audioChunk != null) {
// 发送音频数据块并等待回调
recordfile1?.saveAudioDataToWav(audioChunk)
// 合包布局:[4B 序号(大端)] + [1B 声道(0=左/1=右)] + [N × opus]
// 调试录音文件只保留拼接后的纯 opus 数据,剥离 5 字节包头再写入
if (audioChunk.size > 5) {
val payload = audioChunk.copyOfRange(5, audioChunk.size)
recordfile1?.saveAudioDataToWav(payload)
}
// 解析包头供发送节拍日志使用
if (audioChunk.size >= 5) {
val seq = ((audioChunk[0].toInt() and 0xFF) shl 24) or
((audioChunk[1].toInt() and 0xFF) shl 16) or
((audioChunk[2].toInt() and 0xFF) shl 8) or
(audioChunk[3].toInt() and 0xFF)
val chTag = when (audioChunk[4]) {
OpusAudioManager.CHANNEL_LEFT -> "L"
OpusAudioManager.CHANNEL_RIGHT -> "R"
else -> "?(${audioChunk[4]})"
}
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq size=${audioChunk.size}B queue=${audioSendQueue.size}")
}
sendAudioChunkWithCallback(audioChunk)
Thread.sleep(AUDIO_SEND_INTERVAL)
}

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

@ -1,5 +1,6 @@
package com.yunqiinnovation.ble_service
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
@ -8,6 +9,9 @@ import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
@ -33,6 +37,15 @@ class OpusAudioManager {
// 重发机制相关常量
private const val MAX_RETRY_COUNT = 1 // 最大重试次数
private const val RETRY_DELAY = 30L // 重试延迟时间(毫秒)
// 双声道下行包声道标识:0=左(本端翻译) / 1=右(对端翻译)
const val CHANNEL_LEFT: Byte = 0
const val CHANNEL_RIGHT: Byte = 1
// 下行合包:左右各自累积 BUNDLE_FRAME_COUNT 帧 opus 合并为 1 包后才下发,降低 BLE 发送频率
private const val BUNDLE_FRAME_COUNT = 5
// 合包头部:bytes 0-3 序号(大端 uint32),byte 4 声道标识(0=左/1=右)
private const val BUNDLE_HEADER_SIZE = 5
}
// ======================================================================================================
@ -89,6 +102,24 @@ class OpusAudioManager {
//编码对象
private var encodeOpusManager: OpusManager? = null
private var encodeOption: OpusOption? = null
// 双声道独立编码对象(左右各自独立,互不影响预测状态)
private var leftEncodeManager: OpusManager? = null
private var rightEncodeManager: OpusManager? = null
private var isDualEncoding = false
// 左右各自的 opus 帧缓存与发送序号;累满 BUNDLE_FRAME_COUNT 帧后打包下发
private val bundleLock = Any()
private val leftBundleFrames = ArrayList<ByteArray>(BUNDLE_FRAME_COUNT)
private val rightBundleFrames = ArrayList<ByteArray>(BUNDLE_FRAME_COUNT)
private var leftBundleSeq = 0
private var rightBundleSeq = 0
// 上下文:用于双声道编码模式下,分别为左右声道写入 mono PCM WAV 调试文件
private var appContext: Context? = null
private var leftPcmWavWriter: WavFileWriter? = null
private var rightPcmWavWriter: WavFileWriter? = null
private val mainHandler = Handler(Looper.getMainLooper())
// 解码音频数据队列和处理线程
@ -119,12 +150,15 @@ class OpusAudioManager {
/**
* 初始化Opus音频管理器
* @param context 应用上下文,用于双声道编码模式下的 PCM WAV 调试文件写入;
* 不传则跳过 PCM WAV 调试文件落盘,其他功能不受影响
* @return 是否初始化成功
*/
fun initialize(): Boolean {
fun initialize(context: Context? = null): Boolean {
if (isInitialized) return true
try {
this.appContext = context?.applicationContext
// 初始化OpusManager和OpusOption
decodeOpusManager = OpusManager()
decodeOption = OpusOption()
@ -132,6 +166,10 @@ class OpusAudioManager {
encodeOpusManager = OpusManager()
encodeOption = OpusOption()
// 双声道独立编码器:左右各自维持独立的 OpusManager
leftEncodeManager = OpusManager()
rightEncodeManager = OpusManager()
// 启动队列处理
startAudioQueueProcessing()
@ -180,11 +218,15 @@ class OpusAudioManager {
audioSendQueue.clear()
// 清理Opus相关资源
stopDualEncodeStream()
decodeOpusManager = null
decodeOption = null
encodeOpusManager = null
encodeOption = null
leftEncodeManager = null
rightEncodeManager = null
callback = null
appContext = null
isInitialized = false
Log.d(TAG, "OpusAudioManager资源清理完成")
@ -384,6 +426,208 @@ class OpusAudioManager {
return false
}
// ======================================================================================================
// 双声道独立编码方法(左右声道各用独立 OpusManager,互不污染预测状态)
// 输出格式:每包 = [4B 序号(大端 uint32)] + [1B 声道(0=左/1=右)] + [N × opus 帧]
// 左右各自累积 BUNDLE_FRAME_COUNT 帧后打包一次,通过 onAudioDataEncoded 下发
// ======================================================================================================
/**
* 启动双声道独立编码流
* 左右各创建独立 OpusManager;左右各自累积 5 帧 opus 后合包,
* 通过 onAudioDataEncoded 输出一包(含 5 字节包头 + 5 帧 opus),由下游队列按节拍发送
*/
fun startDualEncodeStream(
hasHeader: Boolean = false,
sampleRate: Int = 16000,
packetSize: Int = 40
): Boolean {
if (!isInitialized) {
Log.e(TAG, "OpusManager未初始化,无法启动双声道编码")
return false
}
if (isDualEncoding) {
Log.d(TAG, "双声道编码流已在运行")
return true
}
synchronized(bundleLock) {
leftBundleFrames.clear()
rightBundleFrames.clear()
leftBundleSeq = 0
rightBundleSeq = 0
}
openPcmWavWriters(sampleRate)
fun makeOption() = OpusOption().apply {
setHasHead(hasHeader)
setChannel(1)
setSampleRate(sampleRate)
setPacketSize(packetSize)
}
return try {
leftEncodeManager?.startEncodeStream(makeOption(), object : OnEncodeStreamCallback {
override fun onEncodeStream(data: ByteArray?) {
if (data != null) emitChannelFrame(CHANNEL_LEFT, data)
}
override fun onStart() { Log.i(TAG, "左声道独立编码已开始") }
override fun onComplete(outPath: String?) {}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "左声道编码错误: [$code] $message")
callback?.onError("encode", code, message)
}
})
rightEncodeManager?.startEncodeStream(makeOption(), object : OnEncodeStreamCallback {
override fun onEncodeStream(data: ByteArray?) {
if (data != null) emitChannelFrame(CHANNEL_RIGHT, data)
}
override fun onStart() { Log.i(TAG, "右声道独立编码已开始") }
override fun onComplete(outPath: String?) {}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "右声道编码错误: [$code] $message")
callback?.onError("encode", code, message)
}
})
isDualEncoding = true
callback?.onEncodeStreamStateChanged(true)
Log.i(TAG, "双声道独立编码流已启动 sampleRate=$sampleRate packetSize=$packetSize")
true
} catch (e: Exception) {
Log.e(TAG, "启动双声道编码流失败: ${e.message}", e)
callback?.onError("encode", -1, e.message)
false
}
}
/**
* 停止双声道独立编码流
*/
fun stopDualEncodeStream(): Boolean {
if (!isDualEncoding) return false
isDualEncoding = false
try { leftEncodeManager?.stopEncodeStream() } catch (e: Exception) { Log.e(TAG, "停止左声道编码异常", e) }
try { rightEncodeManager?.stopEncodeStream() } catch (e: Exception) { Log.e(TAG, "停止右声道编码异常", e) }
synchronized(bundleLock) {
leftBundleFrames.clear()
rightBundleFrames.clear()
}
closePcmWavWriters()
callback?.onEncodeStreamStateChanged(false)
Log.i(TAG, "双声道独立编码流已停止")
return true
}
/**
* 是否正在进行双声道独立编码
*/
fun isDualEncoding(): Boolean = isDualEncoding
/**
* 写入左声道 mono PCM(本端翻译音频)
* 累满 BUNDLE_FRAME_COUNT 帧 opus 后通过 onAudioDataEncoded 下发:[4B 序号] + [1B 声道=0] + [N × opus]
*/
fun writeLeftPcm(pcm: ByteArray) {
if (!isDualEncoding) {
return
}
if (pcm.isEmpty()) return
leftPcmWavWriter?.writePcmData(pcm)
leftEncodeManager?.writeEncodeStream(pcm)
}
/**
* 写入右声道 mono PCM(对端翻译音频)
* 累满 BUNDLE_FRAME_COUNT 帧 opus 后通过 onAudioDataEncoded 下发:[4B 序号] + [1B 声道=1] + [N × opus]
*/
fun writeRightPcm(pcm: ByteArray) {
if (!isDualEncoding) {
return
}
if (pcm.isEmpty()) return
rightPcmWavWriter?.writePcmData(pcm)
rightEncodeManager?.writeEncodeStream(pcm)
}
/**
* 单帧入合包缓冲;左右各自累满 BUNDLE_FRAME_COUNT 帧后整包通过回调下发
* 输出包布局:[seq(4B 大端 uint32)] + [channel(1B)] + [N 帧 opus 顺次拼接]
*/
private fun emitChannelFrame(channelPrefix: Byte, opusFrame: ByteArray) {
val packet: ByteArray? = synchronized(bundleLock) {
when (channelPrefix) {
CHANNEL_LEFT -> {
leftBundleFrames.add(opusFrame)
if (leftBundleFrames.size >= BUNDLE_FRAME_COUNT) {
val p = buildBundlePacket(leftBundleSeq, CHANNEL_LEFT, leftBundleFrames)
leftBundleSeq++
leftBundleFrames.clear()
p
} else null
}
CHANNEL_RIGHT -> {
rightBundleFrames.add(opusFrame)
if (rightBundleFrames.size >= BUNDLE_FRAME_COUNT) {
val p = buildBundlePacket(rightBundleSeq, CHANNEL_RIGHT, rightBundleFrames)
rightBundleSeq++
rightBundleFrames.clear()
p
} else null
}
else -> {
Log.w(TAG, "未知声道前缀: $channelPrefix")
null
}
}
}
if (packet != null) callback?.onAudioDataEncoded(packet)
}
private fun buildBundlePacket(seq: Int, channel: Byte, frames: List<ByteArray>): ByteArray {
var payloadSize = 0
for (f in frames) payloadSize += f.size
val packet = ByteArray(BUNDLE_HEADER_SIZE + payloadSize)
packet[0] = (seq ushr 24 and 0xFF).toByte()
packet[1] = (seq ushr 16 and 0xFF).toByte()
packet[2] = (seq ushr 8 and 0xFF).toByte()
packet[3] = (seq and 0xFF).toByte()
packet[4] = channel
var offset = BUNDLE_HEADER_SIZE
for (f in frames) {
System.arraycopy(f, 0, packet, offset, f.size)
offset += f.size
}
return packet
}
private fun openPcmWavWriters(sampleRate: Int) {
val ctx = appContext ?: return
try {
val ts = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
leftPcmWavWriter = WavFileWriter(ctx).also {
val ok = it.createWavFile("pcm_left_$ts", sampleRate, 1, 16)
if (!ok) leftPcmWavWriter = null
}
rightPcmWavWriter = WavFileWriter(ctx).also {
val ok = it.createWavFile("pcm_right_$ts", sampleRate, 1, 16)
if (!ok) rightPcmWavWriter = null
}
} catch (e: Exception) {
Log.e(TAG, "创建 PCM WAV 调试文件失败: ${e.message}", e)
leftPcmWavWriter = null
rightPcmWavWriter = null
}
}
private fun closePcmWavWriters() {
try { leftPcmWavWriter?.closeWavFile() } catch (e: Exception) { Log.e(TAG, "关闭左声道 PCM WAV 失败", e) }
try { rightPcmWavWriter?.closeWavFile() } catch (e: Exception) { Log.e(TAG, "关闭右声道 PCM WAV 失败", e) }
leftPcmWavWriter = null
rightPcmWavWriter = null
}
/**
* 写入外部音频数据到Opus编码流
* 该函数用于将外部音频数据(如麦克风录音)写入到Opus编码器中

243
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/WavFileWriter.kt

@ -0,0 +1,243 @@
package com.yunqiinnovation.ble_service
import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
/**
* WAV文件写入工具类
* 用于将PCM音频数据保存为标准WAV格式文件
*/
class WavFileWriter(private val context: Context) {
companion object {
private const val TAG = "WavFileWriter"
private const val WAV_HEADER_SIZE = 44
}
private var currentWavFile: File? = null
private var fos: FileOutputStream? = null
// 音频参数
private var sampleRate: Int = 16000
private var channels: Int = 1
private var bitsPerSample: Int = 16
// 异步写入相关
private val writeQueue = LinkedBlockingQueue<ByteArray>()
private val isWriting = AtomicBoolean(false)
private var writeThread: Thread? = null
// 数据统计
private var totalDataSize = 0
/**
* 创建新的WAV文件
* @param fileName 文件名前缀
* @param sampleRate 采样率,默认16000Hz
* @param channels 声道数,默认1(单声道)
* @param bitsPerSample 位深度,默认16位
*/
fun createWavFile(
fileName: String,
sampleRate: Int = 16000,
channels: Int = 1,
bitsPerSample: Int = 16
): Boolean {
try {
if (fos != null || currentWavFile != null) {
closeWavFile()
}
this.sampleRate = sampleRate
this.channels = channels
this.bitsPerSample = bitsPerSample
// 创建文件名
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val timestamp = dateFormat.format(Date())
val filePath =
File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.wav").absolutePath
currentWavFile = File(filePath)
currentWavFile?.createNewFile()
fos = FileOutputStream(currentWavFile, false)
// 写入WAV文件头(先写入占位符,稍后更新)
writeWavHeader(0)
// 启动异步写入线程
startWriteThread()
totalDataSize = 0
writeQueue.clear()
Log.d(TAG, "创建WAV文件: ${currentWavFile?.absolutePath}")
Log.d(
TAG,
"音频参数 - 采样率: ${sampleRate}Hz, 声道: ${channels}, 位深: ${bitsPerSample}bit"
)
return true
} catch (e: Exception) {
Log.e(TAG, "创建WAV文件失败: ${e.message}", e)
return false
}
}
/**
* 写入PCM音频数据
* @param pcmData PCM音频数据字节数组
*/
fun writePcmData(pcmData: ByteArray) {
if (fos == null || currentWavFile == null) {
Log.w(TAG, "WAV文件未创建,无法写入数据")
return
}
if (!isWriting.get()) {
Log.d(TAG, "写入线程停止运行,停止写入数据")
return
}
try {
// 将数据放入队列,由写入线程异步处理
writeQueue.offer(pcmData.clone())
totalDataSize += pcmData.size
// Log.d(TAG, "添加PCM数据到写入队列: ${pcmData.size} 字节,总计: ${totalDataSize} 字节")
} catch (e: Exception) {
Log.e(TAG, "写入PCM数据失败: ${e.message}", e)
}
}
/**
* 关闭WAV文件并更新文件头
*/
fun closeWavFile() {
try {
if (fos == null || currentWavFile == null) {
return
}
// 停止写入线程
isWriting.set(false)
writeThread?.join(1000) // 等待写入线程结束
val finalDataSize = totalDataSize
fos?.close()
fos = null
writeQueue.clear()
// 更新WAV文件头
currentWavFile?.let { file ->
if (file.length() <= WAV_HEADER_SIZE) {
// 如果文件只有头部没有数据,删除文件
file.delete()
Log.d(TAG, "WAV文件无音频数据,已删除: ${file.absolutePath}")
} else {
// 更新文件头中的数据大小信息
updateWavHeader(file, finalDataSize)
Log.d(TAG, "WAV文件保存完成: ${file.absolutePath}")
Log.d(TAG, "文件大小: ${file.length()} 字节,音频数据: ${finalDataSize} 字节")
}
}
} catch (e: Exception) {
Log.e(TAG, "关闭WAV文件失败: ${e.message}", e)
} finally {
currentWavFile = null
totalDataSize = 0
}
}
/**
* 启动异步写入线程
*/
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)
fos?.flush()
}
} catch (e: Exception) {
Log.e(TAG, "异步写入音频数据失败: ${e.message}", e)
}
}
writeThread?.start()
}
/**
* 写入WAV文件头
* @param dataSize 音频数据大小
*/
private fun writeWavHeader(dataSize: Int) {
val header = ByteBuffer.allocate(WAV_HEADER_SIZE)
header.order(ByteOrder.LITTLE_ENDIAN)
// RIFF头
header.put("RIFF".toByteArray())
header.putInt(36 + dataSize) // 文件大小 - 8
header.put("WAVE".toByteArray())
// fmt子块
header.put("fmt ".toByteArray())
header.putInt(16) // fmt子块大小
header.putShort(1) // 音频格式(PCM)
header.putShort(channels.toShort()) // 声道数
header.putInt(sampleRate) // 采样率
header.putInt(sampleRate * channels * bitsPerSample / 8) // 字节率
header.putShort((channels * bitsPerSample / 8).toShort()) // 块对齐
header.putShort(bitsPerSample.toShort()) // 位深度
// data子块
header.put("data".toByteArray())
header.putInt(dataSize) // 音频数据大小
fos?.write(header.array())
fos?.flush()
}
/**
* 更新WAV文件头中的数据大小信息
* @param file WAV文件
* @param dataSize 实际音频数据大小
*/
private fun updateWavHeader(file: File, dataSize: Int) {
try {
RandomAccessFile(file, "rw").use { raf ->
// 更新文件大小(位置4)
raf.seek(4)
raf.write(intToLittleEndianBytes(36 + dataSize))
// 更新数据大小(位置40)
raf.seek(40)
raf.write(intToLittleEndianBytes(dataSize))
}
} catch (e: IOException) {
Log.e(TAG, "更新WAV文件头失败: ${e.message}", e)
}
}
/**
* 将整数转换为小端字节序字节数组
*/
private fun intToLittleEndianBytes(value: Int): ByteArray {
return byteArrayOf(
(value and 0xFF).toByte(),
((value shr 8) and 0xFF).toByte(),
((value shr 16) and 0xFF).toByte(),
((value shr 24) and 0xFF).toByte()
)
}
}
Loading…
Cancel
Save