|
|
|
@ -22,8 +22,9 @@ object RecordFile { |
|
|
|
private val dataBuffer = mutableListOf<ByteArray>() |
|
|
|
private var totalBytesWritten = 0 |
|
|
|
|
|
|
|
// 音频配置 |
|
|
|
private val sampleRate = 16000 |
|
|
|
// 音频配置 - 可配置的采样率和声道数 |
|
|
|
private var sampleRate = 16000 |
|
|
|
private var channels = 2 // 1=单声道, 2=立体声 |
|
|
|
|
|
|
|
// 新增:用于存储最后成功保存的文件 |
|
|
|
private var lastSavedFile: File? = null |
|
|
|
@ -31,6 +32,64 @@ object RecordFile { |
|
|
|
var isPause = false |
|
|
|
var fileName = "" |
|
|
|
|
|
|
|
// 双声道分离相关变量 |
|
|
|
private var leftChannelFile: File? = null |
|
|
|
private var rightChannelFile: File? = null |
|
|
|
private var leftChannelFos: FileOutputStream? = null |
|
|
|
private var rightChannelFos: FileOutputStream? = null |
|
|
|
private val leftChannelQueue = LinkedBlockingQueue<ByteArray>() |
|
|
|
private val rightChannelQueue = LinkedBlockingQueue<ByteArray>() |
|
|
|
private var leftChannelWriteThread: Thread? = null |
|
|
|
private var rightChannelWriteThread: Thread? = null |
|
|
|
private var leftChannelBytesWritten = 0 |
|
|
|
private var rightChannelBytesWritten = 0 |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置音频配置 |
|
|
|
* @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000) |
|
|
|
* @param channels 声道数 (1=单声道, 2=立体声) |
|
|
|
*/ |
|
|
|
internal fun setAudioConfig(sampleRate: Int, channels: Int) { |
|
|
|
this.sampleRate = sampleRate |
|
|
|
this.channels = channels |
|
|
|
Log.d("tag", "音频配置已更新: 采样率=${sampleRate}Hz, 声道数=${channels}") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 获取左声道文件路径 |
|
|
|
* @return 左声道文件的绝对路径,如果不存在则返回null |
|
|
|
*/ |
|
|
|
internal fun getLeftChannelFilePath(): String? { |
|
|
|
return leftChannelFile?.absolutePath |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 获取右声道文件路径 |
|
|
|
* @return 右声道文件的绝对路径,如果不存在则返回null |
|
|
|
*/ |
|
|
|
internal fun getRightChannelFilePath(): String? { |
|
|
|
return rightChannelFile?.absolutePath |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 获取当前音频配置信息 |
|
|
|
* @return Map包含采样率和声道数信息 |
|
|
|
*/ |
|
|
|
internal fun getAudioConfig(): Map<String, Int> { |
|
|
|
return mapOf( |
|
|
|
"sampleRate" to sampleRate, |
|
|
|
"channels" to channels |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 检查是否为双声道模式 |
|
|
|
* @return true表示双声道,false表示单声道 |
|
|
|
*/ |
|
|
|
internal fun isStereoMode(): Boolean { |
|
|
|
return channels == 2 |
|
|
|
} |
|
|
|
|
|
|
|
// 修改:添加 filePath 参数 |
|
|
|
internal fun creatingFiles(filePath: String) { |
|
|
|
@ -54,6 +113,105 @@ object RecordFile { |
|
|
|
|
|
|
|
fos = FileOutputStream(currentAudioFile, true) |
|
|
|
startWriteThread() |
|
|
|
|
|
|
|
// 如果是双声道,创建左右声道文件 |
|
|
|
if (channels == 2) { |
|
|
|
createStereoChannelFiles(filePath) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 创建左右声道文件 |
|
|
|
*/ |
|
|
|
private fun createStereoChannelFiles(originalFilePath: String) { |
|
|
|
val originalFile = File(originalFilePath) |
|
|
|
val parentDir = originalFile.parentFile |
|
|
|
val nameWithoutExt = originalFile.nameWithoutExtension |
|
|
|
|
|
|
|
// 创建左声道文件 |
|
|
|
leftChannelFile = File(parentDir, "${nameWithoutExt}_left.wav") |
|
|
|
leftChannelFile?.createNewFile() |
|
|
|
leftChannelFos = FileOutputStream(leftChannelFile).apply { |
|
|
|
write(generateMonoWavHeader(0)) |
|
|
|
close() |
|
|
|
} |
|
|
|
leftChannelFos = FileOutputStream(leftChannelFile, true) |
|
|
|
|
|
|
|
// 创建右声道文件 |
|
|
|
rightChannelFile = File(parentDir, "${nameWithoutExt}_right.wav") |
|
|
|
rightChannelFile?.createNewFile() |
|
|
|
rightChannelFos = FileOutputStream(rightChannelFile).apply { |
|
|
|
write(generateMonoWavHeader(0)) |
|
|
|
close() |
|
|
|
} |
|
|
|
rightChannelFos = FileOutputStream(rightChannelFile, true) |
|
|
|
|
|
|
|
// 启动左右声道写入线程 |
|
|
|
startStereoWriteThreads() |
|
|
|
|
|
|
|
Log.d("tag", "双声道文件创建完成: ${leftChannelFile?.name}, ${rightChannelFile?.name}") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 生成单声道WAV文件头 |
|
|
|
*/ |
|
|
|
private fun generateMonoWavHeader(dataLength: Int): ByteArray { |
|
|
|
val totalLength = 36 + dataLength |
|
|
|
val bytesPerSample = 2 |
|
|
|
val monoChannels = 1 |
|
|
|
val byteRate = sampleRate * monoChannels * bytesPerSample |
|
|
|
val blockAlign = monoChannels * bytesPerSample |
|
|
|
|
|
|
|
return byteArrayOf( |
|
|
|
'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), |
|
|
|
(totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), |
|
|
|
((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), |
|
|
|
'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), |
|
|
|
'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), |
|
|
|
16, 0, 0, 0, |
|
|
|
1, 0, |
|
|
|
(monoChannels and 0xFF).toByte(), ((monoChannels shr 8) and 0xFF).toByte(), |
|
|
|
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), |
|
|
|
((sampleRate shr 16) and 0xFF).toByte(), ((sampleRate shr 24) and 0xFF).toByte(), |
|
|
|
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), |
|
|
|
((byteRate shr 16) and 0xFF).toByte(), ((byteRate shr 24) and 0xFF).toByte(), |
|
|
|
(blockAlign and 0xFF).toByte(), ((blockAlign shr 8) and 0xFF).toByte(), |
|
|
|
16, 0, |
|
|
|
'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), |
|
|
|
(dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), |
|
|
|
((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 启动左右声道写入线程 |
|
|
|
*/ |
|
|
|
private fun startStereoWriteThreads() { |
|
|
|
// 左声道写入线程 |
|
|
|
leftChannelWriteThread = Thread { |
|
|
|
try { |
|
|
|
while (isWriting.get() || leftChannelQueue.isNotEmpty()) { |
|
|
|
val data = leftChannelQueue.poll() ?: continue |
|
|
|
leftChannelFos?.write(data) |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e("tag", "左声道异步写入失败: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
leftChannelWriteThread?.start() |
|
|
|
|
|
|
|
// 右声道写入线程 |
|
|
|
rightChannelWriteThread = Thread { |
|
|
|
try { |
|
|
|
while (isWriting.get() || rightChannelQueue.isNotEmpty()) { |
|
|
|
val data = rightChannelQueue.poll() ?: continue |
|
|
|
rightChannelFos?.write(data) |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e("tag", "右声道异步写入失败: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
rightChannelWriteThread?.start() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -76,19 +234,65 @@ object RecordFile { |
|
|
|
|
|
|
|
/** |
|
|
|
* 保存音频数据到 WAV 文件(异步) |
|
|
|
* 如果是双声道,会自动拆分成左右声道分别保存 |
|
|
|
*/ |
|
|
|
internal fun saveAudioDataToWav(buffer: ByteArray) { |
|
|
|
if (fos == null || currentAudioFile == null) return |
|
|
|
// 放入队列,由写线程写入 |
|
|
|
writeQueue.offer(buffer.copyOf()) |
|
|
|
totalBytesWritten += buffer.size |
|
|
|
|
|
|
|
if (channels == 2) { |
|
|
|
// 双声道:拆分左右声道 |
|
|
|
splitStereoToMono(buffer) |
|
|
|
} else { |
|
|
|
// 单声道:直接保存 |
|
|
|
writeQueue.offer(buffer.copyOf()) |
|
|
|
totalBytesWritten += buffer.size |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 将双声道音频数据拆分成左右声道 |
|
|
|
* @param stereoBuffer 双声道音频数据(16位PCM,交错格式:L1R1L2R2...) |
|
|
|
*/ |
|
|
|
private fun splitStereoToMono(stereoBuffer: ByteArray) { |
|
|
|
if (stereoBuffer.size % 4 != 0) { |
|
|
|
Log.w("tag", "双声道音频数据长度不正确: ${stereoBuffer.size}") |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
val sampleCount = stereoBuffer.size / 4 // 每个样本4字节(左右声道各2字节) |
|
|
|
val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区 |
|
|
|
val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区 |
|
|
|
|
|
|
|
// 拆分交错的左右声道数据 |
|
|
|
for (i in 0 until sampleCount) { |
|
|
|
val stereoIndex = i * 4 |
|
|
|
val monoIndex = i * 2 |
|
|
|
|
|
|
|
// 左声道(低位字节在前,高位字节在后) |
|
|
|
leftBuffer[monoIndex] = stereoBuffer[stereoIndex] |
|
|
|
leftBuffer[monoIndex + 1] = stereoBuffer[stereoIndex + 1] |
|
|
|
|
|
|
|
// 右声道 |
|
|
|
rightBuffer[monoIndex] = stereoBuffer[stereoIndex + 2] |
|
|
|
rightBuffer[monoIndex + 1] = stereoBuffer[stereoIndex + 3] |
|
|
|
} |
|
|
|
|
|
|
|
// 将左右声道数据分别放入队列 |
|
|
|
leftChannelQueue.offer(leftBuffer) |
|
|
|
rightChannelQueue.offer(rightBuffer) |
|
|
|
leftChannelBytesWritten += leftBuffer.size |
|
|
|
rightChannelBytesWritten += rightBuffer.size |
|
|
|
|
|
|
|
Log.d("tag", "双声道拆分完成: 左声道${leftBuffer.size}字节, 右声道${rightBuffer.size}字节") |
|
|
|
} |
|
|
|
|
|
|
|
// 更新文件头生成(修正RIFF长度计算) |
|
|
|
|
|
|
|
// 更新文件头生成(支持可配置的采样率和声道数) |
|
|
|
private fun generateWavHeader(dataLength: Int): ByteArray { |
|
|
|
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 |
|
|
|
val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 |
|
|
|
val bytesPerSample = 2 // 16位PCM |
|
|
|
val byteRate = sampleRate * channels * bytesPerSample // 采样率 * 声道数 * 字节/样本 |
|
|
|
val blockAlign = channels * bytesPerSample // 声道数 * 字节/样本 |
|
|
|
|
|
|
|
return byteArrayOf( |
|
|
|
'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), |
|
|
|
@ -98,10 +302,12 @@ object RecordFile { |
|
|
|
'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), |
|
|
|
16, 0, 0, 0, // PCM头长度 |
|
|
|
1, 0, // PCM格式 |
|
|
|
1, 0, // 单声道 |
|
|
|
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 |
|
|
|
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 |
|
|
|
2, 0, // 块对齐 (通道数 * 样本位数/8) |
|
|
|
(channels and 0xFF).toByte(), ((channels shr 8) and 0xFF).toByte(), // 声道数 |
|
|
|
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), |
|
|
|
((sampleRate shr 16) and 0xFF).toByte(), ((sampleRate shr 24) and 0xFF).toByte(), // 采样率 |
|
|
|
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), |
|
|
|
((byteRate shr 16) and 0xFF).toByte(), ((byteRate shr 24) and 0xFF).toByte(), // 字节率 |
|
|
|
(blockAlign and 0xFF).toByte(), ((blockAlign shr 8) and 0xFF).toByte(), // 块对齐 |
|
|
|
16, 0, // 样本位数 |
|
|
|
'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), |
|
|
|
(dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), |
|
|
|
@ -225,31 +431,92 @@ object RecordFile { |
|
|
|
writeThread?.join(500) |
|
|
|
fos?.close() |
|
|
|
|
|
|
|
var mainFileSuccess = false |
|
|
|
var leftChannelSuccess = false |
|
|
|
var rightChannelSuccess = false |
|
|
|
|
|
|
|
// 处理主文件 |
|
|
|
currentAudioFile?.let { file -> |
|
|
|
if (file.length() <= 44 || isSave == false) { |
|
|
|
file.delete() |
|
|
|
Log.d("tag", "音频文件过小已删除: ${file.absolutePath}") |
|
|
|
return false |
|
|
|
} else { |
|
|
|
RandomAccessFile(file, "rw").use { raf -> |
|
|
|
raf.seek(0) |
|
|
|
raf.write(generateWavHeader(totalBytesWritten)) |
|
|
|
} |
|
|
|
Log.d("tag", "音频文件保存完成: ${file.absolutePath}") |
|
|
|
|
|
|
|
// 保存成功文件引用 |
|
|
|
lastSavedFile = file |
|
|
|
return true |
|
|
|
mainFileSuccess = true |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 处理左右声道文件(如果是双声道) |
|
|
|
if (channels == 2) { |
|
|
|
leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave) |
|
|
|
rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave) |
|
|
|
|
|
|
|
// 等待左右声道写入线程结束 |
|
|
|
leftChannelWriteThread?.join(500) |
|
|
|
rightChannelWriteThread?.join(500) |
|
|
|
} |
|
|
|
|
|
|
|
return if (channels == 2) { |
|
|
|
leftChannelSuccess && rightChannelSuccess |
|
|
|
} else { |
|
|
|
mainFileSuccess |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e("tag", "更新WAV文件头失败: ${e.message}") |
|
|
|
return false |
|
|
|
} finally { |
|
|
|
// 清理资源 |
|
|
|
fos = null |
|
|
|
currentAudioFile = null |
|
|
|
writeThread = null |
|
|
|
|
|
|
|
// 清理双声道资源 |
|
|
|
leftChannelFos = null |
|
|
|
rightChannelFos = null |
|
|
|
leftChannelFile = null |
|
|
|
rightChannelFile = null |
|
|
|
leftChannelWriteThread = null |
|
|
|
rightChannelWriteThread = null |
|
|
|
leftChannelBytesWritten = 0 |
|
|
|
rightChannelBytesWritten = 0 |
|
|
|
totalBytesWritten = 0 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 关闭单个声道文件 |
|
|
|
*/ |
|
|
|
private fun closeStereoChannelFile( |
|
|
|
channelFile: File?, |
|
|
|
channelFos: FileOutputStream?, |
|
|
|
bytesWritten: Int, |
|
|
|
channelName: String, |
|
|
|
isSave: Boolean |
|
|
|
): Boolean { |
|
|
|
return try { |
|
|
|
channelFos?.close() |
|
|
|
channelFile?.let { file -> |
|
|
|
if (file.length() <= 44 || isSave == false) { |
|
|
|
file.delete() |
|
|
|
Log.d("tag", "${channelName}文件过小已删除: ${file.absolutePath}") |
|
|
|
false |
|
|
|
} else { |
|
|
|
RandomAccessFile(file, "rw").use { raf -> |
|
|
|
raf.seek(0) |
|
|
|
raf.write(generateMonoWavHeader(bytesWritten)) |
|
|
|
} |
|
|
|
Log.d("tag", "${channelName}文件保存完成: ${file.absolutePath}") |
|
|
|
true |
|
|
|
} |
|
|
|
} ?: false |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e("tag", "${channelName}文件关闭失败: ${e.message}") |
|
|
|
false |
|
|
|
} |
|
|
|
return false |
|
|
|
} |
|
|
|
} |