使用Web Audio API实现简单的音频可视化

前言

之前刚好看到Web Audio API方面的内容,因此用了相关api做了个音频可视化的页面。实现:

  • 音频播放/暂停
  • 音频声量控制
  • 音频立体声控制
  • 音频频率可视化
  • 音频切换

预备知识

Web Audio API中一个关键的对象就是音频上下文(AudioContext),可以类比canvas context,在AudioContext我们进行相关的操作。音频处理的一个典型流程为:

  • 创建音频上下文(AudioContext)
  • 在音频上下文里创建源 — 例如 <audio>(HTMLAudioElement), 音频数据(Ajax 获取的 AudioBufferSourceNode ), 流(MediaStreamAudioSourceNode)
  • 创建效果节点,例如混响、双二阶滤波器、平移、压缩
  • 为音频选择一个目的地(destination),例如你的系统扬声器
  • 连接(connect)源到效果器,对目的地进行效果输出

description

  • 具体建立过程
    // 1. 创建上下文
    var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    // 2. 获取音频源(MediaElementAudioSourceNode)
    // 这里音频源可以使用 OscillatorNode 创建,也使用麦克风源等
    var audioElement = document.querySelector('audio');
    var track = audioContext.createMediaElementSource(audioElement);
    // ...这里我们已经可以将声音连接到系统扬声器
    // track.connect(audioContext.destination);
    // 3. 需要先启动音频环境,否则可能会提示需要手势触发的警告
    // resume() 和 suspend() 返回的都是异步对象,最好使用then等待异步处理结束
    if (audioContext.state === 'suspended') {
      audioContext.resume();
    }
    // 4. 播放控制
    audioElement.play();
    audioElement.pause();
    // 5. 声音控制
    const gainNode = audioContext.createGain();
    track.connect(gainNode).connect(audioContext.destination)// 6. 立体声平移控制
    const pannerOptions = { pan: 0 };
    const panner = new StereoPannerNode(audioContext, pannerOptions);
    // 官网使用构造器方法,但是在Edge中会提示错误,可以使用以下的工厂方法代替
    this.panner = this.audioContext.createStereoPanner();
    this.panner.pan.value = 0.0;
    // 7. 连接到系统扬声器
    track.connect(gainNode).connect(panner).connect(audioContext.destination)
    
    track

实现细节

  • 可视化
    // 1. 创建AnalyserNode对象
    var analyser = audioContext.createAnalyser();
    // 2. 获取柱形图需要的数量,初始化一个dataArray
    var bufferLength = analyser.frequencyBinCount;
    var dataArray = new Uint8Array(bufferLength);
    // 3. 获得绘制输出
    var canvasCtx = canvasElement.getContext('2d');
    // 4. 获得 canvas 相关参数
    var WIDTH = canvasElement.width;
    var HEIGHT = canvasElement.height;
    // 5. 定义需要的条形数目,可自己定制
    count = dataArray.length;
    // 6. 绘制canvas
    function draw () {
      canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
      analyser.getByteFrequencyData(dataArray);
      let value = 0,
          step = Math.round(dataArray.length / count), // 获得绘制间隔
          x = 0,
          y = 0,
          lineWidth = canvasCtx.lineWidth = WIDTH / count, // 描边宽度
          index = count;
      canvasCtx.strokeStyle = "#fff"; // 描边颜色
      while (index) {
          value = dataArray[index * step + step]; // 描边高度相关
          x = index * lineWidth;
          y = HEIGHT - value * 1.5;
          canvasCtx.beginPath(); // 开始绘制
          canvasCtx.moveTo(x, HEIGHT); // 从坐标轴x点开始绘制
          canvasCtx.lineTo(x, y); // 到需要的高度结束
          canvasCtx.stroke();
          index -= 2; // 调整绘制间隔
      }
      requestAnimationFrame(() => draw());
    };
    
  • 播放控制

    function playHandler () {
      if (playState === false) {
          audioElement.play();
          playState = true;
          playButton.dataset.playing = 'true';
      } else {
          audioElement.pause();
          playState = false;
          playButton.dataset.playing = 'false';
      }
    }
    
  • 音频切换

    如果多个音频关联到一个 GainNode 对象,切换前注意断开连接
    

    我们通过 audioContext.createMediaElementSource(audioElement) 已经将上下文对象与一个 HTMLAudioElement 对象关联,我们后面进行操作可以通过切换这个媒体对象的src实现(或者重新new一个AudioContext与新的媒体对象关联,不推荐)

    // 假如我们之前有这样的连接
    function setTrack () {
      track.connect(gainNode)
          .connect(panner)
          .connect(analyser)
          .connect(audioContext.destination);
    }
    // 那么我们进行音频切换时要进行如下操作
    function changeAudio (index) {
      let idx = index || 0;
      if (playList.length <= 0 || idx > playList.length) return;
          playState = false;
          track && track.disconnect();
          gainNode && gainNode.disconnect();
          panner && panner.disconnect();
          analyser && analyser.disconnect();
          audioElement.src = playList[idx].src;
          audioElement.load();
          _enableControls();
    }
    
  • 具体实现效果
    实现效果

常用API

  • AudioContext.currentTime 以双精度浮点型数字返回硬件调用的秒数 (readonly)
  • AudioContext.state 返回AudioContext当前状态 (readonly)
  • AnalyserNode.frequencyBinCount 一个无符号长整形 (unsigned long) 的值, 值为fftSize的一半。这通常等于将要用于可视化的数据值的数量。
  • AudioContext.createMediaElementSource() 创建一个 MediaElementAudioSourceNode 接口来关联 HTMLMediaElement . 这可以用来播放和处理来自<video><audio> 元素的音频
  • AudioContext.resume() 重新启动一个已被暂停的音频环境
  • AudioContext.suspend() 暂停音频内容的进度.暂时停止音频硬件访问和减少在过程中的CPU/电池使用
  • AudioContext.close() 关闭一个音频环境, 释放任何正在使用系统资源的音频
  • AudioContext.decodeAudioData() 从ArrayBuffer对象中异步解码音频文件
  • AudioContext.createGain() 创建一个GainNode,它可以控制音频的总音量
  • AudioContext.createPanner() 创建一个PannerNode, 它为音源创建一个3D音源环境
  • AudioContext.createAnalyser() 创建一个AnalyserNode,它可以用来显示音频时间和频率的数据
  • AudioContext.createStereoPanner() 创建一个使用立体声的音频源 StereoPannerNode
  • AudioContext.createAnalyser() 创建一个AnalyserNode,可以用来获取音频时间和频率数据,以及实现数据可视化。
  • AnalyserNode.getByteFrequencyData() 将当前频域数据拷贝进Uint8Array数组
  • 这里注意closed状态是不可逆的(close后要重新new),三种状态操作都是异步操作
      new AudioContext()
            |
            V
      +----------+                 +------------+
      | running  | -- suspend() -> | suspended  |
      |          | <- resume() --- |            |
      +----------+                 +------------+
            |                              |
            | close()                      | close()
            +------------------------------+
            |
            V
      +-----------+
      |  closed   |
      +-----------+
    

不兼容IE
项目预览codepen
项目地址github

参考资料


 上一篇
CSS动画技巧及性能 CSS动画技巧及性能
接着前文看《CSS权威指南》,本文整理一些CSS动画的知识点,同时给出一些例子。 预备知识变形 Transform在CSS动画中我们经常需要通过变形来达到一些奇妙的效果。使用 transform 首先要明确变形是基于笛卡尔坐标系,也就是通常
2019-07-09
下一篇 
整理一些CSS的知识 整理一些CSS的知识
最近在翻《CSS权威指南》,一些零散的知识点平时不太注意,这里记录一下。 CSS属性displaydisplay指定了元素的显示类型,它包含两类基础特征,用于指定元素怎样生成盒模型——外部显示类型定义了元素怎样参与流式布局的处理,内部显示类
2019-06-02
  目录