通过Web Audio API可视化输出MP3音乐频率波形
jopen
10年前
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