通过Web Audio API可视化输出MP3音乐频率波形

jopen 9年前

Web Audio API(网络音频API)过去几年中已经改进很多,通过网页播放声音和音乐已经成为了可能。但这还不够,不同浏览器的行为方式还有不同。但至少已经实现了.

在这篇文章中,我们将通过DOM和Web Audio API创建一个可视化音频的例子。由于Firefox还不能正确处理CORS,Safari浏览器存在一些字节处理问题,这个演示只能在Chrome上使用。 注* 形状会波形而变化.



Audio 组件


首先我们需要创建一个audio组件,通过预加载(preloading)和流式(streaming)播放时时处理.

创建Audio上下文Context


AudioContext是Web Audio API的基石,我们将创建一个全局的AudioContext对象,然后用它"线性"处理字节流.


/* 创建一个 AudioContext */  var context;    /* 尝试初始化一个新的 AudioContext, 如果失败抛出 error */  try {      /* 创建 AudioContext. */      context = new AudioContext();  } catch(e) {      throw new Error('The Web Audio API is unavailable');  }


通过XHR预加载MP3(AJAX)


通过XMLHttpRequest的代理,我们能从服务器获取数据时做一些时髦而有用的处理。在这种情况下,我们将.mp3音频文件转化为数组缓冲区ArrayBuffer,这使得它能更容易地与Web Audio API交互。


/*一个新的 XHR 对象 */  var xhr = new XMLHttpRequest();  /* 通过 GET 请连接到 .mp3 */  xhr.open('GET', '/path/to/audio.mp3', true);  /* 设置响应类型为字节流 arraybuffer */  xhr.responseType = 'arraybuffer';  xhr.onload = function() {      /* arraybuffer 可以在 xhr.response 访问到 */  };  xhr.send();

在 XHR的onload处理函数中,该文件的数组缓冲区将在 response 属性中,而不是通常的responseText。现在,我们有array buffer,我们可以继续将其作为音频的缓冲区源。首先,我们将需要使用decodeAudioData异步地转换ArrayBuffer到 AudioBuffer。


/* demo的音频缓冲缓冲源 */  var sound;    xhr.onload = function() {      sound = context.createBufferSource();        context.decodeAudioData(xhr.response, function(buffer) {          /* 将 buffer 传入解码 AudioBuffer. */          sound.buffer = buffer;          /*连接 AudioBufferSourceNode 到 AudioContext */          sound.connect(context.destination);      });  };


通过XHR预加载文件的方法对小文件很实用,但也许我们不希望用户要等到整个文件下载完才开始播放。这里我们将使用一个稍微不同点的方法,它能让我们使用HTMLMediaElement的流媒体功能。


通过HTML Media元素流式加载


我们可以使用<audio>元素流式加载音乐文件, 在JavaScript中调用createMediaElementSource方式, 直接操作HTMLMediaElement, 像play()和pause()的方法均可调用.


/* 声明我们的 MediaElementAudioSourceNode 变量 */  var sound,      /* 新建一个 `<audio>` 元素. Chrome 支持通过 `new Audio()` 创建,       * Firefox 需要通过 `createElement` 方法创建. */      audio = new Audio();    /* 添加 `canplay` 事件侦听当文件可以被播放时. */  audio.addEventListener('canplay', function() {      /* 现在这个文件可以 `canplay` 了, 从 `<audio>` 元素创建一个       * MediaElementAudioSourceNode(媒体元素音频源结点) . */      sound = context.createMediaElementSource(audio);      /* 将 MediaElementAudioSourceNode 与 AudioContext 关联 */      sound.connect(context.destination);      /*通过我们可以 `play` `<audio>` 元素了 */      audio.play();  });  audio.src = '/path/to/audio.mp3';

这个方法减少了大量的代码,而且对于我们的示例来说更加合适,现在让我们整理一下代码用promise模式来定义一个Sound的Class类.


/* Hoist some variables. */  var audio, context;    /* Try instantiating a new AudioContext, throw an error if it fails. */  try {      /* Setup an AudioContext. */      context = new AudioContext();  } catch(e) {      throw new Error('The Web Audio API is unavailable');  }    /* Define a `Sound` Class */  var Sound = {      /* Give the sound an element property initially undefined. */      element: undefined,      /* Define a class method of play which instantiates a new Media Element       * Source each time the file plays, once the file has completed disconnect        * and destroy the media element source. */      play: function() {           var sound = context.createMediaElementSource(this.element);          this.element.onended = function() {              sound.disconnect();              sound = null;          }          sound.connect(context.destination);            /* Call `play` on the MediaElement. */          this.element.play();      }  };    /* Create an async function which returns a promise of a playable audio element. */  function loadAudioElement(url) {      return new Promise(function(resolve, reject) {          var audio = new Audio();          audio.addEventListener('canplay', function() {              /* Resolve the promise, passing through the element. */              resolve(audio);          });          /* Reject the promise on an error. */          audio.addEventListener('error', reject);          audio.src = url;      });  }    /* Let's load our file. */  loadAudioElement('/path/to/audio.mp3').then(function(elem) {      /* Instantiate the Sound class into our hoisted variable. */      audio = Object.create(Sound);      /* Set the element of `audio` to our MediaElement. */      audio.element = elem;      /* Immediately play the file. */      audio.play();  }, function(elem) {      /* Let's throw an the error from the MediaElement if it fails. */      throw elem.error;  });


现在我们能播放音乐文件,我们将继续尝试来获取audio的频率数据.


处理Audio音频数据



在开始从audio context获取实时数据前,我们要连线两个独立的音频节点。这些节点可以从一开始定义时就进行连接。

/* 声明变量 */  var audio,      context = new (window.AudioContext ||                     window.webAudioContext ||                     window.webkitAudioContext)(),      /* 创建一个1024长度的缓冲区 `bufferSize` */      processor = context.createScriptProcessor(1024),      /*创建一个分析节点 analyser node */      analyser = context.createAnalyser();    /* 将 processor 和 audio 连接 */  processor.connect(context.destination);  /* 将 processor 和 analyser 连接 */  analyser.connect(processor);    /* 定义一个 Uint8Array 字节流去接收分析后的数据 */  var data = new Uint8Array(analyser.frequencyBinCount);


现在我们定义好了analyser节点和数据流,我们需要略微更改一下Sound类的定义,除了将音频源和audio context连接,我们还需要将其与analyser连接.我们同样需要添加一个audioprocess处理processor节点.当播放结束时再移除.


play: function() {       var sound = context.createMediaElementSource(this.element);      this.element.onended = function() {          sound.disconnect();          sound = null;          /* 当文件结束时置空事件处理 */          processor.onaudioprocess = function() {};      }      /* 连接到 analyser. */      sound.connect(analyser);      sound.connect(context.destination);        processor.onaudioprocess = function() {          /* 产生频率数据 */          analyser.getByteTimeDomainData(data);      };      /* 调用 MediaElement 的 `play`方法. */      this.element.play();  }

这也意味着我们的连接关系大致是这样的:


MediaElementSourceNode \=> AnalyserNode => ScriptProcessorNode /=> AudioContext
                        \_____________________________________/


为了获取频率数据, 我们只需要简单地将audioprocess处理函数改成这样:


analyser.getByteFrequencyData(data);


可视化组件


现在所有的关于audio的东西都已经解决了, 现在我们需要将波形可视化地输出来,在这个例子中我们将使用DOM节点和 requestAnimationFrame. 这也意味着我们将从输出中获取更多的功能. 在这个功能中,我们将借助CSS的一些属性如:transofrm和opacity.

初始步骤


我们先在文档中添加一些css和logo.


<div class="logo-container">      <img class="logo" src="/path/to/image.svg"/>  </div>      .logo-container, .logo, .container, .clone {      width: 300px;      height: 300px;      position: absolute;      top: 0; bottom: 0;      left: 0; right: 0;      margin: auto;  }    .logo-container, .clone {      background: black;      border-radius: 200px;  }    .mask {      overflow: hidden;      will-change: transform;      position: absolute;      transform: none;      top: 0; left: 0;  }


现在,最重要的一点,我们将会把图片切成很多列.这是通过JavaScript完成的.


/* 开始可视化组件, 让我们定义一些参数. */  var NUM_OF_SLICES = 300,      /* `STEP` 步长值,       * 影响我们将数据切成多少列 */      STEP = Math.floor(data.length / NUM_OF_SLICES),      /* 当 analyser 不再接收数据时, array中的所有值都值是 128. */      NO_SIGNAL = 128;    /* 获取我们将切片的元素 */  var logo = document.querySelector('.logo-container');    /* 我们稍微会将切好的图片与数据交互 */  var slices = []      rect = logo.getBoundingClientRect(),      /* 感谢 Thankfully在 `TextRectangle`中给我们提供了宽度和高度属性 */      width = rect.width,      height = rect.height,      widthPerSlice = width / NUM_OF_SLICES;    /* 为切好的列,创建一个容器 */  var container = document.createElement('div');  container.className = 'container';  container.style.width = width + 'px';  container.style.height = height + 'px';


创建 'slices' 切片



我们需要为每一列添加一个遮罩层,然后按x轴进行偏移.


/* Let's create our 'slices'. */  for (var i = 0; i < NUM_OF_SLICES; i++) {      /* Calculate the `offset` for each individual 'slice'. */      var offset = i * widthPerSlice;        /* Create a mask `<div>` for this 'slice'. */      var mask = document.createElement('div');      mask.className = 'mask';      mask.style.width = widthPerSlice + 'px';      /* For the best performance, and to prevent artefacting when we       * use `scale` we instead use a 2d `matrix` that is in the form:       * matrix(scaleX, 0, 0, scaleY, translateX, translateY). We initially       * translate by the `offset` on the x-axis. */      mask.style.transform = 'matrix(1,0,0,1,' + offset + '0)';        /* Clone the original element. */      var clone = logo.cloneNode(true);      clone.className = 'clone';      clone.style.width = width + 'px';      /* We won't be changing this transform so we don't need to use a matrix. */      clone.style.transform = 'translate3d(' + -offset + 'px,0,0)';      clone.style.height = mask.style.height = height + 'px';        mask.appendChild(clone);      container.appendChild(mask);        /* We need to maintain the `offset` for when we       * alter the transform in `requestAnimationFrame`. */      slices.push({ offset: offset, elem: mask });  }    /* Replace the original element with our new container of 'slices'. */  document.body.replaceChild(container, logo);


定义我们的渲染函数



每当audioprocess处理函数接收到数据,我们就需要重新渲染,这时 requestAnimationFrame 就派上用场了.


/* Create our `render` function to be called every available frame. */  function render() {      /* Request a `render` on the next available frame.       * No need to polyfill because we are in Chrome. */      requestAnimationFrame(render);        /* Loop through our 'slices' and use the STEP(n) data from the       * analysers data. */      for (var i = 0, n = 0; i < NUM_OF_SLICES; i++, n+=STEP) {          var slice = slices[i],              elem = slice.elem,              offset = slice.offset;            /* Make sure the val is positive and divide it by `NO_SIGNAL`           * to get a value suitable for use on the Y scale. */          var val = Math.abs(data[n]) / NO_SIGNAL;          /* Change the scaleY value of our 'slice', while keeping it's           * original offset on the x-axis. */          elem.style.transform = 'matrix(1,0,0,' + val + ',' + offset + ',0)';          elem.style.opacity = val;      }  }    /* Call the `render` function initially. */  render();


现在我们完成了所有的DOM构建, 完整的在线示例 , 完整的源码文件同样在此DEMO中.

原文地址: fourthof5.com

来自:http://ourjs.com/detail/54d48406232227083e000029