4 changed files with 575 additions and 53 deletions
@ -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…
Reference in new issue