網易雲音樂

模擬製做網易雲音樂(AudioContext)

 

記得好早前在慕課網上看到一款可視化音樂播放器,當前是以爲非常神奇,還能這麼玩。因爲當時剛剛轉行不久,好多東西看得稀裏糊塗不明白,因而趁着如今有時間又從新梳理了一遍,而後參照官網的API模擬作了一款網易播放器。沒有什麼創新的點,只是想到了就想作一下而已。javascript

效果能夠看這裏:http://music.poemghost.com/,若是看不了,說明博主的服務器已經不在工做啦。(建議使用電腦瀏覽器打開,同時切換到手機模式來打開,由於在手機上測試時有問題,並且有很大性能損耗,常常會致使瀏覽器奔潰)html

代碼在這裏:githubjava

效果圖一覽:
xiaoguonode

看着本身洋洋灑灑寫了快1000多行的js,我如今內心也是一萬屁草泥馬飄過。固然其中還有不少代碼沒有通過提煉,不少變量能夠公用,用對象化的方式來講寫這個會更有條理,這個博主之後有時間再梳理一遍。下面來說講主要的實現過程。git

1、總體思路

API能夠到https://webaudio.github.io/web-audio-api/#dom-audiobuffersourcenode上面去看,只是一個草案,並無歸入標準,因此有些地方仍是有問題,在下面我會說到我遇到了什麼問題。可是這個草案上的東西其實能夠作出不少其餘的效果。好比多音頻源來達到混音效果、音頻振盪器效果等等...github

總體的思路圖以下:web

silu

大體上來講就是經過window上的AudioContext方法來建立一個音頻對象,而後鏈接上數據,分析器和音量控制。最後經過BufferSourceNodestart方法來啓動音頻。ajax

2、具體分析

2.1 路由

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資源的時候會有很長時間,所以我把音頻資源放在了七牛雲,而不是從本地獲取,可是數據仍是在本地拿,由於並無用到數據庫。

2.2 主頁面

頁面運用了手淘的flexible,所以在最開始切換到手機模式的時候,可能須要刷新一下瀏覽器才能顯示正常。樣式採用的是預處理sass,感興趣的能夠去看一下代碼

2.3 建立音頻

/**  * 建立音頻  * @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); }

結合上面的圖,這裏建立音頻的代碼就比較好理解了。

2.4 播放

播放實際上是一個很是簡單的API,直接調用BufferSourceNodestart方法便可,start方法有兩個咱們會用到的參數,第一個是開始時間,第二個是時間位移,決定了咱們從何時開始,這將在跳播的時候會用到。

另外有一個注意的點是,不能再同一個BufferSourceNode上調用兩次start方法,不然會報錯。

bufferSource && bufferSource.start(0);

2.5 獲取頻譜圖數據

/**  * 獲取音頻解析數據  * @return void  */ function getByteFrequencyData() { var arr = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(arr); renderCanvas(arr); renderInter = window.requestAnimationFrame(getByteFrequencyData); }

經過不斷觸發這個函數,將最新的數據填充到一個8位的無符號數組中,進而開始渲染數據。此時的音頻範圍默認設置爲256。

2.6 音量調節

音量調節也有現成的API,這點也沒什麼可講的。

// gain 的值默認爲1 // 所以這裏若是想作繼續音量放大的能夠大於1 // 可是太大可能會出現破音效果,你們感興趣能夠試試 gainNode.gain.value = [0 ~ 1];

2.7 暫停與恢復播放

我在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(); }

2.8 跳動播放

跳動播放須要用到開始時間,這裏我默認設置爲0,接下來就是時間位移了。經過跳動播放進度條的遊標,咱們不難計算出咱們應該播放的時間。

這裏有一個問題,我以前也說到過,就是在同一個AudioBufferSourceNode上不能同時start兩次,那麼也就是說,我若是這裏再直接調用start(0, offsetTime)將會報錯,是的,這裏我也卡了很久,最後再一個論壇(是哪一個我卻是忘記了)上給了一個建議,不能同時在一個AudioBufferSourceNodestart兩次,那就在不一樣的AudioBufferSourceNodestart,也就意味着我能夠新建一個節點,而後依然用以前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); }

2.9 列表循環

列表循環用到了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,這樣就不會執行這個方法中默認的行爲。

3、手機端會有的問題

以前說過,建議不要在手機端運行,由於會有一些問題,主要表如今:

  • AudioContext須要兼容,我在ChromeSafari測試的時候一直得不到音頻數據,以後才發現須要兼容寫法,否則頁面播放不了。兼容寫法爲:webkitAudioContext
  • 最開始加載音頻的時候,AudioContext默認的狀態是suspended,這也是我最開始最納悶的事,當我點擊播放按鈕的時候沒有聲音,而點擊跳播的時候會播放聲音,後來調試發現走到了resumeAudio中。
  • 性能仍是有必定的問題,在手機上播放的時候,常常會出現卡死的現象。渲染柱狀條的時候感到有明顯的卡頓。、
  • 因爲手機瀏覽器上頁面高度還包括地址欄、導航條高度,所以,唱片可能會超出範圍

4、總結

我就是發現了一個好玩的東西,而後發了興致好好玩了一下,以前照着別人的代碼敲了一遍代碼,後來發現什麼都忘了,不如本身動手來得牢靠。有些東西一時看不懂,不要死磕,那是由於水平不夠,不過記住就好,慢慢學習,而後再來攻克它,以此共勉。

相關文章
相關標籤/搜索