如今的HTML5技術(shù)正讓網(wǎng)頁變得愈來愈強(qiáng)大,通過其Canvas
標(biāo)簽與AudioContext
對象可以輕松實(shí)現(xiàn)之前在Flash或Native App中才能實(shí)現(xiàn)的頻譜唆使器的功能。
Demo: Cyandev Works - HTML5 Audio Visualizing
The
AudioContext
interface represents an audio-processing graph built from audio modules linked together, each represented by anAudioNode
.
根據(jù)MDN的文檔,AudioContext
是1個(gè)專門用于音頻處理的接口,并且工作原理是將AudioContext
創(chuàng)建出來的各種節(jié)點(diǎn)(AudioNode
)相互連接,音頻數(shù)據(jù)流經(jīng)這些節(jié)點(diǎn)并作出相應(yīng)處理。
由于閱讀器兼容性問題,我們需要為不同閱讀器配置AudioContext
,在這里我們可以用下面這個(gè)表達(dá)式來統(tǒng)1對AudioContext
的訪問。
var AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext(); //實(shí)例化AudioContext對象
附. 閱讀器兼容性
閱讀器 Chrome Firefox IE Opera Safari 支持版本 10.0 25.0 不支持 15.0 6.0
固然,如果閱讀器不支持的話,我們也沒有辦法,用IE的人們我想也不需要這些效果。但最好實(shí)踐是使用的時(shí)候判斷1下上面聲明的變量是不是為空,然后再做其他操作。
讀取到的音頻文件是2進(jìn)制類型,我們需要讓AudioContext
先對其解碼,然后再進(jìn)行后續(xù)操作。
audioContext.decodeAudioData(binary, function(buffer) { ... });
方法decodeAudioData
被調(diào)用后,閱讀器將開始解碼音頻文件,這需要1定時(shí)間,我們應(yīng)當(dāng)讓用戶知道閱讀器正在解碼,解碼成功后會調(diào)用傳進(jìn)去的回調(diào)函數(shù),decodeAudioData
還有第3個(gè)可選參數(shù)是在解碼失敗時(shí)調(diào)用的,我們這里就先不實(shí)現(xiàn)了。
這是最關(guān)鍵的1步,我們需要兩個(gè)音頻節(jié)點(diǎn):
AudioBufferSourceNode
AnalyserNode
前者是用于播放解碼出來的buffer的節(jié)點(diǎn),而后者是用于分析音頻頻譜的節(jié)點(diǎn),兩個(gè)節(jié)點(diǎn)順次連接就可以完成我們的工作。
var audioBufferSourceNode;
audioBufferSourceNode = audioContext.createBufferSource();
var analyser;
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
上面的fftSize
是用于肯定FFT大小的屬性,那FFT是甚么高3的博主還不知道,其實(shí)也不需要知道,總之最后獲得到的數(shù)組長度應(yīng)當(dāng)是fftSize
值的1半,還應(yīng)當(dāng)保證它是以2為底的冪。
audioBufferSourceNode.connect(analyser);
analyser.connect(audioContext.destination);
上面的audioContext.destination
是音頻要終究輸出的目標(biāo),我們可以把它理解為聲卡。所以所有節(jié)點(diǎn)中的最后1個(gè)節(jié)點(diǎn)應(yīng)當(dāng)再連接到audioContext.destination
才能聽到聲音。
所有工作就緒,在解碼終了時(shí)調(diào)用的回調(diào)函數(shù)中我們就能夠開始播放了。
audioBufferSourceNode.buffer = buffer; //回調(diào)函數(shù)傳入的參數(shù)
audioBufferSourceNode.start(0); //部份閱讀器是noteOn()函數(shù),用法相同
參數(shù)代表播放出發(fā)點(diǎn),我們這里設(shè)置為0意味著從頭播放。
HTML5支持文件選擇、讀取的特性,我們利用這個(gè)特性可以實(shí)現(xiàn)不上傳,即播放的功能。
在你的頁面中找個(gè)位置插入:
<input id="fileChooser" type="file" />
var file;
var fileChooser = document.getElementById('fileChooser');
fileChooser.onchange = function() {
if (fileChooser.files[0]) {
file = fileChooser.files[0];
// Do something with 'file'...
}
}
var fileContent;
var fileReader = new FileReader();
fileReader.onload = function(e) {
fileContent = e.target.result;
// Do something with 'fileContent'...
}
fileReader.readAsArrayBuffer(file);
其實(shí)這里的fileContent
就是上面AudioContext
要解碼的那個(gè)binary,至此兩部份的工作就能夠連起來了。
WARNING:
Chrome或Firefox閱讀器的跨域訪問限制會使
FileReader
在本地失效,Chrome用戶可在調(diào)試時(shí)添加命令行參數(shù):chrome.exe --disable-web-security
這1部份我不打算詳細(xì)敘述,就提幾個(gè)重點(diǎn)。
在繪制之前通過下面的方法獲得到AnalyserNode
分析的數(shù)據(jù):
var dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
數(shù)組中每一個(gè)元素是從0到fftSize
屬性值的數(shù)值,這樣我們通過1定比例就可以控制能量條的高度等狀態(tài)。
要使動畫動起來,我們需要不斷重繪Canvas
標(biāo)簽里的內(nèi)容,這就需要requestAnimationFrame
這個(gè)函數(shù)了,它可以幫你以60fps的幀率繪制動畫。
使用方法:
var draw = function() {
// ...
window.requestAnimationFrame(draw);
}
window.requestAnimationFrame(draw);
這段代碼應(yīng)當(dāng)不難理解,就是1個(gè)類似遞歸的調(diào)用,但不是遞歸,有點(diǎn)像Android中的postInvalidate
貼上我寫的1段繪制代碼:
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,大致就是這樣,以后可以加1些css樣式,完善1下業(yè)務(wù)邏輯,這里就不再闡釋了。最后貼上整理好的全部代碼:
<html>
<head>
<title>HTML5 Audio Visualizing</title>
<style type="text/css">
body {
background-color: #222222
}
input {
color: #ffffff
}
#wrapper {
display: table;
width: 100%;
height: 100%;
}
#wrapper-inner {
display: table-cell;
vertical-align: middle;
padding-left: 25%;
padding-right: 25%;
}
#tip {
color: #fff;
opacity: 0;
transition: opacity 1s;
-moz-transition: opacity 1s;
-webkit-transition: opacity 1s;
-o-transition: opacity 1s;
}
#tip.show {
opacity: 1
}
</style>
<script type="text/javascript" src="./index.js"></script>
</head>
<body>
<div id="wrapper">
<div id="wrapper-inner">
<p id="tip">Decoding...</p>
<input id="fileChooser" type="file" />
<br>
<canvas id="visualizer" width="800" height="400">Your browser does not support Canvas tag.</canvas>
</div>
</div>
</body>
</html>
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); //先判斷是不是已調(diào)用1次
}
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');
}
以上。