模拟制做网易云音乐(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、总结

我就是发现了一个好玩的东西,而后发了兴致好好玩了一下,以前照着别人的代码敲了一遍代码,后来发现什么都忘了,不如本身动手来得牢靠。有些东西一时看不懂,不要死磕,那是由于水平不够,不过记住就好,慢慢学习,而后再来攻克它,以此共勉。

相关文章
相关标签/搜索