博客專欄

EEPW首頁 > 博客 > 多媒體文件格式(五):PCM / WAV 格式

多媒體文件格式(五):PCM / WAV 格式

發(fā)布人:電子禪石 時間:2024-08-11 來源:工程師 發(fā)布文章

本文目錄:

  • 一、名詞解析

  • 二、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ī)定的)。

三、WAV

WAV 是 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 文件的前面加了一個文件頭,每個字段的的含義如下:

復(fù)制代碼
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;
復(fù)制代碼

WAV 文件頭解析

這里是一個 WAVE 文件的開頭 72 字節(jié),字節(jié)顯示為十六進(jìn)制數(shù)字:

復(fù)制代碼
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
復(fù)制代碼

字段解析如下圖:

三、PCM & WAV 開發(fā)實踐

1. PCM格式轉(zhuǎn)為WAV格式(基于C語言)
復(fù)制代碼
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;
}
復(fù)制代碼

 

注意:函數(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)降低某個聲道的音量。

復(fù)制代碼
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;
}
復(fù)制代碼

上述代碼做的事情是:在讀出左聲道的 2 Byte 的取樣值之后,將其轉(zhuǎn)成了 C 語言中的一個 short 類型的變量。將該數(shù)值除以 2 之后寫回到了 PCM 文件中。

3. 分離PCM音頻數(shù)據(jù)左右聲道的數(shù)據(jù)

因為PCM音頻數(shù)據(jù)是按照LRLRLR的方式來存儲左右聲道的音頻數(shù)據(jù)的,所以我們可以通過將它們交叉的讀出來的方式來分離左右聲道的數(shù)據(jù):

復(fù)制代碼
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;
}
復(fù)制代碼

4. 從PCM16LE單聲道音頻采樣數(shù)據(jù)中截取一部分?jǐn)?shù)據(jù)

本程序中的函數(shù)可以從PCM16LE單聲道數(shù)據(jù)中截取一段數(shù)據(jù),并輸出截取數(shù)據(jù)的樣值。函數(shù)的代碼如下所示:

復(fù)制代碼
/**
 * 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;
}
復(fù)制代碼

5. 將PCM16LE雙聲道音頻采樣數(shù)據(jù)轉(zhuǎn)換為PCM8音頻采樣數(shù)據(jù)

本程序中的函數(shù)可以通過計算的方式將PCM16LE雙聲道數(shù)據(jù)16bit的采樣位數(shù)轉(zhuǎn)換為8bit。函數(shù)的代碼如下所示:

復(fù)制代碼
/**
 * 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;
}
復(fù)制代碼

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ù)的代碼如下所示:

復(fù)制代碼
/**
 * 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;
}
復(fù)制代碼

  

參考資料:

視音頻數(shù)據(jù)處理入門:PCM音頻采樣數(shù)據(jù)處理   -->  致敬雷神!


*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。



關(guān)鍵詞: pcm

相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉