JS解析wav音频数据并使用wasm加速

本文探讨两个方面的内容,一个是wav音频数据的解析,另一个是使用wasm对其实现加速

音频数据的存储

第一部分我们先讲音频数据是如何存储在计算机上的,对其结构了然,才能更深入一步。

PCM

要了解其存储结构,首先要知道PCM:

PCM(Pulse Code Modulation)脉冲编码调制数字通信编码方式之一。主要过程是将话音、图像等模拟信号每隔一定时间进行取样,使其离散化,同时将抽样值按分层单位四舍五入取整量化,同时将抽样值按一组二进制码来表示抽样脉冲的幅值。
在计算机应用中,能够达到最高保真水平的就是PCM编码,被广泛用于素材保存及音乐欣赏,CD、DVD以及我们常见的 WAV文件中均有应用。因此,PCM约定俗成了无损编码,因为PCM代表了数字音频中最佳的保真水准,并不意味着PCM就能够确保信号绝对保真,PCM也只能做到最大程度的无限接近。要算一个PCM音频流的码率是一件很轻松的事情,采样率值×采样大小值×声道数 bps。一个采样率为44.1KHz,采样大小为16bit,双声道的PCM编码的WAV文件,它的数据速率则为 44.1K×16×2 =1411.2 Kbps。我们常见的Audio CD就采用了PCM编码,一张光盘的容量只能容纳72分钟的音乐信息。

音频的PCM 采样数据的时候有三个指标尤为重要:

  • 采样率: 1秒内运行几次采样, 例如44.1khz意味着1秒运行44.1k次采样
  • 采样精度: 原始数据采样之后,保存的时候用几个字节表示,一般以8-16位居多,一个或者两个字节
  • 声道数: 单或双声道

典型的小端序存储结构如下:

  • 对于单声道来说,存储序列就是按照地址递增的方向依次排列的
  • 对于双声道来说,是以左右左右...的序列交替排列的

WAV

PCM存储的仅仅是原始音频数据,对于正常的用户或者应用层应用程序来说,还不够,因此wav顺势而出:

WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持。WAVE文件通常只是一个具有单个"WAVE"块的RIFF文件,该块由两个子块("fmt"子数据块和"data"子数据块)

wav也是一种无损格式,简单理解,给PCM数据加个头就是wav了,一个wav的格式如下图:

一般解析wav数据需要关注以下几个头:

  • 前四个字节,如上图所示ChunkID有的也叫chunk descriptor,是固定字符串RIFF的ASCII编码;
  • 字节序列[4,7]四个字节描述了chunk的大小;
  • 8,15\] 序列是`WAVEfmt `(注意空格)的ASCII编码;

  • 22\] 位代表声道数

  • 44, ∞) 之后就是PCM数据了

JS的实现

原生JS

JS 实现分以下几步走:

  • 首先借助fileReader或其他方法拿到文件的ArrayBuffer,并将其转化成Unint8Array
  • 然后通过解析其头部拿到格式头,采样率,声道数,data位置等关键数据
  • 通过对比其与标准格式的差异决定其是否为合法格式,进行初步数据校验
javascript 复制代码
 function wav2pcmJS(wavView) {
    //检测wav文件头
    var eq = function (p, s) {
      for (var i = 0; i < s.length; i++) {
        if (wavView[p + i] != s.charCodeAt(i)) {
          return false;
        }
      }
      return true;
    };
    var pcm;
    if (eq(0, 'RIFF') && eq(8, 'WAVEfmt ')) {
      var numCh = wavView[22];
      if (wavView[20] == 1 && (numCh == 1 || numCh == 2)) {
        //raw pcm 单或双声道
        var sampleRate =
          wavView[24] + (wavView[25] << 8) + (wavView[26] << 16) + (wavView[27] << 24);
        var bitRate = wavView[34] + (wavView[35] << 8);
        //搜索data块的位置
        var dataPos = 0; // 44 或有更多块
        for (var i = 12, iL = wavView.length - 8; i < iL; ) {
          if (
            wavView[i] == 100 &&
            wavView[i + 1] == 97 &&
            wavView[i + 2] == 116 &&
            wavView[i + 3] == 97
          ) {
            //eq(i,"data")
            dataPos = i + 8;
            break;
          }
          i += 4;
          i +=
            4 +
            wavView[i] +
            (wavView[i + 1] << 8) +
            (wavView[i + 2] << 16) +
            (wavView[i + 3] << 24);
        } // 这里我自己存疑,有没有大佬解答下?
        // console.log('wav info', sampleRate, bitRate, numCh, dataPos);
        if (dataPos) {
          if (bitRate == 16) {
            pcm = new Int16Array(wavView.buffer.slice(dataPos));
          } else if (bitRate == 8) {
            pcm = new Int16Array(wavView.length - dataPos);
            //8位转成16位
            for (var j = dataPos, d = 0; j < wavView.length; j++, d++) {
              var b = wavView[j];
              pcm[d] = (b - 128) << 8;
            }
          }
        }
        if (pcm && numCh == 2) {
          //双声道简单转单声道
          var pcm1 = new Int16Array(pcm.length / 2);
          for (let i = 0; i < pcm1.length; i++) {
            pcm1[i] = (pcm[i * 2] + pcm[i * 2 + 1]) / 2;
          }
          pcm = pcm1;
        }
      }
    }
    if (!pcm) {
      False && False('非单或双声道wav raw pcm格式音频,无法转码');
      return;
    }

   
  };

原始代码参见(Recorder代码运行和静态分发工具),这里也有不少存疑的地方比如第40行,以及可以使用es6在实际使用的时候进行重构等(注意性能)

WASM

对于大量计算,我们很容易想到用WASM去实现,从而提高性能,逻辑上我们不做任何改动,只需将以上代码重构为WASM版的即可,实现上我们使用AssembleScript实现:

typescript 复制代码
function eq(p: i32,s: string, wavView: Uint8Array): bool{
  for(let i: i32=0;i<s.length;i++){
    if(wavView[p+i]!=s.charCodeAt(i)){
      return false;
    };
  };
  return true;
};
export function wav2pcmWasm (wavView: Uint8Array): Int16Array {
  var pcm =  new Int16Array(0);
  if(eq(0,"RIFF", wavView)&&eq(8,"WAVEfmt ", wavView)){
    var numCh=wavView[22];
    if(wavView[20]==1 && (numCh==1||numCh==2)){//raw pcm 单或双声道
      var sampleRate=wavView[24]+(wavView[25]<<8)+(wavView[26]<<16)+(wavView[27]<<24);
      var bitRate=wavView[34]+(wavView[35]<<8);
      //搜索data块的位置
      var dataPos=0; // 44 或有更多块
      for(var i=12,iL=wavView.length-8;i<iL;){
        // console.log('dataPos search')
        // console.log(i.toString())
        if(wavView[i]==100&&wavView[i+1]==97&&wavView[i+2]==116&&wavView[i+3]==97){//eq(i,"data")
          dataPos=i+8;break;
        }
        i+=4;
        i+=4+wavView[i]+(wavView[i+1]<<8)+(wavView[i+2]<<16)+(wavView[i+3]<<24);
      }
      // console.log("wav info"+sampleRate.toString() + ','+bitRate.toString()+','+numCh.toString()+','+dataPos.toString());
      if(dataPos){
        if(bitRate==16){
          const slicedBuffer = wavView.slice(dataPos)
          const L = slicedBuffer.length
          pcm = new Int16Array(L >> 1 + (L % 2));
          const pcmLength = pcm.length
          // console.log('这儿 bitRate')
          var ii = 0, j = 0
          // for(; j < pcmLength ; ii += 2, j += 1) {
          //   // 两个字节组合成一个16位
          //   var nextNumber = ii === L - 1 ? 0 :slicedBuffer[ii+1]
          //   pcm[j] = (slicedBuffer[ii] << 8) | nextNumber
          // }
        }else if(bitRate==8){
          pcm=new Int16Array(wavView.length-dataPos);
          //8位转成16位
          var jj=dataPos,d=0
          for(;jj<wavView.length;jj++,d++){
            var b=wavView[jj];
            // 0-255 -》 -128-127
            pcm[d]=(b-128)<<8;
          };
        };
      };
      if(pcm && numCh==2){//双声道简单转单声道
        // console.log('这儿numCh')
        var pcm1=new Int16Array(pcm.length/2);
        var k=0
        for(;k<pcm1.length;k++){
          pcm1[k]=(pcm[k*2]+pcm[k*2+1])/2;
        }
        pcm=pcm1;
      };
    };
  } else {
    console.error('RIFF头,WAVEfmt  头不存在')
  };

  return pcm
}

后记,wasm代码要在生产环境运行,请务必慎重,笔者实际测了一下二者的性能,实际数据相差无几,不知道是不是我WASM版的哪里写的不对,如果有大佬看出来了也请指正下哈。

相关推荐
王者鳜錸11 分钟前
VUE+SPRINGBOOT从0-1打造前后端-前后台系统-邮箱重置密码
前端·vue.js·spring boot
独泪了无痕2 小时前
深入浅析Vue3中的生命周期钩子函数
前端·vue.js
小白白一枚1112 小时前
vue和react的框架原理
前端·vue.js·react.js
字节逆旅2 小时前
从一次爬坑看前端的出路
前端·后端·程序员
若梦plus3 小时前
微前端之样式隔离、JS隔离、公共依赖、路由状态更新、通信方式对比
前端
若梦plus3 小时前
Babel中微内核&插件化思想的应用
前端·babel
若梦plus3 小时前
微前端中微内核&插件化思想的应用
前端
若梦plus3 小时前
服务化架构中微内核&插件化思想的应用
前端
若梦plus3 小时前
Electron中微内核&插件化思想的应用
前端·electron
若梦plus3 小时前
Vue.js中微内核&插件化思想的应用
前端