|
|
|
@ -1,4 +1,5 @@ |
|
|
|
package com.yunqiinnovation.azure_speech.tools |
|
|
|
|
|
|
|
import java.io.File |
|
|
|
import java.io.FileOutputStream |
|
|
|
import java.io.RandomAccessFile |
|
|
|
@ -8,41 +9,41 @@ import java.io.FileInputStream |
|
|
|
import java.util.concurrent.LinkedBlockingQueue |
|
|
|
import java.util.concurrent.atomic.AtomicBoolean |
|
|
|
|
|
|
|
object RecordFile { |
|
|
|
private var currentAudioFile: File? = null |
|
|
|
private var fos: FileOutputStream? = null |
|
|
|
object RecordFile { |
|
|
|
private var currentAudioFile: File? = null |
|
|
|
private var fos: FileOutputStream? = null |
|
|
|
|
|
|
|
// 新增:用于异步写入的队列和线程 |
|
|
|
private val writeQueue = LinkedBlockingQueue<ByteArray>() |
|
|
|
private val isWriting = AtomicBoolean(false) |
|
|
|
private var writeThread: Thread? = null |
|
|
|
// 新增:用于异步写入的队列和线程 |
|
|
|
private val writeQueue = LinkedBlockingQueue<ByteArray>() |
|
|
|
private val isWriting = AtomicBoolean(false) |
|
|
|
private var writeThread: Thread? = null |
|
|
|
|
|
|
|
// 用于存储音频数据的缓冲区 |
|
|
|
private val dataBuffer = mutableListOf<ByteArray>() |
|
|
|
private var totalBytesWritten = 0 |
|
|
|
// 用于存储音频数据的缓冲区 |
|
|
|
private val dataBuffer = mutableListOf<ByteArray>() |
|
|
|
private var totalBytesWritten = 0 |
|
|
|
|
|
|
|
// 音频配置 |
|
|
|
private val sampleRate = 16000 |
|
|
|
// 音频配置 |
|
|
|
private val sampleRate = 16000 |
|
|
|
|
|
|
|
// 新增:用于存储最后成功保存的文件 |
|
|
|
private var lastSavedFile: File? = null |
|
|
|
private var lastFileTimestamp: String = "" |
|
|
|
var isPause = false |
|
|
|
var fileName = "" |
|
|
|
// 新增:用于存储最后成功保存的文件 |
|
|
|
private var lastSavedFile: File? = null |
|
|
|
private var lastFileTimestamp: String = "" |
|
|
|
var isPause = false |
|
|
|
var fileName = "" |
|
|
|
|
|
|
|
|
|
|
|
// 修改:添加 filePath 参数 |
|
|
|
// 修改:添加 filePath 参数 |
|
|
|
internal fun creatingFiles(filePath: String) { |
|
|
|
if (fos != null || currentAudioFile != null) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val file = File(filePath) |
|
|
|
val parentDir = file.parentFile |
|
|
|
if (!parentDir.exists()) { |
|
|
|
parentDir.mkdirs() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
currentAudioFile = file |
|
|
|
currentAudioFile?.createNewFile() |
|
|
|
|
|
|
|
@ -56,59 +57,59 @@ import java.util.concurrent.atomic.AtomicBoolean |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 新增:启动写入线程 |
|
|
|
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("tag", "异步写入音频数据失败: ${e.message}") |
|
|
|
// 新增:启动写入线程 |
|
|
|
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("tag", "异步写入音频数据失败: ${e.message}") |
|
|
|
} |
|
|
|
writeThread?.start() |
|
|
|
} |
|
|
|
writeThread?.start() |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 保存音频数据到 WAV 文件(异步) |
|
|
|
*/ |
|
|
|
internal fun saveAudioDataToWav(buffer: ByteArray) { |
|
|
|
if (fos == null || currentAudioFile == null) return |
|
|
|
// 放入队列,由写线程写入 |
|
|
|
writeQueue.offer(buffer.copyOf()) |
|
|
|
totalBytesWritten += buffer.size |
|
|
|
} |
|
|
|
/** |
|
|
|
* 保存音频数据到 WAV 文件(异步) |
|
|
|
*/ |
|
|
|
internal fun saveAudioDataToWav(buffer: ByteArray) { |
|
|
|
if (fos == null || currentAudioFile == null) return |
|
|
|
// 放入队列,由写线程写入 |
|
|
|
writeQueue.offer(buffer.copyOf()) |
|
|
|
totalBytesWritten += buffer.size |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 更新文件头生成(修正RIFF长度计算) |
|
|
|
private fun generateWavHeader(dataLength: Int): ByteArray { |
|
|
|
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 |
|
|
|
val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 |
|
|
|
|
|
|
|
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, // 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) |
|
|
|
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() |
|
|
|
) |
|
|
|
} |
|
|
|
// 更新文件头生成(修正RIFF长度计算) |
|
|
|
private fun generateWavHeader(dataLength: Int): ByteArray { |
|
|
|
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 |
|
|
|
val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 |
|
|
|
|
|
|
|
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, // 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) |
|
|
|
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() |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* 重命名指定路径的音频文件 |
|
|
|
* @param filePath 要重命名的文件完整路径 |
|
|
|
* @param newName 新的文件名(不含扩展名) |
|
|
|
@ -116,28 +117,28 @@ import java.util.concurrent.atomic.AtomicBoolean |
|
|
|
*/ |
|
|
|
internal fun renameFile(filePath: String, newName: String): Boolean { |
|
|
|
val sourceFile = File(filePath) |
|
|
|
|
|
|
|
|
|
|
|
// 验证源文件存在 |
|
|
|
if (!sourceFile.exists()) { |
|
|
|
Log.w("tag", "重命名失败: 文件不存在 $filePath") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 防止重命名正在录制的文件 |
|
|
|
if (sourceFile == currentAudioFile) { |
|
|
|
Log.w("tag", "重命名失败: 不能重命名正在录制的文件") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val parentDir = sourceFile.parentFile |
|
|
|
val newFile = File(parentDir, "$newName.wav") // 保持.wav扩展名 |
|
|
|
|
|
|
|
|
|
|
|
// 处理目标文件已存在的情况 |
|
|
|
if (newFile.exists()) { |
|
|
|
Log.w("tag", "重命名失败: 目标文件已存在 ${newFile.absolutePath}") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 执行重命名 |
|
|
|
return try { |
|
|
|
val success = sourceFile.renameTo(newFile) |
|
|
|
@ -156,7 +157,8 @@ import java.util.concurrent.atomic.AtomicBoolean |
|
|
|
false |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
|
|
|
|
/** |
|
|
|
* 移动文件到新路径 |
|
|
|
* @param sourcePath 原始文件完整路径 |
|
|
|
* @param destPath 目标文件完整路径 |
|
|
|
@ -165,25 +167,25 @@ import java.util.concurrent.atomic.AtomicBoolean |
|
|
|
internal fun moveFile(sourcePath: String, destPath: String): Boolean { |
|
|
|
val sourceFile = File(sourcePath) |
|
|
|
val destFile = File(destPath) |
|
|
|
|
|
|
|
|
|
|
|
if (!sourceFile.exists()) { |
|
|
|
Log.w("tag", "移动失败: 源文件不存在 ${sourceFile.absolutePath}") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 确保目标目录存在 |
|
|
|
val destDir = destFile.parentFile |
|
|
|
if (!destDir.exists() && !destDir.mkdirs()) { |
|
|
|
Log.w("tag", "移动失败: 无法创建目标目录 ${destDir.absolutePath}") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 删除已存在的目标文件 |
|
|
|
if (destFile.exists() && !destFile.delete()) { |
|
|
|
Log.w("tag", "移动失败: 无法覆盖已存在文件 ${destFile.absolutePath}") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return try { |
|
|
|
// 使用renameTo尝试移动(同分区最快) |
|
|
|
if (sourceFile.renameTo(destFile)) { |
|
|
|
@ -213,39 +215,40 @@ import java.util.concurrent.atomic.AtomicBoolean |
|
|
|
false |
|
|
|
} |
|
|
|
} |
|
|
|
internal fun closeFile( isSave:Boolean): Boolean { |
|
|
|
try { |
|
|
|
if (fos == null || currentAudioFile == null) { |
|
|
|
|
|
|
|
internal fun closeFile(isSave: Boolean): Boolean { |
|
|
|
try { |
|
|
|
if (fos == null || currentAudioFile == null) { |
|
|
|
return false |
|
|
|
} |
|
|
|
isWriting.set(false) |
|
|
|
writeThread?.join(500) |
|
|
|
fos?.close() |
|
|
|
|
|
|
|
currentAudioFile?.let { file -> |
|
|
|
if (file.length() <= 44 || isSave == false) { |
|
|
|
file.delete() |
|
|
|
Log.d("tag", "音频文件过小已删除: ${file.absolutePath}") |
|
|
|
return false |
|
|
|
} |
|
|
|
isWriting.set(false) |
|
|
|
writeThread?.join(500) |
|
|
|
fos?.close() |
|
|
|
|
|
|
|
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 |
|
|
|
} else { |
|
|
|
RandomAccessFile(file, "rw").use { raf -> |
|
|
|
raf.seek(0) |
|
|
|
raf.write(generateWavHeader(totalBytesWritten)) |
|
|
|
} |
|
|
|
Log.d("tag", "音频文件保存完成: ${file.absolutePath}") |
|
|
|
|
|
|
|
// 保存成功文件引用 |
|
|
|
lastSavedFile = file |
|
|
|
return true |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e("tag", "更新WAV文件头失败: ${e.message}") |
|
|
|
return false |
|
|
|
} finally { |
|
|
|
fos = null |
|
|
|
currentAudioFile = null |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e("tag", "更新WAV文件头失败: ${e.message}") |
|
|
|
return false |
|
|
|
} finally { |
|
|
|
fos = null |
|
|
|
currentAudioFile = null |
|
|
|
} |
|
|
|
} |
|
|
|
return false |
|
|
|
} |
|
|
|
} |