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