前言
上一篇我們已經學習了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。