html 频谱显示效果,使用HTML5 API(AudioContext)实现可视化频谱效果

HTML5 可视化频谱效果

如今的HTML5技术正让网页变得越来越强大,通过其Canvas标签与AudioContext对象可以轻松实现之前在Flash或Native App中才能实现的频谱指示器的功能。

开始使用AudioContext

The AudioContext interface represents an audio-processing graph built from audio modules linked together, each represented by an AudioNode.

根据MDN的文档,AudioContext是一个专门用于音频处理的接口,并且工作原理是将AudioContext创建出来的各种节点(AudioNode)相互连接,音频数据流经这些节点并作出相应处理。

创建AudioContext对象

由于浏览器兼容性问题,我们需要为不同浏览器配置AudioContext,在这里我们可以用下面这个表达式来统一对AudioContext的访问。

var AudioContext = window.AudioContext || window.webkitAudioContext;

var audioContext = new AudioContext(); //实例化AudioContext对象

附. 浏览器兼容性

浏览器

Chrome

Firefox

IE

Opera

Safari

支持版本

10.0

25.0

不支持

15.0

6.0

当然,如果浏览器不支持的话,我们也没有办法,用IE的人们我想也不需要这些效果。但最佳实践是使用的时候判断一下上面声明的变量是否为空,然后再做其他操作。

解码音频文件

读取到的音频文件是二进制类型,我们需要让AudioContext先对其解码,然后再进行后续操作。

audioContext.decodeAudioData(binary, function(buffer) { ... });

方法decodeAudioData被调用后,浏览器将开始解码音频文件,这需要一定时间,我们应该让用户知道浏览器正在解码,解码成功后会调用传进去的回调函数,decodeAudioData还有第三个可选参数是在解码失败时调用的,我们这里就先不实现了。

创建音频处理节点

这是最关键的一步,我们需要两个音频节点:

AudioBufferSourceNode

AnalyserNode

前者是用于播放解码出来的buffer的节点,而后者是用于分析音频频谱的节点,两个节点顺次连接就能完成我们的工作。

创建AudioBufferSourceNode

var audioBufferSourceNode;

audioBufferSourceNode = audioContext.createBufferSource();

创建AnalyserNode

var analyser;

analyser = audioContext.createAnalyser();

analyser.fftSize = 256;

上面的fftSize是用于确定FFT大小的属性,那FFT是什么高三的博主还不知道,其实也不需要知道,总之最后获取到的数组长度应该是fftSize值的一半,还应该保证它是以2为底的幂。

连接节点

audioBufferSourceNode.connect(analyser);

analyser.connect(audioContext.destination);

上面的audioContext.destination是音频要最终输出的目标,我们可以把它理解为声卡。所以所有节点中的最后一个节点应该再连接到audioContext.destination才能听到声音。

播放音频

所有工作就绪,在解码完毕时调用的回调函数中我们就可以开始播放了。

audioBufferSourceNode.buffer = buffer; //回调函数传入的参数

audioBufferSourceNode.start(0); //部分浏览器是noteOn()函数,用法相同

参数代表播放起点,我们这里设置为0意味着从头播放。

文件读取

HTML5支持文件选择、读取的特性,我们利用这个特性可以实现不上传,即播放的功能。

HTML标签

在你的页面中找个位置插入:

Js逻辑

var file;

var fileChooser = document.getElementById('fileChooser');

fileChooser.onchange = function() {

if (fileChooser.files[0]) {

file = fileChooser.files[0];

// Do something with 'file'...

}

}

使用FileReader异步读取文件

var fileContent;

var fileReader = new FileReader();

fileReader.onload = function(e) {

fileContent = e.target.result;

// Do something with 'fileContent'...

}

fileReader.readAsArrayBuffer(file);

其实这里的fileContent就是上面AudioContext要解码的那个binary,至此两部分的工作就可以连起来了。

WARNING:

Chrome或Firefox浏览器的跨域访问限制会使FileReader在本地失效,Chrome用户可在调试时添加命令行参数:

chrome.exe --disable-web-security

Canvas绘制频谱

这一部分我不打算详细叙述,就提几个重点。

AnalyserNode数据解析

在绘制之前通过下面的方法获取到AnalyserNode分析的数据:

var dataArray = new Uint8Array(analyser.frequencyBinCount);

analyser.getByteFrequencyData(dataArray);

数组中每个元素是从0到fftSize属性值的数值,这样我们通过一定比例就能控制能量条的高度等状态。

requestAnimationFrame的使用

要使动画动起来,我们需要不断重绘Canvas标签里的内容,这就需要requestAnimationFrame这个函数了,它可以帮你以60fps的帧率绘制动画。

使用方法:

var draw = function() {

// ...

window.requestAnimationFrame(draw);

}

window.requestAnimationFrame(draw);

这段代码应该不难理解,就是一个类似递归的调用,但不是递归,有点像Android中的postInvalidate

实例代码

贴上我写的一段绘制代码:

var render = function() {

ctx = canvas.getContext("2d");

ctx.strokeStyle = "#00d0ff";

ctx.lineWidth = 2;

ctx.clearRect(0, 0, canvas.width, canvas.height); //清理画布

var dataArray = new Uint8Array(analyser.frequencyBinCount);

analyser.getByteFrequencyData(dataArray);

var step = Math.round(dataArray.length / 60); //采样步长

for (var i = 0; i < 40; i++) {

var energy = (dataArray[step * i] / 256.0) * 50;

for (var j = 0; j < energy; j++) {

ctx.beginPath();

ctx.moveTo(20 * i + 2, 200 + 4 * j);

ctx.lineTo(20 * (i + 1) - 2, 200 + 4 * j);

ctx.stroke();

ctx.beginPath();

ctx.moveTo(20 * i + 2, 200 - 4 * j);

ctx.lineTo(20 * (i + 1) - 2, 200 - 4 * j);

ctx.stroke();

}

ctx.beginPath();

ctx.moveTo(20 * i + 2, 200);

ctx.lineTo(20 * (i + 1) - 2, 200);

ctx.stroke();

}

window.requestAnimationFrame(render);

}

OK,大致就是这样,之后可以加一些css样式,完善一下业务逻辑,这里就不再阐释了。最后贴上整理好的全部代码:

HTML 部分

HTML5 Audio Visualizing

Decoding...

Your browser does not support Canvas tag.

Js部分

var AudioContext = window.AudioContext || window.webkitAudioContext; //Cross browser variant.

var canvas, ctx;

var audioContext;

var file;

var fileContent;

var audioBufferSourceNode;

var analyser;

var loadFile = function() {

var fileReader = new FileReader();

fileReader.onload = function(e) {

fileContent = e.target.result;

decodecFile();

}

fileReader.readAsArrayBuffer(file);

}

var decodecFile = function() {

audioContext.decodeAudioData(fileContent, function(buffer) {

start(buffer);

});

}

var start = function(buffer) {

if(audioBufferSourceNode) {

audioBufferSourceNode.stop();

}

audioBufferSourceNode = audioContext.createBufferSource();

audioBufferSourceNode.connect(analyser);

analyser.connect(audioContext.destination);

audioBufferSourceNode.buffer = buffer;

audioBufferSourceNode.start(0);

showTip(false);

window.requestAnimationFrame(render); //先判断是否已经调用一次

}

var showTip = function(show) {

var tip = document.getElementById('tip');

if (show) {

tip.className = "show";

} else {

tip.className = "";

}

}

var render = function() {

ctx = canvas.getContext("2d");

ctx.strokeStyle = "#00d0ff";

ctx.lineWidth = 2;

ctx.clearRect(0, 0, canvas.width, canvas.height);

var dataArray = new Uint8Array(analyser.frequencyBinCount);

analyser.getByteFrequencyData(dataArray);

var step = Math.round(dataArray.length / 60);

for (var i = 0; i < 40; i++) {

var energy = (dataArray[step * i] / 256.0) * 50;

for (var j = 0; j < energy; j++) {

ctx.beginPath();

ctx.moveTo(20 * i + 2, 200 + 4 * j);

ctx.lineTo(20 * (i + 1) - 2, 200 + 4 * j);

ctx.stroke();

ctx.beginPath();

ctx.moveTo(20 * i + 2, 200 - 4 * j);

ctx.lineTo(20 * (i + 1) - 2, 200 - 4 * j);

ctx.stroke();

}

ctx.beginPath();

ctx.moveTo(20 * i + 2, 200);

ctx.lineTo(20 * (i + 1) - 2, 200);

ctx.stroke();

}

window.requestAnimationFrame(render);

}

window.onload = function() {

audioContext = new AudioContext();

analyser = audioContext.createAnalyser();

analyser.fftSize = 256;

var fileChooser = document.getElementById('fileChooser');

fileChooser.onchange = function() {

if (fileChooser.files[0]) {

file = fileChooser.files[0];

showTip(true);

loadFile();

}

}

canvas = document.getElementById('visualizer');

}

以上。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值