1. 程式人生 > >C++標準庫實現WAV檔案讀寫

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資料