WAVE音訊格式及及轉換程式碼

2022-06-19 18:03:07

音訊訊號的讀寫、播放及錄音

  python已經支援WAV格式的書寫,而實時的聲音輸入輸出需要安裝pyAudio(http://people.csail.mit.edu/hubert/pyaudio)。最後我們還將使用pyMedia(http://pymedia.org)進行Mp3的解碼和播放。

  音訊訊號是模擬訊號,我們需要將其儲存為數位訊號,才能對語音進行演演算法操作,WAV是Microsoft開發的一種聲音檔案格式,通常被用來儲存未壓縮的聲音資料。

語音訊號有四個重要的引數:聲道數、取樣頻率、量化位數(位深)和位元率。

  • 聲道數:可以是單聲道、雙聲道 ...
  • 取樣頻率(Sample rate):每秒內對聲音訊號取樣樣本的總數目,44100Hz取樣頻率意味著每秒鐘訊號被分解成44100份。換句話說,每隔144100144100秒就會儲存一次,如果取樣率高,那麼媒體播放音訊時會感覺訊號是連續的。
  • 量化位數(Bit depth):也稱為「位深」,每個取樣點中資訊的位元(bit)數。1 byte等於8 bit。通常有8bit、16bit、24bit、32bit...
  • 位元率(Bit rate):每秒處理多少個Bit。比如一個單聲道,用44.1KHz/16Bit的設定來說,它的位元率就為44100*16*1=705600,單位是bit/s(或者bps),因為通常計算出來的數位都比較大,大家就用kbit/s了,也就是705.6kbit/s。在對音訊進行壓縮時,位元率就成為了我們的一個要選的選項了,越高的位元率,其音質也就越好。一些常用的位元率有:
    • 32kbit/s: 一般只適用於語音
    • 96kbit/s: 一般用於語音或低質量串流媒體
    • 128或160kbit/s: 中等位元率質量
    • 192kbit/s: 中等質量位元率
    • 256kbit/s: 常用的高質量位元率
    • 320kbit/s: MP3標準支援的最高水平

  如果你需要自己錄製和編輯聲音檔案,推薦使用Audacity,它是一款開源的、跨平臺、多聲道的錄音編輯軟體。在我的工作中經常使用Audacity進行聲音訊號的錄製,然後再輸出成WAV檔案供Python程式處理。

  如果想要快速看語音波形和語譜圖,推薦使用Adobe Audition,他是Adobe公司開發專門處理音訊的專業軟體,微博關注vposy,下載地址見置頂。他破解了很多adobe公司的軟體,包括PS、PR...

音訊格式

WAV

  WAV格式是微軟公司開發的一種無失真聲音檔案格式,也稱為波形聲音檔案,WAV格式支援多種壓縮演演算法、音訊位數、取樣頻率和聲道。

  WAV 符合 RIFF(Resource Interchange File Format) 規範,所有的WAV都由 44位元組 標頭檔案 PCM檔案 組成,這個檔案頭包含語音訊號的所有引數資訊(聲道數、取樣率、量化位數、位元率....)

   44個位元組的 標頭檔案由 3個區塊組成:

  • RIFF chunk:WAV檔案標識
  • Format chunk: 聲道數、取樣率、量化位數、等資訊
  • Data chunk:存放資料

  相反的,在PCM檔案頭部新增44個位元組的WAV檔案頭,就可以生成WAV格式檔案

RIFF區塊

規範的WAVE格式遵循RIFF頭

名稱 位元組數 內容
ChunkID 4 "RIFF" 識別符號
ChunkSize 4

表示從下個地址開始到檔案尾的總位元組數

更準確的說:等於整個wav檔案大小-8

Format 4 "WAVE" 識別符號

FORMAT區塊

描述聲音資料的格式

名稱 位元組數 內容
 Subchunk1ID 4 "fmt " 識別符號,最後一位是空格
Subchunk1Size 4 該區塊資料的長度(不包含該區塊ID和Size的長度)
AudioFormat 2 音訊格式,PCM音訊資料的值為1
NumChannels 2 通道數
SampleRate 4 取樣率
ByteRate 4 每秒資料位元組數 = SampleRate * NumChannels * BitsPerSample / 8
BlockAlign 2 每個取樣點所需的位元組數 = NumChannels * BitsPerSample / 8
BitsPerSample 2 量化位數(bit)

DATA區塊

包含資料的大小和實際聲音

名稱 位元組數 內容
Subchunk2ID 4 "data" 識別符號
Subchunk2Size 4 該區塊資料的長度,(不包含該區塊ID和Size的長度),也就是PCM位元組數
Data * 音訊資料

檔案範例:

RIFF區塊

  • ChunkID(4位元組 52 49 46 46):對應ASCII中的 RIFF,這裡是ASCII碼對照表
  • ChunkSize(4位元組 76 01 03 00):表示WAV檔案的大小,不包含了前面8個位元組,所以真正的大小等於檔案總位元組減去8。76 01 03 00 對應的正序16進位製為 00 03 01 76大小為196982
  • Format(4位元組 57 41 56 45):對應ASCII中的WAVE

FORMAT區塊

  • Subchunkl ID(4位元組 66 6d 74 20):對應ASCII中的fmt 
  • Subchunkl Size(4位元組 10 00 00 00):正序16進位制 00 00 00 10 對應16
  • AudioFormat(2位元組 01 00):正序16進位制 00 01,對應數位1,表示編碼格式「WAVE_FORMAT_PCM」
  • NumChannels(2位元組 01 00):正序16進位制 00 01,對應數位1,表示聲道數為1
  • SampleRate(4位元組 80 bb 00 00):正序16進位制 00 00 bb 80,表示取樣率為48000
  • ByteRate(4位元組 00 77 01 00):正序16進位制 00 01 77 00,表示傳輸速率為96000
  • BlockAlign(2位元組 02 00):正序16進位制 00 02,每個取樣所需的2位元組數
  • BitsPerSample(2位元組 10 00):正序16進位制 00 10,取樣大小為16 Bits

DATA區塊

  • Subchunk2ID(4位元組 64 61 74 61):表示為ASCII的data,開始資料區
  • Subchunk2 Size(4位元組 52 01 03 00):正序16進位制 00 03 01 52,PCM位元組數,大小為196946
  • wav檔案(wav位元組-44位元組):pcm音訊資料

WAV轉PCM

  因為wav比pcm多44個位元組的檔案頭,也就是說44位元組後的資訊,就是pcm資料

#include <stdio.h>

/**
 * wav2pcm ***.wav **.pcm
 * @param argc 命令列引數的長度
 * @param argv 命令列引數,argv[0]是程式名稱
 * @return
 */
int main(int argc, char *argv[]) {
    FILE *wavfile;
    FILE *pcmfile;
    char buf[1024];
    int read_len;

    if (argc != 3) {
        printf("usage:\n"
               "\t wav2pcm ***.wav **.pcm\n");
    }
    wavfile = fopen(argv[1], "rb");
    if (wavfile == NULL) {
        printf("!Error: Can't open wavfile.\n");
        return 1;
    }
    pcmfile = fopen(argv[2], "wb");
    if (pcmfile == NULL) {
        printf("!Error: Can't open pcmfile.\n");
        return 1;
    }

    fseek(wavfile, 44, SEEK_SET);        // 將檔案指標移動到檔案開頭,後移44位元組

    while ((read_len = fread(buf, 1, sizeof(buf), wavfile)) != 0) {
        fwrite(buf, 1, read_len, pcmfile);
    }

    fclose(pcmfile);
    fclose(wavfile);

    return 0;
}
wav2pcm.c
dd if=1.wav of=1.pcm bs=1 skip=44
wav2pcm.sh
def wav2pcm(wavfile, pcmfile, data_type=np.int16):
    f = open(wavfile, "rb")
    f.seek(0)
    f.read(44)
    data = np.fromfile(f, dtype=data_type)
    data.tofile(pcmfile)
wav2pcm.py

還有一個github開原始碼:wavutils

當我們讀取pcm資料的時候,我們需要弄清楚語音每個取樣點的位深是多少bit,一般來說是16bit,那麼我們去pcm資料的時候就應該2個位元組的去取,應該建立short的buf。

#include <stdio.h>


int main() {
    FILE *pcmfile;
    int frame_len = 480;     // 幀長
    short buf[frame_len];   // 每個取樣點2位元組
    int read_len;
    char pcmpath[]="../p225_001.pcm";

    pcmfile = fopen(pcmpath, "rb");
    if (pcmfile == NULL) {
        printf("!Error: Can't open wavfile.\n");
        return 1;
    }

    while (feof(pcmfile)==0){
        read_len = fread(buf, sizeof(short), frame_len, pcmfile);
        for (int i = 0; i < read_len; i++) {
            printf("%d ", buf[i]);
        }
    }

    fclose(pcmfile);
    return 0;
}
讀取pcm資料

RAW、PCM、SAM

  RAW、PCM(Pulse Code Modulation)、SAM 都是一種儲存 原始資料 的音訊檔格式,未經過任何編碼和壓縮處理,他們的本質一樣,只是副檔名不同,也可以沒有擴充套件名WAVAIFF的大小相比,這音訊檔不包含任何標題資訊取樣率、位深度、通道數)

  如果在PCM檔案的前面新增WAV檔案頭,就可以生成WAV格式檔案。

如果是16位元的話,pcm每個取樣點的值在0~$2^{15}-1$,因為第一位是符號位。所以我們有時候用librosa讀取的音訊每個取樣點都是0~1之間的,如果該音訊是16bit的,如果想將他換成short型應該乘以$2^{15}$。

pcm轉wav

版本一:程式碼參考自:https://github.com/pliu6/pcm2wav

/**
 * https://github.com/pliu6/pcm2wav
 */
#include <stdlib.h>
#include <string.h>
#include <stdio.h>


typedef struct {
    unsigned char chunk_id[4];     /*{'R', 'I', 'F', 'F'}*/
    unsigned int chunk_size;
    unsigned char format[4];
} FIFFChunk;

typedef struct {
    unsigned char chunk_id[4];  /* {'f', 'm', 't', ' '} */
    unsigned int chunk_size;
    unsigned short audio_format;            // 2位元組
    unsigned short channels;             // 4位元組
    unsigned int sample_rate;       // 4位元組
    unsigned int byte_rate;      // 4位元組
    unsigned short block_align;           // 2位元組
    unsigned short bits_per_sample;        // 2位元組
} FormatChunk;

typedef struct {
    unsigned char chunk_id[4];  /* {'d', 'a', 't', 'a'}  */
    unsigned int chunk_size;
} DataChunk;


// pcm2wav ***.pcm ***.wav 通道 取樣率 量化位數
int main(int argc, char *argv[]) {
    FILE *pcmfile, *wavfile;
    long pcmfile_size;
    FIFFChunk fiffchunk;
    FormatChunk formatchunk;
    DataChunk datachunk;
    int read_len;
    char buf[1024];

    if (argc != 6) {
        printf("usage:\n"
               "\t%s pcmfile wavfile channel samplerate bitspersample\n", argv[0]);
        return 1;
    }

    pcmfile = fopen(argv[1], "rb");
    if (pcmfile == NULL) {
        printf("!Error: Can't open pcmfile.\n");
        return 1;
    }
    fseek(pcmfile, 0, SEEK_END);        // 將檔案指標移動到檔案最後
    pcmfile_size = ftell(pcmfile);          // 返回給定流 stream 的當前檔案位置(位元組)
    fseek(pcmfile, 0, SEEK_SET);        // 將檔案指標移動到檔案開頭

    wavfile = fopen(argv[2], "wb");
    if (wavfile == NULL) {
        printf("!Error: Can't create wavfile.\n");
        return 1;
    }
    /* *********** RIFF區塊  ********************* */
    strncpy(fiffchunk.chunk_id,"RIFF", 4);
    fiffchunk.chunk_size = pcmfile_size+36;
    strncpy(fiffchunk.format,"WAVE",4);
    fwrite(&fiffchunk, sizeof(fiffchunk), 1, wavfile);

    /* *********** FORMAT區塊  ********************* */
    strncpy(formatchunk.chunk_id,"fmt ", 4);
    formatchunk.chunk_size = sizeof(FormatChunk) - 8;       // 不包含該區塊ID和Size的長度
    formatchunk.audio_format = 1;   /* 未壓縮的 */
    formatchunk.channels = atoi(argv[3]);          // 通道數,字串轉換成整型
    formatchunk.sample_rate = atoi(argv[4]);    // 取樣率
    formatchunk.bits_per_sample = atoi(argv[5]);     // 量化位數
    formatchunk.byte_rate = formatchunk.sample_rate * formatchunk.channels * (formatchunk.bits_per_sample >> 3);   // 每秒資料位元組數=SampleRate * NumChannels * BitsPerSample/8
    formatchunk.block_align = formatchunk.channels * (formatchunk.bits_per_sample >> 3);    // 每個樣本需要的位元組數
    fwrite(&formatchunk, 1, sizeof(formatchunk), wavfile);

    /* *********** DATA區塊  ********************* */
    strncpy(datachunk.chunk_id, "data",4);
    datachunk.chunk_size = pcmfile_size;
    fwrite(&datachunk, 1, sizeof(datachunk.chunk_id) + sizeof(datachunk.chunk_size), wavfile);

    while ((read_len = fread(buf, 1, sizeof(buf), pcmfile)) != 0) {
        fwrite(buf, 1, read_len, wavfile);
    }

    fclose(pcmfile);
    fclose(wavfile);
}
pcm2wav.c

版本二:程式碼參考自:https://github.com/jwhu1024/pcm-to-wav

/**
* https://github.com/jwhu1024/pcm-to-wav
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    unsigned char chunk_id[4];        // RIFF string
    unsigned int chunk_size;         // overall size of file in bytes (36 + data_size)
    unsigned char sub_chunk1_id[8];   // WAVEfmt string with trailing null char
    unsigned int sub_chunk1_size;    // 16 for PCM.  This is the size of the rest of the Subchunk which follows this number.
    unsigned short audio_format;       // format type. 1-PCM, 3- IEEE float, 6 - 8bit A law, 7 - 8bit mu law
    unsigned short num_channels;       // Mono = 1, Stereo = 2
    unsigned int sample_rate;        // 8000, 16000, 44100, etc. (blocks per second)
    unsigned int byte_rate;          // SampleRate * NumChannels * BitsPerSample/8
    unsigned short block_align;        // NumChannels * BitsPerSample/8
    unsigned short bits_per_sample;    // bits per sample, 8- 8bits, 16- 16 bits etc
    unsigned char sub_chunk2_id[4];   // Contains the letters "data"
    unsigned int sub_chunk2_size;    // NumSamples * NumChannels * BitsPerSample/8 - size of the next chunk that will be read
} wav_header_t;

char *dummy_get_raw_pcm(char *p, int *bytes_read) {
    long lSize;
    char *pcm_buf;
    size_t result;
    FILE *fp_pcm;

    fp_pcm = fopen(p, "rb");
    if (fp_pcm == NULL) {
        printf("File error");
        exit(1);
    }

    // obtain file size:
    fseek(fp_pcm, 0, SEEK_END);     // 將檔案指標移動到檔案最後
    lSize = ftell(fp_pcm);              // 返回給定流 stream 的當前檔案位置(位元組)
    rewind(fp_pcm);                     // 將檔案指標移動到檔案開頭

    // 分配記憶體來包含整個檔案
    pcm_buf = (char *) malloc(sizeof(char) * lSize);
    if (pcm_buf == NULL) {
        printf("Memory error");
        exit(2);
    }

    // 將檔案複製到pcm_buf中:
    result = fread(pcm_buf, 1, lSize, fp_pcm);
    if (result != lSize) {
        printf("Reading error");
        exit(3);
    }

    *bytes_read = (int) lSize;
    return pcm_buf;
}

void get_wav_header(int raw_sz, wav_header_t *wh) {
    // RIFF chunk
    strcpy(wh->chunk_id, "RIFF");
    wh->chunk_size = 36 + raw_sz;

    // fmt sub-chunk (to be optimized)
    strncpy(wh->sub_chunk1_id, "WAVEfmt ", strlen("WAVEfmt "));
    wh->sub_chunk1_size = 16;
    wh->audio_format = 1;
    wh->num_channels = 1;
    wh->sample_rate = 16000;
    wh->bits_per_sample = 16;
    wh->block_align = wh->num_channels * wh->bits_per_sample / 8;
    wh->byte_rate = wh->sample_rate * wh->num_channels * wh->bits_per_sample / 8;

    // data sub-chunk
    strncpy(wh->sub_chunk2_id, "data", strlen("data"));
    wh->sub_chunk2_size = raw_sz;
}

void dump_wav_header(wav_header_t *wh) {
    printf("=========================================\n");
    printf("chunk_id:\t\t\t%s\n", wh->chunk_id);
    printf("chunk_size:\t\t\t%d\n", wh->chunk_size);
    printf("sub_chunk1_id:\t\t\t%s\n", wh->sub_chunk1_id);
    printf("sub_chunk1_size:\t\t%d\n", wh->sub_chunk1_size);
    printf("audio_format:\t\t\t%d\n", wh->audio_format);
    printf("num_channels:\t\t\t%d\n", wh->num_channels);
    printf("sample_rate:\t\t\t%d\n", wh->sample_rate);
    printf("bits_per_sample:\t\t%d\n", wh->bits_per_sample);
    printf("block_align:\t\t\t%d\n", wh->block_align);
    printf("byte_rate:\t\t\t%d\n", wh->byte_rate);
    printf("sub_chunk2_id:\t\t\t%s\n", wh->sub_chunk2_id);
    printf("sub_chunk2_size:\t\t%d\n", wh->sub_chunk2_size);
    printf("=========================================\n");
}

// pcm-to-wav ./time.pcm ./***.wav
int main(int argc, char *argv[]) {
    int raw_sz = 0;
    FILE *fwav;
    wav_header_t wheader;           // 檔案頭 結構體變數宣告

    memset(&wheader, '\0', sizeof(wav_header_t));       // 清除記憶體位置

    // check argument
    if (argc != 2)
        return -1;

    // dummy raw pcm data
    char *pcm_buf = dummy_get_raw_pcm("./time.pcm", &raw_sz);

    // construct wav header
    get_wav_header(raw_sz, &wheader);       // 給檔案頭賦 初值
    dump_wav_header(&wheader);              // 列印檔案頭 資訊

    // write out the .wav file
    fwav = fopen(argv[1], "wb");
    fwrite(&wheader, 1, sizeof(wheader), fwav);
    fwrite(pcm_buf, 1, raw_sz, fwav);
    fclose(fwav);

    if (pcm_buf)
        free(pcm_buf);

    return 0;
}
pcm2wav.c

版本三:使用python的wave庫

def pcm2wav(pcm_file, wav_file, channels=1, bits=16, sample_rate=16000):
    pcmf = open(pcm_file, 'rb')
    pcmdata = pcmf.read()
    pcmf.close()

    if bits % 8 != 0:
        raise ValueError("bits % 8 must == 0. now bits:" + str(bits))

    wavfile = wave.open(wav_file, 'wb')
    wavfile.setnchannels(channels)
    wavfile.setsampwidth(bits // 8)
    wavfile.setframerate(sample_rate)
    wavfile.writeframes(pcmdata)
    wavfile.close()
View Code

還有一個github開原始碼:wavutils

 

其他音訊格式

MP3

MP3利用MPEG Audio Layer3 壓縮方式進行壓縮,所以簡稱為MP3,是一種有失真壓縮格式。 MPEG Audio Layer 3 壓縮技術可以將音樂以1:10 甚至 1:12 的壓縮率,能夠在音質丟失很小的情況下把檔案壓縮到更小的程度。由於MP3體積小,音質高網際網路上音樂幾乎都是這種格式。但Mp3最高位元率320K,高頻部分一刀切是他的缺點,對音質要求高的話還是建議wav格式。

 

ARM格式全稱Adaptive Multi-Rate 和 Adaptive Multi-Rate Wideband,主要用於移動裝置的音訊,壓縮比比較大,但相對其他的壓縮格式質量比較差,多用於人聲,通話,是一種有失真壓縮格式。

Ogg全稱應該是OGG Vobis(ogg Vorbis) 是一種新的聲頻壓縮格式,類似於MP3等現有的音樂格式。相對於MP3壓縮技術它是完全免費、開放和沒有專利限制的,是一種有失真壓縮格式。

AAC(Advanced Audio Coding),中文稱為「高階音訊編碼」,出現於1997年,基於 MPEG-2的音訊編碼技術,是一種有失真壓縮技術。

LAC即是Free Lossless Audio Codec的縮寫,為無失真聲頻壓縮編碼,由於不會丟失任何音訊資訊可以利用演演算法恢復原始編碼,前景廣闊。

參考

微軟官方 對WAV格式的 解釋

音訊檔格式

WAV檔案格式詳解

音訊格式簡介和PCM轉換成WAV

wave檔案(*.wav)格式、PCM資料格式

wav檔案格式分析與詳解