多媒體文件格式(五):PCM / WAV 格式
本文目錄:
一、名詞解析
二、PCM
三、WAV
三、PCM & WAV 開發(fā)實踐
參考資料:
一、名詞解析
PCM(Pulse Code Modulation)也被稱為脈碼編碼調(diào)制,PCM中的聲音數(shù)據(jù)沒有被壓縮,它是由模擬信號經(jīng)過采樣、量化、編碼轉(zhuǎn)換成的標(biāo)準(zhǔn)的數(shù)字音頻數(shù)據(jù)。采樣轉(zhuǎn)換方式參考下圖進(jìn)行了解:
音頻采樣包含以下幾大要素:
1. 采樣率采樣率表示音頻信號每秒的數(shù)字快照數(shù)。該速率決定了音頻文件的頻率范圍。采樣率越高,數(shù)字波形的形狀越接近原始模擬波形。低采樣率會限制可錄制的頻率范圍,這可導(dǎo)致錄音表現(xiàn)原始聲音的效果不佳。根據(jù)奈奎斯特采樣定理,為了重現(xiàn)給定頻率,采樣率必須至少是該頻率的兩倍。例如,一般CD唱片的采樣率為每秒 44,100 個采樣,因此可重現(xiàn)最高為 22,050 Hz 的頻率,此頻率剛好超過人類的聽力極限 20,000 Hz。
圖中A是低采樣率的音頻信號,其效果已經(jīng)將原始聲波進(jìn)行了扭曲,B則是完全重現(xiàn)原始聲波的高采樣率的音頻信號。
數(shù)字音頻常用的采樣率如下:
2. 位深度位深度決定動態(tài)范圍。采樣聲波時,為每個采樣指定最接近原始聲波振幅的振幅值。較高的位深度可提供更多可能的振幅值,產(chǎn)生更大的動態(tài)范圍、更低的噪聲基準(zhǔn)和更高的保真度。
位深度越高,提供的動態(tài)范圍越大。
二、PCM在上面的名詞解析中我們應(yīng)該對PCM有了一定的理解和認(rèn)識,下面我們將對PCM做更多的講解。
1. PCM音頻數(shù)據(jù)存儲方式如果是單聲道的文件,采樣數(shù)據(jù)按時間的先后順序依次存入。如果是單聲道的音頻文件,采樣數(shù)據(jù)按時間的先后順序依次存入(也可能采用 LRLRLR 方式存儲,只是另一個聲道的數(shù)據(jù)為 0)。
如果是雙聲道的話通常按照 LRLRLR 的方式存儲,存儲的時候還和機(jī)器的大小端有關(guān)。(關(guān)于字節(jié)序大小端的相關(guān)內(nèi)容可參考《字節(jié)序問題之大小端模式講解》進(jìn)行了解)
PCM的存儲方式為小端模式,存儲Data數(shù)據(jù)排列如下圖所示:
2. PCM 音頻數(shù)據(jù)的參數(shù)描述 PCM 音頻數(shù)據(jù)的參數(shù)的時候有如下描述方式:
44100HZ 16bit stereo: 每秒鐘有 44100 次采樣, 采樣數(shù)據(jù)用 16 位(2 字節(jié))記錄, 雙聲道(立體聲) 22050HZ 8bit mono: 每秒鐘有 22050 次采樣, 采樣數(shù)據(jù)用 8 位(1 字節(jié))記錄, 單聲道 48000HZ 32bit 51ch: 每秒鐘有 48000 次采樣, 采樣數(shù)據(jù)用 32 位(4 字節(jié)浮點型)記錄, 5.1 聲道
44100Hz 指的是采樣率,它的意思是每秒取樣 44100 次。采樣率越大,存儲數(shù)字音頻所占的空間就越大。
16bit 指的是采樣精度,意思是原始模擬信號被采樣后,每一個采樣點在計算機(jī)中用 16 位(兩個字節(jié))來表示。采樣精度越高越能精細(xì)地表示模擬信號的差異。
Stereo 指的是聲道數(shù),也即采樣時用到的麥克風(fēng)的數(shù)量,麥克風(fēng)越多就越能還原真實的采樣環(huán)境(當(dāng)然麥克風(fēng)的放置位置也是有規(guī)定的)。
三、WAVWAV 是 Microsoft 和 IBM 為 PC 開發(fā)的一種聲音文件格式,它符合 RIFF(Resource Interchange File Format)文件規(guī)范,用于保存 Windows 平臺的音頻信息資源,被 Windows 平臺及其應(yīng)用程序所廣泛支持。WAVE 文件通常只是一個具有單個 “WAVE” 塊的 RIFF 文件,該塊由兩個子塊(”fmt” 子數(shù)據(jù)塊和 ”data” 子數(shù)據(jù)塊),它的格式如下圖所示:
WAV 格式定義
該格式的實質(zhì)就是在 PCM 文件的前面加了一個文件頭,每個字段的的含義如下:
typedef struct { char ChunkID[4]; //內(nèi)容為"RIFF" unsigned long ChunkSize; //存儲文件的字節(jié)數(shù)(不包含ChunkID和ChunkSize這8個字節(jié)) char Format[4]; //內(nèi)容為"WAVE“} WAVE_HEADER; typedef struct { char Subchunk1ID[4]; //內(nèi)容為"fmt" unsigned long Subchunk1Size; //存儲該子塊的字節(jié)數(shù)(不含前面的Subchunk1ID和Subchunk1Size這8個字節(jié)) unsigned short AudioFormat; //存儲音頻文件的編碼格式,例如若為PCM則其存儲值為1。 unsigned short NumChannels; //聲道數(shù),單聲道(Mono)值為1,雙聲道(Stereo)值為2,等等 unsigned long SampleRate; //采樣率,如8k,44.1k等 unsigned long ByteRate; //每秒存儲的bit數(shù),其值 = SampleRate * NumChannels * BitsPerSample / 8 unsigned short BlockAlign; //塊對齊大小,其值 = NumChannels * BitsPerSample / 8 unsigned short BitsPerSample; //每個采樣點的bit數(shù),一般為8,16,32等。} WAVE_FMT; typedef struct { char Subchunk2ID[4]; //內(nèi)容為“data” unsigned long Subchunk2Size; //接下來的正式的數(shù)據(jù)部分的字節(jié)數(shù),其值 = NumSamples * NumChannels * BitsPerSample / 8} WAVE_DATA;
WAV 文件頭解析
這里是一個 WAVE 文件的開頭 72 字節(jié),字節(jié)顯示為十六進(jìn)制數(shù)字:
52 49 46 46 | 24 08 00 00 | 57 41 56 4566 6d 74 20 | 10 00 00 00 | 01 00 02 00 22 56 00 00 | 88 58 01 00 | 04 00 10 0064 61 74 61 | 00 08 00 00 | 00 00 00 00 24 17 1E F3 | 3C 13 3C 14 | 16 F9 18 F934 E7 23 A6 | 3C F2 24 F2 | 11 CE 1A 0D
字段解析如下圖:
三、PCM & WAV 開發(fā)實踐1. PCM格式轉(zhuǎn)為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\n"); return -1; } fpout=fopen(wavepath, "wb+"); if(fpout == NULL) { printf("create wav file error\n"); 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; }
注意:函數(shù)里聲明的數(shù)據(jù)類型unsigned long在有些C編譯器上是64位的,這時候要改成unsigned int才可以,否則wav頭有88bytes,標(biāo)準(zhǔn)的是44bytes,改完就正常了,對C還不熟悉的人小小的心得,另外,聲道數(shù)和采樣率也要注意,一般采樣率有44100/16000/8000,要確認(rèn)是哪個,聲道是1還是2,這兩個參數(shù)要設(shè)置好才會有正確的轉(zhuǎn)換結(jié)果。
2. PCM降低某個聲道的音量(基于C語言)一般來說 PCM 數(shù)據(jù)中的波形幅值越大,代表音量越大,對于 PCM 音頻數(shù)據(jù)而言,它的幅值(即該采樣點采樣值的大?。┐硪袅康拇笮?。
如果我們需要降低某個聲道的音量,可以通過減小某個聲道的數(shù)據(jù)的值來實現(xiàn)降低某個聲道的音量。
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字節(jié) while ( !feof( fp_in ) ){ fread( sample, 1, 4, fp_in ); short* sample_num = ( short* )sample; // 轉(zhuǎn)成左右聲道兩個short數(shù)據(jù) *sample_num = *sample_num / 2; // 左聲道數(shù)據(jù)減半 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 的取樣值之后,將其轉(zhuǎn)成了 C 語言中的一個 short 類型的變量。將該數(shù)值除以 2 之后寫回到了 PCM 文件中。
3. 分離PCM音頻數(shù)據(jù)左右聲道的數(shù)據(jù)因為PCM音頻數(shù)據(jù)是按照LRLRLR的方式來存儲左右聲道的音頻數(shù)據(jù)的,所以我們可以通過將它們交叉的讀出來的方式來分離左右聲道的數(shù)據(jù):
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; }
本程序中的函數(shù)可以從PCM16LE單聲道數(shù)據(jù)中截取一段數(shù)據(jù),并輸出截取數(shù)據(jù)的樣值。函數(shù)的代碼如下所示:
/** * 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,"\n",samplenum); } cnt++; } free(sample); fclose(fp); fclose(fp1); fclose(fp_stat); return 0; }
本程序中的函數(shù)可以通過計算的方式將PCM16LE雙聲道數(shù)據(jù)16bit的采樣位數(shù)轉(zhuǎn)換為8bit。函數(shù)的代碼如下所示:
/** * 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\n",cnt); free(sample); fclose(fp); fclose(fp1); return 0; }
PCM16LE格式的采樣數(shù)據(jù)的取值范圍是-32768到32767,而PCM8格式的采樣數(shù)據(jù)的取值范圍是0到255。所以PCM16LE轉(zhuǎn)換到PCM8需要經(jīng)過兩個步驟:第一步是將-32768到32767的16bit有符號數(shù)值轉(zhuǎn)換為-128到127的8bit有符號數(shù)值,第二步是將-128到127的8bit有符號數(shù)值轉(zhuǎn)換為0到255的8bit無符號數(shù)值。在本程序中,16bit采樣數(shù)據(jù)是通過short類型變量存儲的,而8bit采樣數(shù)據(jù)是通過unsigned char類型存儲的。
6. 將PCM16LE雙聲道音頻采樣數(shù)據(jù)的聲音速度提高一倍本程序中的函數(shù)可以通過抽象的方式將PCM16LE雙聲道數(shù)據(jù)的速度提高一倍,采用采樣每個聲道奇(偶)數(shù)點的樣值的方式,函數(shù)的代碼如下所示:
/** * 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\n",cnt); free(sample); fclose(fp); fclose(fp1); return 0; }
參考資料:
視音頻數(shù)據(jù)處理入門:PCM音頻采樣數(shù)據(jù)處理 --> 致敬雷神!
*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。