如今的HTML5技術正讓網頁變得越來越強大,通過其Canvas
標簽與AudioContext
對象可以輕松實現之前在Flash或Native App中才能實現的頻譜指示器的功能。
Demo: Cyandev Works - HTML5 Audio Visualizing
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標簽
在你的頁面中找個位置插入:
<input id="fileChooser" type="file" />
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 部分
<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>
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'); }