聲明:本文為原創文章,如需轉載,請注明來源WAxes,謝謝!
一轉眼就已經有三個月沒寫博客了,畢業季事情確實多,現在也終於完全畢業了,博客還是不能落下。偶爾還是要寫一下。
玩HTML5的Audio API是因為之前看到博客園里有關於這個的博客,覺得挺好玩的,所以就學習了一下。本文僅作為自己的學習記錄。如有錯誤之處請指出。
最終的效果也就如右圖,園友們可以自己去玩一下

DEMO鏈接:請戳我!!! 可以選擇本地音頻文件進行播放,也可以聽樓主的音樂哈
同時,這個API目前瀏覽器支持度不高,PC瀏覽器支持較好的僅firefox、chrome以及safari,移動端就更少的,android5.0才支持,safari是6.1以上版本支持。
因此,若要用於生產環境,請自行斟酌。
WebAudio從獲取數據到播放整個流程可以用一張圖解釋:

有點像nodejs里的pipe流式傳輸,input是Audio的輸入節點,可以為buffer,也可以為audio對象。Effects為各個操控音頻的節點,我自己用到的就只有GainNode以及AnalyserNode,GainNode可以用來控制音頻音量的大小,默認值為0,也就是靜音,如果設為1才有聲音,如果設的更高的值,就會更高音。而AnalyserNode是用來獲取音頻大小的數值。
還有其他很多節點,其他節點的話個人感覺是一些比較高級的音頻處理,我是不知道怎么用,有興趣的可以自行去mdn上查詢。
回歸正題:說下該效果改如何實現,首先,要做成這種效果,要分幾步:
1、獲取音頻文件,實例化一個音頻容器對象。
2、通過FileReader把音頻文件轉成ArrayBuffer后再對其進行解碼。
3、用解碼后的buffer實例化一個AudioBuffer對象。
4、使用AnalyserNode接口實例化一個分析器節點。
5、使用connect方法將AudioBuffer對象連接至AnalyserNode,如果想用GainNode,就再用connect方法,把AnalyserNode跟GainNode連接,然后再接到最終的音頻播放節點:Destination,開始播放音頻
6、在音頻播放的時候通過AnalyserNode獲取音頻播放時的各個頻率值並轉成8bit的ArrayBuffer。
7、根據上面的arraybuffer里的各個值在canvas上畫出相應的條形圖即可。
大概說起來就以上幾步,具體代碼分析如下:
先將要用到的對象先定義好:其中包括audioContext音頻容器對象,以及canvas的2d繪圖環境對象,requestAnimationFrame的兼容性寫法。
var music = document.getElementById("music"),canvas = document.getElementById("cas"),ctx=canvas.getContext("2d"); window.AudioContext= window.AudioContext||window.webkitAudioContext||window.mozAudioContext; window.RAF = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); }; })(); var AC = new AudioContext();
然后就獲取音頻文件,可以直接通過input file來獲取,或者用xhr也行。樓主為了方便,做的demo上就是直接用input file來獲取音頻文件。代碼如下:通過onchange事件來獲取到音頻文件music.file[0]。再對音頻文件進行解碼,也就是changeBuffer方法要做的事。
music.onchange = function(){ if(music.files.length!==0){ changeBuffer(music.files[0]); } }
獲取到了音頻文件,先用FilreReader將文件轉成ArrayBuffer對象,在加載完后可以通過e.target.result獲取到文件內容。
然后再對文件內容進行解碼,用到的就是audioContext對象里的decodeAudioData方法。根據官方API文檔得知,該方法有三個參數:第一個是就是音頻ArrayBuffer對象,第二個是成功解碼完畢后的回調,第三個是解碼失敗后的回調
function changeBuffer(file){ var fr = new FileReader(); fr.onload = function(e){ var fileResult = e.target.result; AC.decodeAudioData(fileResult , function(buffer){ playMusic(buffer) }, function(e){ console.log(e) alert("文件解碼失敗") }) } fr.readAsArrayBuffer(file); }
解碼成功后調用playMusic方法,並且傳入解碼后的buffer數據,此時實例化一個AudioBufferSource對象,AudioBufferSource對象的屬性有五個。分別是:buffer、playbackRate、loop、loopstart和loopend,buffer自然就是音頻buffer數據,playbackRate是渲染音頻流的速度,其默認值是1。loop則是播放循環屬性,默認為false,如果設為true則會循環播放音頻。loopstart和loopend則是循環開始和結束的時間段,以秒為單位,默認值均為0,只有當loop的值為true的時候這兩個屬性才會起效。
下面的playMusic方法不僅處理了buffer的播放,如果傳入的是audio dom對象也會進行相應的轉換。但是注意,如果是audio對象轉出來的audioSource,就不會有上面AudioBufferSource的方法,畢竟audio dom對象本身有方法可以控制自己的播放。
實例化AudioBufferSource對象后,就像上面說的一樣,接入analyserNode,analyserNode再接入gainnode,然后最終gainnode再接入audioContext.destination。
准備完畢后,如果源是bufferSource則調用start方法播放音頻,否則就是用audio 的play方法播放音頻。
播放后就跳轉到canvas的繪圖方法animate中,將音譜繪制出來。
//音頻播放 function playMusic(arg) { var source; //如果arg是audio的dom對象,則轉為相應的源 if (arg.nodeType) { audioSource = audioSource || AC.createMediaElementSource(arg); source = audioSource; } else { bufferSource = AC.createBufferSource(); bufferSource.buffer = arg; bufferSource.onended = function () { app.trigger(singleLoop ? nowIndex : (nowIndex + 1)); }; //播放音頻 setTimeout(function () { bufferSource.start() }, 0); source = bufferSource; } //連接analyserNode source.connect(analyser); //再連接到gainNode analyser.connect(gainnode); //最終輸出到音頻播放器 gainnode.connect(AC.destination); }
然后獲取到analyser節點里的頻率長度,根據長度實例化一個8位整型數組,通過analyser.getByteFrequencyData將analyser節點中的頻率數據拷貝進數組。因為數組為8位數組,即每個值的大小就為0~256,然后就可以根據這個值即各個頻率的信號量進行繪制不同的條形圖,每個條形圖我也抽象成了對象,在每一幀對各個條形圖進行修改就完成了最簡單的音頻動畫了。
條形圖對象代碼:
//音譜條對象 function Retangle(w, h, x, y) { this.w = w; this.h = h; //小紅塊高度 this.x = x; this.y = y; this.jg = 3; this.power = 0; this.dy = y; //小紅塊位置 this.initY = y; this.num = 0; }; var Rp = Retangle.prototype; Rp.update = function(power){ this.power = power; this.num = ~~(this.power / this.h + 0.5); //更新小紅塊的位置,如果音頻條長度高於紅塊位置,則紅塊位置則為音頻條高度,否則讓小紅塊下降 var nh = this.dy + this.h;//小紅塊當前位置 if (this.power >= this.y - nh) { this.dy = this.y - this.power - this.h - (this.power == 0 ? 0 : 1); } else if (nh > this.y) { this.dy = this.y - this.h; } else { this.dy += 1; } this.draw(); }; Rp.draw = function(){ ctx.fillStyle = grd; var h = (~~(this.power / (this.h + this.jg))) * (this.h + this.jg); ctx.fillRect(this.x, this.y - h, this.w, h) for (var i = 0; i < this.num; i++) { var y = this.y - i * (this.h + this.jg); ctx.clearRect(this.x - 1, y, this.w + 2, this.jg); } ctx.fillStyle = "#950000"; ctx.fillRect(this.x, ~~this.dy, this.w, this.h); };
循環動畫方法:
function animate() { if(!musics[nowIndex].decoding){ ctx.clearRect(0, 0, canvas.width, canvas.height); //出來的數組為8bit整型數組,即值為0~256,整個數組長度為1024,即會有1024個頻率,只需要取部分進行顯示 var array_length = analyser.frequencyBinCount; var array = new Uint8Array(array_length); analyser.getByteFrequencyData(array); //將音頻節點的數據拷貝到Uin8Array中 //數組長度與畫布寬度比例 var bili = array_length / canvas.width; for (var i = 0; i < rt_array.length; i++) { var rt = rt_array[i]; //根據比例計算應該獲取第幾個頻率值,並且緩存起來減少計算 rt.index = ('index' in rt) ? rt.index : ~~(rt.x * bili); rt.update(array[rt.index]); } copy(); }else { showTxt("音頻解碼中...") } RAF(animate); }
為了讓整個條形圖更美觀,所以還加入了一個半透明投影效果:
//制造半透明投影 function copy() { var outctx = outcanvas.getContext("2d"); var imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height / 2); for (var i = 0; i < imgdata.data.length; i += 4) { imgdata.data[i + 3] = 30; } outctx.putImageData(imgdata, 0, 0); ctx.save(); ctx.translate(canvas.width / 2, canvas.height / 2); ctx.rotate(Math.PI); ctx.scale(-1, 1); ctx.drawImage(outcanvas, -canvas.width / 2, -canvas.height / 2) ctx.restore(); }
后期對代碼有所更改,若要最新源碼請見github地址:
https://github.com/whxaxes/canvas-test/blob/gh-pages/src/Funny-demo/musicPlayer/
