模擬制作網易雲音樂(AudioContext)


記得好早前在慕課網上看到一款可視化音樂播放器,當前是覺得很是神奇,還能這么玩。由於當時剛剛轉行不久,好多東西看得稀里糊塗不明白,於是趁着現在有時間又重新梳理了一遍,然后參照官網的API模擬做了一款網易播放器。沒有什么創新的點,只是想到了就想做一下而已。

效果可以看這里:http://music.poemghost.com/,如果看不了,說明博主的服務器已經不在工作啦。(建議使用電腦瀏覽器打開,同時切換到手機模式來打開,因為在手機上測試時有問題,而且有很大性能損耗,經常會導致瀏覽器奔潰)

代碼在這里:github

效果圖一覽:
xiaoguo

看着自己洋洋灑灑寫了快1000多行的js,我現在心里也是一萬屁草泥馬飄過。當然其中還有很多代碼沒有經過提煉,很多變量可以公用,用對象化的方式來說寫這個會更有條理,這個博主以后有時間再梳理一遍。下面來講講主要的實現過程。

一、整體思路

API可以到https://webaudio.github.io/web-audio-api/#dom-audiobuffersourcenode上面去看,只是一個草案,並沒有納入標准,所以有些地方還是有問題,在下面我會說到我遇到了什么問題。但是這個草案上的東西其實可以做出很多其他的效果。比如多音頻源來達到混音效果、音頻振盪器效果等等...

整體的思路圖如下:

silu

大致上來說就是通過window上的AudioContext方法來創建一個音頻對象,然后連接上數據,分析器和音量控制。最后通過BufferSourceNodestart方法來啟動音頻。

二、具體分析

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文件,因此作了一點小處理。

另外由於用的海外服務器,所以請求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,這樣就不會執行這個方法中默認的行為。

三、手機端會有的問題

之前說過,建議不要在手機端運行,因為會有一些問題,主要表現在:

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

四、總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM