web-audio-api一面之缘

683 阅读5分钟

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