zoukankan      html  css  js  c++  java
  • 模拟制作网易云音乐(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中。
    • 性能还是有一定的问题,在手机上播放的时候,经常会出现卡死的现象。渲染柱状条的时候感到有明显的卡顿。、
    • 由于手机浏览器上页面高度还包括地址栏、导航条高度,因此,唱片可能会超出范围

    四、总结

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

  • 相关阅读:
    Mysql中使用FIND_IN_SET解决IN条件为字符串时只有第一个数据可用的问题
    Mysql中游标的使用
    xcode5下cocos2dx横竖屏设置
    VUE 小点 1
    绝对定位居中
    清楚float的方法4种
    socket模拟简单的服务器
    Django + Uwsgi + Nginx 的生产环境部署
    常见排序算法
    mro之C3算法
  • 原文地址:https://www.cnblogs.com/rynxiao/p/7798419.html
Copyright © 2011-2022 走看看