这里将要介绍的HTML5 音频处理接口与Audio标签是不同的。页面上的Audio标签只是HTML5更语义化的一个表现,而HTML5提供给JavaScript编程用的Audio API则让咱们有能力在代码中直接操做原始的音频流数据,对其进行任意加工再造。html 展现HTML5 Audio API 最典型直观的一个例子就是跟随音乐节奏变化的频谱图,也称之为可视化效果。本文即是以此为例子展现JavaScript中操做音频数据的。程序员 文中代码仅供参考,实际代码如下载的源码为准。 |
|
经过AudioContext能够建立不一样各种的 AudioNode,即音频节点,不一样节点做用不一样,有的对音频加上滤镜好比提升音色(好比BiquadFilterNode),改变单调,有的音频进行 分割,好比将音源中的声道分割出来获得左右声道的声音(ChannelSplitterNode),有的对音频数据进行频谱分析即本文要用到的 (AnalyserNode)。![]() ![]() 浏览器中的Audio API window.AudioContext = 这是一种常见的用法,或者操做符'||' 链接起来的表达式中,遇到真值即返回。好比在Chrome中,window.AudioContext为undefined,接着往下走,碰到 window.webkitAudioContext不为undefined,表达式在此判断为真值,因此将其返回,因而此时 window.AudioContext =window.webkitAudioContext ,因此代码中咱们就能够直接使用window.AudioContext 而不用担忧具体Chrome仍是Firefox了。数组 var audioContext=new window.AudioContext();浏览器 考虑浏览器不支持的状况 这样就安全多啦,妈妈再不担忧浏览器报错了。 var Visualizer = function() { this.file = null, //要处理的文件,后面会讲解如何获取文件 this.fileName = null, //要处理的文件的名,文件名 this.audioContext = null, //进行音频处理的上下文,稍后会进行初始化 this.source = null, //保存音频 }; Visualizer.prototype = { _prepareAPI: function() { //统一前缀,方便调用 window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; //这里顺便也将requestAnimationFrame也打个补丁,后面用来写动画要用 window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; //安全地实例化一个AudioContext并赋值到Visualizer的audioContext属性上,方便后面处理音频使用 try { this.audioContext = new AudioContext(); } catch (e) { console.log('!妳的浏览器不支持AudioContext:('); console.log(e); } }, }加载音频文件 不用说,你确定得先在代码中获取到音频文件,才可以对其进一步加工。 文件获取的方法: 读取文件到JavaScript能够有如下三种方法: 1.新开一个Ajax异步请求来获取文件,若是是本地测试须要关掉浏览器的同源安全策略才能获取成功,否则只能把网页放到服务器上才能正常工做。 具体说来,就是先开一个XMLHttpRequest请求,将文件路径做为请求的URL,而且设置请求返回类型为'ArrayBuffer',这种格式方便咱们后续的处理。下面是一个例子。 loadSound("sample.mp3"); //调用 // 定义加载音频文件的函数 function loadSound(url) { var request = new XMLHttpRequest(); //创建一个请求 request.open('GET', url, true); //配置好请求类型,文件路径等 request.responseType = 'arraybuffer'; //配置数据返回类型 // 一旦获取完成,对音频进行进一步操做,好比解码 request.onload = function() { var arraybuffer = request.response; } request.send(); } 2.经过文件类型的input来进行文件选择,监听input的onchnage事件,一担文件选中便开始在代码中进行获取处理,此法方便,且不须要工做在服务器上app 3.经过拖拽的形式把文件拖放到页面进行获取,比前面一种方法稍微繁杂一点(要监听'dragenter','dragover','drop'等事件)但一样能够很好地在本地环境下工做,无需服务器支持。 不用说,方法2和3方便本地开发与测试,因此咱们两种方法都实现,既支持选择文件,也支持文件拖拽。 固然,这里同时也把最后咱们要画图用的canvas也一块儿放上去吧,后面就不用多话了。因此下面就是最终的HTML了,页面基本不会变,大量的工做是在JavaScript的编写上。 向Visualizer对象的原型中新加一个方法,用于监听文件选择既前面讨论的onchange事件,并在事件中获取选择的文件。 _addEventListner: function() { var that = this, audioInput = document.getElementById('uploadedFile'), dropContainer = document.getElementsByTagName("canvas")[0]; //监听是否有文件被选中 audioInput.onchange = function() { //这里判断一下文件长度能够肯定用户是否真的选择了文件,若是点了取消则文件长度为0 if (audioInput.files.length !== 0) { that.file = audioInput.files[0]; //将文件赋值到Visualizer对象的属性上 that.fileName = that.file.name; that._start(); //获取到文件后,开始程序,这个方法会在后面定义并实现 }; }; }上面代码中,咱们假设已经写好了一个进一步处理文件的方法_start(),在获取到文件后赋值给Visualizer对象的file属性,以后在 _start()方法里咱们就能够经过访问this.file来获得该文件了,固然你也能够直接让_start()方法接收一个file参数,但将文件赋 值到Visualizer的属性上的好处之一是咱们能够在对象的任何方法中都能获取该文件 ,不用想怎么用参数传来传去。一样,将文件名赋值到Visualizer的fileName属性当中进行保存,也是为了方便以后在音乐播放过程当中显示当前 播放的文件。 (2)经过拖拽获取 咱们把页面中的canvas做为放置文件的目标,在它身上监听拖拽事件'dragenter','dragover','drop'等。 仍是在上面已经添加好的_ addEventListner方法里,接着写三个事件监听的代码。 dropContainer.addEventListener("dragenter", function() { that._updateInfo('Drop it on the page', true); }, false); dropContainer.addEventListener("dragover", function(e) { e.stopPropagation(); e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; //设置文件放置类型为拷贝 }, false); dropContainer.addEventListener("dragleave", function() { that._updateInfo(that.info, false); }, false); dropContainer.addEventListener("drop", function(e) { e.stopPropagation(); e.preventDefault(); that.file = e.dataTransfer.files[0]; //获取文件并赋值到Visualizer对象 that.fileName = that.file.name; that._start(); }, false); 注意到上面代码中咱们在'dragover'时设置文件拖放模式为'copy',既以复制的形式获取文件,若是不进行设置没法正确获取文件 而后在'drop'事件里,咱们得到文件以进行一下步操做。 _start: function() { //read and decode the file into audio array buffer var that = this, //当前this指代Visualizer对象,赋值给that以以便在其余地方使用 file = this.file, //从Visualizer对象上获取前面获得的文件 fr = new FileReader(); //实例化一个FileReader用于读取文件 fr.onload = function(e) { //文件读取完后调用此函数 var fileResult = e.target.result; //这是读取成功获得的结果ArrayBuffer数据 var audioContext = that.audioContext; //从Visualizer获得最开始实例化的AudioContext用来作解码ArrayBuffer audioContext.decodeAudioData(fileResult,function(buffer){//解码成功则调用此函数,参数buffer为解码后获得的结果 that._visualize(audioContext, buffer); //调用_visualize进行下一步处理,此方法在后面定义并实现 }, function(e) { //这个是解码失败会调用的函数 console.log("!哎玛,文件解码失败:("); }); }; //将上一步获取的文件传递给FileReader从而将其读取为ArrayBuffer格式 fr.readAsArrayBuffer(file); } 注意这里咱们把this赋值给了that,而后再 audioContext.decodeAudioData的回调函数中使用that来指代咱们的Visualizer对象。这是由于做用域的缘由。咱们 知道JavaScript中没法经过花括号来建立代码块级做用域,而惟一能够建立做用域的即是函数。一个函数就是一个做用域。函数内部的this指向的对 象要视状况而定,就上面的代码来讲,它是audioContext。因此若是想要在这个回调函数中调用Visualizer身上方法或属性,则须要经过另 一个变量来传递,这里是that,咱们经过将外层this(指向的是咱们的Viusalizer对象)赋值给新建的局部变量that,此时that即可以 传递到内层做用域中,而不会与内层做用域里面原来的this相冲突。像这样的用法在源码的其余地方也有使用,细心的你能够下载本文的源码慢慢研究。 因此,在 audioContext.decodeAudioData的回调函数里,当解码完成获得audiobuffer文件(buffer参数)后,再把 audioContext和buffer传递给Visualizer的_visualize()方法进一步处理:播放音乐和绘制频谱图。固然此时 _visualize()方法尚未下,下面便开始实现它。 其实到了这里你应该有点晕了,不过不要紧,看代码就会更明白一些,程序员是理解代码优于文字的一种生物。 就这么两名,把音频文件的内容装进了AudioContext。这时已经能够开始播放咱们的音频了。 这里参数是时间,表示从这段音频的哪一个时刻开始播放。注意:在旧版本的浏览器里是使用onteOn()来进行播放的,参数同样,指开始时刻。 但此时是听不到声音的,由于还差一步,须要将audioBufferSouceNode链接到audioContext.destination,这个AudioContext的destination也就相关于speaker(扬声器)。 此刻就可以听到扬声器传过来动听的声音了。 而后再开始播放,此刻全部音频数据都会通过analyser,咱们再从analyser中获取频谱的能量信息,将其画出到Canvas便可。 假设咱们已经写好了画频谱图的方法_drawSpectrum(analyser); 绘制精美的频谱图 接下来的工做,也是最后一步,也就是实现_drawSpectrum()方法,将跟随音乐而灵动的柱状频谱图画出到页面。 经过下面的代码咱们能够从analyser中获得此刻的音频中各频率的能量值。 此刻array中存储了从低频0Hz到高频~Hz的全部数据。频率作为X轴,能量值作为Y轴,咱们能够获得相似下面的图形。 ![]() 因此,好比array[0]=100,咱们就知道在x=0处画一个高为100单位长度的长条,array[1]=50,而后在x=1画一个高为50单位长度的柱条,今后类推,若是用一个for循环遍历array将其所有画出的话,即是你看到的上图。 但咱们要的不是那样的效果,咱们只需在全部数据中进行抽样,好比设定一个步长100,进度抽取,来画出整个频谱图中的部分柱状条。 或者先根据画面的大小,设计好每根柱条的宽度,以及他们的间隔,从而计算出画面中一共须要共多少根,再来推算出这个采样步长该取多少,本例即是这样实现的。说来仍是有点晕,下面看简单的代码: var canvas = document.getElementById('canvas'), 咱们的画布即Canvas宽800px,同时咱们设定柱条宽10px , 柱与柱间间隔为2px,因此获得meterNum为总共能够画的柱条数。再用数组总长度除以这个数目就获得采样的步长,即在遍历array时每隔step 这么长一段咱们从数组中取一个值出来画,这个值为array[i*step]。这样就均匀地取出meterNum个值,从而正确地反应了原来频谱图的形 状。 var canvas = document.getElementById('canvas'),cwidth = canvas.width, cheight = canvas.height - 2, meterWidth = 10, //能量条的宽度 gap = 2, //能量条间的间距 meterNum = 800 / (10 + 2), //计算当前画布上能画多少条 ctx = canvas.getContext('2d'), array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / meterNum);计算从analyser中的采样步长 ctx.clearRect(0, 0, cwidth, cheight); //清理画布准备画画 //定义一个渐变样式用于画图 gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(1, '#0f0'); gradient.addColorStop(0.5, '#ff0'); gradient.addColorStop(0, '#f00'); ctx.fillStyle = gradient; //对信源数组进行抽样遍历,画出每一个频谱条 for (var i = 0; i < meterNum; i++) { var value = array[i * step]; ctx.fillRect(i * 12 /*频谱条的宽度+条间间距*/ , cheight - value + capHeight, meterWidth, cheight); } 使用requestAnimationFrame让柱条动起来 但上面绘制的仅仅是某一刻的频谱,要让整个画面动起来,咱们须要不断更新画面,window.requestAnimationFrame()正好提供了更新画面获得动画效果的功能,这里直接给出简单改造后的代码,即获得咱们要的效果了:跟随音乐而灵动的频谱柱状图。 var canvas = document.getElementById('canvas'), cwidth = canvas.width, cheight = canvas.height - 2, meterWidth = 10, //能量条的宽度 gap = 2, //能量条间的间距 meterNum = 800 / (10 + 2), //计算当前画布上能画多少条 ctx = canvas.getContext('2d'); //定义一个渐变样式用于画图 gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(1, '#0f0'); gradient.addColorStop(0.5, '#ff0'); gradient.addColorStop(0, '#f00'); ctx.fillStyle = gradient; var drawMeter = function() { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / meterNum); //计算采样步长 ctx.clearRect(0, 0, cwidth, cheight); //清理画布准备画画 for (var i = 0; i < meterNum; i++) { var value = array[i * step]; ctx.fillRect(i * 12 /*频谱条的宽度+条间间距*/ , cheight - value + capHeight, meterWidth, cheight); } requestAnimationFrame(drawMeter); } requestAnimationFrame(drawMeter); 绘制缓慢降落的帽头 首先要搞清楚的一点是,咱们拿一根柱条来讲明问题,当此刻柱条高度高于前一时刻时,咱们看到的是往上冲的一根频谱,因此这时帽头是紧贴着正文 柱条的,这个好画。考虑相反的状况,当此刻高度要低于前一时刻的高度时,下方柱条是当即缩下去的,同时咱们须要记住上一时刻帽头的高度位置,此刻画的时候 就按照前一时刻的位置将Y-1来画。若是下一时刻频谱柱条仍是没有超过帽头的位置,继续让它降低,Y-1画出帽头。 经过上面的分析,因此咱们在每次画频谱的时刻,须要将此刻频谱及帽头的Y值(即垂直方向的位置)记到一个循环外的变量中,在下次绘制的时刻从这个变量中读取,将此刻的值与变量中保存的上一刻的值进行比较,而后按照上面的分析做图。 最后给出实现的代码: _drawSpectrum: function(analyser) { var canvas = document.getElementById('canvas'), cwidth = canvas.width, cheight = canvas.height - 2, meterWidth = 10, //频谱条宽度 gap = 2, //频谱条间距 capHeight = 2, capStyle = '#fff', meterNum = 800 / (10 + 2), //频谱条数量 capYPositionArray = []; //将上一画面各帽头的位置保存到这个数组 ctx = canvas.getContext('2d'), gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(1, '#0f0'); gradient.addColorStop(0.5, '#ff0'); gradient.addColorStop(0, '#f00'); var drawMeter = function() { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / meterNum); //计算采样步长 ctx.clearRect(0, 0, cwidth, cheight); for (var i = 0; i < meterNum; i++) { var value = array[i * step]; //获取当前能量值 if (capYPositionArray.length < Math.round(meterNum)) { capYPositionArray.push(value); //初始化保存帽头位置的数组,将第一个画面的数据压入其中 }; ctx.fillStyle = capStyle; //开始绘制帽头 if (value < capYPositionArray[i]) { //若是当前值小于以前值 ctx.fillRect(i *12,cheight-(--capYPositionArray[i]),meterWidth,capHeight);//则使用前一次保存的值来绘制帽头 } else { ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); //不然使用当前值直接绘制 capYPositionArray[i] = value; }; //开始绘制频谱条 ctx.fillStyle = gradient; ctx.fillRect(i * 12, cheight - value + capHeight, meterWidth, cheight); } requestAnimationFrame(drawMeter); } requestAnimationFrame(drawMeter); |
转载:http://www.108js.com/article/article7/70196.html?id=983