|
|
@ -28,6 +28,7 @@ import java.util.concurrent.TimeUnit |
|
|
import java.io.File |
|
|
import java.io.File |
|
|
import java.io.FileOutputStream |
|
|
import java.io.FileOutputStream |
|
|
import java.io.RandomAccessFile |
|
|
import java.io.RandomAccessFile |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 |
|
|
* Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 |
|
|
* |
|
|
* |
|
|
@ -53,12 +54,16 @@ class AzureAsrHelper(private val context: Context) { |
|
|
private var region = "" |
|
|
private var region = "" |
|
|
|
|
|
|
|
|
// 音频源配置 |
|
|
// 音频源配置 |
|
|
private var audioSourceType = AudioSourceType.MICROPHONE |
|
|
var audioSourceType = AudioSourceType.MICROPHONE |
|
|
|
|
|
|
|
|
// 音频处理 |
|
|
// 音频处理 |
|
|
private var microphoneStream: MicrophoneStream? = null |
|
|
private var microphoneStream: MicrophoneStream? = null |
|
|
private var externalAudioStream: ExternalAudioPullStream? = null |
|
|
private var externalAudioStream: ExternalAudioPullStream? = null |
|
|
|
|
|
|
|
|
|
|
|
// 录音文件处理 |
|
|
|
|
|
private var recordfile: RecordingFile? = null |
|
|
|
|
|
private var isRecording = false |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 音频来源类型 |
|
|
* 音频来源类型 |
|
|
*/ |
|
|
*/ |
|
|
@ -130,7 +135,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
// 设置分段策略为时间模式 |
|
|
// 设置分段策略为时间模式 |
|
|
setProperty("Speech_SegmentationStrategy", "Time") |
|
|
setProperty("Speech_SegmentationStrategy", "Time") |
|
|
} |
|
|
} |
|
|
|
|
|
// 录音文件类 |
|
|
|
|
|
recordfile = RecordingFile() |
|
|
// 创建识别器 |
|
|
// 创建识别器 |
|
|
return setupRecognizer() |
|
|
return setupRecognizer() |
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
@ -152,7 +158,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
when (audioSourceType) { |
|
|
when (audioSourceType) { |
|
|
AudioSourceType.MICROPHONE -> { |
|
|
AudioSourceType.MICROPHONE -> { |
|
|
// 判断是否使用手机扬声器输出和耳机连接状态 |
|
|
// 判断是否使用手机扬声器输出和耳机连接状态 |
|
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
|
|
val audioManager = |
|
|
|
|
|
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
|
|
val isSpeakerphoneOn = audioManager.isSpeakerphoneOn |
|
|
val isSpeakerphoneOn = audioManager.isSpeakerphoneOn |
|
|
|
|
|
|
|
|
// 检查是否连接了有线耳机 |
|
|
// 检查是否连接了有线耳机 |
|
|
@ -178,6 +185,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
audioConfig = AudioConfig.fromDefaultMicrophoneInput() |
|
|
audioConfig = AudioConfig.fromDefaultMicrophoneInput() |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
AudioSourceType.EXTERNAL -> { |
|
|
AudioSourceType.EXTERNAL -> { |
|
|
// 改用拉流方式处理外部音频 |
|
|
// 改用拉流方式处理外部音频 |
|
|
setupExternalAudioStream() |
|
|
setupExternalAudioStream() |
|
|
@ -186,7 +194,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
|
|
|
|
|
// 创建识别器 |
|
|
// 创建识别器 |
|
|
recognizer = if (isAutoDetectLanguage) { |
|
|
recognizer = if (isAutoDetectLanguage) { |
|
|
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) |
|
|
val autoDetectConfig = |
|
|
|
|
|
AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) |
|
|
SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig) |
|
|
SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig) |
|
|
} else { |
|
|
} else { |
|
|
SpeechRecognizer(speechConfig, audioConfig) |
|
|
SpeechRecognizer(speechConfig, audioConfig) |
|
|
@ -245,6 +254,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
|
|
|
|
|
// 使用拉流模式,将数据推入队列 |
|
|
// 使用拉流模式,将数据推入队列 |
|
|
externalAudioStream?.pushAudio(data) |
|
|
externalAudioStream?.pushAudio(data) |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
@ -252,7 +262,10 @@ class AzureAsrHelper(private val context: Context) { |
|
|
* |
|
|
* |
|
|
* @param callback 识别结果回调 |
|
|
* @param callback 识别结果回调 |
|
|
*/ |
|
|
*/ |
|
|
fun recognizeOnce(callback: RecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE) { |
|
|
fun recognizeOnce( |
|
|
|
|
|
callback: RecognizeCallback, |
|
|
|
|
|
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE |
|
|
|
|
|
) { |
|
|
if (speechConfig == null) { |
|
|
if (speechConfig == null) { |
|
|
callback.onError("语音服务未初始化") |
|
|
callback.onError("语音服务未初始化") |
|
|
return |
|
|
return |
|
|
@ -280,7 +293,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
stopAudioProcessing() |
|
|
stopAudioProcessing() |
|
|
|
|
|
|
|
|
if (result != null && result.reason == ResultReason.RecognizedSpeech) { |
|
|
if (result != null && result.reason == ResultReason.RecognizedSpeech) { |
|
|
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language ?: supportedLanguages[0] |
|
|
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language |
|
|
|
|
|
?: supportedLanguages[0] |
|
|
callback.onResult(result.text, detectedLanguage) |
|
|
callback.onResult(result.text, detectedLanguage) |
|
|
} else { |
|
|
} else { |
|
|
callback.onError("未能识别语音") |
|
|
callback.onError("未能识别语音") |
|
|
@ -297,7 +311,10 @@ class AzureAsrHelper(private val context: Context) { |
|
|
* @param callback 连续识别结果回调 |
|
|
* @param callback 连续识别结果回调 |
|
|
* @return 是否成功开始识别 |
|
|
* @return 是否成功开始识别 |
|
|
*/ |
|
|
*/ |
|
|
fun startContinuousRecognition(callback: ContinuousRecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE): Boolean { |
|
|
fun startContinuousRecognition( |
|
|
|
|
|
callback: ContinuousRecognizeCallback, |
|
|
|
|
|
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE |
|
|
|
|
|
): Boolean { |
|
|
if (speechConfig == null) { |
|
|
if (speechConfig == null) { |
|
|
callback.onError("语音服务未初始化") |
|
|
callback.onError("语音服务未初始化") |
|
|
return false |
|
|
return false |
|
|
@ -323,7 +340,10 @@ class AzureAsrHelper(private val context: Context) { |
|
|
// 开始连续识别 |
|
|
// 开始连续识别 |
|
|
recognizer?.startContinuousRecognitionAsync() |
|
|
recognizer?.startContinuousRecognitionAsync() |
|
|
isContinuousRecognitionActive = true |
|
|
isContinuousRecognitionActive = true |
|
|
|
|
|
Log.e(tag, "开始连续语音识别 是否录音${isRecording}") |
|
|
|
|
|
if (isRecording) { |
|
|
|
|
|
enableRecord() |
|
|
|
|
|
} |
|
|
return true |
|
|
return true |
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
stopAudioProcessing() |
|
|
stopAudioProcessing() |
|
|
@ -340,7 +360,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
// 识别中事件 |
|
|
// 识别中事件 |
|
|
recognizer?.recognizing?.addEventListener( |
|
|
recognizer?.recognizing?.addEventListener( |
|
|
EventHandler<SpeechRecognitionEventArgs> { _, event -> |
|
|
EventHandler<SpeechRecognitionEventArgs> { _, event -> |
|
|
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" |
|
|
val detectedLanguage = |
|
|
|
|
|
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" |
|
|
// 直接在当前线程调用回调 |
|
|
// 直接在当前线程调用回调 |
|
|
callback.onRecognizing(event.result.text, detectedLanguage) |
|
|
callback.onRecognizing(event.result.text, detectedLanguage) |
|
|
} |
|
|
} |
|
|
@ -350,7 +371,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
recognizer?.recognized?.addEventListener( |
|
|
recognizer?.recognized?.addEventListener( |
|
|
EventHandler<SpeechRecognitionEventArgs> { _, event -> |
|
|
EventHandler<SpeechRecognitionEventArgs> { _, event -> |
|
|
if (event.result.reason == ResultReason.RecognizedSpeech) { |
|
|
if (event.result.reason == ResultReason.RecognizedSpeech) { |
|
|
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" |
|
|
val detectedLanguage = |
|
|
|
|
|
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" |
|
|
// 直接在当前线程调用回调 |
|
|
// 直接在当前线程调用回调 |
|
|
callback.onResult(event.result.text, detectedLanguage) |
|
|
callback.onResult(event.result.text, detectedLanguage) |
|
|
} |
|
|
} |
|
|
@ -416,7 +438,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
|
|
|
|
|
// 停止音频处理 |
|
|
// 停止音频处理 |
|
|
stopAudioProcessing() |
|
|
stopAudioProcessing() |
|
|
|
|
|
// 停止录音 |
|
|
|
|
|
recordfile!!.closeFile() |
|
|
// 会话结束事件会设置isContinuousRecognitionActive = false |
|
|
// 会话结束事件会设置isContinuousRecognitionActive = false |
|
|
return true |
|
|
return true |
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
@ -426,7 +449,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
|
|
|
|
|
// 停止音频处理 |
|
|
// 停止音频处理 |
|
|
stopAudioProcessing() |
|
|
stopAudioProcessing() |
|
|
|
|
|
recordfile!!.closeFile() |
|
|
// 尝试强制关闭识别器 |
|
|
// 尝试强制关闭识别器 |
|
|
try { |
|
|
try { |
|
|
recognizer?.close() |
|
|
recognizer?.close() |
|
|
@ -458,7 +481,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
|
|
|
|
|
// 停止音频处理 |
|
|
// 停止音频处理 |
|
|
stopAudioProcessing() |
|
|
stopAudioProcessing() |
|
|
|
|
|
// 停止录音 |
|
|
|
|
|
recordfile!!.closeFile() |
|
|
// 释放recognizer |
|
|
// 释放recognizer |
|
|
recognizer?.close() |
|
|
recognizer?.close() |
|
|
recognizer = null |
|
|
recognizer = null |
|
|
@ -500,6 +524,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
// 拉流模式不需要额外启动,SDK会自动拉取数据 |
|
|
// 拉流模式不需要额外启动,SDK会自动拉取数据 |
|
|
// 无需执行任何操作 |
|
|
// 无需执行任何操作 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
AudioSourceType.EXTERNAL -> { |
|
|
AudioSourceType.EXTERNAL -> { |
|
|
// 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData |
|
|
// 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData |
|
|
} |
|
|
} |
|
|
@ -531,6 +556,30 @@ class AzureAsrHelper(private val context: Context) { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 开启录音 |
|
|
|
|
|
*/ |
|
|
|
|
|
fun enableRecord() { |
|
|
|
|
|
Log.i(tag, "开启录音:") |
|
|
|
|
|
isRecording = true |
|
|
|
|
|
recordfile!!.closeFile() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isContinuousRecognitionActive) { |
|
|
|
|
|
recordfile!!.creatingFiles() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 停止录音 |
|
|
|
|
|
*/ |
|
|
|
|
|
fun stopRecord() { |
|
|
|
|
|
Log.i(tag, "停止连续录音:") |
|
|
|
|
|
isRecording = false |
|
|
|
|
|
recordfile!!.closeFile() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 麦克风流 - 拉流模式 |
|
|
* 麦克风流 - 拉流模式 |
|
|
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据 |
|
|
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据 |
|
|
@ -538,12 +587,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
private inner class MicrophoneStream : PullAudioInputStreamCallback() { |
|
|
private inner class MicrophoneStream : PullAudioInputStreamCallback() { |
|
|
private var audioRecord: AudioRecord? = null |
|
|
private var audioRecord: AudioRecord? = null |
|
|
private var echoCanceler: AcousticEchoCanceler? = null |
|
|
private var echoCanceler: AcousticEchoCanceler? = null |
|
|
private var currentAudioFile: File? = null |
|
|
|
|
|
private var fos: FileOutputStream? = null |
|
|
|
|
|
|
|
|
|
|
|
// 用于存储音频数据的缓冲区 |
|
|
|
|
|
private val dataBuffer = mutableListOf<ByteArray>() |
|
|
|
|
|
private var totalBytesWritten = 0 |
|
|
|
|
|
// 音频配置 |
|
|
// 音频配置 |
|
|
private val sampleRate = 16000 |
|
|
private val sampleRate = 16000 |
|
|
private val channelConfig = AudioFormat.CHANNEL_IN_MONO |
|
|
private val channelConfig = AudioFormat.CHANNEL_IN_MONO |
|
|
@ -561,20 +605,6 @@ class AzureAsrHelper(private val context: Context) { |
|
|
*/ |
|
|
*/ |
|
|
private fun initMicrophone() { |
|
|
private fun initMicrophone() { |
|
|
try { |
|
|
try { |
|
|
// 创建新的音频文件 |
|
|
|
|
|
val timestamp = System.currentTimeMillis() |
|
|
|
|
|
val filePath = context.getExternalFilesDir(null)?.absolutePath + "/recorded_audio_$timestamp.wav" |
|
|
|
|
|
currentAudioFile = File(filePath) |
|
|
|
|
|
currentAudioFile?.createNewFile() |
|
|
|
|
|
|
|
|
|
|
|
// 仅写入初始文件头 |
|
|
|
|
|
fos = FileOutputStream(currentAudioFile).apply { |
|
|
|
|
|
write(generateWavHeader(0)) // 初始0长度 |
|
|
|
|
|
close() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 追加音频数据到文件 |
|
|
|
|
|
fos = FileOutputStream(currentAudioFile, true) |
|
|
|
|
|
// 创建录音对象 |
|
|
// 创建录音对象 |
|
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { |
|
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { |
|
|
val format = AudioFormat.Builder() |
|
|
val format = AudioFormat.Builder() |
|
|
@ -632,7 +662,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
/** |
|
|
/** |
|
|
* 获取音频格式 |
|
|
* 获取音频格式 |
|
|
*/ |
|
|
*/ |
|
|
fun getFormat(): AudioStreamFormat = AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort()) |
|
|
fun getFormat(): AudioStreamFormat = |
|
|
|
|
|
AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort()) |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Azure SDK 调用此方法获取音频数据 |
|
|
* Azure SDK 调用此方法获取音频数据 |
|
|
@ -645,80 +676,23 @@ class AzureAsrHelper(private val context: Context) { |
|
|
Log.e(tag, "读取音频数据失败: $result") |
|
|
Log.e(tag, "读取音频数据失败: $result") |
|
|
return 0 |
|
|
return 0 |
|
|
} |
|
|
} |
|
|
|
|
|
Log.i(tag, "麦克风:") |
|
|
// 将音频数据保存成wav格式的音频文件 |
|
|
// 将音频数据保存成wav格式的音频文件 |
|
|
saveAudioDataToWav(buffer, result) |
|
|
recordfile?.saveAudioDataToWav(buffer) |
|
|
return result |
|
|
return result |
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
Log.e(tag, "读取音频异常: ${e.message}") |
|
|
Log.e(tag, "读取音频异常: ${e.message}") |
|
|
return 0 |
|
|
return 0 |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
/** |
|
|
|
|
|
* 保存音频数据到 WAV 文件 |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun saveAudioDataToWav(buffer: ByteArray, length: Int) { |
|
|
|
|
|
// 复制数据到新数组 |
|
|
|
|
|
val copy = ByteArray(length) |
|
|
|
|
|
System.arraycopy(buffer, 0, copy, 0, length) |
|
|
|
|
|
|
|
|
|
|
|
// 添加到缓冲区 |
|
|
|
|
|
dataBuffer.add(copy) |
|
|
|
|
|
totalBytesWritten += length |
|
|
|
|
|
|
|
|
|
|
|
// 写入文件 |
|
|
|
|
|
try { |
|
|
|
|
|
fos?.write(copy, 0, length) |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(tag, "写入音频数据失败: ${e.message}") |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 更新文件头生成(修正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() |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
/** |
|
|
/** |
|
|
* 关闭音频资源 |
|
|
* 关闭音频资源 |
|
|
*/ |
|
|
*/ |
|
|
override fun close() { |
|
|
override fun close() { |
|
|
releaseAudioResources() |
|
|
releaseAudioResources() |
|
|
try { |
|
|
|
|
|
// 关闭文件流 |
|
|
|
|
|
fos?.close() |
|
|
|
|
|
|
|
|
|
|
|
// 更新WAV文件头 |
|
|
|
|
|
currentAudioFile?.let { file -> |
|
|
|
|
|
RandomAccessFile(file, "rw").use { raf -> |
|
|
|
|
|
raf.seek(0) |
|
|
|
|
|
raf.write(generateWavHeader(totalBytesWritten)) |
|
|
|
|
|
} |
|
|
|
|
|
Log.d(tag, "音频文件保存完成: ${file.absolutePath}") |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(tag, "更新WAV文件头失败: ${e.message}") |
|
|
|
|
|
} finally { |
|
|
|
|
|
fos = null |
|
|
|
|
|
currentAudioFile = null |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
@ -757,6 +731,8 @@ private fun saveAudioDataToWav(buffer: ByteArray, length: Int) { |
|
|
* @param data 音频数据 |
|
|
* @param data 音频数据 |
|
|
*/ |
|
|
*/ |
|
|
fun pushAudio(data: ByteArray) { |
|
|
fun pushAudio(data: ByteArray) { |
|
|
|
|
|
// 将音频数据保存成wav格式的音频文件 |
|
|
|
|
|
recordfile?.saveAudioDataToWav(data) |
|
|
if (!closed) { |
|
|
if (!closed) { |
|
|
queue.offer(data) |
|
|
queue.offer(data) |
|
|
} |
|
|
} |
|
|
@ -769,10 +745,12 @@ private fun saveAudioDataToWav(buffer: ByteArray, length: Int) { |
|
|
*/ |
|
|
*/ |
|
|
override fun read(buffer: ByteArray): Int { |
|
|
override fun read(buffer: ByteArray): Int { |
|
|
try { |
|
|
try { |
|
|
|
|
|
|
|
|
// 阻塞等待下一块数据 |
|
|
// 阻塞等待下一块数据 |
|
|
val chunk = queue.take() |
|
|
val chunk = queue.take() |
|
|
val toCopy = minOf(chunk.size, buffer.size) |
|
|
val toCopy = minOf(chunk.size, buffer.size) |
|
|
System.arraycopy(chunk, 0, buffer, 0, toCopy) |
|
|
System.arraycopy(chunk, 0, buffer, 0, toCopy) |
|
|
|
|
|
Log.i(tag, "外部音频:") |
|
|
return toCopy |
|
|
return toCopy |
|
|
} catch (e: InterruptedException) { |
|
|
} catch (e: InterruptedException) { |
|
|
Thread.currentThread().interrupt() |
|
|
Thread.currentThread().interrupt() |
|
|
@ -857,4 +835,111 @@ private fun saveAudioDataToWav(buffer: ByteArray, length: Int) { |
|
|
*/ |
|
|
*/ |
|
|
fun onError(error: String) |
|
|
fun onError(error: String) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private inner class RecordingFile { |
|
|
|
|
|
private var currentAudioFile: File? = null |
|
|
|
|
|
private var fos: FileOutputStream? = null |
|
|
|
|
|
|
|
|
|
|
|
// 用于存储音频数据的缓冲区 |
|
|
|
|
|
private val dataBuffer = mutableListOf<ByteArray>() |
|
|
|
|
|
private var totalBytesWritten = 0 |
|
|
|
|
|
|
|
|
|
|
|
// 音频配置 |
|
|
|
|
|
private val sampleRate = 16000 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
internal fun creatingFiles() { |
|
|
|
|
|
if (fos != null || currentAudioFile != null) { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
// 创建新的音频文件 |
|
|
|
|
|
val timestamp = System.currentTimeMillis() |
|
|
|
|
|
val filePath = |
|
|
|
|
|
context.getExternalFilesDir(null)?.absolutePath + "/recorded_audio_$timestamp.wav" |
|
|
|
|
|
currentAudioFile = File(filePath) |
|
|
|
|
|
currentAudioFile?.createNewFile() |
|
|
|
|
|
|
|
|
|
|
|
// 仅写入初始文件头 |
|
|
|
|
|
fos = FileOutputStream(currentAudioFile).apply { |
|
|
|
|
|
write(generateWavHeader(0)) // 初始0长度 |
|
|
|
|
|
close() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 追加音频数据到文件 |
|
|
|
|
|
fos = FileOutputStream(currentAudioFile, true) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 保存音频数据到 WAV 文件 |
|
|
|
|
|
*/ |
|
|
|
|
|
internal fun saveAudioDataToWav(buffer: ByteArray) { |
|
|
|
|
|
if (fos == null || currentAudioFile == null) { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
// 复制数据到新数组 |
|
|
|
|
|
val copy = ByteArray(buffer.size) |
|
|
|
|
|
System.arraycopy(buffer, 0, copy, 0, buffer.size) |
|
|
|
|
|
|
|
|
|
|
|
// 添加到缓冲区 |
|
|
|
|
|
dataBuffer.add(copy) |
|
|
|
|
|
totalBytesWritten += buffer.size |
|
|
|
|
|
|
|
|
|
|
|
// 写入文件 |
|
|
|
|
|
try { |
|
|
|
|
|
fos?.write(copy, 0, buffer.size) |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(tag, "写入音频数据失败: ${e.message}") |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 更新文件头生成(修正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() |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
internal fun closeFile() { |
|
|
|
|
|
try { |
|
|
|
|
|
if (fos == null || currentAudioFile == null) { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
// 关闭文件流 |
|
|
|
|
|
fos?.close() |
|
|
|
|
|
|
|
|
|
|
|
// 更新WAV文件头 |
|
|
|
|
|
currentAudioFile?.let { file -> |
|
|
|
|
|
RandomAccessFile(file, "rw").use { raf -> |
|
|
|
|
|
raf.seek(0) |
|
|
|
|
|
raf.write(generateWavHeader(totalBytesWritten)) |
|
|
|
|
|
} |
|
|
|
|
|
Log.d(tag, "音频文件保存完成: ${file.absolutePath}") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(tag, "更新WAV文件头失败: ${e.message}") |
|
|
|
|
|
} finally { |
|
|
|
|
|
fos = null |
|
|
|
|
|
currentAudioFile = null |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |