WEB端實現PCM裸流播放

0x00 序

近日有這樣一個需求,在web端播放PCM裸流,即數據提供方給出的都是**.pcm文件,而我們需要在頁面上給出該音頻的播放控制器(至少可以支持playpause)。至於爲什麼不讓數據提供方直接給wav文件呢?因爲數據提供方是Ng(匿..)

0x01 HTML5 Audio

在HTML5標準網頁中,我們可以運用<audio><embed>元素來實現瀏覽器兼容的網頁聲音調用及播放:

  • <embed>標籤定義外部(非 HTML)內容的容器,如果瀏覽器不支持該文件格式,沒有插件的話就無法播放該音頻
  • <audio>元素是一個HTML5元素,在老式瀏覽器中不起作用

以下是示例的調用方法:

<audio controls="controls">
    <source src="alex-car-00010.ogg" type="audio/ogg">
    <source src="alex-car-00010.mp3" type="audio/mp3">
    <embed height="40" width="100" autostart="false" src="alex-car-00010.m4a.wav">
</audio>

上述代碼將會提供一個音頻播放的控制器,HTML5 <audio>元素使用了兩個不同的音頻格式,會嘗試以oggmp3來播放音頻;如果失敗,代碼將回退嘗試<embed>元素。

其中<audio>的屬性主要有:

  • controls: 唯一可選值爲controls,出現controls屬性並準確賦值時,音頻播放控件將會顯示,控件包括:播放、暫停、定位、音量、全屏切換、字幕(如果可用)、音軌(如果可用)。
  • autoplay: 唯一可選值爲autoplay,出現autoplay屬性並準確賦值時,音頻將會自動播放
  • loop: 唯一可選值爲loop,出現loop屬性並準確賦值時,音頻將會循環播放。
  • preload: 可選值有auto(當頁面加載後載入整個音頻)、meta(當頁面加載後只載入元數據)和none(當頁面加載後不載入音頻) 如果設置了前面的autoplay屬性,那麼preload將會被忽略。
  • src: 指定音頻URL地址,可以是相對的URL也可以是絕對的URL;當然還可以像上述例子一樣,用source標籤來指定源。

0x02 PCM文件 & WAV文件

雖然如上節所述,當前HTML5已經對音頻播放提供了很多便利,但是是否<audio>就可以滿足在0x00中提出的需求呢?答案是否定的:<audio>並不支持.pcm文件的播放。

PCM

那麼PCM文件到底是什麼樣的格式呢?

PCM(Pulse Code Modulation),也被稱爲脈碼編碼調製。PCM文件是模擬音頻信號經模數轉換(A/D變換)直接形成的二進制序列,該文件沒有附加的文件頭和文件結束標誌。PCM中的聲音數據沒有被壓縮,如果是單聲道的文件,採樣數據按時間的先後順序依次存入。

但是隻有這些數字化的音頻二進制序列並不能夠播放,因爲任何的播放器都不知道應該以什麼樣的聲道數、採樣頻率和採樣位數播放,這個二進制序列沒有任何自描述性。

WAV

在這裏,就必須要提提WAV文件了。

WAVE(Waveform Audio File Format),又或者是因爲擴展名而被大衆所知的WAV,也是一種無損音頻編碼。WAV文件可以當成是PCM文件的wrapper,實際上查看pcm和對應wav文件的hex文件,可以發現,wav文件只是在pcm文件的開頭多了44bytes,來表徵其聲道數、採樣頻率和採樣位數等信息。

由於其具有自描述性,WAV文件可以被基本所有的音頻播放器播放,包括HTML5的<audio>

自然而言的認爲,若我們需要在web端播放純純的PCM碼流,是否只需要在其頭部加上44bytes轉成對應的WAV文件,就可以播放了。

那麼這44bytes應該怎麼加呢?

WAV文件格式

WAV是微軟開發的一種聲音文件格式,符合RIFF(Resource Interchange File Format)文件規範,用於保存音頻信息資源。RIFF文件都在數據塊前有文件頭。

標準的wav文件格式如下圖所示:

標準WAV文件格式

  • 文件開頭是RIFF頭:0 4 數據塊ID包含了“RIFF”在ASCII編碼中的值(大端是0x52494646);4 4 數據塊大小 36 + subChunk2Size,即 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size),也即整個文件的大小減去ChunkID和ChunkSize所佔8bytes;8 4 Format包含了字母“WAVE”(大端是0x57415645)。
  • “WAVE” format包含了2個子塊:“fmt”和“data”
  • “fmt”子塊描述了聲音數據的格式:12 4 SubChunk1ID 包含了“fmt”(大端是0x666d7420);16 4 SubChunk1Size是隨後SubChunk的大小;20 2 AudioFormat;22 2 聲道數,單聲道是1,雙聲道是2;24 4 採樣頻率,一般有8000,44100等值;28 4 字節頻率 = 採樣頻率 * 聲道數 * 採樣位數 / 8(ByteRate == SampleRate * NumChannels * BitsPerSample/8);32 2 (BlockAlign == NumChannels * BitsPerSample/8 );34 2 採樣位數,8對應8bits,16對應16bits
  • “data”子塊包含了音頻數據大小和真實的聲音數據:36 4 SubChunk2ID包含了字母“data”(大端格式下是0x64617461);40 4 數據字節數(Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8);44 * 實際的聲音數據。

舉例說明(hex格式):

52 49 46 46 24 08 00 00 57 41 56 45 66 6d 74 20
10 00 00 00 01 00 02 00 22 56 00 00 88 58 01 00
04 00 10 00 64 61 74 61 00 08 00 00 00 00 00 00
24 17 1e f3 3c 13 3c 14 16 f9 18 f9 34 e7 23 a6
3c f2 24 f2 11 ce 1a 0d

WAV文件格式示例

0x03 PCM轉WAV

至此,我們已經知道了WAV文件頭的格式,感覺就可以通過PCM碼流生成對應的WAV格式文件了,那就開動了~

  1. 定義WAV文件頭的格式

    this.header = {                         // OFFS SIZE NOTES
        chunkId      : [0x52,0x49,0x46,0x46], // 0    4    "RIFF" = 0x52494646
        chunkSize    : 0,                     // 4    4    36+SubChunk2Size = 4+(8+SubChunk1Size)+(8+SubChunk2Size)
        format       : [0x57,0x41,0x56,0x45], // 8    4    "WAVE" = 0x57415645
        subChunk1Id  : [0x66,0x6d,0x74,0x20], // 12   4    "fmt " = 0x666d7420
        subChunk1Size: 16,                    // 16   4    16 for PCM
        audioFormat  : 1,                     // 20   2    PCM = 1
        numChannels  : 1,                     // 22   2    Mono = 1, Stereo = 2...
        sampleRate   : 8000,                  // 24   4    8000, 44100...
        byteRate     : 0,                     // 28   4    SampleRate*NumChannels*BitsPerSample/8
        blockAlign   : 0,                     // 32   2    NumChannels*BitsPerSample/8
        bitsPerSample: 8,                     // 34   2    8 bits = 8, 16 bits = 16
        subChunk2Id  : [0x64,0x61,0x74,0x61], // 36   4    "data" = 0x64617461
        subChunk2Size: 0                      // 40   4    data size = NumSamples*NumChannels*BitsPerSample/8
    };
  2. 給定了PCM碼流和指定了wav文件頭中的一些參數後,生成wav文件形式

    this.Make = function(data) {
        if (data instanceof Array) this.data = data;
        this.header.blockAlign = (this.header.numChannels * this.header.bitsPerSample) >> 3;
        this.header.byteRate = this.header.blockAlign * this.sampleRate;
        this.header.subChunk2Size = this.data.length * (this.header.bitsPerSample >> 3);
        this.header.chunkSize = 36 + this.header.subChunk2Size;
    
        this.wav = this.header.chunkId.concat(
            u32ToArray(this.header.chunkSize),
            this.header.format,
            this.header.subChunk1Id,
            u32ToArray(this.header.subChunk1Size),
            u16ToArray(this.header.audioFormat),
            u16ToArray(this.header.numChannels),
            u32ToArray(this.header.sampleRate),
            u32ToArray(this.header.byteRate),
            u16ToArray(this.header.blockAlign),
            u16ToArray(this.header.bitsPerSample),    
            this.header.subChunk2Id,
            u32ToArray(this.header.subChunk2Size),
            (this.header.bitsPerSample == 16) ? split16bitArray(this.data) : this.data
        );
        this.dataURI = 'data:audio/wav;base64,'+FastBase64.Encode(this.wav);
    };
  3. 直接給出wav文件的dataURI作爲<audio>的src

    var FastBase64 = {
        chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
        encLookup: [],
    
        Init: function() {
            for (var i=0; i<4096; i++) {
                this.encLookup[i] = this.chars[i >> 6] + this.chars[i & 0x3F];
            }
        },
    
        Encode: function(src) {
            var len = src.length;
            var dst = '';
            var i = 0;
            while (len > 2) {
                n = (src[i] << 16) | (src[i+1]<<8) | src[i+2];
                dst+= this.encLookup[n >> 12] + this.encLookup[n & 0xFFF];
                len-= 3;
                i+= 3;
            }
            if (len > 0) {
                var n1= (src[i] & 0xFC) >> 2;
                var n2= (src[i] & 0x03) << 4;
                if (len > 1) n2 |= (src[++i] & 0xF0) >> 4;
                dst+= this.chars[n1];
                dst+= this.chars[n2];
                if (len == 2) {
                    var n3= (src[i++] & 0x0F) << 2;
                    n3 |= (src[i] & 0xC0) >> 6;
                    dst+= this.chars[n3];
                }
                if (len == 1) dst+= '=';
                dst+= '=';
            }
            return dst;
        } // end Encode
    }

如此,便可以通過獲取原始PCM碼流,給定wav頭中的對應參數,生成對應wav文件,並直接給定dataURI作爲<audio>的src,如此頁面上原先就已創建好的音頻控制器就可以播放純純的PCM了。

0x04 文件和二進制數據的操作

但現在又有問題了,數據提供方給出的都是存放在服務器的*.pcm的文件,可以通過url進行下載,但是如何獲取到pcm文件的數組形式呢?

實際上,XMLHttpRequest在Level2的時候就引入了responseType和response兩個屬性。可以通知瀏覽器把請求到得數據按照某種格式進行處理。

  • xhr.responseType:在發送請求之前,根據需求把xhr.responseType設置爲”text”、”arraybuffer”、”blob”或者”document”,默認值是”text”。
  • xhr.response:獲取了數據之後,根據之前的responseType的值,response屬性就是DOMString、ArrayBuffer、Blob或者Document格式的數據。

獲取文件內容,我們可以通過XHR方案:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/*.pcm', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function (e) {
    var uInt8Array = new Uint8Array(this.response);
    // do something
}
xhr.send();

0x05 後續

後來啊,需求又變了啊..

要麼數據提供方會直接給出wav格式文件..要麼是數據方給定pcm文件後,由我們自己轉成wav之後再傳到服務器上去..

所以如果能在後端直接轉好,也不需要在js端轉了呢= =
就寫了一個簡單的java版本的wav轉pcm放到github上去了,可以分析出wav文件的參數,也可以將pcm轉爲wav文件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章