音频处理之音频文件拼接录音及裁剪

这篇博客是我在开发一个“音频文件拼接录音及裁剪”功能过程当中的笔记,由于学习到了不少没接触过的内容,因此在这里作个记录,这不是个教程贴,文中不涉及业务代码,只有一小部分核心代码用来解释我描述的内容而已,基本上是给我本身看的哈哈。html

本文是讲我在作我的项目中开发中涉及到的一个小需求,主要实现下面4个功能:ios

  1. 实现录制音频的基本功能,录制一段开始到结束的音频,并能导出文件形式;
  2. 能够对录制好的音频进行播放,能够拖动进度条改变进度,并能在进度条的位置继续录制音频,将新录制的音频覆盖进度条以后的音频,并与进度条以前的音频合并;
  3. 将音频文件导入并进行录音,录音完成后会将录制的音频与导入的音频合并;
  4. 一样是将音频文件导入后与第2条的功能相同;

上面提到的功能我在开源社区没有找到同时知足的插件,基本只有第一个功能,因此我找了一个插件进行改造,我用的是 github.com/2fps/record… 这个插件,后面的功能都是基于这个插件进行改造扩展的,很是感谢这个插件做者让我学习到了实现录制音频的原理,也是由于这个插件让我有信心完成了后面3个功能。git

实现这几个小功能花了3天时间,主要是学习吧,我就按第x天来说吧。github

第一天

研究encodePCM,尝试逆向decode,但对于二进制概念基础还不够牢固,因此先研究二进制的各类对象 ArrayBuffer、TypedArray和DataView之间的关系与数据规律,总结出了如下特性:ajax

Arraybuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区,它不能直接被用来读写,须要转成DataView才能读写其中的数据,通常用来数据传输。axios

DataView视图是一个能够从 ArrayBuffer 对象中读写多种数值类型的底层接口,它的构造函数传参必须是Arraybuffer数组,主要就是用来按数据类型和位数读写数据,用到的方法有setInt1六、getInt16这两个,还有不少set和get的方法可使用,本项目还用到它来生成Blob对象进行文件处理。api

TypedArray 能够认为是用来描述ArrayBuffer的类型化数组,与DataView没有直接关系,但做用和DataView相似都是对ArrayBuffer的数据进行读写,只不过是以特定的数据类型以数组来处理。数组

当你经过DataView对一个ArrayBuffer对象进行setInt8(0, 1)的操做,表明ArrayBuffer按Int8读取时的第一个值为1,用TypedArray的方式读取就能够用new Int8Array(buffer)来按Int8的数据类型来读取ArrayBuffer。这样就能获得第一个值为1的类数组对象,而后就能够按数组的方式对其进行处理。若是用Float32Array来读取这段ArrayBuffer的话,获得的第一个值就会自动转为Float32类型的值。 函数

理清这三个家伙的关系回来看就知道该怎么作了,因而开始摸索怎样把encode出来并生成Blob的音频文件解码回能进行剪切的格式。性能

从录制音频的源头开始看起,AudioContext录制时的事件会返回buffer数据,是Float32Array类型的,值为[-1,1]区间的32位浮点型。因为音频采样率很是的高(最高达到每秒48000 次),因此每4096次采集一次存放在this.buffer数组当中,因此this.buffer是一个包含了Float32Array的二维数组。

因此咱们若是要对音频进行截取拼接等操做都是对这个this.buffer进行操做的,实质上就是对采集获得的Float32Array数据进行处理。

知道了应该操做哪一个数据就能够知道实现整个音频裁剪拼接功能的完整流程了。

大概是以下流程:

  1. 将音频文件进行decode解码,获得咱们能够处理的this.buffer数据;
  2. 对this.buffer数据作相应的裁剪拼接处理;
  3. 将处理完的this.buffer再encode回去,编码成能够播放的音频文件;

第1步是最难的,后面主要讲第一步。 第2步其实就是像数组同样操做裁剪想要的长度,拼接也是跟数组同样的 第3步其实自己就提供了encodePCM、encodeWAV等方法,不须要作

第一天大部分是对概念的和流程的理清,从音频流接收的二进制到编码再到音频文件,明白了这其中是怎么进行转换的,就能够对须要处理的数据this.buffer进行想要的操做(包括但不限于音频裁剪和拼接),最后能编码成可播放的音频文件。

次日

接下来开始写decodePCM了,先来分析encodePCM作了什么,它的主要做用就是把元数据的[-1,1]的Float32Array转换成Int16或者Int8的的DataView数据。那咱们要作的decode固然就是把Int16或者Int8的的DataView数据转换成[-1,1]的Float32Array啦。

贴上encode的核心代码:

data = new Float32Array(dataview.byteLength)
      for (var i = 0; i < bytes.length; i++ , offset += 2) {
        var s = Math.max(-1, Math.min(1, bytes[i]));
        // 16位的划分的是2^16=65536份,范围是-32768到32767
        // 由于咱们收集的数据范围在[-1,1],那么你想转换成16位的话,只须要对负数*32768,对正数*32767,便可获得范围在[-32768,32767]的数据。
        data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
      }
复制代码

再贴上decode的解码代码:

// byteLength是8位一个字节的长度,因此16位就要除以2
      data = new Float32Array(dataview.byteLength / 2)
      for (var i = 0; i < data.length; i++, offset += 2) {
        // 在encodePCM的时候是setInt16,因此用getInt16的时候取到的是整数没有小数点,这里会有些偏差
        var s = Math.max(-32768, Math.min(32767, bytes.getInt16(offset, true)));
        // 16位的划分的是2^16=65536份,范围是-32768到32767
        // 转换为[-1, 1]的数据
        data[i] = s < 0 ? s / 0x8000 : s / 0x7FFF
      }
复制代码

字节序

实际上我在开发过程遇到了两个问题:

一个是字节序的问题,在解码的时候bytes.getInt16(offset)发现get出来的数据跟编码时set的值不一致!好像还比set的值要大,调试了半天最后发现set的时候带了第三个参数填了true,查文档是这么描述的:  应该是规定setInt16时用什么字节序,与之对应的getInt16也有这个参数,那么字节序是啥,我一开始也不懂,后来看到阮一峰大佬的这篇文章http://www.ruanyifeng.com/blog/2016/11/byte-order.html 就很好理解了。

字节序分大端和小端,参数为true时是用小端字节序,因此在get的时候也要对应一样的字节序传true,上面的decode代码就是正确的,这样子set和get就一致了。

第二个问题是get后的数值与set时的数据只是比较接近而已,并非彻底相等,这是由于咱们用了setInt16就会以16位整数存在Buffer中,这样子就会把set的值小数点后的数都截掉,因此get出来的值就是个整数,因此这里会存在一点小偏差,大概是0.003051850948%的偏差,这样的偏差几乎能够忽略不计,若是想更精确的话能够改成setFloat32和getFloat32,可是文件体积应该会加大一倍。

解决了以后,decodePCM算是完成了,接下来再写一个接收文件blob对象,转换为ArrayBuffer对象再交给decodePCM函数解码的一个函数。

Blob对象怎么转ArrayBuffer?有几种办法,一种是请求的方式,fetch、Response、ajax均可以,而我用的是另外一种方式,用的是FileReader,直接贴代码:

function Blob2Arraybuffer(blob, cb) {
  var reader = new FileReader();
  reader.readAsArrayBuffer(blob);
  reader.onload = function () {
    cb(reader.result);
  }
}
复制代码

原生就有提供readAsArrayBuffer的接口,仍是很容易转的。接下来就是验证从纯文件解码出来的buffer到底能不能被处理过以后还能再编码出来播放了。

文件解码

咱们能够开出一个load方法,让外部传文件进来作解码。

  Recorder.prototype.load = function(blob) {
    var _this = this;
    Recorder.decodePCM(blob, this.oututSampleBits, function(data) {
      // 将一维data提高二维,this.buffer的格式
      var buffer = [];
      var index = 0;
      while (index < data.length - 1){
        buffer.push(data.slice(index, index+=4096));
      }
      var size = data.length
      var duration = data.length / _this.inputSampleRate;
      _this.buffer = buffer;
      _this.size = size;
      _this.duration = duration;	// 再编码成音频文件
	const audio = new Audio(URL.createObjectURL(_this.getWAVBlob()));
      _this.audio = audio;
    })
  };
复制代码

上面有句代码是 const audio = new Audio(URL.createObjectURL(_this.getWAVBlob()));就是看解码完成以后再编码成音频文件,而后咱们再调用_this.audio.play()播放,是能够正常播放的,说明我解码出来的this.buffer是能够被正常编码回文件播放的,声音听起来是没有差异的。

而后在业务页面加载外部文件,获得blob调用load方法:

axios.get('/data.wav’, {
  responseType: 'blob',
}).then((res) => {
  this.recorder.load(res.data)
})
复制代码

其实请求头的responseType能够是arraybuffer的,只是我以为Blob做为数据交换会更通用一些。

验证完这步可行以后要把它改回直接用传入的文件Blob来播放会好一些,

const audio = new Audio(URL.createObjectURL(blob));

第三天

第三天就把需求的功能实现(音频裁剪、拼接),基本上只要对this.buffer作处理就行了,这里就不放具体代码了,说说思路就行了。

裁剪

传入一个进度相关的参数,我传的是[0,1]的百分比,而后把解码后的Float32Array大数组按百分比裁剪,size和duration直接拿裁剪后的数组长度填上便可,不过裁剪后的Float32Array大数组要再次合并成this.buffer的二维数组。

拼接

拼接就更简单了,先把裁剪后的数据存放在一个临时的对象里,而后清空this.buffer等数据从新录制,录制完后再把临时对象里的裁剪音频插入到this.buffer的前面,size和duration相加便可,

最后就是作一些代码结构的优化和小修改的优化的工做了。算是基本完成了想要的这两个功能了。

接下来还有一些计划要作的东西:

  • 优化性能,毕竟是在内存中处理二进制数据,内存回收释放还须要注意的,也能够考虑新开一个worker处理二进制;
  • 导出mp3格式的文件,能支持更多客户端环境播放。
  • 考虑改变人声,这个可能接触到更多我不知道的内容,好比波形、音调、之类的,这须要进行技术攻坚,个人目标是能把人声变成小黄人的声音。

总结

从用户角度仅仅是简单的录音功能,在程序中则要以极其微观的机器角度来思考怎样以数据的形式处理。经过此次开发,我也算是打开了音频处理程序的第一扇门吧,也是第一次感觉到了ArrayBuffer、DataView和TypedArray 这三个js原生api真正的魅力。

我学到了不少音频相关的概念,这些概念不只仅是用来开发出我需求的那几个小功能,我能够经过这些概念结合本身已有的知识碰撞出更酷的想法,创造出更酷的程序,如今的我说出这种话就像屁话,但能够做为目标,作更好的本身。

相关文章
相关标签/搜索