前言

最近在做一个项目,需要对webRTC录制的音频进行处理,包括音频的裁剪、多音频合并,甚至要将某个音频的某一部分替换成另一个音频。

原本笔者打算将这件工作交给服务端去完成,但考虑,其实无论是前端还是后台,所做的工作是差不多的,而且交给服务端还需要再额外走一个上传、下载音频的流程,这不仅增添了服务端的压力,而且还有网络流量的开销,于是萌生出一个想法:为什么音频处理这件事不能让前端来做呢?

于是在笔者的半摸索半实践下,产生出了这篇文章。废话少说,先上仓库地址,这是一个开箱即用的前端音频剪辑sdk(点进去了不如就star一下吧)

ffmpeg

ffmpeg是实现前端音频处理的非常核心的模块,当然,不仅是前端,ffmpge作为一个提供了录制、转换以及流化音视频的业界成熟完整解决方案,它也应用在服务端、APP应用等多种场景下。关于ffmpeg的介绍,大家自行google即可,这里不说太多。

由于ffmpeg在处理过程中需要大量的计算,直接放在前端页面上去运行是不可能的,因为我们需要单独开个web worker,让它自己在worker里面运行,而不至于阻塞页面交互。

可喜的是,万能的github上已经有开发者提供了ffmpge.js,并且提供worker版本,可以拿来直接使用。

于是我们便有了大体的思路:当获取到音频文件后,将其解码后传送给worker,让其进行计算处理,并将处理结果以事件的方式返回,这样我们就可以对音频为所欲为了:)

开启美妙之旅前的必要工作

需要提前声明的是,由于笔者的项目需求,是仅需对.mp3格式进行处理的,因此下面的代码示例以及仓库地址里面所涉及的代码,也主要是针对mp3,当然,其实不管是哪种格式,思路是类似的。

创建worker

创建worker的方式非常简单,直接new之,注意的是,由于同源策略的限制,要使worker正常工作,则要与父页面同源,由于这不是重点,所以略过

function createWorker(workerPath: string) {const worker = newWorker(workerPath);returnworker;

}

postMessage转promise

仔细看ffmpeg.js文档的童鞋都会发现,它在处理音频的不同阶段都会发射事件给父页面,比如stdout,start和done等等,如果直接为这些事件添加回调函数,在回调函数里去区分、处理一个又一个音频的结果,是不大好维护的。个人更倾向于将其转成promise:

functionpmToPromise(worker, postInfo) {return new Promise((resolve, reject) =>{//成功回调

const successHandler = function(event) {switch(event.data.type) {case "stdout":

console.log("worker stdout: ", event.data.data);break;case "start":

console.log("worker receive your command and start to work:)");break;case "done":

worker.removeEventListener("message", successHandler);

resolve(event);break;default:break;

}

};//异常捕获

const failHandler = function(error) {

worker.removeEventListener("error", failHandler);

reject(error);

};

worker.addEventListener("message", successHandler);

worker.addEventListener("error", failHandler);

postInfo&&worker.postMessage(postInfo);

});

}

通过这层转换,我们就可以将一次postMessage请求,转换成了promise的方式来处理,更易于空间上的拓展

audio、blob与arrayBuffer的互相转换

ffmpeg-worker所需要的数据格式是arrayBuffer,而一般我们能直接使用的,要么是音频文件对象blob,或者音频元素对象audio,甚至有可能仅是一条链接url,因此这几种格式的转换是非常有必要的:

audio转arrayBuffer

functionaudioToBlob(audio) {

const url=audio.src;if(url) {returnaxios({

url,

method:'get',

responseType:'arraybuffer',

}).then(res=>res.data);

}else{return Promise.resolve(null);

}

}

笔者暂时想到的audio转blob的方式,就是发起一段ajax请求,将请求类型设置为arraybuffer,即可拿到arrayBuffer.

blob转arrayBuffer

这个也很简单,只需要借助FileReader将blob内容提取出来即可

functionblobToArrayBuffer(blob) {return new Promise(resolve =>{

const fileReader= newFileReader();

fileReader.οnlοad= function() {

resolve(fileReader.result);

};

fileReader.readAsArrayBuffer(blob);

});

}

arrayBuffer转blob

利用File创建出一个blob

functionaudioBufferToBlob(arrayBuffer) {

const file= new File([arrayBuffer], 'test.mp3', {

type:'audio/mp3',

});returnfile;

}

blob转audio

blob转audio是非常简单的,js提供了一个原生API——URL.createObjectURL,借助它我们可以把blob转成本地可访问链接进行播放

functionblobToAudio(blob) {

const url=URL.createObjectURL(blob);return newAudio(url);

}

接下来我们进入正题。

音频裁剪——clip

所谓裁剪,即是指将给定的音频,按给定的起始、结束时间点,提取这部分的内容,形成新的音频,先上代码:

class Sdk {

end= "end";//other code...

/**

* 将传入的一段音频blob,按照指定的时间位置进行裁剪

* @param originBlob 待处理的音频

* @param startSecond 开始裁剪时间点(秒)

* @param endSecond 结束裁剪时间点(秒)*/clip= async (originBlob, startSecond, endSecond) =>{

const ss=startSecond;//获取需要裁剪的时长,若不传endSecond,则默认裁剪到末尾

const d = isNumber(endSecond) ? endSecond - startSecond : this.end;//将blob转换成可处理的arrayBuffer

const originAb =await blobToArrayBuffer(originBlob);

let resultArrBuf;//获取发送给ffmpge-worker的指令,并发送给worker,等待其裁剪完成

if (d === this.end) {

resultArrBuf=(await pmToPromise(this.worker,

getClipCommand(originAb, ss)

)).data.data.MEMFS[0].data;

}else{

resultArrBuf=(await pmToPromise(this.worker,

getClipCommand(originAb, ss, d)

)).data.data.MEMFS[0].data;

}//将worker处理过后的arrayBuffer包装成blob,并返回

returnaudioBufferToBlob(resultArrBuf);

};

}

我们定义了该接口的三个参数:需要被剪裁的音频blob,以及裁剪的开始、结束时间点,值得注意的是这里的getClipCommand函数,它负责将传入的arrayBuffer、时间包装成ffmpeg-worker约定的数据格式

/**

* 按ffmpeg文档要求,将带裁剪数据转换成指定格式

* @param arrayBuffer 待处理的音频buffer

* @param st 开始裁剪时间点(秒)

* @param duration 裁剪时长*/

functiongetClipCommand(arrayBuffer, st, duration) {return{

type:"run",

arguments: `-ss ${st} -i input.mp3 ${

duration? `-t ${duration} ` : ""}-acodec copy output.mp3`.split(" "),

MEMFS: [

{

data:newUint8Array(arrayBuffer),

name:"input.mp3"}

]

};

}

多音频合成——concat

多音频合成很好理解,即将多个音频按数组先后顺序合并成一个音频

class Sdk {//other code...

/**

* 将传入的一段音频blob,按照指定的时间位置进行裁剪

* @param blobs 待处理的音频blob数组*/concat= async blobs =>{

const arrBufs=[];for (let i = 0; i < blobs.length; i++) {

arrBufs.push(await blobToArrayBuffer(blobs[i]));

}

const result=await pmToPromise(this.worker,

await getCombineCommand(arrBufs),

);return audioBufferToBlob(result.data.data.MEMFS[0].data);

};

}

上述代码中,我们是通过for循环来将数组里的blob一个个解码成arrayBuffer,可能有童鞋会好奇:为什么不直接使用数组自带的forEach方法去遍历呢?写for循环未免麻烦了点。其实是有原因的:我们在循环体里使用了await,是期望这些blob一个个解码完成后,才执行后面的代码,for循环是同步执行的,但forEach的每个循环体是分别异步执行的,我们无法通过await的方式等待它们全部执行完成,因此使用forEach并不符合我们的预期。

同样,getCombineCommand函数的职责与上述getClipCommand类似:

async functiongetCombineCommand(arrayBuffers) {//将arrayBuffers分别转成ffmpeg-worker指定的数据格式

const files = arrayBuffers.map((arrayBuffer, index) =>({

data:newUint8Array(arrayBuffer),

name: `input${index}.mp3`,

}));//创建一个txt文本,用于告诉ffmpeg我们所需进行合并的音频文件有哪些(类似这些文件的一个映射表)

const txtContent = [files.map(f => `file '${f.name}'`).join('\n')];

const txtBlob= new Blob(txtContent, { type: 'text/txt'});

const fileArrayBuffer=await blobToArrayBuffer(txtBlob);//将txt文件也一并推入到即将发送给ffmpeg-worker的文件列表中

files.push({

data:newUint8Array(fileArrayBuffer),

name:'filelist.txt',

});return{

type:'run',

arguments: `-f concat -i filelist.txt -c copy output.mp3`.split(' '),

MEMFS: files,

};

}

在上面代码中,与裁剪操作不同的是,被操作的音频对象不止一个,而是多个,因此需要创建一个“映射表”去告诉ffmpeg-worker一共需要合并哪些音频以及它们的合并顺序。

音频裁剪替换——splice

它有点类似clip的升级版,我们从指定的位置删除音频A,并在此处插入音频B:

class Sdk {

end= "end";//other code...

/**

* 将一段音频blob,按指定的位置替换成另一端音频

* @param originBlob 待处理的音频blob

* @param startSecond 起始时间点(秒)

* @param endSecond 结束时间点(秒)

* @param insertBlob 被替换的音频blob*/splice= async (originBlob, startSecond, endSecond, insertBlob) =>{

const ss=startSecond;

const es= isNumber(endSecond) ? endSecond : this.end;//若insertBlob不存在,则仅删除音频的指定内容

insertBlob =insertBlob?insertBlob

: endSecond&& !isNumber(endSecond)?endSecond

:null;

const originAb=await blobToArrayBuffer(originBlob);

let leftSideArrBuf, rightSideArrBuf;//将音频先按指定位置裁剪分割

if (ss === 0 && es === this.end) {//裁剪全部

return null;

}else if (ss === 0) {//从头开始裁剪

rightSideArrBuf =(await pmToPromise(this.worker,

getClipCommand(originAb, es)

)).data.data.MEMFS[0].data;

}else if (ss !== 0 && es === this.end) {//裁剪至尾部

leftSideArrBuf =(await pmToPromise(this.worker,

getClipCommand(originAb,0, ss)

)).data.data.MEMFS[0].data;

}else{//局部裁剪

leftSideArrBuf =(await pmToPromise(this.worker,

getClipCommand(originAb,0, ss)

)).data.data.MEMFS[0].data;

rightSideArrBuf=(await pmToPromise(this.worker,

getClipCommand(originAb, es)

)).data.data.MEMFS[0].data;

}//将多个音频重新合并

const arrBufs =[];

leftSideArrBuf&&arrBufs.push(leftSideArrBuf);

insertBlob&&arrBufs.push(await blobToArrayBuffer(insertBlob));

rightSideArrBuf&&arrBufs.push(rightSideArrBuf);

const combindResult=await pmToPromise(this.worker,

await getCombineCommand(arrBufs)

);return audioBufferToBlob(combindResult.data.data.MEMFS[0].data);

};

}

上述代码有点类似clip和concat的复合使用。

到这里,就基本实现了我们的需求,仅需借助worker,前端自己也能处理音频,岂不美哉?

上述这些代码只是为了更好的说明讲解,所以做了些简化,有兴趣的童鞋可直接源码,欢迎交流、拍砖:)

ffmpeg js转换音频_实现纯前端下的音频剪辑处理相关推荐

  1. ffmpeg js转换音频_实践!实现纯前端下的音频剪辑处理

    前言 最近在做一个项目,需要对 webRTC 录制的音频进行处理,包括音频的裁剪.多音频合并,甚至要将某个音频的某一部分替换成另一个音频. 原本笔者打算将这件工作交给服务端去完成,但考虑,其实无论是前 ...

  2. ffmpeg js转换音频_将微信jssdk录制的speex高清音频转换为wav/mp3

    今天在做微信开发的时候需要将jssdk录制的音频上传到微信平台,但是jssdk录制的是speex高清格式,不能直接转换为mp3或者wav格式, 微信官方提供了一个说明: [如果speex音频格式不符合 ...

  3. JS纯前端实现audio音频剪裁剪切复制播放与上传

    背景是这样的,用户上传音频文件,可能只需要几十秒就够了,但是常规的音乐都要3~5分钟,80%的流量都是不需要的,要是就这么传上去,其实是流量的浪费,如果可以在前端就进行剪裁,也就是只取前面一段时间的音 ...

  4. ffmpeg js转换音频_webRTC使用ffmpeg.js将webm转换为mp4

    我正尝试使用ffmpeg.js将webM文件转换为mp4. 我正在从画布上录制视频(叠加了一些信息)并录制了视频中的音频数据.webRTC使用ffmpeg.js将webm转换为mp4 stream = ...

  5. 网页二维码生成器纯js代码带logo图纯前端合成

    不能光看人家,自己的贡献一点.前天把这个账户要回来了,今天写一个贴. 通过在静态页面里,插入一段js代码,实现在页面上自动显示网址的二维码.最好放在body最后. 特点:二维码中心带有logo小图,可 ...

  6. ffmpeg 合并转换文件_使用FFmpeg转换媒体文件的快速指南

    ffmpeg 合并转换文件 有许多开源工具可用于编辑,调整和将多媒体准确地转换为您所需的内容. 诸如Audacity或Handbrake之类的工具非常出色,但有时您只想快速将文件从一种格式更改为另一种 ...

  7. java web 播放音频_用webAudio和canvas实现音频可视化

    前两天遇到了要显示音频波形图的需求,因为时间紧,就直接用了wavesufer.js,这两天有空,就研究了一下怎么用webAudio实现音频的可视化. 大致流程是对音源进行解析,解析得到的数据是个频谱数 ...

  8. cmd xcopy 拷贝文件夹_在纯dos下用xcopy命令怎么复制文件夹

    在纯 dos 下用 xcopy 命令怎么复制文件夹 XCOPY --目录复制命令 1 .功能:复制指定的目录和目录下的所有文件连同目录结构. 2 .类型:外部命令 3 .格式: XCOPY [ 源盘: ...

  9. ffmpeg js转换音频_微信FFMPEG 扩展转换音频格式

    项目背景: 微信开发中有保存到语音文件到服务器上,便于网页浏览中使用播放. 但是微信下载下来的是 amr格式,但是H5网页元素支持 Mp3格式. 说下流程: 微信 -> JSSDK 录音 --& ...

  10. ffmpeg js转换音频_linux下使用ffmpeg将amr转成mp3

    接到需求要在Linux环境下将amr转换为mp3,windows下直接使用第三方jar包封装的exe方法即可,但不支持Linux,上网爬完资料说是用ffmpeg加上amr插件可以实现,根据教程尝试了一 ...

最新文章

  1. webrtc android ndk,webrtc 针对 android 平台的编译和运行
  2. java gps时间转换工具_java 时间戳和时间互转工具 和 时间偏移量计算
  3. 【js】版本号对比处理方案
  4. 特殊方法,类之间的关系,分页
  5. 普通人学python好吗_有人问,普通人学python真的有意义吗?看看大家都是怎么说的吧...
  6. Android2.2查看svn历史提交记录
  7. Reactor和Proactor对比以及优缺点 (netty的底层原理reactor模型)
  8. Qt JSON解析生成笔记
  9. mysql 分页 order_mysql学习笔记:九.排序和分页(order by、limit)
  10. 行式 Excel 文件比对
  11. 第一阶段✦第二章☞信息系统集成及服务管理
  12. 容器监控工具cAdvisor
  13. 云和恩墨大讲堂 - Oracle RAC系列课程强势来袭
  14. 计算机类的本科学校有哪些专业考研,计算机专业考研学校推荐有哪些
  15. [从零开始学FPGA编程-3]:快速入门篇 - 操作步骤1 - 导体、半导体、晶体管、常见电子元器件、集成电路
  16. java实战小结-Controller报错:Content type ‘multipart/form-data;boundary=----WebKitFormBoundaryxxxx not supp
  17. odd ratio置信区间的计算,你学会了吗?
  18. 两种方法:在 PowerPoint 中插入视频
  19. 笔记 | 笨方法学Python
  20. gyp info it worked if it ends with ok npm ERR 解决办法

热门文章

  1. python PyEnchant(拼写检查)
  2. 转载:医疗保险,公积金、养老、生育、工伤、失业保险
  3. 拳皇FANS们不得不看的动画
  4. 基于共振解调的轴承故障诊断方法总结(一)
  5. Easyx图形库小游戏---迷宫
  6. mysql 需要什么硬件配置_Mysql的硬件优化和配置优化
  7. smtp匿名邮件发送小记
  8. 给定一个数组,求数组的最大连续子数组,使得该子数组的和最大
  9. 读书笔记 --- 组织结构设计
  10. oracle 工具ODM,oracle ODM 数据挖掘笔记