PCM淺析

最近有個需求:對音頻裁剪時,裁剪條的縱座標必須是音頻音量,以幫助用戶更好的選擇音頻區域,因此就須要快速準確的提取出音頻的音量列表。本文主要介紹下從mp4文件中提取音軌音量的方式,以及相關的知識點。html

音頻基礎知識

聲音的本質是空氣壓力差形成的空氣振動,振動產生的聲波能夠在介質中快速傳播,當聲波到達接收端時(好比:人耳、話筒),引發相應的振動,最終被聽到。 java

聲音

聲音有兩個基本屬性:頻率與振幅。聲音的振幅就是音量,頻率的高低就是音調,頻率的單位是赫茲(Hz)。shell

當聲波傳遞到話筒時,話筒裏的碳膜會隨着聲音一塊兒振動,而碳膜下面是一個電極,碳膜振動時會觸碰電極,接觸時間的長短跟振動幅度有關(即:聲音響度),這樣就完成了聲音信號到電壓信號的轉換。後面通過電路放大後,就獲得了模擬音頻信號。框架

模擬音頻:用連續的電流或電壓表示的音頻信號,在時間和振幅上是連續。過去記錄的聲音都是模擬音頻,好比:機械錄音(以留聲機、機械唱片爲表明)、磁性錄音(以磁帶錄音爲表明)等模擬錄音方式。函數

計算機不能直接處理連續的模擬信號,因此須要進行A/D轉換,以必定的頻率對模擬信號進行採樣(就是獲取必定時間間隔的波形振幅值,採樣後模擬出的波形與原始波形之間的偏差稱爲採樣噪音),而後再進行量化和存儲,就獲得了數字音頻。測試

數字音頻:經過採樣和量化得到的離散的、數字化的音頻信號,即:計算機能夠處理的二進制的音頻數據。編碼

相反的,當經過揚聲器播放聲音時,計算機內部的數字信號經過D/A轉換,還原成了強弱不一樣的電壓信號。這種強弱變化的電壓會推進揚聲器的振動單元產生震動,就產生了聲音。整個流程能夠用下圖來表示: spa

聲音採集和播放

PCM元數據

最多見的A/D轉換是經過脈衝編碼調製PCM(Pulse Code Modulation)。要將連續的電壓信號轉換爲PCM,須要進行採樣和量化,咱們通常從以下幾個維度描述PCM:.net

  1. 採樣頻率(Sampling Rate):單位時間內採集的樣本數,即:採樣週期的倒數,指兩個採樣之間的時間間隔。採樣頻率越高,聲音質量越好,但同時佔用的帶寬越大。通常狀況下,22KHz至關於普通FM的音質,44KHz至關於CD音質,目前的經常使用採樣頻率都不超過48KHz。
  2. 採樣位數:表示一個樣本的二進制位數,即:每一個採樣點用多少比特表示。計算機中音頻的量化深度通常爲四、八、1六、32位(bit)等。例如:採樣位數爲8 bit時,每一個採樣點能夠表示256個不一樣的採樣值,而採樣位數爲16 bit時,每一個採樣點能夠表示65536個不一樣的採樣值。採樣位數的大小影響聲音的質量,採樣位數越多,量化後的波形越接近原始波形,聲音的質量越高,而須要的存儲空間也越多;位數越少,聲音的質量越低,須要的存儲空間越少。通常狀況下,CD音質的採樣位數是16 bit,移動通訊是8 bit。
  3. 聲道數:記錄聲音時,若是每次生成一個聲波數據,稱爲單聲道;每次生成兩個聲波數據,稱爲雙聲道(立體聲)。單聲道的聲音只能使用一個喇叭發聲,雙聲道的PCM可使兩個喇叭同時發聲(通常左右聲道有分工),更能感覺到空間效果。
  4. 時長:採樣時長
數字音頻文件大小(Byte) = 採樣頻率(Hz)× 採樣時長(S)×(採樣位數 / 8)× 聲道數(單聲道爲1,立體聲爲2複製代碼

採樣點數據有有符號和無符號之分,好比:8 bit的樣本數據,有符號的範圍是-128 ~ 127,無符號的範圍是0 ~ 255。大多數PCM樣本使用整形表示,可是在一些對精度要求比較高的場景,可使用浮點類型表示PCM樣本數據。3d

下面看一個具體的採樣示例:

採樣示例

其中,黑色曲線表示要採集的聲音波形,紅色曲線表示採樣量化後的PCM數據波形。 上圖中,採樣位數是4 bit,每一個紅點對應一個Pcm採樣數據,很明顯:

  • 採樣頻率越高,x軸採樣點越密集,聲音越接近原始數據。
  • 採樣位數越高,y軸量化越精確,聲音越接近原始數據。

PCM數據存儲

接下來看下PCM數據存儲方式,若是是單聲道音頻,採樣數據按照時間的前後順序依次存儲,若是是雙聲道音頻,則按照LRLRLR方式存儲,每一個採樣點的存儲方式還與機器大小端有關。大端模式以下圖所示:

PCM數據存儲-大端模式

Pcm文件沒有頭部信息,所有是採樣量化後的未壓縮音頻數據。

PCM音量計算

咱們通常用分貝(db)描述聲音響度。聲學領域中,分貝的定義是聲源功率與基準聲源功率比值的對數乘以20的數值。根據人耳的特性,咱們對聲音的大小感知呈對數關係,而不是線性關係。人類的聽覺反應是基於聲音的相對變化而非絕對變化。對數函數正好能模仿人耳對聲音的反應。因此用分貝描述聲音強度更符合人類對聲音強度的感知。以下圖所示,橫軸表示PCM採樣值,縱軸表示人耳感知到的音量,圖中截取了兩塊橫軸變化相同的區域,可是人耳感受到的音量變化是不同的。在較安靜的左側,感受到的音量變化較大;在叫喧囂的右側,人耳感受到的音量變化較小。

人耳響度差別

具體來講,分貝計算公式以下所示:

db = 20 * \log_{10}(\frac {P_{rms}} {P_{ref}})

其中,\frac {P_{rms}} {P_{ref}}表示兩個採樣值的比值。在計算某個採樣值的分貝時,直接把P_{ref}當成最小採樣值1處理就能夠了。因此若是採樣位數是16 bit,那麼無符號狀況下,最大分貝是:

20 * \log_{10}(65535) = 96

有符號狀況下,最大分貝是:

20 * \log_{10}(32768) = 90

OK,瞭解了PCM格式和db計算方式以後,咱們看下從音頻文件提取db值的總體流程:

PCM提取流程

Android

首先,咱們基於Android平臺的多媒體API來實現PCM的數據提取,而後計算分貝值。 簡單概述就是:首先經過MediaExtractor解封裝Mp4提取AAC編碼流,而後經過MediaCodec解碼AAC數據,獲得PCM。核心代碼以下所示:

// 解封裝器
val audioExtractor = MediaExtractor()
// 設置路徑
audioExtractor.setDataSource(audioInputPath)
// 找到音軌
for (i in 0 until audioExtractor.trackCount) {
    val format = audioExtractor.getTrackFormat(i)
    if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
        audioExtractor.selectTrack(i)
        // 音軌Format
        inputAudioFormat = format
        break
    }
}

// 音頻聲道數
audioChannel = inputAudioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
// 音頻採樣率
audioSampleRate = inputAudioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
val mime = inputAudioFormat.getString(MediaFormat.KEY_MIME)
val sampleBitStr = inputAudioFormat.getString(MediaFormat.KEY_PCM_ENCODING)
val sampleBit = if (sampleBitStr != null) {
                    try {
                        Integer.parseInt(sampleBitStr)
                        } catch (e: Exception) {
                            AudioFormat.ENCODING_PCM_16BIT
                        }
                } else {
                    AudioFormat.ENCODING_PCM_16BIT
                }

// 一個採樣點佔用的字節數
sampleByte = when (sampleBit) {
    AudioFormat.ENCODING_PCM_8BIT -> 1
    AudioFormat.ENCODING_PCM_16BIT -> 2
    else -> 2
}

// 啓動解碼器
val audioDecoder = MediaCodec.createDecoderByType(mime)
audioDecoder.configure(inputAudioFormat, null, null, 0)
audioDecoder.start()

// 解碼器的輸入和輸出Buffer列表
val decoderInputBuffer = audioDecoder.inputBuffers
var decoderOutputBuffer = audioDecoder.outputBuffers
val bufferInfo = MediaCodec.BufferInfo()
while (!decodeDone) {
    if (!inputDone) { // 提取AAC,進行編碼
        val inputIndex = audioDecoder.dequeueInputBuffer(0L)
        if (inputIndex >= 0) {
            val inputBuffer = decoderInputBuffer[inputIndex]
            inputBuffer.clear()
            val readSampleSize = localAudioExtractor.readSampleData(inputBuffer, 0)
            if (readSampleSize > 0) {
                audioDecoder.queueInputBuffer(inputIndex, 0, readSampleSize, localAudioExtractor.sampleTime, localAudioExtractor.sampleFlags)
                // 移動到下一幀
                audioDecoder.advance()
            } else { // 結束幀
                audioDecoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                inputDone = true
            }
        }
    }
    
    if (!decodeDone) {
        val outputIndex = localAudioDecoder.dequeueOutputBuffer(bufferInfo, 0)
        if (outputIndex >= 0) {
            if(bufferInfo.size > 0){
                val outputBuffer = decoderOutputBuffer[outputIndex]
                // 大小端
                val isBigEndian = (outputBuffer.order() == ByteOrder.BIG_ENDIAN)
                outputBuffer.position(bufferInfo.offset)
                outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
                val pcmByteArray = ByteArray(bufferInfo.size)
                // copy出PCM數據
                outputBuffer.get(pcmByteArray)
                outputBuffer.clear()
                // 當前幀採樣點個數
                val curSampleNum = pcmByteArray.size / sampleByte / audioChannel
                // 計算出當前幀的DB值
                val db = compute(isBigEndian,pcmByteArray,audioChannel,sampleByte)
                // 處理db值
                ......
            }
            
            // 歸還Buffer
            audioDecoder.releaseOutputBuffer(outputIndex, false)
            // 判斷是不是最後的幀
            if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
                decodeDone = true
            }
        }
    }
}
複製代碼

上述代碼是經過MediaExtractorMediaCodec解碼音視頻的標準流程,已經添加了詳細的註釋,咱們看下基於PCM計算db的具體函數:

fun compute(isBigEndian : Boolean ,pcmByteArray : ByteArray,audioChannel : Int,sampleByte : Int){
// 計算出步長:MediaCodec解碼出的PCM數據是按照Packed模式存儲的
val step = if (audioChannel == 2) {
            if (sampleByte == 2) {
                4
            } else {
                2
            }
        } else {
            if (sampleByte == 2) {
                2
            } else {
                1
            }
        }

var i = 0
var sum = 0.0
while (i < pcmByteArray.size) {
    // 絕對值求和
    sum += if (sampleByte == 2) {
                // 根據大小端把兩個byte轉換成short
                val sample = byteToShort(isBigEndian, pcmArray[i], pcmArray[i + 1])
                Math.abs(sample.toInt()).toDouble()
            } else {
                Math.abs(pcmByteArray[i].toInt()).toDouble()
            }
            i += step
    }

// 基於平均採樣點,計算出db值 
return (20 * log10(sum / (pcmByteArray.size / step))).toInt()
}
複製代碼

經過上述代碼,咱們能夠基於解碼出的PCM,計算出對應的db值,可是這種方式存在一個最大的缺點就是耗時嚴重,一個5分鐘的音頻,須要二三十秒,甚至更長,這徹底是沒法忍受的。咱們不得不尋求更高效的解決方案。

IOS

IOS平臺提供了AVFoundation庫,用於音視頻操做。咱們能夠基於它直接提取出整首歌的PCM數據,而後計算出分貝值。大致流程以下所示:

  1. 首先經過AVAudioFile加載本地音頻文件,獲取採樣率、聲道數等音頻信息。
  2. 接着經過上述採樣率、聲道數以及採樣點格式AVAudioCommonFormat構建AVAudioFormat,表示一種音頻格式。
  3. 而後經過AVAudioFormat和音頻採樣幀數(等於採樣率乘以時長)構建AVAudioPCMBuffer,而且經過AVAudioFile.read把音頻數據解碼到AVAudioPCMBuffer,獲取到解碼後的PCM Buffer。
  4. AVAudioPCMBuffer包含了多個聲道的數據,多個聲道的數據是如何存儲的那?能夠經過AVAudioFormat.isInterleaved進行判斷,如果true,則表示多個聲道數據是交替存儲的,即:LRLRLRLR方式,如果false,則表示多個聲道數據是分開存儲的,即:LLLLRRRR模式。
  5. 最後基於AVAudioPCMBuffer提供的PCM數據,針對單一聲道,計算出分貝值,計算方式與Android平臺相似,此處再也不贅述。

可見,iOS平臺對音頻數據的提取提供了很是友好的API,而且測試下來發現,同一首5分鐘的歌曲,耗時只有兩三秒,各個方面,都吊打Android。

跨平臺

除了Android和iOS平臺的多媒體框架,咱們還能夠基於FFmpeg實現跨平臺的PCM數據提取。FFmpeg是一個開源的跨平臺多媒體框架,關於FFmpeg的介紹,網上的資料不少,這裏就再也不贅述了。

經過FFmpeg解碼本地音視頻文件,仍是比較簡單的,總體流程以下所示:

FFmpeg解碼音頻

  1. 首先註冊全部的解封裝和封裝格式(av_register_all)。
  2. 接着打開本地文件,獲取音頻流信息(avformat_open_input -> av_dump_format)。
  3. 其次建立解碼音頻流的解碼上下文,並設置解碼參數(avcodec_alloc_context3 -> avcodec_open2)。
  4. 而後從本地文件讀取音頻裸流幀AVPacket,而後交給解碼器解碼,最後從解碼器獲取PCM原始數據幀AVFrame(av_packet_alloc -> avcodec_receive_frame)。
  5. 由於FFmpeg解碼出的PCM數據存儲格式有不少種,因此咱們會統一重採樣到AV_SAMPLE_FMT_S16P格式(swr_convert)。
  6. 最後針對重採樣後的PCM數據計算出分貝值,而且釋放各類資源。

不一樣於MediaCodec解碼出的PCM是按照LRLRLR方式存儲,FFmpeg解碼出的PCM存儲格式更加豐富,以下所示:

enum AVSampleFormat {
    AV_SAMPLE_FMT_NONE = -1,
    AV_SAMPLE_FMT_U8,          ///< unsigned 8 bits
    AV_SAMPLE_FMT_S16,         ///< signed 16 bits
    AV_SAMPLE_FMT_S32,         ///< signed 32 bits
    AV_SAMPLE_FMT_FLT,         ///< float
    AV_SAMPLE_FMT_DBL,         ///< double

    AV_SAMPLE_FMT_U8P,         ///< unsigned 8 bits, planar
    AV_SAMPLE_FMT_S16P,        ///< signed 16 bits, planar
    AV_SAMPLE_FMT_S32P,        ///< signed 32 bits, planar
    AV_SAMPLE_FMT_FLTP,        ///< float, planar
    AV_SAMPLE_FMT_DBLP,        ///< double, planar
    AV_SAMPLE_FMT_S64,         ///< signed 64 bits
    AV_SAMPLE_FMT_S64P,        ///< signed 64 bits, planar

    AV_SAMPLE_FMT_NB           ///< Number of sample formats. DO NOT USE if linking dynamically
};
複製代碼

除了有有符號和無符號的區別外,還能夠是short、float和double類型,採樣位數也能夠是8 bit、16 bit、32 bit和64 bit。除此以外,即便一樣是signed 16 bits,也存在PackedPlanar的區別。

對於雙聲道音頻來講,Packed表示兩個聲道的數據交錯存儲,交織在一塊兒,即:LRLRLRLR的存儲方式;Planar表示兩個聲道分開存儲,也就是平鋪分開,即:LLLLRRRR的存儲方式。經過MediaCodec解碼出的PCM是按照Packed方式存儲的,而FFmpeg解碼出的PCM則多是其中的任意一種。

因此爲了更好的歸一化處理,咱們會對FFmpeg解碼出的PCM進行重採樣,統一採樣成AV_SAMPLE_FMT_S16P格式,即:每一個採樣點是兩字節的有符號short類型,而且按照Planar方式存儲。

重採樣:對PCM數據進行從新採樣,能夠改變它的聲道數、採樣率和採樣格式。 好比:原先的PCM音頻數據是2個聲道,44100採樣率,32 bit單精度型。那麼能夠重採樣成:2個聲道,44100採樣率,有符號short類型。

關於分貝值的計算,與上述基於Android平臺的計算方式基本一致,此處就再也不贅述了。

同一首5分鐘的歌,經過FFmpeg提取PCM的耗時只有一兩秒,提取效率至少提高了10倍以上,基本上與iOS持平,至此終於能夠鬆一口氣了。

PCM播放

PCM是原始採樣數據,必須指定採樣率、聲道數和採樣位數(大小端)才能播放。 經過ffplay播放PCM的命令以下所示:

fplay -ar 44100 -channels 2 -f s16le -i test.pcm

參數說明:
1. -ar PCM採樣率
2. -channels PCM通道數
3. -f PCM格式:sample_fmts + le(小端)或者be(大端)
sample_fmts能夠經過ffplay -sample_fmts來查詢
複製代碼

除此以外,經過Audacity也能夠直接播放PCM數據:文件 -> 導入 -> 原始數據,而後選擇對應的採樣率、聲道數、採樣位數和大小端就能夠播放了。

Audacity功能很強大,對於PCM的波形(採樣點值)、響度(db)和頻譜,均可以直接查看,以下所示: PCM-波形

PCM-波形

PCM-響度

PCM-響度

PCM-頻譜

PCM-頻譜

疑問點

爲何Android平臺解封裝、解碼音頻提取PCM的速度這麼慢? 具體緣由我也沒法猜想,待深刻研究以後再來解答吧,若是音視頻的大佬有相關經驗,也麻煩告知。

參考文檔

  1. PCM音量控制
  2. PCM音量控制(高級篇)
相關文章
相關標籤/搜索