|
|
|
@ -30,6 +30,10 @@ import java.io.FileOutputStream |
|
|
|
import java.io.RandomAccessFile |
|
|
|
import java.text.SimpleDateFormat |
|
|
|
import java.util.Date |
|
|
|
|
|
|
|
import android.os.Handler |
|
|
|
import android.os.HandlerThread |
|
|
|
|
|
|
|
/** |
|
|
|
* Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 |
|
|
|
* |
|
|
|
@ -128,15 +132,15 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
// 设置指定的识别语言 |
|
|
|
speechRecognitionLanguage = currentLanguage |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") |
|
|
|
setProperty("Speech_SegmentationSilenceTimeoutMs", "300") |
|
|
|
|
|
|
|
|
|
|
|
// 优化:添加低延迟连接配置 |
|
|
|
setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "200") |
|
|
|
setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE") |
|
|
|
setProperty("Speech_PushStreamFormat", "PCM") |
|
|
|
|
|
|
|
|
|
|
|
// 设置分段策略为时间模式 |
|
|
|
setProperty("Speech_SegmentationStrategy", "Time") |
|
|
|
} |
|
|
|
@ -189,13 +193,13 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
val needEchoCancellation = !isHeadsetConnected |
|
|
|
|
|
|
|
if (needEchoCancellation) { |
|
|
|
Log.i(tag, "使用拉流方式进行回音消除") |
|
|
|
Log.i(tag, "使用拉流方式进行回音消除") |
|
|
|
// 使用拉流方式进行回音消除 |
|
|
|
setupMicrophoneStream() |
|
|
|
} else { |
|
|
|
Log.i(tag, "使用拉流方式进行回音消除2") |
|
|
|
Log.i(tag, "使用拉流方式进行回音消除2") |
|
|
|
audioConfig = AudioConfig.fromDefaultMicrophoneInput() |
|
|
|
setupMicrophoneStream() |
|
|
|
setupMicrophoneStream() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -237,7 +241,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
microphoneStream = null |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 预热识别器(减少首次识别延迟) |
|
|
|
*/ |
|
|
|
@ -247,7 +251,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
val warmupData = ByteArray(320) // 10ms 16kHz 单声道 |
|
|
|
// 模拟静音数据 |
|
|
|
warmupData.fill(0) |
|
|
|
|
|
|
|
|
|
|
|
// 如果是外部音频流,进行预热 |
|
|
|
if (audioSourceType == AudioSourceType.EXTERNAL) { |
|
|
|
externalAudioStream?.pushAudio(warmupData) |
|
|
|
@ -259,7 +263,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置外部音频流 - 使用拉流方式 |
|
|
|
*/ |
|
|
|
@ -412,7 +416,8 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
EventHandler<SpeechRecognitionEventArgs> { _, event -> |
|
|
|
if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) { |
|
|
|
val detectedLanguage = if (isAutoDetectLanguage) { |
|
|
|
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: supportedLanguages[0] |
|
|
|
AutoDetectSourceLanguageResult.fromResult(event.result)?.language |
|
|
|
?: supportedLanguages[0] |
|
|
|
} else { |
|
|
|
currentLanguage |
|
|
|
} |
|
|
|
@ -536,7 +541,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
// 释放音频配置 |
|
|
|
audioConfig?.close() |
|
|
|
audioConfig = null |
|
|
|
|
|
|
|
isRecording = false |
|
|
|
// 确保状态被重置 |
|
|
|
isContinuousRecognitionActive = false |
|
|
|
microphoneStream = null |
|
|
|
@ -546,6 +551,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
speechConfig = null |
|
|
|
} catch (e: Exception) { |
|
|
|
// 确保状态被重置 |
|
|
|
isRecording = false |
|
|
|
isContinuousRecognitionActive = false |
|
|
|
microphoneStream = null |
|
|
|
externalAudioStream = null |
|
|
|
@ -603,9 +609,11 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
*/ |
|
|
|
fun enableRecord() { |
|
|
|
Log.i(tag, "开启录音:") |
|
|
|
isRecording = true |
|
|
|
|
|
|
|
recordfile!!.closeFile() |
|
|
|
if (isContinuousRecognitionActive) { |
|
|
|
isRecording = true |
|
|
|
if (isContinuousRecognitionActive) { |
|
|
|
|
|
|
|
recordfile!!.creatingFiles() |
|
|
|
} |
|
|
|
|
|
|
|
@ -628,15 +636,17 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
private inner class MicrophoneStream : PullAudioInputStreamCallback() { |
|
|
|
private var audioRecord: AudioRecord? = null |
|
|
|
private var echoCanceler: AcousticEchoCanceler? = null |
|
|
|
|
|
|
|
// 音频配置 |
|
|
|
private val sampleRate = 16000 |
|
|
|
private val channelConfig = AudioFormat.CHANNEL_IN_MONO |
|
|
|
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT |
|
|
|
|
|
|
|
// 优化:减少缓冲区大小以降低延迟 |
|
|
|
private val bufferSize = AudioRecord.getMinBufferSize( |
|
|
|
sampleRate, channelConfig, audioFormat |
|
|
|
).let { if (it < 0) 2048 else it } |
|
|
|
|
|
|
|
|
|
|
|
init { |
|
|
|
initMicrophone() |
|
|
|
} |
|
|
|
@ -694,7 +704,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
*/ |
|
|
|
private fun applyAudioEffects() { |
|
|
|
val sessionId = audioRecord?.audioSessionId ?: return |
|
|
|
|
|
|
|
|
|
|
|
// 优化:只在真正需要时启用回音消除 |
|
|
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
|
|
|
if (audioManager.isSpeakerphoneOn) { |
|
|
|
@ -726,7 +736,7 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
Log.e(tag, "读取音频数据失败: $result") |
|
|
|
return 0 |
|
|
|
} |
|
|
|
recordfile?.saveAudioDataToWav(buffer) |
|
|
|
recordfile?.saveAudioDataToWav(buffer) |
|
|
|
return result |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(tag, "读取音频异常: ${e.message}") |
|
|
|
@ -792,12 +802,12 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
override fun read(buffer: ByteArray): Int { |
|
|
|
try { |
|
|
|
val chunk = queue.take() |
|
|
|
|
|
|
|
|
|
|
|
// 检查是否是结束标志(空数组) |
|
|
|
if (chunk.isEmpty()) { |
|
|
|
return 0 |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val toCopy = minOf(chunk.size, buffer.size) |
|
|
|
System.arraycopy(chunk, 0, buffer, 0, toCopy) |
|
|
|
return toCopy |
|
|
|
@ -890,6 +900,11 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
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 dataBuffer = mutableListOf<ByteArray>() |
|
|
|
private var totalBytesWritten = 0 |
|
|
|
@ -904,9 +919,12 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
} |
|
|
|
// 创建新的音频文件 |
|
|
|
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss") |
|
|
|
val timestamp = dateFormat.format(Date()) |
|
|
|
val filePath = |
|
|
|
context.getExternalFilesDir(null)?.absolutePath + "/${fileName}_${timestamp}.wav" |
|
|
|
val timestamp = dateFormat.format(Date()) |
|
|
|
val parentDir = File(context.filesDir, "TranslatAudio") |
|
|
|
if (!parentDir.exists()) { |
|
|
|
parentDir.mkdirs() |
|
|
|
} |
|
|
|
val filePath = File(parentDir, "${fileName}_${timestamp}.wav").absolutePath |
|
|
|
currentAudioFile = File(filePath) |
|
|
|
currentAudioFile?.createNewFile() |
|
|
|
|
|
|
|
@ -918,31 +936,38 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
|
|
|
|
// 追加音频数据到文件 |
|
|
|
fos = FileOutputStream(currentAudioFile, true) |
|
|
|
startWriteThread() // 新增:启动写入线程 |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 新增:启动写入线程 |
|
|
|
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() |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 保存音频数据到 WAV 文件 |
|
|
|
* 保存音频数据到 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) |
|
|
|
if (fos == null || currentAudioFile == null) return |
|
|
|
// 放入队列,由写线程写入 |
|
|
|
writeQueue.offer(buffer.copyOf()) |
|
|
|
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字节 + 音频数据 |
|
|
|
@ -967,35 +992,36 @@ class AzureAsrHelper(private val context: Context) { |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
internal fun closeFile() { |
|
|
|
try { |
|
|
|
if (fos == null || currentAudioFile == null) { |
|
|
|
return |
|
|
|
} |
|
|
|
// 关闭文件流 |
|
|
|
fos?.close() |
|
|
|
|
|
|
|
// 更新WAV文件头 |
|
|
|
currentAudioFile?.let { file -> |
|
|
|
// 如果文件小于44字节,直接删除 |
|
|
|
if (file.length() <= 44) { |
|
|
|
file.delete() |
|
|
|
Log.d(tag, "音频文件过小已删除: ${file.absolutePath}") |
|
|
|
} else { |
|
|
|
RandomAccessFile(file, "rw").use { raf -> |
|
|
|
raf.seek(0) |
|
|
|
raf.write(generateWavHeader(totalBytesWritten)) |
|
|
|
internal fun closeFile() { |
|
|
|
try { |
|
|
|
if (fos == null || currentAudioFile == null) { |
|
|
|
return |
|
|
|
} |
|
|
|
isWriting.set(false) |
|
|
|
writeThread?.join(500) // 等待写线程结束 |
|
|
|
fos?.close() |
|
|
|
|
|
|
|
// 更新WAV文件头 |
|
|
|
currentAudioFile?.let { file -> |
|
|
|
// 如果文件小于44字节,直接删除 |
|
|
|
if (file.length() <= 44) { |
|
|
|
file.delete() |
|
|
|
Log.d(tag, "音频文件过小已删除: ${file.absolutePath}") |
|
|
|
} else { |
|
|
|
RandomAccessFile(file, "rw").use { raf -> |
|
|
|
raf.seek(0) |
|
|
|
raf.write(generateWavHeader(totalBytesWritten)) |
|
|
|
} |
|
|
|
Log.d(tag, "音频文件保存完成: ${file.absolutePath}") |
|
|
|
} |
|
|
|
} |
|
|
|
Log.d(tag, "音频文件保存完成: ${file.absolutePath}") |
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(tag, "更新WAV文件头失败: ${e.message}") |
|
|
|
} finally { |
|
|
|
fos = null |
|
|
|
currentAudioFile = null |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(tag, "更新WAV文件头失败: ${e.message}") |
|
|
|
} finally { |
|
|
|
fos = null |
|
|
|
currentAudioFile = null |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |