C++標準庫實現WAV文件讀寫

在上一篇文章RIFF和WAVE音頻文件格式中對WAV的文件格式做了介紹,本文將使用標準C++庫實現對數據爲PCM格式的WAV文件的讀寫操作,只使用標準C++庫函數,不依賴於其他的庫。

WAV文件結構

WAV是符合RIFF標準的多媒體文件,其文件結構可以如下:

WAV 文件結構
RIFF塊
WAVE FOURCC
fmt 塊
fact 塊(可選)
data塊(包含PCM數據)

首先是一個RIFF塊,有塊標識RIFF,指明該文件是符合RIFF標準的文件;接着是一個FourCC,WAVE,該文件爲WAV文件;fmt塊包含了音頻的一些屬性:採樣率、碼率、聲道等;fact 塊是一個可選塊,不是PCM數據格式的需要該塊;最後data塊,則包含了音頻的PCM數據。實際上,可以將一個WAV文件看着由兩部分組成:文件頭和PCM數據,則WAV文件頭各字段的意義如下:
這裏寫圖片描述

本文實現的是一個能夠讀取PCM數據格式的單聲道或者雙聲道的WAV文件,是沒有fact塊以及擴展塊。

結構體定義

通過上面的介紹發現,WAV的頭文件所包含的內容有兩種:RIFF文件格式標準中需要的數據和關於音頻格式的信息。對於RIFF文件格式所需的信息,聲明結構體如下:

// The basic chunk of RIFF file format
struct Base_chunk{

    FOURCC fcc;    // FourCC id
    uint32_t cb_size; // 數據域的大小

    Base_chunk(FOURCC fourcc)
        : fcc(fourcc)
    {
        cb_size = 0;
    }
};

chunk是RIFF文件的基本單元,首先一個4字節的標識FOURCC,用來指出該塊的類型;cb_size則是改塊數據域中數據的大小。

文件頭中另一個信息則是音頻的格式信息,實際上是frm chunk的數據域信息,其聲明如下:

// Format chunk data field
struct Wave_format{

    uint16_t format_tag;      // WAVE的數據格式,PCM數據該值爲1
    uint16_t channels;        // 聲道數
    uint32_t sample_per_sec;  // 採樣率
    uint32_t bytes_per_sec;   // 碼率,channels * sample_per_sec * bits_per_sample / 8
    uint16_t block_align;     // 音頻數據塊,每次採樣處理的數據大小,channels * bits_per_sample / 8
    uint16_t bits_per_sample; // 量化位數,8、16、32等
    uint16_t ex_size;         // 擴展塊的大小,附加塊的大小

    Wave_format()
    {
        format_tag      = 1; // PCM format data
        ex_size         = 0; // don't use extesion field

        channels        = 0;
        sample_per_sec  = 0;
        bytes_per_sec   = 0;
        block_align     = 0;
        bits_per_sample = 0;
    }

    Wave_format(uint16_t nb_channel, uint32_t sample_rate, uint16_t sample_bits)
        :channels(nb_channel), sample_per_sec(sample_rate), bits_per_sample(sample_bits)
    {
        format_tag    = 0x01;                                            // PCM format data
        bytes_per_sec = channels * sample_per_sec * bits_per_sample / 8; // 碼率
        block_align   = channels * bits_per_sample / 8;
        ex_size       = 0;                                               // don't use extension field
    }
};

關於各個字段的信息,在上面圖中有介紹,這裏主要說明兩個字段:

  • format_tag表示以何種數據格式存儲音頻的sample值,這裏設置爲0x01表示用PCM格式,非壓縮格式,不需要fact塊。
  • ex_size表示的是擴展塊的大小。有兩種方法來設置不使用擴展塊,一種是設置fmt中的size字段爲16(無ex_size字段);或者,有ex_size,設置其值爲0.在本文中,使用第二種方法,設置ex_size的值爲0,不使用擴展塊。

有了上面兩個結構體的定義,對於WAV的文件頭,可以表示如下:

/*

    數據格式爲PCM的WAV文件頭
    --------------------------------
    | Base_chunk | RIFF |
    ---------------------
    | WAVE              |
    ---------------------
    | Base_chunk | fmt  |   Header
    ---------------------
    | Wave_format|      |
    ---------------------
    | Base_chunk | data |
    --------------------------------
*/

struct Wave_header{

    shared_ptr<Base_chunk> riff;
    FOURCC wave_fcc;
    shared_ptr<Base_chunk> fmt;
    shared_ptr<Wave_format>  fmt_data;
    shared_ptr<Base_chunk> data;

    Wave_header(uint16_t nb_channel, uint32_t sample_rate, uint16_t sample_bits)
    {
        riff      = make_shared<Base_chunk>(MakeFOURCC<'R', 'I', 'F', 'F'>::value);
        fmt       = make_shared<Base_chunk>(MakeFOURCC<'f', 'm', 't', ' '>::value);
        fmt->cb_size = 18;

        fmt_data  = make_shared<Wave_format>(nb_channel, sample_rate, sample_bits);
        data      = make_shared<Base_chunk>(MakeFOURCC<'d', 'a', 't', 'a'>::value);

        wave_fcc = MakeFOURCC<'W', 'A', 'V', 'E'>::value;
    }

    Wave_header()
    {
        riff         = nullptr;
        fmt          = nullptr;

        fmt_data     = nullptr;
        data         = nullptr;

        wave_fcc     = 0;
    }
};

在WAV的文件頭中有三種chunk,分別爲:RIFF,fmt,data,然後是音頻的格式信息Wave_format。在RIFF chunk的後面是一個4字節非FOURCC:WAVE,表示該文件爲WAV文件。另外,Wave_format的構造函數只需要三個參數:聲道數、採樣率和量化精度,關於音頻的其他信息都可以使用這三個數值計算得到。注意,這裏設置fmt chunk的size爲18。

實現

有了上面結構體後,再對WAV文件進行讀寫就比較簡單了。由於RIFF文件中使用FOURCC老標識chunk的類型,這裏有兩個FOURCC的實現方法:使用宏和使用模板,具體如下:

#define FOURCC uint32_t 

#define MAKE_FOURCC(a,b,c,d) \
( ((uint32_t)d) | ( ((uint32_t)c) << 8 ) | ( ((uint32_t)b) << 16 ) | ( ((uint32_t)a) << 24 ) )

template <char ch0, char ch1, char ch2, char ch3> struct MakeFOURCC{ enum { value = (ch0 << 0) + (ch1 << 8) + (ch2 << 16) + (ch3 << 24) }; };

Write WAVE file

寫WAV文件過程,首先是填充文件頭信息,對於Wave_format只需要三個參數:聲道數、採樣率和量化精度,將文件頭信息寫入後,緊接這寫入PCM數據就完成了WAV文件的寫入。其過程如下:

    Wave_header header(1, 48000, 16);

    uint32_t length = header.fmt_data->sample_per_sec * 10 * header.fmt_data->bits_per_sample / 8;
    uint8_t *data = new uint8_t[length];

    memset(data, 0x80, length);

    CWaveFile::write("e:\\test1.wav", header, data, length);

首先夠着WAV文件頭,然後寫入文件即可。將數據寫入的實現也比較簡單,按照WAv的文件結構,依次將數據寫入文件。在設置各個chunk的size值時要注意其不同的意義:

  • RIFF chunk 的size表示的是其數據的大小,其包含各個chunk的大小以及PCM數據的長度。該值 + 8 就是整個WAV文件的大小。
  • fmt chunk 的size是Wave_format的大小,這裏爲18
  • data chunk 的size 是寫入的PCM數據的長度

Read WAVE file

知道了WAV的文件結構後,讀取其數據就更爲簡單了。有一種直接的方法,按照PCM相對於文件起始的位置的偏移位置,直接讀取PCM數據;或者是按照其文件結構依次讀取信息,本文的將依次讀取WAV文件的信息填充到相應的結構體中,其實現代碼片段如下:

    header = make_unique<Wave_header>();

    // Read RIFF chunk
    FOURCC fourcc;
    ifs.read((char*)&fourcc, sizeof(FOURCC));

    if (fourcc != MakeFOURCC<'R', 'I', 'F', 'F'>::value) // 判斷是不是RIFF
        return false;
    Base_chunk riff_chunk(fourcc);
    ifs.read((char*)&riff_chunk.cb_size, sizeof(uint32_t));

    header->riff = make_shared<Base_chunk>(riff_chunk);

    // Read WAVE FOURCC
    ifs.read((char*)&fourcc, sizeof(FOURCC));
    if (fourcc != MakeFOURCC<'W', 'A', 'V', 'E'>::value)
        return false;
    header->wave_fcc = fourcc;
    ...

實例

調用本文的實現,寫入一個單聲道,16位量化精度,採樣率爲48000Hz的10秒鐘WAV文件,代碼如下:

    Wave_header header(1, 48000, 16);

    uint32_t length = header.fmt_data->sample_per_sec * 10 * header.fmt_data->bits_per_sample / 8;
    uint8_t *data = new uint8_t[length];

    memset(data, 0x80, length);

    CWaveFile::write("e:\\test1.wav", header, data, length);

這裏將所有的sample按字節填充爲0x80,以16進制打開該wav文件,結果如下:
這裏寫圖片描述

可以參照上圖給出的WAV文件頭信息,看看各個字節的意義。音頻的格式信息在FOURCC fmt後面

  • 4字節 00000012 fmt數據的長度 18字節
  • 2字節 0001 數據的存儲格式爲PCM
  • 2字節 0001 聲道個數
  • 4字節 0000BB80 採樣率 48000Hz
  • 4字節 00017700 碼率 96000bps
  • 2字節 0002 數據塊大小
  • 2字節 0010 量化精度 16位
  • 2字節 0000 擴展塊的大小
  • 4字節 FOURCC data
  • 4字節 數據長度 0x000EA600

代碼

最後將本文的代碼封裝在了類CWaveFile中,使用簡單。

  • 寫WAV文件
    Wave_header header(1, 48000, 16);

    uint32_t length = header.fmt_data->sample_per_sec * 10 * header.fmt_data->bits_per_sample / 8;
    uint8_t *data = new uint8_t[length];

    memset(data, 0x80, length);

    CWaveFile::write("e:\\test1.wav", header, data, length);
  • 讀取WAV文件
    CWaveFile wave;
    wave.read("e:\\test1.wav");
    wave.data // PCM數據

源代碼只有一個不到300行的cpp文件, CSDN下載

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