多媒體文件格式(五):PCM / WAV 格式
本文目錄:
一、名詞解析
二、PCM
三、WAV
三、PCM & WAV 開發實踐
參考資料:
一、名詞解析
PCM(Pulse Code Modulation)也被稱為脈碼編碼調制,PCM中的聲音數據沒有被壓縮,它是由模擬信號經過采樣、量化、編碼轉換成的標準的數字音頻數據。采樣轉換方式參考下圖進行了解:
音頻采樣包含以下幾大要素:
1. 采樣率采樣率表示音頻信號每秒的數字快照數。該速率決定了音頻文件的頻率范圍。采樣率越高,數字波形的形狀越接近原始模擬波形。低采樣率會限制可錄制的頻率范圍,這可導致錄音表現原始聲音的效果不佳。根據奈奎斯特采樣定理,為了重現給定頻率,采樣率必須至少是該頻率的兩倍。例如,一般CD唱片的采樣率為每秒 44,100 個采樣,因此可重現最高為 22,050 Hz 的頻率,此頻率剛好超過人類的聽力極限 20,000 Hz。
圖中A是低采樣率的音頻信號,其效果已經將原始聲波進行了扭曲,B則是完全重現原始聲波的高采樣率的音頻信號。
數字音頻常用的采樣率如下:
位深度決定動態范圍。采樣聲波時,為每個采樣指定最接近原始聲波振幅的振幅值。較高的位深度可提供更多可能的振幅值,產生更大的動態范圍、更低的噪聲基準和更高的保真度。
位深度越高,提供的動態范圍越大。
二、PCM在上面的名詞解析中我們應該對PCM有了一定的理解和認識,下面我們將對PCM做更多的講解。
1. PCM音頻數據存儲方式如果是單聲道的文件,采樣數據按時間的先后順序依次存入。如果是單聲道的音頻文件,采樣數據按時間的先后順序依次存入(也可能采用 LRLRLR 方式存儲,只是另一個聲道的數據為 0)。
如果是雙聲道的話通常按照 LRLRLR 的方式存儲,存儲的時候還和機器的大小端有關。(關于字節序大小端的相關內容可參考《字節序問題之大小端模式講解》進行了解)
PCM的存儲方式為小端模式,存儲Data數據排列如下圖所示:
描述 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 指的是聲道數,也即采樣時用到的麥克風的數量,麥克風越多就越能還原真實的采樣環境(當然麥克風的放置位置也是有規定的)。
三、WAVWAV 是 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 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
字段解析如下圖:
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; }
注意:函數里聲明的數據類型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; }
本程序中的函數可以從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,"\n",samplenum); } cnt++; } free(sample); fclose(fp); fclose(fp1); fclose(fp_stat); return 0; }
本程序中的函數可以通過計算的方式將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\n",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\n",cnt); free(sample); fclose(fp); fclose(fp1); return 0; }
參考資料:
視音頻數據處理入門:PCM音頻采樣數據處理 --> 致敬雷神!
*博客內容為網友個人發布,僅代表博主個人觀點,如有侵權請聯系工作人員刪除。