Android Kotlin:将RecognitionIntent用户音频保存至应用缓存文件夹
解决方案:Android跟读音频录制与WAV格式处理
核心问题分析
你遇到的问题本质是:
- 系统SpeechRecognizer不支持直接返回录制音频:
onBufferReceived仅在少数识别引擎中触发,ACTION_RECOGNIZE_SPEECH也不会返回音频URI,getData()返回null是正常行为。 - PCM转WAV失败:WAV文件必须包含描述采样率、声道数等参数的文件头,仅写入PCM原始数据会导致播放器无法识别。
首选方案:用AudioRecord直接录制PCM并生成标准WAV
放弃依赖SpeechRecognizer,改用AudioRecord直接录制PCM原始数据,再手动添加正确的WAV文件头,确保输出格式符合musicg要求。
步骤1:实现AudioRecord录制逻辑
添加必要权限(已申请RECORD_AUDIO,若使用外部存储需额外申请WRITE_EXTERNAL_STORAGE),然后编写录制代码:
private var audioRecord: AudioRecord? = null private var recordingThread: Thread? = null private var isRecording = false private var tempPcmFile: File? = null // 注意:采样率、声道数要和TTS生成的WAV保持一致,这里以16000Hz单声道为例 private val sampleRate = 16000 private val channelConfig = AudioFormat.CHANNEL_IN_MONO private val audioFormat = AudioFormat.ENCODING_PCM_16BIT private fun startRecording() { val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) audioRecord = AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, bufferSize ) tempPcmFile = File(cacheDir, "temp_recording.pcm") val outputStream = FileOutputStream(tempPcmFile) audioRecord?.startRecording() isRecording = true recordingThread = Thread { val buffer = ByteArray(bufferSize) while (isRecording) { val readCount = audioRecord?.read(buffer, 0, bufferSize) ?: 0 if (readCount > 0) { outputStream.write(buffer, 0, readCount) } } outputStream.close() // 将PCM转为标准WAV convertPcmToWav(tempPcmFile!!, File(cacheDir, "user_recording.wav")) } recordingThread?.start() } private fun stopRecording() { isRecording = false audioRecord?.apply { stop() release() } audioRecord = null recordingThread?.join() // 录制完成后可调用对比逻辑 compareWithTtsAudio() }
步骤2:PCM转标准WAV工具方法
手动写入符合规范的WAV文件头,确保播放器和musicg能识别:
private fun convertPcmToWav(pcmFile: File, wavFile: File) { val pcmDataSize = pcmFile.length().toInt() val wavOutputStream = FileOutputStream(wavFile) // 写入WAV文件头 val totalFileSize = 36 + pcmDataSize val byteRate = sampleRate * 1 * 16 / 8 // 1声道、16位深 // RIFF chunk wavOutputStream.write("RIFF".toByteArray()) wavOutputStream.write(totalFileSize.toLittleEndianByteArray(4)) wavOutputStream.write("WAVE".toByteArray()) // fmt chunk wavOutputStream.write("fmt ".toByteArray()) wavOutputStream.write(16.toLittleEndianByteArray(4)) // PCM格式大小 wavOutputStream.write(1.toLittleEndianByteArray(2)) // 线性PCM wavOutputStream.write(1.toLittleEndianByteArray(2)) // 声道数 wavOutputStream.write(sampleRate.toLittleEndianByteArray(4)) wavOutputStream.write(byteRate.toLittleEndianByteArray(4)) wavOutputStream.write(2.toLittleEndianByteArray(2)) // 块对齐(1声道*16位/8=2) wavOutputStream.write(16.toLittleEndianByteArray(2)) // 位深 // data chunk wavOutputStream.write("data".toByteArray()) wavOutputStream.write(pcmDataSize.toLittleEndianByteArray(4)) // 写入PCM原始数据 val pcmInputStream = FileInputStream(pcmFile) val buffer = ByteArray(4096) var readCount: Int while (pcmInputStream.read(buffer).also { readCount = it } != -1) { wavOutputStream.write(buffer, 0, readCount) } pcmInputStream.close() wavOutputStream.close() // 删除临时PCM文件 pcmFile.delete() } // 扩展函数:Int转小端字节数组(WAV格式要求小端存储) private fun Int.toLittleEndianByteArray(length: Int): ByteArray { val result = ByteArray(length) for (i in 0 until length) { result[i] = (this shr (8 * i)).toByte() } return result }
步骤3:替换原有SpeechRecognizer逻辑
修改toggleButton的点击事件和权限回调,改用新的录制逻辑:
override fun onCreate(savedInstanceState: Bundle?) { // ... 其他原有代码 ... toggleButton.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { progressBar.visibility = View.VISIBLE progressBar.isIndeterminate = true ActivityCompat.requestPermissions(this@AudioLessonsActivity, arrayOf(Manifest.permission.RECORD_AUDIO), permission) } else { progressBar.isIndeterminate = false progressBar.visibility = View.GONE stopRecording() } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { permission -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startRecording() } else { Toast.makeText(this@AudioLessonsActivity, "Permission Denied!", Toast.LENGTH_SHORT).show() } } }
步骤4:用musicg对比音频
确保TTS生成的WAV和用户录制的WAV参数一致(采样率、声道数、位深),然后调用musicg API计算相似度:
private fun compareWithTtsAudio() { val ttsWavFile = File(cacheDir, "file1.wav") val userWavFile = File(cacheDir, "user_recording.wav") if (!ttsWavFile.exists() || !userWavFile.exists()) { returnedText.text = "音频文件不存在" return } val ttsWave = Wave(ttsWavFile.absolutePath) val userWave = Wave(userWavFile.absolutePath) // 计算相似度(0-1之间),转成百分制分数 val similarityScore = ttsWave.similarity(userWave) * 100 returnedText.text = "跟读得分:${similarityScore.toInt()}分" }
验证TTS生成的WAV参数
如果TTS生成的WAV采样率不是16000Hz,需要修改sampleRate变量匹配,可通过musicg的Wave类获取参数:
val ttsWave = Wave(ttsWavFile.absolutePath) val ttsSampleRate = ttsWave.waveHeader.sampleRate val ttsChannels = ttsWave.waveHeader.channels
内容的提问来源于stack exchange,提问作者Diego Perez
相关产品推荐
相关产品推荐

