WAVE文件信息(头参数,PCM数据)存储规则
关于wave文件的头信息参看如下两篇博客:
(1) http://www.cnblogs.com/liyiwen/archive/2010/04/19/1715715.html
(2) http://syp0316.blog.163.com/blog/static/49178801200793133424635/
在对WAVE格式音频文件有个大致了解之后,下面我们结合朴实的代码逐字节读取wave格式音频文件信息,来分析wave文件数据的存放和存储的规则。(Word中代码加注释有点乱,但不影响复制到VS中调试,建议先看最后的实验结果。见谅!)
/*****传统方法(C语言),运行环境为VS2010*****/
#include <iostream>
using namespace std;
#include "stdlib.h"
#include "stdio.h"
#include <math.h>
#define N 160
float *Read_Data(FILE *fp_speech,int front_info);
void main()
{
float *a;
int i,flag=0; //i用作循环计数,flag用作判断是否存在
unsigned char ch[100]; //用来存储wav文件的头信息
FILE *fp;
short buf[N];//用来存放PCM数据,看是否读取正确
fp=fopen("E:\\Audio\\Result\\sc02_single.wav","rb");//为读,打开一个wav文件
if((fp=fopen("E:\\Audio\\Result\\sc02_single.wav","rb"))==NULL) //若打开文件失败,退出
{
printf("can't open this file\n");
exit(0);
}
/*****
注意:这里的去读方式是比较直接的,现将字符逐个存储再按照数据块大小逐个读出翻译成我们想要的形式。有些事字符,如data,只需按顺序逐个输出即可。有些是进制数,分高低位,我们这里是逐个有高位到底为组成一串。如整个文件大小size,从高位到地位输出之后为0x00 07 68 aa,一个字节等于八位,若读取这个字节数小于16,则用第四位就能表示,所以高四位用16进制0表示,组合起来正好四个字节
*****/
/**********输出wav文件的所有信息**********/
printf("该wav文件的所有信息:");
for(i=0;i<46;i++)
{
ch[i]=fgetc(fp); //每次读取一个字符,存在数组ch中
if(i%16==0) //每行输出16个字符对应的十六进制数
printf("\n");
if(ch[i]<16) //对小于16的数,在前面加0,使其用8bit显示出来,保持结构的完整性,也符合我们从左到右的阅读习惯
printf("0%x ",ch[i]);
else
printf("%x ",ch[i]);
}
/*********RIFF WAVE Chunk的输出*********/
/****************************************
RIFF WAVE Chunk
==================================
| |所占字节数| 具体内容 |
==================================
| ID | 4 Bytes | 'RIFF' |
----------------------------------
| Size | 4 Bytes | |
----------------------------------
| Type | 4 Bytes | 'WAVE' |
******************************************/
printf("\n\nRIFF WAVE Chunk信息:");
//输出RIFF标志
printf("\nRIFF标志:");
for(i=0;i<4;i++)
{
printf("%c ",ch[i]);
}
//输出size大小
printf("\nsize:ox");
for(i=7;i>=4;i--) //低字节表示数值低位,高字节表示数值高位,有高位开始逐个读取,在放到一起
{
if(ch[i]<16)
printf("0%x",ch[i]); //这里SIZE大小为4个字节,1个字节有8位,而1个16进制数用4位就可以表示
else //若读到该字节发现数小于16,则前面补零
printf("%x",ch[i]); //运行程序中该段内容显示为:0x 00 07 68 aa 形式上比较严格,八位表示一字节,不大于四位前面补零显示
}
//输出WAVE标志
printf("\nWAVE标志:");
for(i=8;i<12;i++)
{
if(ch[i]<16)
printf("0%c ",ch[i]);
else
printf("%c ",ch[i]);
}
/*******Format Chunk的输出*******/
/****************************************
Format Chunk
====================================================================
| | 字节数 | 具体内容 |
====================================================================
| ID | 4 Bytes | 'fmt ' |
--------------------------------------------------------------------
| Size | 4 Bytes | 数值为16或18,18则最后又附加信息|
-------------------------------------------------------------------- ----
| FormatTag| 2 Bytes | 编码方式,一般为0x0001 | |
-------------------------------------------------------------------- |
| Channels | 2 Bytes | 声道数目,1--单声道;2--双声道 |
-------------------------------------------------------------------- |
| SamplesPerSec | 4 Bytes | 采样频率
-------------------------------------------------------------------- |
| AvgBytesPerSec| 4 Bytes | 每秒所需字节数
-------------------------------------------------------------------- |
| BlockAlign| 2 Bytes | 数据块对齐单位(每个采样需要的字节数) |
-------------------------------------------------------------------- |
| BitsPerSample | 2 Bytes | 每个采样需要的bit数
-------------------------------------------------------------------- |
| | 2 Bytes | 附加信息(可选,通过Size来判断有无) |
-------------------------------------------------------------------- ----
******************************************/
printf("\n\nFormat Chunk信息:");
//输出fmt 标志
printf("\nfmt 标志:");
for(i=12;i<16;i++)
{
//if(ch[i]<16)
//printf("0%c ",ch[i]);
//else
printf("%c ",ch[i]); //输出字符型,格式类型直接用 %c 即可
}
//输出size段
printf("\nsize:ox");
for(i=19;i>15;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出编码方式
printf("\n编码方式:ox");
for(i=21;i>19;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出声道数目
printf("\n声道数目:ox");
for(i=23;i>21;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
if(ch[i+1]==1) //1表示单声道,2表示双声道,这里写i++ 是因为出循环之后执行了i--
printf(" 单声道");
else
printf(" 双声道");
//输出采样频率,用十六进制表示
printf("\n采样频率:ox");
for(i=27;i>23;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出每秒所需字节数
printf("\n每秒所需字节数:ox");
for(i=31;i>27;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出数据块对齐单位
printf("\n数据块对齐单位:ox");
for(i=33;i>31;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出每个采样所需bit数 16 bit
printf("\n每个采样所需bit数:ox");
for(i=35;i>33;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出附加信息,附加信息的有无直接进行判断即可
if(ch[16]==18) //若Format Chunk的size大小为18,则该模块的最后两个字节为附加信息
{ //若为16,则无附加信息
printf("\n附加信息:ox");
for(i=37;i>35;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
}
if (ch[38]=='f') //判断是否有Fact Chunk信息段,以fact标志f a c t为特征进行判断
{ //若括号里面为真,则直接按顺序输出Fact Chunk信息段和data Chunk信息段
/*******Fact Chunk的输出*******/
/****************************************
Fact Chunk
==================================
| |所占字节数| 具体内容 |
==================================
| ID | 4 Bytes | 'fact' |
----------------------------------
| Size | 4 Bytes | 数值为4 |
----------------------------------
| data | 4 Bytes | |
----------------------------------
******************************************/
printf("\n\nFact Chunk信息:");
//输出fact标志
printf("\nfact标志:");
for(i=38;i<42;i++)
{
if(ch[i]<16)
printf("0%c ",ch[i]);
else
printf("%c ",ch[i]);
}
//输出size
printf("\nsize:ox");
for(i=45;i>41;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出data段数据
printf("\ndata段数据:");
for(i=46;i<50;i++)
{
if(ch[i]<16)
printf("0%x ",ch[i]);
else
printf("%x ",ch[i]);
}
/*******Data Chunk的输出*******/
/****************************************
Data Chunk
==================================
| |所占字节数| 具体内容 |
==================================
| ID | 4 Bytes | 'data' |
----------------------------------
| Size | 4 Bytes | |
----------------------------------
| data | | |
----------------------------------
******************************************/
printf("\n\nData Chunk信息:");
//输出data标志
printf("\ndata标志:");
for(i=50;i<54;i++)
{
if(ch[i]<16)
printf("0%x ",ch[i]);
else
printf("%x ",ch[i]);
}
//输出数据大小
printf("\n数据大小:ox");
for(i=57;i>53;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
printf("\n输出PCM数据:");
fseek(fp,58,0);//注意偏移量
fread(&buf,sizeof(buf),1,fp);
cout<<float(buf[0])/(1024*32)<<" "<<float(buf[1])/(1024*32)<<" "<<float(buf[2])/(1024*32)<<" "<<float(buf[3])/(1024*32)<<" "<<float(buf[4])/(1024*32)<<endl;
}
if (ch[38]=='d') //只有Data Chunk信息,而没有Fact Chunk信息的情况。
{ //此时Data Chunk信息的存放位置取代了Fact Chunk信息的位置,i要要提前到Fact Chunk信息段
printf("\n\nData Chunk信息:");
//输出fact标志
printf("\ndata标志:");
for(i=38;i<42;i++)
{
if(ch[i]<16)
printf("0%c ",ch[i]);
else
printf("%c ",ch[i]);
}
//输出数据大小
printf("\nsize:ox");
for(i=45;i>41;i--)
{
if(ch[i]<16)
printf("0%x",ch[i]);
else
printf("%x",ch[i]);
}
//输出PCM数据
printf("\n输出PCM数据:");
fseek(fp,46,0);
fread(&buf,sizeof(buf),1,fp);
cout<<float(buf[0])/(1024*32)<<" "<<float(buf[1])/(1024*32)<<" "<<float(buf[2])/(1024*32)<<" "<<float(buf[3])/(1024*32)<<" "<<float(buf[4])/(1024*32)<<endl;
/*
注意该文件的采样值(每个采样所需的比特数)为16bit,我们读取的是short型数据(具有两个字节)是用16 个比特位来描述得到,音频文件波形值应该是浮点型,需要用短整型值除以1024*32(2的15次方),即可得到浮点型数据。此处应该先转浮点,再除以1024*32,不然结果为零。当我们已知PCM数据浮点型数值,我们可以先乘(1024*32)再强制转换为短整型存入。理解原理,顺序不能搞错了。
*/
}
printf("\n");
fclose(fp);
}
//这是个读取PCM数据的函数,按短整型读,然后转换成float型,注意偏移量
float *Read_Data(FILE *fp_speech,int front_info)
{
short data; //为什么用short?参看文献中的相应wav文件的pcm数据存放格式。16位单声道为两个字节,故为short
float static a[N];
fseek(fp_speech,front_info,0);//跳过头,经过分析该文件头部信息占46个字节,之后的存放PCM数据
for(int i=0;i<N;i++)
{
fread(&data,sizeof(short),1,fp_speech);
a[i]=(float)data/(float)(1024*32);
}
return a;
}
实验结果对比:
前部读出了音频文件所有信息,都是十六进制数。如十六进制数52,转换为十进制即为82,正好是R对应ASCII码。以此类推,你会发现读取正确。
这里我们在意的是最重要的PCM数据是否读取正确。我们用MATLAB读取音频文件信息作对比(MATLAB读取音频文件信息比较直观,而且简单),对比结果发现PCM数据完全一致。说明我们的读取方式正确的。
希望通过以上简单的实验让大家对Wave格式音频文件有更加深入的了解,在以后从事音频相关的编码工作时更加得心应手。
以上代码是最原始的读取方式,大家要习惯用一些对文件操作的函数如fread(),fwrite()和fseek()等函数来处理,会更加快捷,安全。
最后,大家自己写的时候,一定不要忘了判断附加信息段是否有信息,0也算信息,也要占两个字节,只有当没有信息时,fmt chunk后面的信息段才要往前移动两个字节。我这里附加信息段为0,也占用两个字节,所以后面的信息段不用前移两个字节。
C语言实验结果图:
Matlab实验结果图: