在上一篇文章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下載