一、名词解析
PCM(Pulse Code Modulation)也被称为脉码编码调制,PCM中的声音数据没有被压缩,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。采样转换方式参考下图进行了解:
音频采样包含以下几大要素:
1. 采样率
采样率表示音频信号每秒的数字快照数。该速率决定了音频文件的频率范围。采样率越高,数字波形的形状越接近原始模拟波形。低采样率会限制可录制的频率范围,这可导致录音表现原始声音的效果不佳。根据奈奎斯特采样定理,为了重现给定频率,采样率必须至少是该频率的两倍。例如,一般CD唱片的采样率为每秒 44,100 个采样,因此可重现最高为 22,050 Hz 的频率,此频率刚好超过人类的听力极限 20,000 Hz。
图中A是低采样率的音频信号,其效果已经将原始声波进行了扭曲,B则是完全重现原始声波的高采样率的音频信号。
数字音频常用的采样率如下:
2. 位深度
位深度决定动态范围。采样声波时,为每个采样指定最接近原始声波振幅的振幅值。较高的位深度可提供更多可能的振幅值,产生更大的动态范围、更低的噪声基准和更高的保真度。
位深度越高,提供的动态范围越大。
二、PCM
在上面的名词解析中我们应该对PCM有了一定的理解和认识,下面我们将对PCM做更多的讲解。
1. PCM音频数据存储方式
如果是单声道的文件,采样数据按时间的先后顺序依次存入。如果是单声道的音频文件,采样数据按时间的先后顺序依次存入(有的时候也会采用 LRLRLR 方式存储,只是另一个声道的数据为 0)。
如果是双声道的话通常按照 LRLRLR 的方式存储,存储的时候还和机器的大小端有关。(TODO:大小端问题整理)
大端模式如下图所示:
2. PCM 音频数据的参数
描述 PCM 音频数据的参数的时候有如下描述方式:
44100HZ 16bit stereo: 每秒钟有 44100 次采样, 采样数据用 16 位(2 字节)记录, 双声道(立体声) 22050HZ 8bit mono: 每秒钟有 22050 次采样, 采样数据用 8 位(1 字节)记录, 单声道 48000HZ 32bit 51ch: 每秒钟有 48000 次采样, 采样数据用 32 位(4 字节浮点型)记录, 5.1 声道
44100Hz 指的是采样率,它的意思是每秒取样 44100 次。采样率越大,存储数字音频所占的空间就越大。
16bit 指的是采样精度,意思是原始模拟信号被采样后,每一个采样点在计算机中用 16 位(两个字节)来表示。采样精度越高越能精细地表示模拟信号的差异。
Stereo 指的是声道数,也即采样时用到的麦克风的数量,麦克风越多就越能还原真实的采样环境(当然麦克风的放置位置也是有规定的)。
三、WAV
WAV 是 Microsoft 和 IBM 为 PC 开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format)文件规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。WAVE 文件通常只是一个具有单个 “WAVE” 块的 RIFF 文件,该块由两个子块(”fmt” 子数据块和 ”data” 子数据块),它的格式如下图所示:
WAV 格式定义
该格式的实质就是在 PCM 文件的前面加了一个文件头,每个字段的的含义如下:
typedef struct { char ChunkID[4]; //内容为"RIFF" unsigned long ChunkSize; //存储文件的字节数(不包含ChunkID和ChunkSize这8个字节) char Format[4]; //内容为"WAVE“ } WAVE_HEADER; typedef struct { char Subchunk1ID[4]; //内容为"fmt" unsigned long Subchunk1Size; //存储该子块的字节数(不含前面的Subchunk1ID和Subchunk1Size这8个字节) unsigned short AudioFormat; //存储音频文件的编码格式,例如若为PCM则其存储值为1。 unsigned short NumChannels; //声道数,单声道(Mono)值为1,双声道(Stereo)值为2,等等 unsigned long SampleRate; //采样率,如8k,44.1k等 unsigned long ByteRate; //每秒存储的bit数,其值 = SampleRate * NumChannels * BitsPerSample / 8 unsigned short BlockAlign; //块对齐大小,其值 = NumChannels * BitsPerSample / 8 unsigned short BitsPerSample; //每个采样点的bit数,一般为8,16,32等。 } WAVE_FMT; typedef struct { char Subchunk2ID[4]; //内容为“data” unsigned long Subchunk2Size; //接下来的正式的数据部分的字节数,其值 = NumSamples * NumChannels * BitsPerSample / 8 } WAVE_DATA;
WAV 文件头解析
这里是一个 WAVE 文件的开头 72 字节,字节显示为十六进制数字:
52 49 46 46 | 24 08 00 00 | 57 41 56 45 66 6d 74 20 | 10 00 00 00 | 01 00 02 00 22 56 00 00 | 88 58 01 00 | 04 00 10 00 64 61 74 61 | 00 08 00 00 | 00 00 00 00 24 17 1E F3 | 3C 13 3C 14 | 16 F9 18 F9 34 E7 23 A6 | 3C F2 24 F2 | 11 CE 1A 0D
字段解析如下图:
三、PCM & WAV 开发实践
1. PCM格式转为WAV格式(基于C语言)
int simplest_pcm16le_to_wave(const char *pcmpath,int channels,int sample_rate,const char *wavepath) { typedef struct WAVE_HEADER{ char fccID[4]; unsigned long dwSize; char fccType[4]; }WAVE_HEADER; typedef struct WAVE_FMT{ char fccID[4]; unsigned long dwSize; unsigned short wFormatTag; unsigned short wChannels; unsigned long dwSamplesPerSec; unsigned long dwAvgBytesPerSec; unsigned short wBlockAlign; unsigned short uiBitsPerSample; }WAVE_FMT; typedef struct WAVE_DATA{ char fccID[4]; unsigned long dwSize; }WAVE_DATA; if(channels==0||sample_rate==0){ channels = 2; sample_rate = 44100; } int bits = 16; WAVE_HEADER pcmHEADER; WAVE_FMT pcmFMT; WAVE_DATA pcmDATA; unsigned short m_pcmData; FILE *fp,*fpout; fp=fopen(pcmpath, "rb"); if(fp == NULL) { printf("open pcm file error "); return -1; } fpout=fopen(wavepath, "wb+"); if(fpout == NULL) { printf("create wav file error "); return -1; } //WAVE_HEADER memcpy(pcmHEADER.fccID,"RIFF",strlen("RIFF")); memcpy(pcmHEADER.fccType,"WAVE",strlen("WAVE")); fseek(fpout,sizeof(WAVE_HEADER),1); //WAVE_FMT pcmFMT.dwSamplesPerSec=sample_rate; pcmFMT.dwAvgBytesPerSec=pcmFMT.dwSamplesPerSec*sizeof(m_pcmData); pcmFMT.uiBitsPerSample=bits; memcpy(pcmFMT.fccID,"fmt ",strlen("fmt ")); pcmFMT.dwSize=16; pcmFMT.wBlockAlign=2; pcmFMT.wChannels=channels; pcmFMT.wFormatTag=1; fwrite(&pcmFMT,sizeof(WAVE_FMT),1,fpout); //WAVE_DATA; memcpy(pcmDATA.fccID,"data",strlen("data")); pcmDATA.dwSize=0; fseek(fpout,sizeof(WAVE_DATA),SEEK_CUR); fread(&m_pcmData,sizeof(unsigned short),1,fp); while(!feof(fp)){ pcmDATA.dwSize+=2; fwrite(&m_pcmData,sizeof(unsigned short),1,fpout); fread(&m_pcmData,sizeof(unsigned short),1,fp); } pcmHEADER.dwSize=44+pcmDATA.dwSize; rewind(fpout); fwrite(&pcmHEADER,sizeof(WAVE_HEADER),1,fpout); fseek(fpout,sizeof(WAVE_FMT),SEEK_CUR); fwrite(&pcmDATA,sizeof(WAVE_DATA),1,fpout); fclose(fp); fclose(fpout); return 0; }
注意:函数里声明的数据类型unsigned long在有些C编译器上是64位的,这时候要改成unsigned int才可以,否则wav头有88bytes,标准的是44bytes,改完就正常了,对C还不熟悉的人小小的心得,另外,声道数和采样率也要注意,一般采样率有44100/16000/8000,要确认是哪个,声道是1还是2,这两个参数要设置好才会有正确的转换结果。
2. PCM降低某个声道的音量(基于C语言)
一般来说 PCM 数据中的波形幅值越大,代表音量越大,对于 PCM 音频数据而言,它的幅值(即该采样点采样值的大小)代表音量的大小。
如果我们需要降低某个声道的音量,可以通过减小某个声道的数据的值来实现降低某个声道的音量。
int pcm16le_half_volume_left( char *url ) { FILE *fp_in = fopen( url, "rb+" ); FILE *fp_out = fopen( "output_half_left.pcm", "wb+" ); unsigned char *sample = ( unsigned char * )malloc(4); // 一次读取一个sample,因为是2声道,所以是4字节 while ( !feof( fp_in ) ){ fread( sample, 1, 4, fp_in ); short* sample_num = ( short* )sample; // 转成左右声道两个short数据 *sample_num = *sample_num / 2; // 左声道数据减半 fwrite( sample, 1, 2, fp_out ); // L fwrite( sample + 2, 1, 2, fp_out ); // R } free( sample ); fclose( fp_in ); fclose( fp_out ); return 0; }
上述代码做的事情是:在读出左声道的 2 Byte 的取样值之后,将其转成了 C 语言中的一个 short 类型的变量。将该数值除以 2 之后写回到了 PCM 文件中。
3. 分离PCM音频数据左右声道的数据
因为PCM音频数据是按照LRLRLR的方式来存储左右声道的音频数据的,所以我们可以通过将它们交叉的读出来的方式来分离左右声道的数据:
int simplest_pcm16le_split(char *url) { FILE *fp=fopen(url,"rb+"); FILE *fp1=fopen("output_l.pcm","wb+"); FILE *fp2=fopen("output_r.pcm","wb+"); unsigned char *sample=(unsigned char *)malloc(4); while(!feof(fp)){ fread(sample,1,4,fp); //L fwrite(sample,1,2,fp1); //R fwrite(sample+2,1,2,fp2); } free(sample); fclose(fp); fclose(fp1); fclose(fp2); return 0; }
4. 从PCM16LE单声道音频采样数据中截取一部分数据
本程序中的函数可以从PCM16LE单声道数据中截取一段数据,并输出截取数据的样值。函数的代码如下所示:
/** * Cut a 16LE PCM single channel file. * @param url Location of PCM file. * @param start_num start point * @param dur_num how much point to cut */ int simplest_pcm16le_cut_singlechannel(char *url,int start_num,int dur_num){ FILE *fp=fopen(url,"rb+"); FILE *fp1=fopen("output_cut.pcm","wb+"); FILE *fp_stat=fopen("output_cut.txt","wb+"); unsigned char *sample=(unsigned char *)malloc(2); int cnt=0; while(!feof(fp)){ fread(sample,1,2,fp); if(cnt>start_num&&cnt<=(start_num+dur_num)){ fwrite(sample,1,2,fp1); short samplenum=sample[1]; samplenum=samplenum*256; samplenum=samplenum+sample[0]; fprintf(fp_stat,"%6d,",samplenum); if(cnt%10==0) fprintf(fp_stat," ",samplenum); } cnt++; } free(sample); fclose(fp); fclose(fp1); fclose(fp_stat); return 0; }
5. 将PCM16LE双声道音频采样数据转换为PCM8音频采样数据
本程序中的函数可以通过计算的方式将PCM16LE双声道数据16bit的采样位数转换为8bit。函数的代码如下所示:
/** * Convert PCM-16 data to PCM-8 data. * @param url Location of PCM file. */ int simplest_pcm16le_to_pcm8(char *url){ FILE *fp=fopen(url,"rb+"); FILE *fp1=fopen("output_8.pcm","wb+"); int cnt=0; unsigned char *sample=(unsigned char *)malloc(4); while(!feof(fp)){ short *samplenum16=NULL; char samplenum8=0; unsigned char samplenum8_u=0; fread(sample,1,4,fp); //(-32768-32767) samplenum16=(short *)sample; samplenum8=(*samplenum16)>>8; //(0-255) samplenum8_u=samplenum8+128; //L fwrite(&samplenum8_u,1,1,fp1); samplenum16=(short *)(sample+2); samplenum8=(*samplenum16)>>8; samplenum8_u=samplenum8+128; //R fwrite(&samplenum8_u,1,1,fp1); cnt++; } printf("Sample Cnt:%d ",cnt); free(sample); fclose(fp); fclose(fp1); return 0; }
PCM16LE格式的采样数据的取值范围是-32768到32767,而PCM8格式的采样数据的取值范围是0到255。所以PCM16LE转换到PCM8需要经过两个步骤:第一步是将-32768到32767的16bit有符号数值转换为-128到127的8bit有符号数值,第二步是将-128到127的8bit有符号数值转换为0到255的8bit无符号数值。在本程序中,16bit采样数据是通过short类型变量存储的,而8bit采样数据是通过unsigned char类型存储的。
6. 将PCM16LE双声道音频采样数据的声音速度提高一倍
本程序中的函数可以通过抽象的方式将PCM16LE双声道数据的速度提高一倍,采用采样每个声道奇(偶)数点的样值的方式,函数的代码如下所示:
/** * Re-sample to double the speed of 16LE PCM file * @param url Location of PCM file. */ int simplest_pcm16le_doublespeed(char *url){ FILE *fp=fopen(url,"rb+"); FILE *fp1=fopen("output_doublespeed.pcm","wb+"); int cnt=0; unsigned char *sample=(unsigned char *)malloc(4); while(!feof(fp)){ fread(sample,1,4,fp); if(cnt%2!=0){ //L fwrite(sample,1,2,fp1); //R fwrite(sample+2,1,2,fp1); } cnt++; } printf("Sample Cnt:%d ",cnt); free(sample); fclose(fp); fclose(fp1); return 0; }
参考资料:
视音频数据处理入门:PCM音频采样数据处理 --> 致敬雷神!