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

时间:2022-09-27 15:39:58

在上一篇文章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文件头各字段的意义如下:

C++标准库实现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文件,结果如下:

C++标准库实现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下载

C++标准库实现WAV文件读写的更多相关文章

  1. 用 &num;include &OpenCurlyDoubleQuote;filename&period;h” 格式来引用非标准库的头文件

    用 #include “filename.h” 格式来引用非标准库的头文件(编译器将 从用户的工作目录开始搜索) #include <iostream> /* run this progr ...

  2. 用 &num;include &lt&semi;filename&period;h&gt&semi; 格式来引用标准库的头文件

    用 #include <filename.h> 格式来引用标准库的头文件(编译器将从 标准库目录开始搜索). #include <iostream> /* run this p ...

  3. c&sol;c&plus;&plus;标准库中的文件操作总结

    1 stdio.h是c标准库中的标准输入输出库 2 在c++中调用的方法 直接调用即可,但是最好在函数名前面加上::,以示区分类的内部函数和c标准库函数. 3 c标准输入输出库的使用 3.1 核心结构 ...

  4. Linux音频编程--使用ALSA库播放wav文件

    在UBUNTU系统上使用alsa库完成了对外播放的wav文件的案例. 案例代码: /** *test.c * *注意:这个例子在Ubuntu 12.04.1环境下编译运行成功. * */ #inclu ...

  5. Python 标准库 csv —— csv 文件的读写

    csv 文件,逗号分割文件. 0. 读取 csv 到 list from csv import reader def load_csv(csvfile): dataset = [] with open ...

  6. python --标准库 路径与文件 &lpar;os&period;path包&comma; glob包&rpar;

    os.path包 os.path包主要是处理路径字符串,提取出有用信息. #coding:utf-8 import os.path path = 'D:\\Python7\\test\\data.tx ...

  7. C&plus;&plus;标准库头文件找不到的问题

    当你写C++程序时,在头文件中包含C++标准库的头文件,比如#include <string>,而编译器提示你找不到头文件! 原因就是你的实现源文件扩展名是".c"而不 ...

  8. Python解析Wav文件并绘制波形的方法

    资源下载 #本文PDF版下载 Python解析Wav文件并绘制波形的方法 #本文代码下载 Wav波形绘图代码 #本文实例音频文件night.wav下载 音频文件下载 (石进-夜的钢琴曲) 前言 在现在 ...

  9. Python标准库、第三方库和外部工具汇总

    导读:Python数据工具箱涵盖从数据源到数据可视化的完整流程中涉及到的常用库.函数和外部工具.其中既有Python内置函数和标准库,又有第三方库和工具. 这些库可用于文件读写.网络抓取和解析.数据连 ...

随机推荐

  1. Windows Phone 十四、磁贴通知

    磁贴(Tile) Windows Phone 磁贴种类: 小尺寸 SmallLogo:71x71: Square71x71 中等 Logo:150x150: Square150x150 宽 WideL ...

  2. Digital Root - SGU 118(高精度运算)

    题目大意:有K组测试数据,然后每组有N个正整数,A1,A2,A3.....An,求出 A1 + A1*A2 + A1*A2*A3 + .......A1*A2*...An 的数根. 分析:有个对9取余 ...

  3. Arduino 3G shield using SoftwareSerial to control

    On the 3G shield, by default the power pin is on D8 and reset pin is on D9. Make it HIGH then it wor ...

  4. Java中Dom解析xml文档

    xml文档 <?xml version="1.0" encoding="UTF-8"?> <bookstore> <book id ...

  5. js创建对象,放进js集合

    var list=[]; for (var i=0;i<nodes.length;i++){ if(nodes[i].type=='user'){ person=new Object(); pe ...

  6. java开发人员win10配置

    1.让win10的cmd支持ll命令 新建ll.bat 编辑类容为: @echo off dir3.将该文件移动到C:\Windows下. CMD里就可以用ls来代替dir命令显示目录列表了. 2.i ...

  7. DWZ&lpar;JUI&rpar; 教程 跨域请求 iframeNavTab

    如果想navTab访问其他的网址,可以使用 iframe  navTab 使用时也非常简单 <li><a href="http://www.baidu.com"  ...

  8. 集合框架的类和接口均在java&period;util包中。 任何对象加入集合类后,自动转变为Object类型,所以在取出的时候,需要进行强制类型转换。

    集合框架的类和接口均在java.util包中. 任何对象加入集合类后,自动转变为Object类型,所以在取出的时候,需要进行强制类型转换.

  9. python&plus;anaconda&plus;pycharm工具包安装

    更新额外包 $ conda update conda 更新pip python -m pip install --upgrade pip 更新所有 conda update --all 安装ffmpe ...

  10. Swift,简单语法

    1.创建变量 var a=0 //变量 let b=0 //常量 let b:String="你好" //:后可以定义类型(变量一旦定义,类型不可改变)(类型不填Swift会自动判 ...