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

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

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

代碼在這裏:githubnode

效果圖一覽:
xiaoguogit

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

1、總體思路

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

總體的思路圖以下:ajax

silu

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

2、具體分析

2.1 路由

routes/index.jswindows

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文件,所以做了一點小處理。api

另外因爲用的海外服務器,因此請求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、總結

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

相關文章
相關標籤/搜索