記得好早前在慕課網上看到一款可視化音樂播放器,當前是以爲非常神奇,還能這麼玩。因爲當時剛剛轉行不久,好多東西看得稀裏糊塗不明白,因而趁着如今有時間又從新梳理了一遍,而後參照官網的API模擬作了一款網易播放器。沒有什麼創新的點,只是想到了就想作一下而已。javascript
效果能夠看這裏:http://music.poemghost.com/,若是看不了,說明博主的服務器已經不在工做啦。(建議使用電腦瀏覽器打開,同時切換到手機模式來打開,由於在手機上測試時有問題,並且有很大性能損耗,常常會致使瀏覽器奔潰)html
代碼在這裏:githubjava
效果圖一覽:
node
看着本身洋洋灑灑寫了快1000多行的js
,我如今內心也是一萬屁草泥馬飄過。固然其中還有不少代碼沒有通過提煉,不少變量能夠公用,用對象化的方式來講寫這個會更有條理,這個博主之後有時間再梳理一遍。下面來說講主要的實現過程。git
API
能夠到https://webaudio.github.io/web-audio-api/#dom-audiobuffersourcenode上面去看,只是一個草案,並無歸入標準,因此有些地方仍是有問題,在下面我會說到我遇到了什麼問題。可是這個草案上的東西其實能夠作出不少其餘的效果。好比多音頻源來達到混音效果、音頻振盪器效果等等...github
總體的思路圖以下:web
大體上來講就是經過window
上的AudioContext
方法來建立一個音頻對象,而後鏈接上數據,分析器和音量控制。最後經過BufferSourceNode
的start
方法來啓動音頻。ajax
routes/index.js
數據庫
router.get('/', function(req, res, next) { fs.readdir(media, function(err, names) { var first = names[0]; // 若是第一個文件不是mp3文件,說明是MAC系統 if (first.indexOf('mp3') === -1) { first = names[1]; names = names.slice(1); } var song = first.split(' - ')[1].replace('.mp3', ''); var singer = first.split(' - ')[0]; if (err) { console.log(err); } else { res.render('index', { title: '網易雲音樂', music: names, posts: listPosts, song: song, singer: singer, post: listPosts[0] }); } }); });
這裏mac
平臺和windows
不一樣,mac
文件夾會有一個.DS_Store
文件,所以做了一點小處理。windows
另外因爲用的海外服務器,因此請求mp3
資源的時候會有很長時間,所以我把音頻資源放在了七牛雲,而不是從本地獲取,可是數據仍是在本地拿,由於並無用到數據庫。
頁面運用了手淘的flexible
,所以在最開始切換到手機模式的時候,可能須要刷新一下瀏覽器才能顯示正常。樣式採用的是預處理sass
,感興趣的能夠去看一下代碼
/** * 建立音頻 * @param AudioBuffer buffer AudioBuffer對象 * @return void */ function createAudio(buffer) { // 若是音頻是關閉狀態,則從新新建一個全局音頻上下文 if (ac.state === 'closed') { ac = new (window.AudioContext || window.webkitAudioContext)(); } audioBuffer = buffer; ac.onstatechange = onStateChange; // 建立BufferSrouceNode bufferSource = ac.createBufferSource(); bufferSource.buffer = buffer; // 建立音量節點 gainNode = ac.createGain(); gainNode.gain.value = gainValue; // 建立分析節點 analyser = ac.createAnalyser(); analyser.fftSize = fftSize; bufferSource.onended = onPlayEnded; // 嵌套鏈接 bufferSource.connect(analyser); analyser.connect(gainNode); gainNode.connect(ac.destination); }
結合上面的圖,這裏建立音頻的代碼就比較好理解了。
播放實際上是一個很是簡單的API
,直接調用BufferSourceNode
的start
方法便可,start
方法有兩個咱們會用到的參數,第一個是開始時間,第二個是時間位移,決定了咱們從何時開始,這將在跳播的時候會用到。
另外有一個注意的點是,不能再同一個BufferSourceNode
上調用兩次start
方法,不然會報錯。
bufferSource && bufferSource.start(0);
/** * 獲取音頻解析數據 * @return void */ function getByteFrequencyData() { var arr = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(arr); renderCanvas(arr); renderInter = window.requestAnimationFrame(getByteFrequencyData); }
經過不斷觸發這個函數,將最新的數據填充到一個8位的無符號數組中,進而開始渲染數據。此時的音頻範圍默認設置爲256。
音量調節也有現成的API
,這點也沒什麼可講的。
// gain 的值默認爲1 // 所以這裏若是想作繼續音量放大的能夠大於1 // 可是太大可能會出現破音效果,你們感興趣能夠試試 gainNode.gain.value = [0 ~ 1];
我在AudioBufferSourceNode
上找了很久,原本覺得有start/stop
方法,那麼就會有相似於puase
方法之類的,可是遺憾的是,確實沒有。最開始我也不知道怎麼作播放和暫停,可是好在天無絕人之路,意外發如今全局的AudioContext
上有兩個方法resume/suspend
,這也是實現播放和暫停的兩個方法。
/** * 恢復播放 * @return null */ function resumeAudio() { playState = PLAY_STATE.RUNNING; // 放下磁頭 downPin(); // 在當前AudioContext被掛起的狀態下,才能使用resume進行從新激活 ac.resume(); // 從新恢復可視化 resumeRenderCanvas(); // 重啓定時器 startInter && clearInterval(startInter); startInter = setInterval(function() { renderTime(start, executeTime(startSecond)); updateProgress(startSecond, totalTime); startSecond++; }, 1000); } /** * 暫停播放 * @return null */ function suspendAudio() { playState = PLAY_STATE.SUSPENDED; // 中止可視化 stopRenderCanvas(); // 收起磁頭 upPin(); startInter && clearInterval(startInter); // 掛起當前播放 ac.suspend(); }
跳動播放須要用到開始時間,這裏我默認設置爲0,接下來就是時間位移了。經過跳動播放進度條的遊標,咱們不難計算出咱們應該播放的時間。
這裏有一個問題,我以前也說到過,就是在同一個AudioBufferSourceNode
上不能同時start
兩次,那麼也就是說,我若是這裏再直接調用start(0, offsetTime)
將會報錯,是的,這裏我也卡了很久,最後再一個論壇(是哪一個我卻是忘記了)上給了一個建議,不能同時在一個AudioBufferSourceNode
上start
兩次,那就在不一樣的AudioBufferSourceNode
上start
,也就意味着我能夠新建一個節點,而後依然用以前ajax
請求到的數據來建立一個新的音頻數據。實驗是能夠行的。
/** * 跳動播放 * @param number time 跳躍時間秒數 * @return void */ function skipAudio(time) { // 先釋放以前的AudioBufferSourceNode對象 // 而後再從新鏈接 // 由於不容許在一個Node上start兩次 analyser && analyser.disconnect(gainNode); gainNode && gainNode.disconnect(ac.destination); bufferSource = ac.createBufferSource(); bufferSource.buffer = audioBuffer; // 建立音頻節點 gainNode = ac.createGain(); gainNode.gain.value = gainValue; // 建立分析節點 analyser = ac.createAnalyser(); analyser.fftSize = fftSize; bufferSource.connect(analyser); analyser.connect(gainNode); gainNode.connect(ac.destination); bufferSource.onended = onPlayEnded; bufferSource.start(0, time); playState = PLAY_STATE.RUNNING; changeSuspendBtn(); // 開始分析 getByteFrequencyData(); // 填充當前播放的時間 renderTime(start, executeTime(time)); startSecond = time; // 放下磁頭 downPin(); // 從新開始計時 startInter && clearInterval(startInter); startSecond++; startInter = setInterval(function() { renderTime(start, executeTime(startSecond)); updateProgress(startSecond, totalTime); startSecond++; }, 1000); }
列表循環用到了bufferSource
上的一個回調方法onended
,在播放完成以後就自動執行下一曲。
/** * 播放完成後的回調 * @return null */ function onPlayEnded() { var acState = ac.state; // 在進行上一曲和下一曲或者跳躍播放的時候 // 若是調用stop方法,會進入當前回調,所以要做區分 // 上一曲和下一曲的時候,因爲是新的資源,所以採用關閉當前的AduioContext, load的時候從新生成 // 這樣acState的狀態就是suspended,這樣就不會出現播放錯位 // 而在跳躍播放的時候,因爲是同一個資源,所以加上skip標誌就能夠判斷出來 // 發現若是是循環播放,onPlayEnded方法不會被執行,所以採用加載相同索引的方式 if (acState === 'running' && !skip) { var index = getNextPlayIndex(); loadMusic(playItems[index], index); } }
這裏有一個坑就是當我點擊了上一曲和下一曲的時候,發現也會執行這個回調,所以點擊下一曲的時候,實際上播放的是下兩曲的歌曲。所以這裏作了區分,當點擊上一曲和下一曲的時候,會給skip
設置爲true
,這樣就不會執行這個方法中默認的行爲。
以前說過,建議不要在手機端運行,由於會有一些問題,主要表如今:
AudioContext
須要兼容,我在Chrome
和Safari
測試的時候一直得不到音頻數據,以後才發現須要兼容寫法,否則頁面播放不了。兼容寫法爲:webkitAudioContext
。AudioContext
默認的狀態是suspended
,這也是我最開始最納悶的事,當我點擊播放按鈕的時候沒有聲音,而點擊跳播的時候會播放聲音,後來調試發現走到了resumeAudio
中。我就是發現了一個好玩的東西,而後發了興致好好玩了一下,以前照着別人的代碼敲了一遍代碼,後來發現什麼都忘了,不如本身動手來得牢靠。有些東西一時看不懂,不要死磕,那是由於水平不夠,不過記住就好,慢慢學習,而後再來攻克它,以此共勉。