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数据

源代码只有一个不到300行的cpp文件,我们下载

到此这篇关于C++标准库实现WAV文件读写的文章就介绍到这了,更多相关C++ WAV文件读写内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++将音频PCM数据封装成wav文件的方法

    前言 使用声音设备采集的声音数据通常是PCM数据,直接写入文件是无法播放的,通常的做法是将其封装成wav格式,这样播放器就能够识别且播放了.本文将介绍如何将PCM封装成wav的方法. 一.如何实现? 首先需要构造wav头部,wav文件音频信息全部保存在头部,我们要做的就是在PCM数据的前面加入wav头,并且记录PCM的相关参数. 1.定义头结构 只定义PCM格式的wav文件头 //WAV头部结构-PCM格式 struct WavPCMFileHeader; 2.预留头部空间 创建文件时预留头部空

  • C++读取WAV音频文件的头部数据的实现方法

    C++读取WAV音频文件的头部数据的实现方法 前言: 在这里分享一下自己的心得,希望和大家一起分享技术,如果有什么不足,还请大家指正.写出这篇目的,就是希望大家一起成长,我也相信技术之间没有高低,只有互补,只有分享,才能使彼此更加成长. 实现代码: #include <iostream> #include <string> #include <fstream> using namespace std; using std::string; using std::fstr

  • C++读取wav文件中的PCM数据

    前言 wav文件通常会使用PCM格式数据存储音频,这种格式的数据读取出来直接就可以播放,要在wav文件中读取数据,我们首先要获取头部信息,wav的文件结构里面分为多个chunk,我们要做的就是识别这些chunk的信息,获取音频的格式以及数据. 一.如何实现? 首先需要构造wav头部,wav文件音频信息全部保存在头部,我们要做的就是读取wav头部信息,并且记录PCM的相关参数. 1.定义头结构 只定义PCM格式的wav文件头,对于PCM格式的数据只需要下面3个结构体即可. struct WaveR

  • 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,该文件为WA

  • Python 标准库zipfile将文件夹加入压缩包的操作方法

    zipfile模块是python中一个处理压缩文件的模块,解决了不少我们平常需要处理压缩文件的需求.大家还知道Python zipfile 库可用于压缩/解压 zip 文件. 本文介绍一下如何创建压缩包. 将 "文件" 加入压缩包 假设目录结构如下: my_project |- 唐诗三百首.txt # 这是要打包的文件 |- demo.py # 演示代码会在这里编写 |- _______ # 我们想要在这里生成一个名为 "output.zip" 的文件 "

  • C语言Iniparser库实现ini文件读写

    目录 一.概述 二.使用 下载 方式一 方式二 三.API函数 四.演示 一.概述 iniparser是针对INI文件的解析器.ini文件则是一些系统或者软件的配置文件.iniparser库的API可以对ini文件(配置文件)进行解析.设置.删除等操作. 常见的 ini 读写开源库有:minIni.inifile.iniparser 二.使用 下载 Github:https://github.com/ndevilla/iniparser 方式一 1.编译 下载后进入文件根目录,使用 make 命

  • Python文件读写保存操作的示例代码

    记录下第一次使用Python读写文件的过程,虽然很简单,第一次实现其实也有些注意的事项. 单个文件的读操作: 我们先假设一个需求如下: 读取一个test.txt文件 删除指定字符之前的文本 需求明白之后,下面开始动手写代码,代码很简单.就直接上全部的,细节看注释: import sys filePath = "/Users/xxxxxx/Desktop/test.txt" # 打开文件 files = open(filePath, 'r') # 转成list f_list = file

  • C++超详细讲解标准库

    目录 一.有趣的重载 二.C++ 标准库 三.小结 一.有趣的重载 操作符 << 的原生意义是按位左移,例:1 <<2; 其意义是将整数 1 按位左移2位,即:0000 0001 → 0000 0100 现在来尝试一下重载左移操作符,将变量或常量左移到一个对象中! #include <stdio.h> const char endl = '\n'; class Console { public: Console& operator << (int i

  • Go标准库日志打印及同时输出到控制台与文件

    目录 打印 log包 如何输出日志到文件? 如何同时输出到控制台和文件? 附:日志切割(按文件大小切割.按日期切割) 总结 打印 在使用go写一些小程序时,我们没必要引入额外的包,直接使用fmt标准包打印即可: import "fmt" func main() { fmt.Println("line1") fmt.Print("line2") fmt.Printf("line%d \n", 3) str1 := fmt.Spr

  • 用python标准库difflib比较两份文件的异同详解

    [需求背景] 有时候我们要对比两份配置文件是不是一样,或者比较两个文本是否异样,可以使用linux命令行工具diff a_file b_file,但是输出的结果读起来不是很友好.这时候使用python的标准库difflib就能满足我们的需求. 下面这个脚本使用了difflib和argparse,argparse用于解析我们给此脚本传入的两个参数(即两份待比较的文件),由difflib执行比较,比较的结果放到了一个html里面,只要找个浏览器打开此html文件,就能直观地看到比较结果,两份文件有差

  • 解析Go 标准库 http.FileServer 实现静态文件服务

    http.FileServer 方法属于标准库 net/http,返回一个使用 FileSystem 接口 root 提供文件访问服务的 HTTP 处理器.可以方便的实现静态文件服务器. http.ListenAndServe(":8080", http.FileServer(http.Dir("/files/path"))) 访问 http://127.0.0.1:8080,即可看到类似 Nginx 中 autoindex 目录浏览功能. 源码解析 我们现在开始将

  • Python标准库pathlib操作目录和文件

    目录 pathlib 基本组件 常用属性和基本方法 总结 学习 Python 时,尤其是在进行文件操作和数据处理时,经常会处理路径问题.最常用和常见的是 os.path 模块,它将路径当做字符串进行处理,如果使用不当可能导致难以察觉的错误,而且代码很难跨平台复用.pathlib 就是一个非常棒的Python标准库,超级好用. pathlib模块提供了一种在 POSIX 系统(如 Linux 和 Windows)下运行良好的高级抽象,它抽象了资源路径和资源命名结构,把文件系统接口从os模块中隔离出

  • 200个Python 标准库总结

    目录 1.文本 2.数学 3.函数式编程 4.文件与目录 5.持久化 6.压缩 7.加密 8.操作系统工具 9.并发 10.进程间通信 11.互联网 12.互联网协议与支持 13.多媒体 14.国际化 15.编程框架 16.Tk图形用户接口 17.开发工具 18.调试 19.运行时 20.解释器 21.导入模块 22.Python语言 23.其他 24.Windows相关 25.Unix相关 1.文本 string:通用字符串操作 re:正则表达式操作 difflib:差异计算工具 textwr

随机推荐