WEB端实现PCM裸流播放

来源:互联网 发布:sql 2005 sp4 x64 编辑:程序博客网 时间:2024/05/16 15:38

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文件。

0 0