Kotlin实现AudioRecord按钮松手停录、存wav、传服务及音频块播放问题
问题原因分析
你出现卡死的核心原因是:OnTouchListener的回调运行在主线程(UI线程),直接在ACTION_DOWN里写死循环会完全阻塞主线程,后续所有UI事件包括ACTION_UP都无法被分发处理,自然不可能触发停止逻辑。
完整解决方案
1. 正确实现录音+流式发送的启停逻辑
首先添加全局状态控制变量,把录音、发送逻辑放到子线程执行,避免阻塞主线程:
// 全局变量,加@Volatile保证子线程可以及时读取到状态变化 @Volatile private var isRecording = false private var recordThread: Thread? = null private lateinit var audioBuffer: ByteArray
触摸事件逻辑修改如下:
btn_send.setOnTouchListener(object : View.OnTouchListener { override fun onTouch(v: View?, event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { Log.i("tou7ch", "apertou") isRecording = true audioBuffer = ByteArray(minBufSize) audiorecord!!.startRecording() // 启动子线程处理录音和发送 recordThread = Thread { while (isRecording) { val readSize = audiorecord!!.read(audioBuffer, 0, minBufSize) if (readSize > 0) { // 只发送实际读取到的有效数据 val chunk = audioBuffer.copyOfRange(0, readSize).toByteString() webSocket.send(chunk) // 同时缓存数据到本地,后续生成WAV用 savePcmToCache(chunk.toByteArray()) } } // 循环结束后释放录音资源 audiorecord?.apply { stop() release() } audiorecord = null // 生成最终WAV文件 generateWavFile() } recordThread?.start() } MotionEvent.ACTION_UP -> { Log.i("tou7ch", "soltou") // 修改状态标记,终止子线程循环 isRecording = false // 等待线程结束避免内存泄漏 try { recordThread?.join(1000) } catch (e: InterruptedException) { e.printStackTrace() } recordThread = null } } // 返回true表示消费当前触摸事件,避免后续事件丢失 return true } })
2. 保存为完整WAV格式文件
WAV文件本质是PCM裸数据前加44字节的格式头,实现逻辑如下:
// 全局变量,用于缓存PCM数据 private var cachePcmFile: File? = null private var cacheOutputStream: FileOutputStream? = null // 录制过程中缓存PCM数据 private fun savePcmToCache(buffer: ByteArray) { try { if (cacheOutputStream == null) { cachePcmFile = File(cacheDir, "temp_${System.currentTimeMillis()}.pcm") cacheOutputStream = FileOutputStream(cachePcmFile) } cacheOutputStream?.write(buffer) } catch (e: Exception) { e.printStackTrace() } } // 录制结束后生成WAV文件 private fun generateWavFile(): File? { cacheOutputStream?.flush() cacheOutputStream?.close() cacheOutputStream = null val pcmFile = cachePcmFile ?: return null // 输出WAV文件路径 val wavFile = File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "record_${System.currentTimeMillis()}.wav") return try { // 以下参数必须和你创建AudioRecord时的参数完全一致 val sampleRate = 16000 // 替换为实际采样率 val channels = 1 // 单声道传1,双声道传2 val bitWidth = 16 // 16位PCM传16,8位传8 val pcmSize = pcmFile.length() val wavTotalSize = pcmSize + 36 // 组装44字节WAV头 val header = ByteArray(44) // RIFF标识 header[0] = 'R'.code.toByte() header[1] = 'I'.code.toByte() header[2] = 'F'.code.toByte() header[3] = 'F'.code.toByte() // 总文件大小 header[4] = (wavTotalSize and 0xff).toByte() header[5] = (wavTotalSize shr 8 and 0xff).toByte() header[6] = (wavTotalSize shr 16 and 0xff).toByte() header[7] = (wavTotalSize shr 24 and 0xff).toByte() // WAVE标识 header[8] = 'W'.code.toByte() header[9] = 'A'.code.toByte() header[10] = 'V'.code.toByte() header[11] = 'E'.code.toByte() // fmt子块标识 header[12] = 'f'.code.toByte() header[13] = 'm'.code.toByte() header[14] = 't'.code.toByte() header[15] = ' '.code.toByte() // 格式长度(PCM固定为16) header[16] = 16 header[17] = 0 header[18] = 0 header[19] = 0 // 编码格式(PCM固定为1) header[20] = 1 header[21] = 0 // 声道数 header[22] = channels.toByte() header[23] = 0 // 采样率 header[24] = (sampleRate and 0xff).toByte() header[25] = (sampleRate shr 8 and 0xff).toByte() header[26] = (sampleRate shr 16 and 0xff).toByte() header[27] = (sampleRate shr 24 and 0xff).toByte() // 码率 = 采样率*声道数*位深/8 val byteRate = sampleRate * channels * bitWidth / 8 header[28] = (byteRate and 0xff).toByte() header[29] = (byteRate shr 8 and 0xff).toByte() header[30] = (byteRate shr 16 and 0xff).toByte() header[31] = (byteRate shr 24 and 0xff).toByte() // 块对齐 = 声道数*位深/8 val blockAlign = channels * bitWidth / 8 header[32] = blockAlign.toByte() header[33] = 0 // 位深 header[34] = bitWidth.toByte() header[35] = 0 // data子块标识 header[36] = 'd'.code.toByte() header[37] = 'a'.code.toByte() header[38] = 't'.code.toByte() header[39] = 'a'.code.toByte() // PCM数据大小 header[40] = (pcmSize and 0xff).toByte() header[41] = (pcmSize shr 8 and 0xff).toByte() header[42] = (pcmSize shr 16 and 0xff).toByte() header[43] = (pcmSize shr 24 and 0xff).toByte() // 写入WAV头和PCM数据 val wavOutputStream = FileOutputStream(wavFile) wavOutputStream.write(header) val pcmInputStream = FileInputStream(pcmFile) val buffer = ByteArray(1024) var len: Int while (pcmInputStream.read(buffer).also { len = it } != -1) { wavOutputStream.write(buffer, 0, len) } pcmInputStream.close() wavOutputStream.flush() wavOutputStream.close() // 删除临时PCM文件 pcmFile.delete() wavFile } catch (e: Exception) { e.printStackTrace() null } }
生成的WAV文件可以直接发送给服务器使用。
3. 接收端播放流式音频块
接收端使用AudioTrack即可实现流式播放,参数必须和录制端完全一致:
// 初始化AudioTrack,参数和录制端完全一致 val sampleRate = 16000 val channelConfig = AudioFormat.CHANNEL_OUT_MONO val audioFormat = AudioFormat.ENCODING_PCM_16BIT val minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) val audioTrack = AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, minBufferSize, AudioTrack.MODE_STREAM ) // 启动播放 audioTrack.play() // WebSocket收到音频块时直接写入AudioTrack webSocket.setListener(object : WebSocketListener() { override fun onMessage(webSocket: WebSocket, bytes: ByteString) { super.onMessage(webSocket, bytes) val audioData = bytes.toByteArray() audioTrack.write(audioData, 0, audioData.size) } }) // 播放结束后释放资源 // audioTrack.stop() // audioTrack.release()
内容的提问来源于stack exchange,提问作者moigamijunior
相关产品推荐
相关产品推荐

