Android音視頻系列(七):PCM音頻單聲道與雙聲道的相互轉換

前言

上一篇我們已經學習了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與Byte的關係圖
所以我們一定要確保高低位的順序,否則得到的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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章