音頻編碼——PCM

2021-02-07 西紅柿炒蛋炒飯
1. 聲音三要素

聲音主觀感受上主要有響度、音高、音色以及掩蔽效應等特徵,其中響度、音高、音色在物理上可以量化成具有振幅、頻率、相位的波,故稱它們為聲音的"三要素"。

音高。表示人耳對音調高低的主觀感受,物理上用頻率與之對應,頻率越高,音高越高。人耳可以識別的聲音頻率範圍是 20~20kHz。音色。從音樂的角度來講,音色由樂器的材質決定。物理上,音色是是眾多相位不同波形疊加產生,其中波形的基頻產生的聽得最清楚的音稱為基音,各諧波(其它相位)微小震動產生的聲音稱為泛音。2. A/D轉換與PCM

要將自然界中的信號進行傳輸,聲音轉換成計算機能夠識別的形式,前者稱為模擬信號,後者成為數位訊號,模擬信號與數位訊號之間的轉換過程就叫做數模轉換(A/D)

PCM(Pulse Code Modulation) 是數字通信中編碼方式的一種,也即計算所能識別的信號形式。PCM 通過對模擬信號進行採樣、量化、編碼而產生,接收 PCM 信號的端則將編碼"還原"成模擬信號。採樣過程將連續的信號按照固定時間間隔離散化(聲音是一個連續信號),根據奈奎斯特採樣定理,為了保證最終的數位訊號能比較完整的還原成模擬信號,採樣頻率必須是原信號頻率的2倍及以上。採樣完成了信號在時間緯度上的離散,但仍是模擬信號,因為樣值在一定範圍內仍然具有無限多取值可能,所以將取值範圍按照一定步長劃分為有限個取值,這就是量化。將量化後的樣值按照一定規則排列就是編碼了。

簡單來說,A/D 轉換就是把連續變成離散,把無限變成有限。

PCM Encoded Signal

在音視頻領域,PCM 常用來保存原始音頻數據,並且約定 PCM 等價於無損編碼。很多高保真的音頻也都採用 PCM 保存,缺點就是佔用空間會比較多一些。

3. 音頻編碼3.1. 音頻參數

音頻數據參數有採樣率採樣位數以及聲道數

採樣率指聲音信號在 A/D 轉換過程中單位時間內的採樣次數,單位是 Hz; 採樣位數是指用多少 bit 數據對聲音進行量化,常用的有 8 bit、16bit; 聲道數又稱音軌,不準確地理解是聲源,人聽到聲音時會對聲源進行定位,不同位置的聲道數越多,效果就越逼真,常見聲道數有:

雙聲道,stereo,最常見的類型,包含左聲道以及右聲道。5.1聲道,包含一個正面聲道、左前方聲道、右前方聲道、左環繞聲道、右環繞聲道、一個低音聲道,最早應用於早期的電影院。7.1聲道,在5.1聲道的基礎上,把左右的環繞聲道拆分為左右環繞聲道以及左右後置聲道,主要應用於BD以及現代的電影院。

若音頻PCM格式描述為 44100kHz 16LE stereo,意思是採樣率是 44100Hz,採樣位數是 16bit(無符號數)並且單個採樣採用小端法存儲,stereo 表示雙聲道。

量化的值可能是整數也可能是浮點數。

3.2. 音頻PCM存儲

如果是單聲道音頻,那麼採樣數據按照時間順序一次存儲。如果音頻是雙聲道,則左右聲道採樣按照時間順序交錯存儲。

PCM Data

當使用 ffplay 播放 PCM 數據時,需要指定音頻的採樣率、採樣位寬以及聲道數:

ffplay -autoexit -ar 44100 -channels 2 -f s16le -i raw.pcm

3.3. WAV 格式

原始PCM數據的一個問題就是每次播放時需要顯式指定採樣率等參數,比較直接的解決方案就是將這些參數也寫入音頻文件,讓播放器幫我們解析這些參數。WAV 是 Microsoft 和 IBM 為 PC 開發的音頻文件格式,它做的事情就是在文件頭部寫入一些描述信息,用來告訴播放器所需要的參數。

WAV 採用 RIFF 規範進行數據存儲:

WAV format

第一列表示對應區塊是採用大端法還是小端法進行存儲; 第二列是區塊在文件中的偏移位置,第四列是每個區塊的大小,限定了區塊的固定佔用空間; 中間第三列就是每個區塊的定義,規定了每個區塊需要存放什麼數據。

圖中每個 Chunk 都有 ChunkID 和 ChunkSize,後面的 Chunk 都是第一個 Chunk 的 SubChunk。

第一個 ChunkID 內容是 "RIFF",指明文件存儲格式,隨後 ChunkSize 內容是剩餘文件長度(byte,4+(4+4+SubChunk1Size)+(4+4+SubChunk2Size))。Format 中存儲 "WAVE",表明這個文件存儲的是 PCM 數據,確定了 Format 也就決定了如何解析剩餘文件內容。

第二個 Chunk 描述了音頻參數。AudioFormat 指明音頻數據格式,PCM = 1,如果不是 1 就表示音頻數據是其它相應的壓縮格式(比如 MP3)。BlockAlign = NumChannels * BitsPerSample/8,每次對音頻數據的讀寫大小必須是 BlockAlign 的整數倍,並且只能從一個完整的 Block 的起始地址開始讀寫,從其它位置開始讀寫都是非法的。

前兩個 Chunk 描述了音頻數據的基本信息,第三個 Chunk 存儲實際音頻數據。

3.4. 讀寫 WAV

下面演示如何將 PCM 存儲為 WAV 文件以及如何從 WAV 文件中讀取出原始 PCM 數據並列印音頻參數。

pcm 文件使用 ffmpeg -i audio.mp3 -f s16le -acodec pcm_s16le out.pcm 從音頻文件中提取。

/**
 * @param0 executable program's file path
 * @param1 raw pcm data file path
 * @param2 sample rate
 * @param3 sample size in bits
 * @param4 number of channels
 * @param4 output file path
 */
void write_wav(int argc, char const *argv[])
{
    std::string pcm_file = std::string(argv[1]);
    int sample_rate = std::atoi(argv[2]);
    int sample_size = std::atoi(argv[3]);
    int nr_channels = std::atoi(argv[4]);
    std::string wav_file = std::string(argv[5]);

    // read/write in binary mode
    std::ifstream pcm_st;
    pcm_st.exceptions(std::fstream::failbit | std::fstream::badbit);
    try
    {
        pcm_st.open(pcm_file, std::ios::in | std::ios::binary);
    }
    catch (const std::fstream::failure &e)
    {
        LOG_E("Failed to open: %s-%d %s", argv[1], pcm_st.fail(), e.what());
        return;
    }

    std::ofstream wav_st;
    wav_st.exceptions(std::fstream::failbit | std::fstream::badbit);
    try
    {
        wav_st.open(wav_file, std::ios::out | std::ios::binary);
    }
    catch (const std::fstream::failure &e)
    {
        LOG_E(e.what());
    }
    // define Header Chunk
    ChunkHeader wav_header{};
    memcpy(wav_header.ChunkID, "RIFF", strlen("RIFF"));
    memcpy(wav_header.Format, "WAVE", strlen("WAVE"));
    wav_st.seekp(sizeof(ChunkHeader), std::ios::cur);

    // define SubChunk1
    ChunkFmt wav_fmt{};
    memcpy(wav_fmt.ChunkID, "fmt ", strlen("fmt "));
    wav_fmt.ChunkSize = sizeof(wav_fmt) - 8;
    wav_fmt.AudioFormat = 1;
    wav_fmt.NrChannels = nr_channels;
    wav_fmt.SampleRate = sample_rate;
    wav_fmt.ByteRate = sample_rate * nr_channels * sample_size / 8;
    wav_fmt.BlockAlign = nr_channels * sample_size / 8;
    wav_fmt.BitsPerSample = sample_size;
    wav_st.seekp(sizeof(ChunkFmt), std::ios::cur);
    // define SubChunk2
    ChunkData wav_data{};
    memcpy(wav_data.ChunkID, "data", strlen("data"));
    wav_st.seekp(sizeof(ChunkData), std::ios::cur);

    LOG_I("Writing pcm data");
    int total_raw_data_size = 0;
    int readed_size = 0;
    char *buffer = static_cast<char *>(malloc(1024));

    try
    {
        do
        {
            pcm_st.read(buffer, 1024);
            readed_size = pcm_st.gcount();
            total_raw_data_size += readed_size;
            wav_st.write(buffer, readed_size);
        } while (pcm_st.gcount() > 0);
    }
    catch (const std::fstream::failure &e)
    {
        LOG_E("Oops, some error occured: %s", e.what());
    }

    wav_header.ChunkSize = 4 + sizeof(ChunkFmt) + sizeof(ChunkData) + total_raw_data_size;
    wav_data.ChunkSize = total_raw_data_size;

    // seek at the beggining of the output file
    wav_st.seekp(0, std::ios::beg);
    // write the header
    LOG_I("Writing header chunk");
    wav_st.write(reinterpret_cast<char *>(wav_header.ChunkID), sizeof(char) * 4);
    wav_st.write(reinterpret_cast<char *>(&wav_header.ChunkSize), sizeof(uint32_t));
    wav_st.write(reinterpret_cast<char *>(wav_header.Format), sizeof(char) * 4);

    LOG_I("Writing format chunk");
    wav_st.write(reinterpret_cast<char *>(wav_fmt.ChunkID), sizeof(char) * 4);
    wav_st.write(reinterpret_cast<char *>(&wav_fmt.ChunkSize), sizeof(uint32_t));
    wav_st.write(reinterpret_cast<char *>(&wav_fmt.AudioFormat), sizeof(uint16_t));
    wav_st.write(reinterpret_cast<char *>(&wav_fmt.NrChannels), sizeof(uint16_t));
    wav_st.write(reinterpret_cast<char *>(&wav_fmt.SampleRate), sizeof(uint32_t));
    wav_st.write(reinterpret_cast<char *>(&wav_fmt.ByteRate), sizeof(uint32_t));
    wav_st.write(reinterpret_cast<char *>(&wav_fmt.BlockAlign), sizeof(uint16_t));
    wav_st.write(reinterpret_cast<char *>(&wav_fmt.BitsPerSample), sizeof(uint16_t));

    LOG_I("Writing data chunk");
    wav_st.write(reinterpret_cast<char *>(wav_data.ChunkID), sizeof(char) * 4);
    wav_st.write(reinterpret_cast<char *>(&wav_data.ChunkSize), sizeof(uint32_t));

    pcm_st.close();
    wav_st.close();
    LOG_I("completed, %s", argv[5]);
}

/**
 * @param0 executable program's file path
 * @param1 wav file path
 * @param2 output file path
 */
void read_wav(int argc, char const *argv[])
{
    std::string wav_file = std::string(argv[1]);
    std::string pcm_file = std::string(argv[2]);

    // read/write in binary mode
    std::ifstream wav_st;
    wav_st.exceptions(std::fstream::failbit | std::fstream::badbit);
    try
    {
        wav_st.open(wav_file, std::ios::in | std::ios::binary);
    }
    catch (const std::fstream::failure &e)
    {
        LOG_E(e.what());
    }
    std::ofstream pcm_st;
    pcm_st.exceptions(std::fstream::failbit | std::fstream::badbit);
    try
    {
        pcm_st.open(pcm_file, std::ios::out | std::ios::binary);
    }
    catch (const std::fstream::failure &e)
    {
        LOG_E("Failed to open: %s-%d %s", argv[1], pcm_st.fail(), e.what());
        return;
    }

    ChunkHeader wav_header{};
    ChunkFmt wav_fmt{};
    ChunkData wav_data{};

    // read RIFF chunk
    LOG_I("Reading RIFF chunk");
    wav_st.read(reinterpret_cast<char *>(wav_header.ChunkID), sizeof(char) * 4);
    wav_st.read(reinterpret_cast<char *>(&wav_header.ChunkSize), sizeof(uint32_t));
    wav_st.read(reinterpret_cast<char *>(wav_header.Format), sizeof(char) * 4);
    if (std::string(wav_header.Format) != "WAVE")
    {
        LOG_E("Invalid format: %s", wav_header.Format);
        return;
    }
    // read fmt chunk
    LOG_I("Reading fmt chunk");
    wav_st.read(reinterpret_cast<char *>(wav_fmt.ChunkID), sizeof(char) * 4);
    wav_st.read(reinterpret_cast<char *>(&wav_fmt.ChunkSize), sizeof(uint32_t));
    wav_st.read(reinterpret_cast<char *>(&wav_fmt.AudioFormat), sizeof(uint16_t));
    wav_st.read(reinterpret_cast<char *>(&wav_fmt.NrChannels), sizeof(uint16_t));
    wav_st.read(reinterpret_cast<char *>(&wav_fmt.SampleRate), sizeof(uint32_t));
    wav_st.read(reinterpret_cast<char *>(&wav_fmt.ByteRate), sizeof(uint32_t));
    wav_st.read(reinterpret_cast<char *>(&wav_fmt.BlockAlign), sizeof(uint16_t));
    wav_st.read(reinterpret_cast<char *>(&wav_fmt.BitsPerSample), sizeof(uint16_t));

    // read data chunk
    LOG_I("Reading data chunk");
    wav_st.read(reinterpret_cast<char *>(wav_data.ChunkID), sizeof(char) * 4);
    wav_st.read(reinterpret_cast<char *>(&wav_data.ChunkSize), sizeof(uint32_t));

    LOG_I("Reading pcm data");
    int total_raw_data_size = 0;
    int readed_size = 0;
    char *buffer = static_cast<char *>(malloc(1024));

    try
    {
        do
        {
            wav_st.read(buffer, 1024);
            readed_size = wav_st.gcount();
            total_raw_data_size += readed_size;
            pcm_st.write(buffer, readed_size);
        } while (wav_st.gcount() > 0);
    }
    catch (const std::fstream::failure &e)
    {
        LOG_E("Oops, some error occured: %s", e.what());
    }

    pcm_st.close();
    wav_st.close();
    LOG_I("completed, %s", argv[2]);
}

程序運行執行後可以通過 ffplay 測試音頻是否能正常播放:

# 寫 pcm 寫入 wav 文件
ffplay -autoexit -i build/out.wav
# 從 wav 文件提取 pcm
ffplay -autoexit -ar 44100 -channels 2 -f s16le -i build/out.pcm

4. References

[1] 聲音"三要素": https://blog.csdn.net/junllee/article/details/7217435

[2] 音頻屬性相關: https://www.cnblogs.com/yongdaimi/p/10722355.html

[3] WAV: https://zh.wikipedia.org/wiki/WAV

[4] WAVE PCM soundfile format: http://soundfile.sapp.org/doc/WaveFormat

相關焦點

  • iOS 錄音、音頻的拼接剪切以及邊錄邊壓縮轉碼
    -(void)updateMeters{    [self.audioRecorder updateMeters];}提示:更新音頻測量值,注意如果要更新音頻測量值必須設置meteringEnabled為YES,通過音頻測量值可以即時獲得音頻分貝等信息 @property(getter=isMeteringEnabled
  • 音頻技術解析:純理論對比PCM和DSD
    根據奈奎斯特採樣定理(為了不失真地恢復模擬信號,採樣頻率應該不小於模擬信號頻譜中最高頻率的2倍),採樣率44.1KHz的數字音頻格式可以無損地記錄22.05KHz以下頻率的音頻信號(參考自香農採樣定理),其剛好超過了人耳的聽力範圍20kHz。對於PCM波形來說更高的採樣率意味著曲線更加接近真實。
  • 新手小白必讀 1分鐘了解音頻編碼黑科技DSD
    聲音素質優秀卻處境尷尬的DSD格式DSD的全稱是「Direct Stream Digital」,是一項屬於Sony和飛利浦的專利,利用脈衝密度調製(pulse-density modulation)編碼將音頻信號存儲在數字媒體上的科技,這項技術的應用對象是SACD(同樣是Sony與飛利浦合力研發的音樂碟片規格,是繼CD的發明之後,成功超越
  • iOS14充電提示音快捷指令編碼怎麼弄 base64編碼設置教程
    iOS14想必很多朋友都更新了,一些小夥伴想設置iOS14的充電提示音進行編碼設置,但是不知道怎麼弄,下面就來為大家分享一下iOS14充電提示音快捷指令編碼設置教程。
  • 完全解析:視頻編碼與封裝
    -----       理解了碼率之後我們就應當理解視頻的編碼,視頻編碼的存在意義就是正確的拋棄視頻信息來達到壓縮視頻容量的目的。所謂正確就是指編碼高效率,智能的在儘可能不改變畫面觀感的情況下捨棄信息。常規的有損編碼可以分為二種:幀內壓縮與幀間壓縮。幀內壓縮顧名思義,代表對於每一幀畫面進行獨立的壓縮,這種壓縮方式的優點就是對於電腦的負擔較小,缺點則是文件的大小會非常巨大。幀間壓縮則是利用多幀比較進行壓縮,通過這個方法文件大小可以被大大縮減,但是卻對電腦有著較大的負擔。
  • 移動輕直播的春天——禾苗直播編碼棒實測
    編碼棒的兩側分布著它的主要接口,由於我的這臺編碼棒為專業型,在輸入端有HDMI和3.5mm音頻兩個接口,而普通型編碼棒是沒有外接音頻接口的。編碼棒的這一側還有兩個指示燈,分別在編碼工作和供電時亮起。再來看另一側,USB接口用來連接推流用的安卓手機,AUDIO/HDMI開關用來選擇音頻輸入源(專業型獨有),Micro USB接口用來外接供電。
  • Python處理音頻文件的實用姿勢
    其中功能最全也最流行的就是ffmpeg,它是開源視頻處理軟體,支持絕大多數的音視頻格式編碼,被廣泛引用於各大視頻網站和商業軟體。ffmpegffmpeg安裝ffmpeg需要獨立安裝,網上大部分教程都已過時,最好參考官方文檔。
  • 愛奇藝編碼團隊:我們讓AV1編碼速度提升5倍
    愛奇藝綜藝《青春有你》THE9成團編者按:如果說VVC是編碼標準中的白富美,那麼AV1就代表了廣大的網際網路玩家——承受不起高昂的專利版稅。為了實現免專利費的目標,AV1不得不「捨近求遠」的方式躲開已有的專利,這導致其計算複雜度非常高。愛奇藝科學家王志航透露,在同等的畫質下,編碼速度是開源SVT-AV1的5倍。
  • 《視頻直播技術詳解》之(四):編碼和封裝
    FFmpeg 是一個自由軟體,可以運行音頻和視頻多種格式的錄影、轉換、流功能,包含了 libavcodec -這是一個用於多個項目中音頻和視頻的解碼器庫,以及 libavformat -一個音頻與視頻格式轉換庫。FFmpeg 這個單詞中的 FF 指的是 Fast Forward。
  • MPEG-4編碼技術所具有的優點
    1 高壓縮性優點MPEC-4為一種效率非常高的編碼標準,最低碼率為5~65kbs。在進行編碼時,要對視頻對象所具有的可操作性以及交互性加強重視,同時還要對多媒體各個應用領域之中的編碼進行兼容處理。此外,MPEC-4還可以對在同一時間發生的不同種數據流進行編碼處理。
  • 一對一直播開發,連麥音頻的處理經過了哪些步驟
    在一對一直播開發中,視頻直播的流程步驟如下:採集、處理、編碼、推流、分發、播放。主打語音聊天的一對一直播,在音頻方面的處理步驟和視頻差不多,但要比視頻處理起來更快,我們今天來說一下,一對一直播開發中音頻的處理步驟的詳細信息。
  • 耳聽為虛眼見為實,來聊聊音頻技術參數解析
    四、幀音頻的幀的概念沒有視頻幀那麼清晰,幾乎所有視頻編碼格式都可以簡單的認為一幀就是編碼後的一副圖像。但音頻幀跟編碼格式相關,它是各個編碼標準自己實現的。因為如果以PCM(未經編碼的音頻數據)來說,它根本就不需要幀的概念,根據採樣率和採樣精度就可以播放了。
  • 支持av1、hevc編碼以及m3u8的轉換
    MP4 GUI(MP4圖形器)主要功能:>>>轉換功能:1.支持常見的多媒體文件轉換,如視頻、動畫、音頻、圖片、字幕歌詞文件。2.支持絕大多數的編碼格式,如:視頻的AV1、HEVC(H.265)、H264、VP9等格式,包括編碼屬性的檔次、級別、質量、速度的參數操作。
  • ISRC編碼?發布音樂流媒體平臺作品確權之徑
    本辦法所稱的音樂錄像製品,是指由音頻信號和視頻信號錄製的製品,其中構成該表演性音樂製品的全部或主要部分為音頻信號,主要包括MTV、MV、卡拉OK、演唱會等。第三條 每一可獨立使用的錄音製品或音樂錄像製品均須分配一個單獨的中國標準錄音製品編碼(以下稱ISRC編碼)。該編碼只標識被編碼對象,不能作為出版物標識。
  • 怎麼用音頻轉換軟體轉換AAC格式音頻
    AAC是一種專為聲音數據設計的文件壓縮格式,它與MP3不同,採用了全新的算法進行編碼,更加高效,但其屬於有損壓縮的格式,與時下流行的FLAC等無損格式相比音質存在「本質上」的差距。那怎麼用音頻轉換軟體轉換AAC格式音頻呢?下面就來為你演示具體的操作方法,一定要認真看下去哦!
  • 音頻,視頻的格式及區別
    就是Moving Pictures Experts Group(動態圖像專家組)的縮寫,由國際標準化組織ISO(International Standards Organization)與IEC(International Electronic Committee)於1988年聯合成立,專門致力於運動圖像(MPEG視頻)及其伴音編碼(MPEG音頻)標準化工作。
  • 音頻剪輯軟體哪個好用?分享3款高質量的音頻分割合併軟體
    音頻剪輯軟體哪個好用?大家首先肯定會想到Logic Pro X、Cubase、ProTools等專業的音頻剪輯軟體,但是這些軟體體積龐大,功能複雜,對電腦硬體和系統有一定要求,而且購買軟體需要花費一筆不小的費用,對新手來說並不友好。那有沒有其他好用的音頻剪輯軟體呢?
  • MPEG-H音頻標準將落地中國
    展臺上還有與合作夥伴Merging Pyramix一起聯合展示了支持MPEG-H的數字音頻工作站。同時與兩家實時監聽公司Telos、Junger Audio合作,支持Fraunhofer的技術並提供相關的設備,希望能更好地助力廣電構建生態圈。此外,Fraunhofer IIS還專門搭設了一個視聽室,讓大家更直觀地感受到MPEG-H音頻技術的魅。
  • 推介|浙江衛視「領跑2018」跨年演唱會音頻系統設計與總結
    一、系統基本模塊組成本次跨年演唱會音頻轉播系統由現場擴聲和返聽系統、音樂混音系統、語言混音及流程製作系統、終混製作系統、編碼與QC(質量控制)系統、轉播車系統、傳輸與播出系統組成,其中擴聲、返聽及音樂混音系統由金少剛老師團隊負責,語言混音及流程製作、終混製作、編碼與審聽系統由浙江廣電集團音頻團隊負責,後續的轉播車及傳輸、播出系統由浙江廣電集團視頻等其他團隊負責