Web Audio API(网络音频API)过去几年中已经改进很多,通过网页播放声音和音乐已经成为了可能。但这还不够,不同浏览器的行为方式还有不同。但至少已经实现了.
在这篇文章中,我们将通过DOM和Web Audio API创建一个可视化音频的例子。由于Firefox还不能正确处理CORS,Safari浏览器存在一些字节处理问题,这个演示只能在Chrome上使用。 注* 形状会波形而变化.
首先我们需要创建一个audio组件,通过预加载(preloading)和流式(streaming)播放时时处理.
AudioContext是Web Audio API的基石,我们将创建一个全局的AudioContext对象,然后用它"线性"处理字节流.
通过XMLHttpRequest的代理,我们能从服务器获取数据时做一些时髦而有用的处理。在这种情况下,我们将.mp3音频文件转化为数组缓冲区ArrayBuffer,这使得它能更容易地与Web Audio API交互。
在XHR的onload处理函数中,该文件的数组缓冲区将在 response 属性中,而不是通常的responseText。现在,我们有array buffer,我们可以继续将其作为音频的缓冲区源。首先,我们将需要使用decodeAudioData异步地转换ArrayBuffer到AudioBuffer。
通过XHR预加载文件的方法对小文件很实用,但也许我们不希望用户要等到整个文件下载完才开始播放。这里我们将使用一个稍微不同点的方法,它能让我们使用HTMLMediaElement的流媒体功能。
我们可以使用<audio>元素流式加载音乐文件, 在JavaScript中调用createMediaElementSource方式, 直接操作HTMLMediaElement, 像play()和pause()的方法均可调用.
这个方法减少了大量的代码,而且对于我们的示例来说更加合适,现在让我们整理一下代码用promise模式来定义一个Sound的Class类.
现在我们能播放音乐文件,我们将继续尝试来获取audio的频率数据.
在开始从audio context获取实时数据前,我们要连线两个独立的音频节点。这些节点可以从一开始定义时就进行连接。
现 在我们定义好了analyser节点和数据流,我们需要略微更改一下Sound类的定义,除了将音频源和audio context连接,我们还需要将其与analyser连接.我们同样需要添加一个audioprocess处理processor节点.当播放结束时再 移除.
这也意味着我们的连接关系大致是这样的:
为了获取频率数据, 我们只需要简单地将audioprocess处理函数改成这样:
现在所有的关于audio的东西都已经解决了, 现在我们需要将波形可视化地输出来,在这个例子中我们将使用DOM节点和 requestAnimationFrame. 这也意味着我们将从输出中获取更多的功能. 在这个功能中,我们将借助CSS的一些属性如:transofrm和opacity.
我们先在文档中添加一些css和logo.
现在,最重要的一点,我们将会把图片切成很多列.这是通过JavaScript完成的.
我们需要为每一列添加一个遮罩层,然后按x轴进行偏移.
每当audioprocess处理函数接收到数据,我们就需要重新渲染,这时 requestAnimationFrame 就派上用场了.
现在我们完成了所有的DOM构建, 完整的在线示例 , 完整的源码文件同样在此DEMO中.
在这篇文章中,我们将通过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中.
文章好极了。
var audio = new Audio(); firefox已经支持了
修改了MP3地址就无法播放,用他的地址就可以正常播放。
说明你的mp3的格式有问题,可以重新编码一下。
能否用一个特别大的mp3文件做测试?浏览器会crash掉。 mp3文件超过140M。
不错不错,很有学习借鉴意义的,给楼主点赞。这个分享挺棒的,辛苦了。 https://www.cnblogs.com/xiaoyan2017/p/10163043.html