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