使用HTML5 API(AudioContext)實現可視化頻譜效果


如今的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'); }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM