C語言解析WAV音頻文件

轉載:http://www.cnblogs.com/LexMoon/p/wave-c.htmlhtml

1.C語言解析WAV音頻文件

代碼地址: Github : https://github.com/CasterWx/c-wave-mastergit

在計算機中有着各式各樣的文件,好比說EXE這種可執行文件,JPG這種圖片文件,也有咱們平時看的TXT,或者C,CPP,PHP等代碼文件。
pgithub

若是把這些文件用記事本或者其餘純文本編輯器打開,會發現前面這類文件打開以後基本上都是亂碼,也就是非人類可讀的字符,然後面這類代碼或者TXT文件打開以後都是人類可讀的字符串。算法

若是咱們把這些文件統一作一個分類,那麼前面的EXE,JPG之類的這種打開以後都是咱們看不懂的外星球文字的文件叫作二進制文件,然後面那些文件能夠稱爲是文本文件。編程

後面那種分類是文本文件很好理解,畢竟都是咱們認識的文本文字,可是前面的那些亂碼爲何叫他二進制文件呢?這些二進制文件是怎麼被計算機識別的,爲何這些亂碼就能被計算機識別,而且放出悠揚動聽的音樂或者栩栩如生的圖片呢?咱們學編程,搞計算機的人能不能也本身寫一個程序把這些數據解析出來呢?請跟聽本專欄欄豬一塊兒慢慢道來。網絡

2.前言

咱們將一步一步來了解C語言的一些基本庫的使用,以及如何使用這些庫來解析一個wav格式的音頻文件,將其中的元數據(也就是該音頻文件的一些屬性)提取出來。所以您須要有基本的計算機基礎知識以及瞭解C語言,最好還對音頻或者信號處理感興趣。數據結構

2.1.瞭解WAV音頻文件

下面是百度百科的解釋編輯器

WAV爲微軟公司(Microsoft)開發的一種聲音文件格式,它符合RIFF(Resource Interchange File Format)文件規範,用於保存Windows平臺的音頻信息資源,被Windows平臺及其應用程序所普遍支持,該格式也支持MSADPCM,CCITT A LAW等多種壓縮運算法,支持多種音頻數字,取樣頻率和聲道,標準格式化的WAV文件和CD格式同樣,也是44.1K的取樣頻率,16位量化數字,所以在聲音文件質量和CD相差無幾! WAV打開工具是WINDOWS的媒體播放器。
一般使用三個參數來表示聲音,量化位數,取樣頻率和採樣點振幅量化位數分爲8位,16位,24位三種,聲道有單聲道和立體聲之分,單聲道振幅數據爲n1矩陣點,立體聲爲n2矩陣點,取樣頻率通常有11025Hz(11kHz) ,22050Hz(22kHz)和44100Hz(44kHz) 三種,不過儘管音質出色,但在壓縮後的文件體積過大!相對其餘音頻格式而言是一個缺點,其文件大小的計算方式爲:WAV格式文件所佔容量(B) = (取樣頻率 X量化位數X 聲道) X 時間 / 8 (字節= 8bit) 每一分鐘WAV格式的音頻文件的大小爲10MB,其大小不隨音量大小及清晰度的變化而變化。ide

咱們一般在各類音樂播放器中下載歌曲的時候會看到各類參數,好比說普通音質的碼流爲128k,高品質是320k,還有無損的APE,FLAC等格式。還有的時候咱們在使用各類音頻格式轉換工具中會遇到各類參數,好比說採樣率,量化精度,以及該音頻文件是單聲道仍是雙聲道等等。函數

咱們如今都是聽MP3格式的音樂,WAV如今除了Windows的錄音機之外,基本上沒有地方會用了,爲何還要用他來作示例呢?這是由於WAV本質上是無壓縮的原始音頻文件,並且他的文件結構不算很是複雜,所以能夠做爲咱們初學者的學習示例格式。你能夠按照一樣的思路本身去學習其餘格式。

2.2什麼是二進制文件

二進制文件,本質上就是一種使用二進制方式存儲文件內容的文件統稱,咱們前面有講過使用記事本等工具打開以後看到的是亂碼,那麼咱們怎麼分析他呢,可使用UltraEditor,HxD,C32Asm等等。好比我這裏使用HxD打開Windows 7的關機音樂(C:\Windows\Media\Windows Shutdown.wav)就是這個樣子,左邊就是這個WAV音頻文件的二進制表示,右邊則是這個二進制數字對應的ASCII表示因爲像00之類的數字在ASCII中並無有效的圖像來顯示,因此在這個界面的右邊顯示的就是一個點。而左邊這些52,49之類的數字分別對應什麼呢?其實這些二進制數字看似亂碼,其實都是有必定的規範的,只要咱們或者咱們計算機上面的應用程序瞭解這個規範,就能夠按照這個規範去解讀它。

3.WAV的二進制格式解析

根據網絡上的各類資料能夠得知WAVE文件本質上就是一種RIFF格式,它能夠抽象成一顆樹(數據結構的一種)來看。

ss

咱們看到這張圖上面,從上到下分別對應着二進制數據在文件中相對於起始位置的偏移量。每個格子對應一個字段field size表示每一個字段所佔據的大小,根據這個大小以及當前的偏移量,咱們也能夠計算出下一個字段的起始地址(偏移量)。

WAV 是Microsoft開發的一種音頻文件格式,它符合上面提到的RIFF文件格式標準,能夠看做是RIFF文件的一個具體實例。既然WAV符合RIFF規範,其基本的組成單元也是chunk。一個WAV文件一般有三個chunk以及一個可選chunk,其在文件中的排列方式依次是:RIFF chunk,Format chunk,Fact chunk(附加塊,可選),Data chunk。各個chunk中字段的意義以下:

3.1 RIFF chunk

根據RIFF的規範,整個WAV文件的頂級chunk(數據塊)就是最頂上的ChunkID爲RIFF的這個chunk,這也能夠解釋爲何以前那張圖片中咱們能夠看出wav文件開頭都是RIFF幾個字母。而接下來的ChunkSize則表示這個chunk下的那些子chunk的大小,若是按照「樹結構」來理解,那麼每個子chunk(Subchunk)則爲樹的樹枝。而Format則爲這個chunk的實際數據

說白了一個chunk結構其實就是三個部分,第一個部分標識符用於說明這個chunk是存什麼內容的,第二個部分則是說明這個chunk的內容到底有多大,用於讓程序知道若是要找到下一個chunk該把地址偏移多少去讀取,而第三個部分則是實際內容

  • RIFF chunk
    • id
      FOURCC 值爲'R' 'I' 'F' 'F'
    • size
      其data字段中數據的大小 字節數
    • data
      包含其餘的chunk

3.2 第一個子chunk-fmt

好了說完了頂級chunk,咱們就來看看子chunk,第一個子chunk的Subchunk1ID在WAV文件中恆定爲fmt,表示該subchunk的內容爲該WAV音頻文件的一些元數據,也就是該WAV音頻的一些格式信息。好比說AudioFormat這個字段通常爲1,表示這個WAV音頻爲PCM編碼。NumChannels則是該WAV音頻文件的聲道數量。SampleRate則爲採樣率,ByteRate則爲採樣率。BlockAlign則是每一個block的平均大小,它等於NumChannels * BitsPerSample/8,至於block是什麼,以及它的計算公式是怎麼得來的須要來看看另外一個Subchunk。BitsPerSample則爲每秒採樣比特,有的地方稱它爲量化精度或者PCM位寬。(未考究)

  • Format chunk
    • id
      FOURCC 值爲 'f' 'm' 't' ' '
    • size
      數據字段包含數據的大小。如無擴展塊,則值爲16;有擴展塊,則值爲= 16 + 2字節擴展塊長度 + 擴展塊長度或者值爲18(只有擴展塊的長度爲2字節,值爲0)
    • data
      存放音頻格式、聲道數、採樣率等信息
      • format_tag
        2字節,表示音頻數據的格式。如值爲1,表示使用PCM格式。
      • channels
        2字節,聲道數。值爲1則爲單聲道,爲2則是雙聲道。
      • samples_per_sec
        採樣率,主要有22.05KHz,44.1kHz和48KHz。
      • bytes_per sec
        音頻的碼率,每秒播放的字節數。samples_per_sec * channels * bits_per_sample / 8,能夠估算出使用緩衝區的大小
      • block_align
        數據塊對齊單位,一次採樣的大小,值爲聲道數 * 量化位數 / 8,在播放時須要一次處理多個該值大小的字節數據。
      • bits_per_sample
        音頻sample的量化位數,有16位,24位和32位等。
      • cbSize
        擴展區的長度
      • 擴展塊內容
        22字節,具體介紹,後面補充。

3.3 第二個子chunk-data(附加塊,可選)

採用壓縮編碼的WAV文件,必需要有Fact chunk,該塊中只有一個數據,爲每一個聲道的採樣總數。

    • Fact chunk
      • id
        FOURCC 值爲 'f' 'a' 'c' 't'
      • size
        數據域的長度,4(最小值爲4)
      • 採樣總數 4字節

3.4 第三個子chunk-data

另外一個子chunk也就是Subchunk2ID是在WAV文件中恆定爲data,也就是這個WAV音頻文件的實際音頻數據,說專業一點,這裏面存儲的是音頻的採樣數據。可是咱們的音頻若是是雙聲道,那麼實際上某一個採樣時刻採樣的數據是由左聲道和右聲道共同組成的。而這個共同組成的採樣咱們把他成爲block。前面有講到BlockAlign = NumChannels * BitsPerSample / 8,這個如今就很好理解了,至於爲何末尾要除以8,這是由於計算機中是以8個二進制數表示一個字節,因此要除以8來求出字節數。

至於音頻的持續長度,咱們能夠經過Subchunk2Size除以ByteRate,也就是實際音頻data的chunk總長度除以每秒字節數獲得持續多少秒。

  • Data chunk
    • id
      FOURCC 值爲'd' 'a' 't' 'a'
    • size
      數據域的長度
    • data
      具體的音頻數據存放在這裏

4.C語言解析WAV音頻文件

前面講了這麼多,如今問題來了,怎麼編程來實現解析上面所說的這些元數據呢。C語言基本的二進制文件操做函數有fopen,fread等等。(注意是二進制文件操做函數,因此咱們不討論fgets,這是普通的文本文件操做函數)

4.1 fopen(打開文件)

相關函數

   open,fclose

表頭文件

  #include<stdio.h>

定義函數

   FILE * fopen(const char * path,const char * mode);

函數說明

  返回值文件順利打開後,指向該流的文件指針就會被返回。

  參數path字符串包含欲打開的文件路徑及文件名,參數mode字符串則表明着流形態。

mode有下列幾種形態字符串:

  • r 打開只讀文件,該文件必須存在。

  • r+ 打開可讀寫的文件,該文件必須存在。

  • w 打開只寫文件,若文件存在則文件長度清爲0,即該文件內容會消失。若文件不存在則創建該文件。

  • w+ 打開可讀寫文件,若文件存在則文件長度清爲零,即該文件內容會消失。若文件不存在則創建該文件。

  • a 以附加的方式打開只寫文件。若文件不存在,則會創建該文件,若是文件存在,寫入的數據會被加到文件尾,即文件原先的內容會被保留。

  • a+ 以附加方式打開可讀寫的文件。若文件不存在,則會創建該文件,若是文件存在,寫入的數據會被加到文件尾後,即文件原先的內容會被保留。

  • 上述的形態字符串均可以再加一個b字符,如rb、w+b或ab+等組合,加入b字符用來告訴函數庫打開的文件爲二進制文件,而非純文字文件。

  不過在POSIX系統,包含Linux都會忽略該字符。由fopen()所創建的新文件會具備S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH(0666)權限,此文件權限也會參考umask 值。 返回值文件順利打開後,指向該流的文件指針就會被返回。若果文件打開失敗則返回NULL,並把錯誤代碼存在errno 中。

附加說明 

  通常而言,打開文件後會做一些文件讀取或寫入的動做,若打開文件失敗,接下來的讀寫動做也沒法順利進行,因此通常在fopen()後做錯誤判斷及處理

例:

#include<stdio.h>
main()
{
    FILE * fp;
    fp=fopen(「noexist」,」a+」);
    if(fp= =NULL)
    return;
    fclose(fp);
}

4.2 fread和fwrite

  用來讀寫一個數據塊。

通常調用形式

  fread(buffer,size,count,fp);

  fwrite(buffer,size,count,fp);

說明

  若是調用成功返回實際讀取到的項個數(小於或等於count),若是不成功或讀到文件末尾返回 0

  (1)buffer:是一個指針,對fread來講,它是讀入數據的存放地址。對fwrite來講,是要輸出數據的地址。

  (2)size:要讀寫的字節數;

  (3)count:要進行讀寫多少個size字節的數據項;

  (4)fp:文件型指針。

注意:

  1 完成次寫操(fwrite())做後必須關閉流(fclose());

  2 完成一次讀操做(fread())後,若是沒有關閉流(fclose()),則指針(FILE * fp)自動向後移動前一次讀寫的長度,不關閉流繼續下一次讀操做則接着上次的輸出繼續輸出;

4.3 完整代碼

代碼以下:

wave.c

#include <stdio.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include "wave.h"
 int main()
 {
    FILE *fp = NULL;
    
     Wav wav;
     RIFF_t riff;
     FMT_t fmt;
     Data_t data;
 
     fp = fopen("test.wav", "rb");
     if (!fp) {
         printf("can't open audio file\n");
         exit(1);
     }
 
     fread(&wav, 1, sizeof(wav), fp);
     
     riff = wav.riff;
     fmt = wav.fmt;
     data = wav.data;
 
     printf("ChunkID \t%c%c%c%c\n", riff.ChunkID[0], riff.ChunkID[1], riff.ChunkID[2], riff.ChunkID[3]);
     printf("ChunkSize \t%d\n", riff.ChunkSize);
     printf("Format \t\t%c%c%c%c\n", riff.Format[0], riff.Format[1], riff.Format[2], riff.Format[3]);
     
     printf("\n");
     
     printf("Subchunk1ID \t%c%c%c%c\n", fmt.Subchunk1ID[0], fmt.Subchunk1ID[1], fmt.Subchunk1ID[2], fmt.Subchunk1ID[3]);
     printf("Subchunk1Size \t%d\n", fmt.Subchunk1Size);
     printf("AudioFormat \t%d\n", fmt.AudioFormat);
     printf("NumChannels \t%d\n", fmt.NumChannels);
     printf("SampleRate \t%d\n", fmt.SampleRate);
     printf("ByteRate \t%d\n", fmt.ByteRate);
     printf("BlockAlign \t%d\n", fmt.BlockAlign);
     printf("BitsPerSample \t%d\n", fmt.BitsPerSample);
     
     printf("\n");
 
     printf("blockID \t%c%c%c%c\n", data.Subchunk2ID[0], data.Subchunk2ID[1], data.Subchunk2ID[2], data.Subchunk2ID[3]);
     printf("blockSize \t%d\n", data.Subchunk2Size);
     
     printf("\n");
    
     printf("duration \t%d\n", data.Subchunk2Size / fmt.ByteRate);
 }
wave.c

wave.h

typedef struct WAV_RIFF {
    /* chunk "riff" */
    char ChunkID[4];   /* "RIFF" */
    /* sub-chunk-size */
    uint32_t ChunkSize; /* 36 + Subchunk2Size */
    /* sub-chunk-data */
    char Format[4];    /* "WAVE" */
} RIFF_t;

typedef struct WAV_FMT {
    /* sub-chunk "fmt" */
    char Subchunk1ID[4];   /* "fmt " */
    /* sub-chunk-size */
    uint32_t Subchunk1Size; /* 16 for PCM */
    /* sub-chunk-data */
    uint16_t AudioFormat;   /* PCM = 1*/
    uint16_t NumChannels;   /* Mono = 1, Stereo = 2, etc. */
    uint32_t SampleRate;    /* 8000, 44100, etc. */
    uint32_t ByteRate;  /* = SampleRate * NumChannels * BitsPerSample/8 */
    uint16_t BlockAlign;    /* = NumChannels * BitsPerSample/8 */
    uint16_t BitsPerSample; /* 8bits, 16bits, etc. */
} FMT_t;

typedef struct WAV_data {
    /* sub-chunk "data" */
    char Subchunk2ID[4];   /* "data" */
    /* sub-chunk-size */
    uint32_t Subchunk2Size; /* data size */
    /* sub-chunk-data */
//    Data_block_t block;
} Data_t;

//typedef struct WAV_data_block {
//} Data_block_t;

typedef struct WAV_fotmat {
   RIFF_t riff;
   FMT_t fmt;
   Data_t data;
} Wav;  
wave.h

執行結果

4.4兩個細節

一、fopen的時候咱們的mode要設置爲"rb",r表示read,b表示binary,也就是二進制讀取方式。這一點是和讀取傳統的文本文件格式有所區別的。

二、struct類型裏面我用的是uint32_t等類型,而不是傳統的int,short等等,這是爲了考慮到不一樣的編譯器,不一樣的平臺下對於int類型分配的內存空間不一致的問題。而這些類型是由stdint.h頭文件提供的,所以咱們須要在頭部導入它。

5.fcm文件轉WAV文件

  PCM(Pulse Code Modulation----脈碼調製錄音)。所謂PCM錄音就是將聲音等模擬信號變成符號化的脈衝列,再予以記錄。PCM信號是由[1]、[0]等符號構成的數字信號,而未通過任何編碼和壓縮處理。與模擬信號比,它不易受傳送系統的雜波及失真的影響。動態範圍寬,可獲得音質至關好的影響效果。

  在Windows平臺下,基於PCM編碼的WAV是被支持得最好的音頻格式,全部音頻軟件都能完美支持,因爲自己能夠達到較高的音質的要求,所以,WAV也是音樂編輯創做的首選格式,適合保存音樂素材。所以,基於PCM編碼的WAV被做爲了一種中介的格式,經常使用在其餘編碼的相互轉換之中,例如MP3轉換成WMA。

  簡單一句,PCM就是沒有壓縮的格式。

pcm 是沒有頭信息的,wav有44字節的頭文件,pcm文件轉wav在開始位置加44字節頭文件便可

代碼以下:

fcm轉wav轉換函數

/*
 * fptr:要轉的文件指針
 * WriteWaveHeader(FILE *fptr,Uint32 num):向fptr文件中寫入WAV所需的頭文件
 * WriteWaveFile(FILE *fptr,Uint32 num,short *ptr):
*/
void WriteWaveHeader(FILE *fptr,Uint32 num)
{
    WAVE_HEADER WaveHeader;
    FORMAT WaveFormat;
    DATA WaveData;
    /**************RIFF chunk 參數定義*********************/
    WaveHeader.riffid[0]='R';                                //ChunkID
    WaveHeader.riffid[1]='I';                                
    WaveHeader.riffid[2]='F';
    WaveHeader.riffid[3]='F';
    WaveHeader.dwSize=num*BYTES_EACH_SAMPLE+36;                //ChunkSize
    
    WaveHeader.riffType[0]='W';                                //Format
    WaveHeader.riffType[1]='A';
    WaveHeader.riffType[2]='V';
    WaveHeader.riffType[3]='E';

    /**************fmt chunk 參數定義**********************/
    WaveFormat.fccid[0]='f';                                //SubChunkID
    WaveFormat.fccid[1]='m';
    WaveFormat.fccid[2]='t';
    WaveFormat.fccid[3]=' ';

    WaveFormat.dwSize=16;                                    //SubChunkSize
    WaveFormat.wFormatTag=1;                                //AudioFormat
    WaveFormat.wChannels=CHANNEL_NUN;                        //NumChannels
    WaveFormat.dwSamplesPerSec=SAMPLE_RATE;                    //SampleRate
    WaveFormat.dwAvgBytePerSec=BYTES_EACH_SAMPLE*SAMPLE_RATE*CHANNEL_NUN;        //ByteRate
    WaveFormat.wBlockAlign=BYTES_EACH_SAMPLE;                //BlockAlign
    WaveFormat.uiBitsPerSample=QUANTIZATION;                //BitsPerSample

    /**************Data chunk 參數定義**********************/
    WaveData.fccid[0]='d';                                    //BlockID
    WaveData.fccid[1]='a';
    WaveData.fccid[2]='t';
    WaveData.fccid[3]='a';
    WaveData.dwSize=num*BYTES_EACH_SAMPLE;                    //BlockSize

    fwrite(&WaveHeader,sizeof(WAVE_HEADER),1,fptr);
    fwrite(&WaveFormat,sizeof(FORMAT),1,fptr);
    fwrite(&WaveData,sizeof(DATA),1,fptr);
}

void WriteWaveFile(FILE *fptr,Uint32 num,short *ptr)
{
    WriteWaveHeader(fptr,num);                                //向fptr指向的wav文件寫入wav標準頭文件
    fwrite(buffer,sizeof(short),num,fptr);                    //向fptr指向的wav文件寫入buffer指向的fcm源文件
}
fcm轉wav

主函數:

#include "wav.h"
FILE *frpcm,*fwwav;

frpcm=fopen("music2.pcm","rb");
fwwav=fopen("music.wav","wb");
numOfword=fread(buffer,sizeof(short),N,frpcm);//將music2.pcm文件寫入buffer指針地址
WriteWaveFile(fwwav,numOfword,buffer);//將wav頭文件和buffer指向的pcm文件合併,寫入fwav指向的wav文件中 
fclose(fwwav);
main

wav頭文件:

#ifndef _WAV_H
#define _WAV_H

#include <stdio.h>
#include <stdlib.h>
#define SAMPLE_RATE 8000
#define QUANTIZATION 16
#define BYTES_EACH_SAMPLE 2
#define CHANNEL_NUN 1
#define FORMAT_TAG 1

struct RIFF_CHUNK
{
    char riffid[4];
    Uint32 dwSize;
    char riffType[4];
};
typedef struct RIFF_CHUNK WAVE_HEADER;

struct FORMAT_CHUNK
{
    char fccid[4];
    Uint32 dwSize;
    short wFormatTag;
    short wChannels;
    Uint32 dwSamplesPerSec;
    Uint32 dwAvgBytePerSec;
    short wBlockAlign;
    short uiBitsPerSample;
};
typedef struct FORMAT_CHUNK FORMAT;

struct DATA_CHUNK
{
    char fccid[4];
    Uint32 dwSize;
};
typedef struct DATA_CHUNK DATA;

void WriteWaveHeader(FILE *fptr,Uint32 num);
void WriteWaveFile(FILE *fptr,Uint32 num,short *ptr);

#endif
wav.h

6.總結

  其實任何二進制數據都是有着屬於它本身的解析規範,這就有點像咱們學計算機網絡的時候所說的「協議」,只要咱們遵循這個規範或者「協議」,那麼咱們就能夠將該文件真正隱含的信息讀取出來。

咱們這裏僅僅是讀取了一段WAV音頻文件的元數據,沒有把它的data chunk,也就是實際音頻的數字信號讀取出來,由於這涉及到數模信號的轉換等知識,超出了咱們的研究範圍,

代碼參考 https://github.com/CasterWx/c-wave-master

相關文章
相關標籤/搜索