5.1声道WAVE音频文件信息的写入及生成
环境
C程序编写环境:VS2010
数据分析环境:Matlab R2010a
音频测试环境:Adobe Audition
关于wave格式音频文件的声道简介
wave格式的音频文件多为单声道和立体声(左右双声道),多声道的音频文件常见的为5.1声道,即6个扬声器通道。
单声道:左右扬声器中的信号完全一致,左右声道完全不存在差别。如下图1:用Adobe Audition打开一个单声道音频文件。
立体声:左右扬声器中的信号有差异,主观感受上更有空间感。如下图2:用Adobe Audition打开一个立体声音频文件。
5.1声道:具有6个通道,六个扬声器中的信号各不相同。如下图3:用Adobe Audition打开一个5.1声道音频文件。前两个为左声道和右声道,后面四个信号分别分配到其他扬声器上。
实验过程
要完成上述试验,我们必须准备六个单声道的wave格式的音频文件,这个可以通过Adobe Audition动手制作,例如输入6首歌,可以分别选择另存为实验需要的参数的音频文件。Adobe Audition很容易学习,根据自己需要的参数另存为更简单,大家不妨试试。这里,我将电脑中的六个立体声文件另存为如下格式的单声道文件:采样率44.1Khz,采样值(量化比特数)16bit,单声道。
获得了实验原始材料,还要要学会分析数据,了解不同声道音频文件的构成,找到他们之间的内在联系便不难实现从单声道文件到5.1声道的合成。这里我们尤其要重视Matlab和Adobe Audition的使用,实验内容用以上两款软件非常容易实现,这里只能帮我们分析数据,最后的C代码实现还是要靠自己用VS2010亲自动手编写。在动手之前我们先熟悉一下wave文件的构造,可以参看如下两个博客:http://www.cnblogs.com/liyiwen/archive/2010/04/19/1715715.html
http://syp0316.blog.163.com/blog/static/49178801200793133424635/
首先先用Matlab画出单声道文件的波形,观察单声道文件的PCM数据。
Matlab代码:
clc
clear
[s01,f1,nbit]=wavread('E:\Audio\Mono\es01_single.wav');
[s02,f2,nbit]=wavread('E:\Audio\Mono\es02_single.wav');
[es03,f3,nbit]=wavread('E:\Audio\Mono\es03_single.wav');
[s04,f4,nbit]=wavread('E:\Audio\Mono\sc01_single.wav');
[s05,f5,nbit]=wavread('E:\Audio\Mono\sc02_single.wav');
[s06,f6,nbit]=wavread('E:\Audio\Mono\sc03_single.wav');
%画图
subplot(3,2,1)
plot(s01),title('s01');
subplot(3,2,2)
plot(s02),title('s02');
subplot(3,2,3)
plot(s03),title('s03');
subplot(3,2,4)
plot(s04),title('s04');
subplot(3,2,5)
plot(s05),title('s05');
subplot(3,2,6)
plot(s06),title('s06');
6个单声道文件的PCM数据存放在s01-s06中,他们均为列向量(一维数组)。读取立体声文件我们不难发现,其PCM数据存放在一个M*2的二维数组中,再根据博客中介绍的不同声道文件PCM数据的摆放方式,不难联想到5,1声道文件的PCM数据的存放方式,显然是放在一个M*6的多维数组中。接着写matlab代码,用以上的单声道文件合成一个5.1声道的wave文件。
%补零
Len=[length(s01),length(s02),length(s03),length(s04),length(s05),length(s06)];
LEN_MAX=max(Len);
s01(Len(1)+1:LEN_MAX,1)=0;
s02(Len(2)+1:LEN_MAX,1)=0;
s03(Len(3)+1:LEN_MAX,1)=0;
s04(Len(4)+1:LEN_MAX,1)=0;
s05(Len(5)+1:LEN_MAX,1)=0;
s06(Len(6)+1:LEN_MAX,1)=0;
%合并
s=[];
s(:,1)=s01;
s(:,2)=s02;
s(:,3)=s03;
s(:,4)=s04;
s(:,5)=s05;
s(:,6)=s06;
%写文件
wavwrite(s,f1,nbit,'E:\Audio\Result\Channel_6.WAV');
figure
plot(s),title('5.1声道音频文件')
用Adobe Audition打开该文件,如上图3,并且能够播放,分别点击右侧L和R分别播放的是第一个文件es01_single.wav和第二个文件es02_single.wav.其他声道听不见是因为我们的耳机是左右声道的,需要并接其他四个扬声器才能听见另外四个声道。可见我们生成的多声道文件是正确的,即在C语言编写时可以按照类似思路进行。同时,在MATLAB观察5.1声道文件数据包可以发现S是一个M*6的多维数组,有6列,每一列代表一个单声道文件的PCM数据。
完成了单声道,多声道文件的制作并观察数据之后,就需要思考单声道wave文件中的信息和多声道中的信息有什么不一样,哪些地方需要改变,哪些地方不变。
大家可以阅读我前几篇关于wave文件信息的读取C代码和以上两个博客的内容,可以通过C语言代码对任何wave音频文件信息的读取。阅读过程中大家会发现,可以把wave文件信息宏观上分为两类,头部参数信息和PCM数据(真正的声音数据)。
头部信息需要做哪些变化:
根据已有的C代码,读取6个单声道文件的头部参数和用matlab合成的5.1声道的文件的头部参数。
单声道文件的头部参数如下如:
5.1声道文件的头部参数如下如:
大家自己拷贝程序运行观察比较结果会发现,6个的单声道文件合称为一个5,1声道的文件,文件大小上肯定是单声道的6倍,哪些参数需要扩大六倍记录下来,如声道数,文件大小等,便于在C代码中改写文件信息。
接下来我就通过C代码分析实验的过程。
工程包含三个文件:
WAVE_Struct.h 用于定义wave文件结构体和声明一些函数
Read_H_Data.cpp 编写函数体;
Write_WAVE_H.cpp 写新的文件。
WAVE_Struct.h 根据wave文件的描述定义结构体
#include <iostream>
using namespace std;
#define Num 6//声道数
/***************RIFF WAVE Chunk***********/
typedef struct RIFF_Header
{
char szRiff[4];//RIFF
int dwszRiff; // *6
char szRiffFormat[4];//WAVE
}RIEF_H;
/***************Format Chunk***********/
typedef struct WAVE_FORMAT
{
char szFmtID[4];//fmt
int dwFmtSize; // 18-16
short wFormatTag;
short wChannels; // *6
int dwSamplesPerSec;
int dwAvgBytePerSec; //*6
short wBlockAlign; // *6
short wBitsPerSample;
}WAVE_F;
/***************Fact Chunk***********/
typedef struct FACT_BLOCK
{
char szFactID[4];
int dwFactSize;
}FACT_B;
/***************Data Chunk***********/
typedef struct DATA_BLOCK
{
char szDataID[4];
int dwDataSize;//*6
}DATA_B;
int open(FILE *fp_result);
int READ_H_DATA(RIEF_H &f_riff, WAVE_F &f_format,DATA_B &f_data);
float *Read_P_Data(FILE *fp_speech,int front_info);
int Write_PCM_DATA(FILE *fp_result,RIEF_H f_riff, WAVE_F f_format,DATA_B f_data,short p[]);
Read_H_Data.cpp
#include "WAVE_Struct.h"
int open(FILE *fp_result)//打开文件函数
{
if((fp_result=fopen("E:\\Audio\\Result\\result2.wav","wb"))==NULL)
{
printf("can't open this file\n");
exit(0);
}
return 0;
}
//读取文件数据函数(很重要,细心体会)
int READ_H_DATA(RIEF_H &f_riff, WAVE_F &f_format,DATA_B &f_data)
{
FILE *fp;
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);
}
/*********RIFF WAVE Chunk的输出*********/
cout<<endl<<"RIFF WAVE Chunk的输出: "<<endl;
//读取ID=RIFF 4字节
fseek(fp,0L,0);
fread(f_riff.szRiff,sizeof(f_riff.szRiff),1,fp);
cout<<"RIFF标识:"<<f_riff.szRiff[0]<<f_riff.szRiff[1]<<f_riff.szRiff[2]<<f_riff.szRiff[3]<<endl;
//读取文件大小 4字节
fseek(fp,4L,0);
fread(&f_riff.dwszRiff,sizeof(f_riff.dwszRiff),1,fp);
cout<<"文件t大洙小:阰 "<<f_riff.dwszRiff<<endl;
//读取WAVE标识 4字节
fseek(fp,8L,0);
fread(f_riff.szRiffFormat,sizeof(f_riff.szRiffFormat),1,fp);
cout<<"WAVE标识:"<<f_riff.szRiffFormat[0]<<f_riff.szRiffFormat[1]<<f_riff.szRiffFormat[2]<<f_riff.szRiffFormat[3]<<endl;
///*******Format Chunk的输出*******/
cout<<endl<<"Format Chunk的输出: "<<endl;
//读取fam标识 4字节
fseek(fp,12L,0);
fread(f_format.szFmtID,sizeof(int),1,fp);
cout<<"fmt标识:"<<f_format.szFmtID[0]<<f_format.szFmtID[1]<<f_format.szFmtID[2]<<endl;
//读取size 4字节
fseek(fp,16L,0);//size_FORMAT=16,则最后没有那两个用来存放附加信息的字节,若为18,则有那两个字节,注意这会影响头信息的整体长度
fread(&f_format.dwFmtSize,sizeof(int),1,fp); //查看是否有附加信息,size_FORMAT=18,则有附加信息
cout<<"size大小: "<<f_format.dwFmtSize<<endl;
//编括码方式 2字节
fseek(fp,20L,0);
fread(&f_format.wFormatTag,sizeof(short),1,fp);
cout<<"编码方式:"<<f_format.wFormatTag<<endl;
//声道数 2字节
fseek(fp,22L,0);
fread(&f_format.wChannels,sizeof(short),1,fp);
cout<<"声道数目:"<<f_format.wChannels<<endl;
//采样频率 4字节
fseek(fp,24L,0);
fread(&f_format.dwSamplesPerSec,sizeof(int),1,fp);
cout<<"采样频率:"<<f_format.dwSamplesPerSec<<endl;
//每秒所需字节数 4字节
fseek(fp,28L,0);
fread(&f_format.dwAvgBytePerSec,sizeof(int),1,fp);
cout<<"每秒所需字节数:"<<f_format.dwAvgBytePerSec<<endl;
//数据块对齐单位 2字节
fseek(fp,32L,0);
fread(&f_format.wBlockAlign,sizeof(short),1,fp);
cout<<"数据块对齐单位:"<<f_format.wBlockAlign<<endl;
//每个采样需要的bit数 2字节
fseek(fp,34L,0);
fread(&f_format.wBitsPerSample,sizeof(short),1,fp);
cout<<"每个采样需要的bit数:"<<f_format.wBitsPerSample<<endl;
/*********************Data Chunk的输出************************/
cout<<endl<<"Data Chunk的输出:"<<endl;
//DATA标识 4字节
fseek(fp,38L,0);
fread(f_data.szDataID,sizeof(int),1,fp);
cout<<"DATA标识: "<<f_data.szDataID[0]<<f_data.szDataID[1]<<f_data.szDataID[2]<<f_data.szDataID[3]<<endl;
//PCM数据长度 4字节
fseek(fp,42L,0);
fread(&f_data.dwDataSize,sizeof(int),1,fp);
cout<<"PCM数据长度:"<<f_data.dwDataSize<<endl;
return 0;
}
float *Read_P_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);
}
rewind(fp_speech);
return a;
}
int Write_PCM_DATA(FILE *fp_result,RIEF_H f_riff, WAVE_F f_format,DATA_B f_data,short p[])
{
fseek(fp_result,0L,0);
//写入RIFF Chunk段
fseek(fp_result,0L,0);
if (fwrite(&f_riff,sizeof(f_riff),1,fp_result)!=1)
{
cout<<"error"<<endl;
}
//写Fromat Chunk段
if (fwrite(&f_format,sizeof(f_format),1,fp_result)!=1)
{
cout<<"error"<<endl;
}
//写′Data Chunk段 这里我们不写附加段
if (fwrite(&f_data,sizeof(f_data),1,fp_result)!=1)
{
cout<<"error"<<endl;
}
//写Pcm Data Chunk段
for (int k=0;k<6*1122984;k++)
{
if (fwrite(&p[k],sizeof(short),1,fp_result)!=1)
{
cout<<"error"<<endl;
}
}
fclose(fp_result);
return 0;
}
Write_WAVE_H.cpp
#include "WAVE_Struct.h"
char str[Num][30]={ //存放的是单声道文件的路径,大家根据自己的情况更改
"E:\\Audio\\Mono\\es01_single.wav",
"E:\\Audio\\Mono\\es02_single.wav",
"E:\\Audio\\Mono\\es03_single.wav",
"E:\\Audio\\Mono\\sc01_single.wav",
"E:\\Audio\\Mono\\sc02_single.wav",
"E:\\Audio\\Mono\\sc03_single.wav"
};
int main()
{
RIEF_H f_Riff; f_Riff.dwszRiff=0;//初始化ˉ
WAVE_F f_format; f_format.dwAvgBytePerSec=0;
DATA_B f_data;f_data.dwDataSize=0;
FILE *fp_Mono[Num]; //存放六个单蹋声道文件的文件指针
int data_size[Num]; //记录六个单蹋声道文件PCM数据的长度
int max_len=0; //因为最后数据合并,所以找出data_size[]中的最大值为基准,否则最长序列信息会丢失,长度不足的序列自动补零
/**************获取各单声道文件PCM数据的长度**************/
for (int i=0;i<Num;i++)
{
fp_Mono[i]=fopen(str[i],"rb+");//为读,打开一个wav文件
if((fp_Mono[i]=fopen(str[i],"rb+"))==NULL) //若打开a文件失败,退出
{
printf("can't open this file\n");
exit(0);
}
fseek(fp_Mono[i],42L,0);//跳转到存放PCM数据长度的地方{研究wave文件头的结构),读取长度以便后面分配内存空间
fread(&data_size[i],sizeof(int),1,fp_Mono[i]);//读取各个文件中PCM数据的长度放入缓存中
if (data_size[i]>=max_len)//找最大值过程,max_len存放最大长度
{
max_len=data_size[i];
}
cout<<"序ò列的PCM数簓据Y的样ù本点数簓:阰 "<<data_size[i]<<endl;
}
cout<<max_len<<endl;//显示看是否读取正确
/**************按长度获取各单声道文件PCM数据**************/
//储存六个指针,指针用来存储pCM数据
//这里采用动态分配,因为文件长度未知,且为了防止溢出
short *wav_data[Num];
for (int j=0;j<6;j++)
{
wav_data[j]=(short*)malloc(max_len*sizeof(short));
if(wav_data==NULL)
{
printf("Can not get menory for that many value..\n");
exit(EXIT_FAILURE);
}
}
for (int i=0;i<Num;i++)
{
fseek(fp_Mono[i],46L,0);//跳转到存放PCM数据的地方,按恪各自文已知长度(data_size[i])读取PCM数据
if (fread(wav_data[i],data_size[i],1,fp_Mono[i])!=1)
{
cout<<"read error !"<<endl;//数据读取正确
}
fclose(fp_Mono[i]); // 依次关闭文件流
}
/**************合并数据,各个单声道pCM数据依次读取**************/
short *M_C_data;
M_C_data=(short*)malloc(Num*max_len*sizeof(short));
if(M_C_data==NULL)
{
printf("Can not get menory for that many value..\n");
exit(EXIT_FAILURE);
}
for (int k=0,j=0;j<max_len;k=k+6,j++)//合并数据
{
M_C_data[k+0]=wav_data[0][j];
M_C_data[k+1]=wav_data[1][j];
M_C_data[k+2]=wav_data[2][j];
M_C_data[k+3]=wav_data[3][j];
M_C_data[k+4]=wav_data[4][j];
M_C_data[k+5]=wav_data[5][j];
}
//可以多显示几组,看是否全部读取
cout<<M_C_data[0]<<" "<<M_C_data[1]<<" "<<M_C_data[2]<<" "<<M_C_data[3]<<" "<<M_C_data[4]<<" "<<M_C_data[5]<<endl;
/**************获取PCM数据长度最大的单声道文件的头部信息**************/
READ_H_DATA(f_Riff,f_format,f_data);//读取之后显示出基准文件的头信息
/**************改写数据,存入新的wave头*************/
f_Riff.dwszRiff=f_Riff.dwszRiff*6;//整个文件的大小,不包含RIFF的ID所占内存和自身内存大小这里粗略表括示,精确的是将字节数加起来即可。
f_format.dwFmtSize=f_format.dwFmtSize-2;
f_format.wChannels=f_format.wChannels*6;
f_format.dwAvgBytePerSec=f_format.dwAvgBytePerSec*6;
f_format.wBlockAlign=f_format.wBlockAlign*6;
f_data.dwDataSize=max_len*6;
/**************为a读á写′建¨立ⅰ一个新的二t进制文件t**************/
FILE *fp_result=fopen("E:\\Audio\\Result\\result2.wav","wb+");//"wb+" 为读写建立一个新的二进制文件
open(fp_result);
//数据的写入都是整块整块的写入,避免出错
Write_PCM_DATA(fp_result,f_Riff,f_format,f_data,M_C_data);
//释放动态分配的内存
free(wav_data[0]);free(wav_data[1]);free(wav_data[2]);
free(wav_data[3]);free(wav_data[4]);free(wav_data[5]);
free(M_C_data);
return 0;
}
/*到这里,新的5.1声道的wave文件就合成完毕,其中wave头中没有添加附加信息段和fact chunk段,感兴趣的同学可以进一步完善*/
代码复制完毕。
写程序的过程中需要注意一下几点:
1,善于运用动态分配的方式分配内存,读取各个单声道文件的PCM数据时注意读取的长度。别忘了释放内存。
2.运用文件处理函数,fread(),fseek(),fwrite()等要注意,自己好好探究。并注意在什么时候关闭文件最合适,fwrite()最容易导致出错。
3.别忘了用C生成的wave文件用matlab和Adobe Audition验证,看是否正确。
4.READ_H_DATA(RIEF_H &f_riff, WAVE_F &f_format,DATA_B &f_data),运用这个函数读取我们生成的新文件的信息,看是否正确。思考,如果形参不是引用的形式会出现什么情况?