实践!实现纯前端下的音频剪辑处理。本来笔者是打算把这个工作交给服务端来完成的,但是考虑到,其实不管是前端还是后端,所做的工作都是类似的,交给服务端需要一个上传和下载音频的附加过程。这样不仅增加了服务器的压力,而且还有网络流量的开销,于是萌生了一个想法:为什么音频处理不能交由前端来做呢?于是在笔者半探索半实践之下,产生了这篇文章。话不多说,先上仓库地址吧。这是一个开箱即用的前端音频编辑sdk(点进去后不妨加星标)。ffmpegffmpeg是一个非常核心的前端音频处理模块。当然,不仅仅是前端,ffmpge作为业界成熟完整的提供音视频录制、转换、流化的解决方案,也被应用在服务端、APP应用等各种场景。关于ffmpeg的介绍大家可以自行google,这里就不多说了。由于ffmpeg在处理过程中需要大量的计算,所以不可能直接在前端页面运行,因为我们需要单独开一个webworker,让它在worker本身运行,不阻塞页面交互。好在万能的github上有开发者提供了ffmpge.js和worker版本,可以直接使用。于是我们就有了一个大概的思路:获取音频文件后,解码后发送给worker进行计算处理,将处理结果作为事件返回,这样我们就可以对音频做任何想做的事情了:)必要的精彩旅程开始前的工作需要提前声明,因为笔者的项目需求只需要处理.mp3格式,所以下面的代码示例和仓库地址涉及的代码主要是针对mp3的,当然,不管是哪种格式,思路差不多。创建worker的方法很简单。只需创建一个新的。注意,由于同源策略的限制,要使worker正常工作,必须与父页面同源。因为这不是重点,所以跳过functioncreateWorker(workerPath:string){constworker=newWorker(workerPath);returnworker;}postMessagetopromise如果你仔细看ffmpeg。Done等,如果直接为这些事件添加回调函数,在回调函数中不容易维护区分和处理一个接一个的音频结果。就我个人而言,我更喜欢把它变成一个promise:){case"stdout":console.log("workerstdout:",event.data.data);break;case"start":console.log("worker收到你的命令并开始工作:)");休息;案例“完成”:worker.removeEventListener(“消息”,successHandler);解决(事件);休息;默认值:中断;}};//异常捕获constfailHandler=function(error){worker.removeEventListener("error",failHandler);拒绝(错误);};worker.addEventListener("消息",successHandler);worker.addEventListener("error",failHandler);postInfo&&worker.postMessage(postInfo);});}通过这个转换,我们可以将一个postMessage请求转化为一个promise进行处理,这样更容易扩展空间,在audio、blob和arrayBuffer之间转换ffmpeg-worker需要的数据格式是arrayBuffer,一般我们可以直接使用的要么是音频文件对象blob,要么是音频元素对象audio,甚至是链接url,所以这些格式的转换是非常必要的:audiotoarrayBuffer函数audioToBlob(audio){consturl=audio.src;if(url){returnaxios({url,method:'get',responseType:'arraybuffer',}).then(res=>res.data);}else{returnPromise.resolve(null);}}作者想到的把audio转blob的方法是发起ajax请求,设置请求类型为arraybuffer,然后获取arrayBuffer.blob为arrayBuffer,很简单,直接用FileReader提取blob内容函数blobToArrayBuffer(blob){returnnewPromise(resolve=>{constfileReader=newFileReader();fileReader.onload=function(){resolve(fileReader.result);};fileReader.readAsArrayBuffer(blob);});}arrayBuffer以blob使用创建blob文件functionaudioBufferToBlob(arrayBuffer){constfile=newFile([arrayBuffer],'test.mp3',{type:'audio/mp3',});returnfile;}blobtoaudioblobtoaudio很简单,js提供了一个原生的API——URL.createObjectURL,通过它我们可以将blob转化为本地可访问的链接,用于播放函数blobToA音频(blob){consturl=URL.createObjectURL(blob);returnnewAudio(url);}接下来进入正题音频裁剪——所谓clip的裁剪是指根据给定的起止时间点,提取给定音频的内容,形成新的音频,首先添加代码:classSdk{end="end";//其他代码.../***根据指定的时间位置剪切一个传入的音频blob*@paramoriginBlob待处理的音频*@paramstartSecond开始剪切时间(秒)*@paramendSecond结束剪切时间(秒)*/clip=async(originBlob,startSecond,endSecond)=>{constss=startSecond;//获取要裁剪的时长,如果没有传endSecond,默认裁剪到最后constd=isNumber(endSecond)?endSecond-startSecond:this.end;//将blob转换为可处理的arrayBufferconstoriginAb=awaitblobToArrayBuffer(originBlob);让resultArrBuf;//获取发送给ffmpge-worker的命令并发送给worker,等待其裁剪完成if(d===this.end){resultArrBuf=(awaitpmToPromise(this.worker,getClipCommand(originAb,ss))).data.data.MEMFS[0].data;}else{resultArrBuf=(awaitpmToPromise(this.worker,getClipCommand(originAb,ss,d))).data.data.MEMFS[0].data;}//将worker处理后的arrayBuffer包装成blob返回returnaudioBufferToBlob(resultArrBuf);};}我们定义了这个接口的三个参数:要剪辑的音频块,剪辑的开始和结束时间点。值得注意的是,这里的getClipCommand函数负责传入arrayBuffer,将其打包成ffmpeg-worker约定的数据格式/***根据ffmpeg文档的要求,将裁剪后的数据转换成指定的格式*@paramarrayBuffer要处理的音频缓冲区*@paramst开始剪辑的时间点(秒)*@paramduration剪辑持续时间*/functiongetClipCommand(arrayBuffer,st,duration){return{type:"run",arguments:`-ss${st}-iinput.mp3${持续时间?`-t${duration}`:""}-acodeccopyoutput.mp3`.split(""),MEMFS:[{data:newUint8Array(arrayBuffer),name:"input.mp3"}]};}Multi-audiosynthesis——concatmulti-audiosynthesis很容易理解,就是把多个audio合并成一个audioclasssdk{//othercode.../***根据指定时间裁剪一个传入的audioblobposition*@paramblobs是要处理的音频blob数组*/concat=asyncblobs=>{constarrBufs=[];for(leti=0;i
