前言
上一篇我们已经学习了PCM音频的保存格式,这一篇我们通过掌握的知识,完成PCM音频的单声道和双声道的互相转换。
正文
首先我们把上一篇的最核心部分贴出来:
我们首先完成单声道转双声道的操作。
单声道转双声道
单声道转双声道的基本原理:
由图可知,我们需要把单声道的每一份数据都拷贝一份到右声道,这样使用双声道播放就没有问题了。
首先我录制了一个音频保存到ArrayList中:
private val recordThread = Thread(
Runnable {
val iMinBufferSize = AudioRecord.getMinBufferSize(
Constants.SAMPLE_RATE,
currentChannel,
AudioFormat.ENCODING_PCM_16BIT
)
val audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
Constants.SAMPLE_RATE,
currentChannel,
AudioFormat.ENCODING_PCM_16BIT,
iMinBufferSize
)
audioRecord.startRecording()
monoByteList.clear()
val recordBytes = ByteArray(iMinBufferSize)
var lastTime = 0L
var pcmSize = 0
while (lastTime < recordTime * 1000000L) {
val readSize = audioRecord.read(recordBytes, 0, recordBytes.size)
// 保存音频数据到ArrayList中
monoByteList.addAll(recordBytes.asList())
pcmSize += readSize
lastTime = pcmSize * 1000000L / 2 / Constants.SAMPLE_RATE
}
audioRecord.stop()
audioRecord.release()
recordCallback()
}
)
录制的是16位的数据,所以我们每一个采样的数据会占据两位,所以在拷贝的过程中,我们也要每两位拷贝一次:
private val convertMonoToStereoThread = Thread(Runnable {
// 单声道转双声道
// 双声道的存储格式为 LRLRLR
// 所以把左声道的内容拷贝到右声道即可
for (index in 0 until monoByteList.size step 2) {
// 目前保存的是16位的数据,所以要复制前两位
stereoByteList.add(monoByteList[index])
stereoByteList.add(monoByteList[index + 1])
// 目前保存的是16位的数据,所以要复制前两位
stereoByteList.add(monoByteList[index])
stereoByteList.add(monoByteList[index + 1])
}
convertCallback()
})
单声道转声道的操作就完成了。
双声道转单声道的操作
双声道转单声道的原理:
双声道转单声道有两种做法:
1、丢弃其中一路数据(丢失左声道或右声道的数据)
2、两路数据相加的平局值。(也可以是其他算法)
第一种做法:丢弃一路数据
我们可以按照单声道双声道的做法,每四位取前两位或后两位的数据即可。但是这里我们换一种做法。
// 保存了录制的16位双声道音频数据,过程省略,里面保存类型Byte
stereoByteList
// 目标输出ArrayList,类型为Short,如果你需要Byte数据,可以再自行转换一次
monoByteList
// 开始转换
private fun convertStereoToMono() {
thread {
// 双声道转单声道
// 方案1:丢掉一路数据,此方法最简单
// 这里只取左声道的声音
monoByteList.clear()
// ByteOrder.LITTLE_ENDIAN 从小到大 ,高位在后
// ByteOrder.BIG_ENDIAN 从大到小,高位在前,默认
val shortBuffer = ByteBuffer.wrap(stereoByteList.toByteArray()).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()
for (index in 0 until shortBuffer.capacity() step 2) {
monoByteList.add(shortBuffer.get(index))
}
convertCallback()
}
}
这里我们使用了ByteBuffer帮助我们把Byte转成Short。其中有一个很重要的坑,就是设置Byte转Short的规则:
ByteOrder.LITTLE_ENDIAN 从小到大 ,高位在后
ByteOrder.BIG_ENDIAN 从大到小,高位在前,默认short的长度为16位,所以需要两个8位的Byte一起保存,其中一个Byte保存的是前8位,也就是高位另外的一个Byte保存的后8位,也就是低位。
所以我们一定要确保高低位的顺序,否则得到的Short一定是错的,经过测试,录制的音频是低位在前,所以我们修改ByteBuffer默认的高位在前的配置:
ByteBuffer.wrap(stereoByteList.toByteArray()).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()
// 读取指定位置的Short
val short = shortBuffer.get(index)
相同的原理,我们需要Byte转Int都可以借助对应的Buffer进行读取,非常的方便。
第二种做法:左右声道取平局值
// 保存了录制的16位双声道音频数据,过程省略,里面保存类型Byte
stereoByteList
// 目标输出ArrayList,类型为Short,如果你需要Byte数据,可以再自行转换一次
monoByteList
private fun convertStereoToMono() {
thread {
// 双声道转单声道
monoByteList.clear()
// ByteOrder.LITTLE_ENDIAN 从小到大 ,高位在后
// ByteOrder.BIG_ENDIAN 从大到小,高位在前,默认
val shortBuffer = ByteBuffer.wrap(stereoByteList.toByteArray()).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()
// 方案二:把左右声道的声音相加,取平均值
// 使用kotlin的位运算 and shl等,无法得到正确的byte转short,short转init
for (index in 0 until shortBuffer.capacity() step 2) {
monoByteList.add((shortBuffer.get(index) + shortBuffer.get(index + 1) / 2).toShort())
}
convertCallback()
}
}
基本流程和第一种方法一样,如果是你用的Java,你还可以通过位运算进行Short和Byte的转换,但是kotlin的对应的运算符却无法正确转换,具体原因还不清楚,这也是为什么我使用了Buffer进行转换的原因。
总结
只要我们掌握了PCM的保存格式,单声道和双声道的互相转换还是非常轻松的,下一篇我们来了解一下新的音频格式:WAV。