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的節點,而后者是用於分析音頻頻譜的節點,兩個節點順次連接就能完成我們的工作。
創建AudioBufferSourceNodevar audioBufferSourceNode;
audioBufferSourceNode = audioContext.createBufferSource();
創建AnalyserNodevar 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 Visualizingbackground-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}
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');
}
以上。