Android PCM录音实践
前言
本来弄着RemoteView的活,也差不多了,临时接了工作上的麦克风录音PCM格式的任务,正好我这hardware模块也需要这东西,就先抽出通用部分写了下,一来能实践,二来还能满足工作要求,下面就记录下。
需求
任务很简单,主要就是下面几个需求:
- 录制PCM音频
- 将数据按1280字节分段储存
- 播放PCM音频
就简单三项,其中第二项的1280字节储存是因为需要和讯飞对接,把数据实时传给它并获取文字,下面开干。
PCM录制
录制前先加下麦克风权限,以及动态申请吧,这里就不详细说了。
PCM音频实际就是模拟信号采样嘛,首先要看你需要的一些配置,采样率之类的:
kotlin
companion object {
// 采样率
const val SAMPLE_RATE = 16000
// 单声道
const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
// PCM编码位数
const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
}
然后拿着采样率、声道数量、PCM编码位数创建AudioRecord,启动startRecording,就能在它的输出流里面读取数据了,当然最好在线程里面读取。
kotlin
/**
* 开始录音
*/
@SuppressLint("MissingPermission")
fun startCapture(executor: Executor) {
// 计算下应该设置的bufferSize
val minBufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT
)
val bufferSize = if (minBufferSize > PCM_PACKET_SIZE) PCM_PACKET_SIZE else minBufferSize
mAudioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT,
bufferSize
)
val buffer = ByteArray(bufferSize)
mAudioRecord!!.startRecording()
mIsRecording = true
// 使用自带线程池执行
executor.execute {
while (mIsRecording) {
val bytesRead = mAudioRecord!!.read(buffer, 0, bufferSize)
if (bytesRead > 0) {
enqueueData(buffer.sliceArray(0 until bytesRead))
}
}
}
}
/**
* 结束录音
*/
fun stopCapture() {
mIsRecording = false
mAudioRecord?.stop()
mAudioRecord?.release()
mAudioRecord = null
}
数据分段储存
上面写到了从AudioRecord的输出流读取数据,接下来就需要按1280字节给它分段,这里我用的queue:
kotlin
// 转换packet时多余的数据
private var mRemainingData: ByteArray = byteArrayOf()
// 数据按1280字节储存,每40ms向H5发送
private val mDataQueue: Queue<ByteArray> = ConcurrentLinkedQueue()
// 数据转换成1280字节的包
private fun enqueueData(data: ByteArray) {
var combinedData = mRemainingData + data
while (combinedData.isNotEmpty()) {
val packetSize = minOf(PCM_PACKET_SIZE, combinedData.size)
val packet = ByteArray(packetSize)
System.arraycopy(combinedData, 0, packet, 0, packetSize)
mDataQueue.offer(packet)
combinedData = if (packetSize < combinedData.size) {
combinedData.drop(packetSize).toByteArray()
} else {
byteArrayOf()
}
}
mRemainingData = combinedData
}
/**
* 读取封装成1280字节packet
*
* @return 封装成1280字节的packet
*/
fun dequeueData(): ByteArray? {
return mDataQueue.poll()
}
在enqueueData的时候,通过一个mRemainingData配合,把数据封成1280字节,再放到queue里面,这里用了个线程安全的queue,外部要数据从queue里面取就行了。
PCM播放
上面拿到数据我们保存一份到文件里面去,后面从文件流里面读取数据,放到audioTrack的缓冲流里面就可以播放了。
kotlin
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRecord
import android.media.AudioTrack
import java.io.File
import java.io.FileInputStream
import java.util.concurrent.Executor
/**
* PCM音频播放
*
* @author lfq
* @date 2024-04-12
*/
class PcmPlayer {
/**
* 播放pcm音频
*
* @param file pcm音频文件
* @param executor 线程池
*/
fun play(file: File, executor: Executor) {
// 计算下应该设置的bufferSize
val bufferSize = AudioRecord.getMinBufferSize(
PCMDataCapture.SAMPLE_RATE,
PCMDataCapture.CHANNEL_CONFIG,
PCMDataCapture.AUDIO_FORMAT
)
// builder需要兼容API23
@Suppress("DEPRECATION")
val audioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,
PCMDataCapture.SAMPLE_RATE,
// 注意这里用CHANNEL_OUT_MONO
AudioFormat.CHANNEL_OUT_MONO,
PCMDataCapture.AUDIO_FORMAT,
bufferSize,
AudioTrack.MODE_STREAM
)
// 开始播放,这一步要先执行
audioTrack.play()
executor.execute {
// 读取数据到audioTrack缓存区
FileInputStream(file).use { inputStream->
val pcmData = ByteArray(bufferSize)
var bytesRead: Int
while (inputStream.read(pcmData).also { bytesRead = it } != -1) {
audioTrack.write(pcmData, 0, bytesRead)
}
}
// 播放完释放资源
audioTrack.stop()
audioTrack.release()
}
}
}
简单使用
为了配合讯飞40ms发一次消息的需求,我还加了一个Handler,来定时读取queue里面的数据:
kotlin
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import androidx.core.util.Consumer
import java.io.File
import java.io.RandomAccessFile
import java.util.concurrent.Executor
/**
* 定期发送pcm packet的主线程handler
*
* @author lfq
* @date 2024-04-11
*/
class PcmHandler(
private var executor: Executor,
private var callback: Consumer<ByteArray>
): Handler(Looper.getMainLooper()) {
companion object {
// 开始录制
const val MSG_TYPE_START = 0
// 循环发送数据到H5
const val MSG_TYPE_CONTINUE = 1
// 结束录制
const val MSG_TYPE_STOP = 2
// H5接收PCM数据的间隔
const val PCM_SEND_DELAY = 40L
/**
* 以追加形式保存packet到文件里面
*
* @param executor 线程池,用来写IO文件
* @param
*/
fun saveToFile(executor: Executor, packet: ByteArray, targetFile: File) {
executor.execute {
RandomAccessFile(targetFile, "rwd").use { raf->
raf.seek(targetFile.length())
raf.write(packet)
}
}
}
}
// PCM音频录制器
private var mPcmCapture: PCMDataCapture? = null
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when(msg.what) {
MSG_TYPE_START -> {
// 启动录制
mPcmCapture = PCMDataCapture().also {
it.startCapture(executor)
}
// 开启循环,接收数据
Log.d("TAG", "handleMessage sendEmptyMessageDelayed: ")
sendEmptyMessageDelayed(MSG_TYPE_CONTINUE, PCM_SEND_DELAY)
}
MSG_TYPE_CONTINUE -> {
if (mPcmCapture == null) {
throw IllegalStateException("PCMDataCapture not start!")
}
val packet = mPcmCapture!!.dequeueData()
// 向H5传递数据
if (packet != null) {
callback.accept(packet)
}
// 定时发送缓存的packet队列数据
if (packet != null || mPcmCapture!!.isRecording()) {
sendEmptyMessageDelayed(MSG_TYPE_CONTINUE, PCM_SEND_DELAY)
}else {
callback.accept(null)
}
}
MSG_TYPE_STOP -> {
// 关闭录制
mPcmCapture?.stopCapture()
}
}
}
}
代码很简单,定义了三种消息,我们要录音直接向handler发消息就行了,下面就简单使用下:
kotlin
// 处理PCM录制相关逻辑
private var mPcmHandler: PcmHandler? = null
private val mExecutor = Executors.newSingleThreadExecutor()
/**
* 开始录制音频
*/
@SuppressLint("SetTextI18n")
private fun startPcmRecord() {
val file = File(requireContext().externalCacheDir, "test.pcm")
if (!file.exists()) {
file.createNewFile()
}
FileUtil.clearFile(file.absolutePath)
mPcmHandler = PcmHandler(mExecutor) {
it?.let {
PcmHandler.saveToFile(mExecutor, it, file)
}
}
mPcmHandler!!.sendEmptyMessage(PcmHandler.MSG_TYPE_START)
}
/**
* 结束录制音频
*/
private fun stopPcmRecord() {
mPcmHandler!!.sendEmptyMessage(PcmHandler.MSG_TYPE_STOP)
mPcmHandler = null
}
private fun playPcmRecord() {
val file = File(requireContext().externalCacheDir, "test.pcm")
PcmPlayer().play(file, mExecutor)
}
很easy,不过千万别忘了stopPcmRecord,不然会一直录制,而且PCM格式的文件大小还挺大的,必须注意下。
源码及DEMO
源码及DEMO我也提交到我练手的仓库了,后续可能会有所修改,可以参考下:
小结
简单记录了一下利用麦克风录制PCM格式的音频,并通过AudioRecord播放。