阅读 597

C++标准库实现WAV文件读写的操作

本文将使用标准C++库实现对数据为PCM格式的WAV文件的读写操作,只使用标准C++库函数,不依赖于其他的库,对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文件格式所需的信息,声明结构体如下:

1
2
3
4
5
6
7
8
9
10
11
// 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的数据域信息,其声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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的文件头,可以表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
 
    数据格式为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的实现方法:使用宏和使用模板,具体如下:

1
2
3
4
5
#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文件的写入。其过程如下:

1
2
3
4
5
6
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文件的信息填充到相应的结构体中,其实现代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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文件,代码如下:

1
2
3
4
5
6
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文件

1
2
3
4
5
6
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文件

1
2
3
CWaveFile wave;
    wave.read("e:\\test1.wav");
    wave.data // PCM数据

源代码只有一个不到300行的cpp文件

到此这篇关于C++标准库实现WAV文件读写的文章就介绍到这了

原文链接:https://www.cnblogs.com/wangguchangqing/p/5970516.html


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐