web-audio-api可视化音乐播放器,实现暂停切换歌曲功能,粉色系专场~

可视化的音乐播放器,可戳我观看效果

了解Web-Audio-Api

  • 基础知识

<audio>标签是HTML5的新标签,通过添加src属性实现音乐播放。

AudioContext是音频播放环境,原理与canvas的绘制环境类似,都是需要创建环境上下文,通过上下文的调用相关的创建音频节点,控制音频流播放暂停操作等操作,这一些操作都需要发生在这个环境之中。

try{
    var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 
}catch(e){
    alert('Web Audio API is not supported in this browser');
}

AudioNode接口是一个处理音频的通用模块,它可以是音频音源模块,音频播放设备模块,也可以是中间音频处理模块。不同的音频节点的连接(通过AudioContext.connect()),以及终点连接AudioContext.destination(可以看作是连接到耳机或扬声器设备)完成后,才能输出音乐。

常见的音频节点:
AudioBufferSourceNode: 播放和处理音频数据
AnalyserNode: 显示音频时间和频率数据 (通过分析频率数据可以绘制出波形图之类的视图,可视化的主要途径)
GainNode: 音量节点,控制音频的总音量
MediaElementAudioSourceNode: 关联HTMLMediaElement,播放和处理来自<video>和<audio>元素的音频
OscillatorNode: 一个周期性波形,只创建一个音调
...
  • 运行模式
  1. 创建音频上下文
  2. 在上下文中,创建音频源
  3. 创建音频节点,处理音频数据并连接
  4. 输出设备


    image

创建音频上下文

try{
    var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 
}catch(e){
    alert('Web Audio API is not supported in this browser');
}

创建音频源

由于音频文件的数据是二进制(非文本),所以要设置请求头的responseTypearraybuffer,将.mp3音频文件转换成数组缓冲区ArrayBuffer

AudioContext.decodeAudioData解码成功之后获取buffer,执行回调函数,将数据放入AudioBufferSourceNode

方法一采用流式加载音乐文件,简单易懂,缺点是通过createMediaElementSource加载的src文件必须是同源,不允许跨域

下面步骤主要根据方法2。

  • 方法一:通过HTMLMediaElement流式加载
  <audio src="1.mp3"></audio>
  <script>
    let audio = document.querySelector('audio');
    let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    audio.addEventListener('canplay', function () {
      let source = audioCtx.createMediaElementSource(audio);
      source.connect(audioCtx.destination);
      audio.play()
    })
  </script>
  • 方法二:通过XMLHttpRequest获取资源
    let xhr = new XMLHttpRequest();
    xhr.open('GET', '1.mp3', true);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function () {
      audioCtx.decodeAudioData(xhr.response, function (buffer) {
        getBufferSuccess(buffer)
      })
    }
  • 方法三:通过input file获取
    let input = document.querySelector('input');
    input.addEventListener('change', function () {
      if (this.files.length !== 0) {
        let file = this.files[0];
        let fr = new FileReader();
        fr.onload = function () {
          let fileRet = e.target.result;
          audioCtx.decodeAudioData(fileRet, function (buffer) {
            getBufferSuccess(buffer);
          }, function (err) {
            console.log(err)
          })
        }
        fr.readAsArrayBuffer(file);
      }
    })

处理音频数据

function getBufferSuccess(buffer) {
      // 创建频率分析节点
      let analyser = audioCtx.createAnalyser();
      // 确定频域的快速傅里叶变换大小
      analyser.fftSize = 2048;
      // 这个属性可以让最后一个分析帧的数据随时间使值之间的过渡更平滑。
      analyser.smoothingTimeConstant = 0.6;
      // 创建播放对象节点
      let source = audioCtx.createBufferSource();
      // 填充音频buffer数据
      source.buffer = buffer;
      // 创建音量节点(如果你需要用调整音量大小的话)
      let gainNode = audioCtx.createGain();
      
      // 连接节点对象
      source.connect(gainNode);
      gainNode.connect(analyser);
      analyser.connect(audioCtx.destination);
    }

获取音频频率

  • 方法一:用js的方法获取(通过监听audioprocess事件,由于性能问题,将会被弃用,不做详细说明,感兴趣的可以了解一下)
      // 此方法需要补充节点的连接
      let javascriptNode = audioCtx.createScriptProcessor(2048, 1, 1);
      javascriptNode.connect(audioCtx.destination);
      analyser.connect(javascriptNode);
      
        this.javascriptNode.onaudioprocess = function () {
            currData = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(currData);
        }

  • 方法二:用AnalyserNode获取

获取AnalyserNode节点里的频率长度frequencyBinCount,实例化长度为8位的整型数组,通过AnalyserNode.getByteFrequencyData将节点中的频率数据拷贝到数组中去,值的大小在0 - 256之间,数值越高表明频率越高;AnalyserNode.getByteTimeDomainData原理一样,不过获取的是频率大小,两种方法根据需求选一种即可。

    function getData () {
      // analyser.frequencyBinCount 可视化值的数量,是前面fftSize的一半
      let currData = new Uint8Array(analyser.frequencyBinCount);
      analyser.getByteFrequencyData(currData);
      analyser.getByteTimeDomainData(currData);
    }

输出设备

AudioBufferSourceNode.start(n) n表示开始的时间,默认为0,开始播放音频
AudioBufferSourceNode.stop(n) 音频在第n秒时间停止,若没有传值表示立即停止

其他api

AudioContext.resume() 控制音频的播放
AudioContext.suspend() 控制音频的暂停
AudioContext.currentTime 获取当前音频播放时间
AudioBufferSourceNode.buffer.duration 获取音频的播放总时长
GainNode.gain.value 控制音量大小 [0, 1]
GainNode.gain.linearRampToValueAtTime 实现音量的渐入渐出

Canvas绘制可视化效果

了解上面的api,就可以来着手绘制啦~,你想绘啥就绘啥,频繁的调用canvas的api很耗性能问题,这里讲下我在测试中提高性能的小技巧。

  • 多分层canvas,一些不需要频繁改动的绘制,例如背景,固定的装饰绘制,可以采用另一个canvas的上下文来绘制
  • 离屏绘制,原理是生成一个没有出现在页面的canvas,在这个缓存的canvas中绘制,而真正展示的canvas只需要通过drawImage这个api将画面绘制出来即可,参考此博文
  • 固定好lineWidth的长度,而不是每绘制一个就设定一次lineWidth
  • 绘制区域提前计算好,不要让canvas边绘制同时还要计算位置(canvas:好累哦~)
    总而言之,少调用canvas api,可是也不要为了提高性能而抛弃你的一些天马星空的想法哦

遇到的问题

在切换歌曲中,遇到了这个报错Failed to set the 'buffer' property on 'AudioBufferSourceNode': Cannot set buffer to non-null after it has been already been set to a non-null buffer at AudioContext,大致是讲AudioBufferSourceNode的buffer属性在之前我已经设置过了,不能被重新设置新的buffer值,由于播放歌曲主要是通过其数组缓冲区ArrayBuffer来进行,可看看issue,解决办法就是当需要切换歌曲情况下,将当前的AudioBufferSourceNode销毁,重新创建上下文环境,音频节点,连接等操作。

源码在这,交互部分写得有点乱,因为当时原来只是想练练可视化,之后想到啥功能就加,所以导致代码看起来冗余繁琐,大家可以参考看看audio实现,主要在MusicPlay对象。

小白第一次发表博文,发现写博文比写一个demo还要时间长,怕写出来的东西有错误会误导大家(有错误请大家评论指出~),所以会去查很多相关资料,这个过程也是学习的过程,以后会经常写写博文滴!最后,希望大家通过这篇文章也能学会自己做这种可视化的效果,配合一些可视化库还能做出很酷炫的效果呢,一起互相学习进步吧,加油!(。・д・。)

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容