【HTML5】Web Audio API打造超炫的音樂可視化效果


HTML5真是太多炫酷的東西了,其中Web Audio API算一個,琢磨着弄了個音樂可視化的demo,先上效果圖:

sreenshot

 項目演示:別說話,點我!  源碼已經掛到github上了,有興趣的同學也可以去star或者fork我,源碼注釋超清楚的哦~~之前看劉大神的文章和源碼,感覺其他方面的內容太多了,對初學者來說可能一下子難以抓到Web Audio API的重點,所以我就從一個初學者的角度來給大家說說Web Audio API這些事吧。

 

Web Audio API與HTML5提供的Audio標簽並不是同樣的東西,他們之間的區別可以自行搜索。簡單的說Audio就是一個自帶GUI的標簽,他對音頻的操作還是比較弱的,而Web Audio API則封裝了非常多的對音頻的操作,功能十分強大。Web Audio API目前還是一個草案,最新版本可以瀏覽這里:https://www.w3.org/TR/webaudio/,當然還有MDN上面的介紹:https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API#Example

注意:由於Web Audio API還是非常新,所以現在瀏覽器的支持不是非常好,具體兼容性如下:

 

一、整體流程:

一般來說,一個典型的Audio應用的流程都是這樣的:

1、創建音頻環境(audio Context)

      2、創建音源,可以通過audio標簽、文件流等方式

3、創建效果節點,可以是混響、壓縮、延遲、增益等效果器、也可以是分析節點。

4、為音頻選一個輸出,比說說你的揚聲器。然后將源、效果器和輸出連接起來。

玩過吉他或者貝斯的哥們應該很容易理解了:音頻環境就是你整個樂器硬件系統,音源就是你的樂器,效果節點就是各種效果器,當然也需要音箱輸出和各種連線了。要對音樂進行處理,最重要的就是效果器節點了。這個項目沒有增加其他效果,只用了一個分析節點,用來量化音頻信號再關聯到圖形元素上面,得到可視化效果。

 

二、最簡版本代碼

 1 <!DOCTYPE html>
 2 <html>
 3     <head>
 4         <meta charset="UTF-8">
 5         <title>簡單測試</title>
 6     </head>
 7     <body>
 8         <canvas id="myCanvas" width="400" height="400">您的瀏覽器不支持canvas標簽</canvas>
 9         <audio id="myAudio" src="simplelove.mp3" controls="controls" >您的瀏覽器不支持audio標簽</audio>
10         <script type="text/javascript">
11         window.onload = function () {
12             try {
13                 var audioCtx = new (window.AudioContext ||window.webkitAudioContext)();
14             } catch (err) {
15                 alert('!Your browser does not support Web Audio API!');
16             };
17             var myCanvas = document.getElementById('myCanvas'),
18                 canvasCtx = myCanvas.getContext("2d"),
19                 myAudio = document.getElementById("myAudio"),
20                 source = audioCtx.createMediaElementSource(myAudio),
21                 analyser = audioCtx.createAnalyser();
22             source.connect(analyser);
23             analyser.connect(audioCtx.destination);
24             myAudio.oncanplaythrough = function draw () {
25                 var cwidth = myCanvas.width,
26                     cheight = myCanvas.height,
27                     array = new Uint8Array(128);
28                 analyser.getByteFrequencyData(array);
29                 canvasCtx.clearRect(0, 0, cwidth, cheight);
30                 for (var i = 0; i < array.length; i++) {
31                     canvasCtx.fillRect(i * 3, cheight - array[i], 2, cheight);
32                 }
33                 requestAnimationFrame(draw);
34             };
35         };
36         </script>
37     </body>
38 </html>

這個是最精簡的版本,相信可以幫助大家快速理解整個流程,但是需要注意的是這個版本不兼容移動端。 

 

三、關鍵代碼分析 

1、創建音頻環境(audio Context)

創建音頻環境非常簡單,但是要考慮到瀏覽器兼容問題,需要用一個或語句來兼容不同瀏覽器。另外,對於不支持AudioContext的瀏覽器,還需要加一個try catch來避免報錯。

try {
  var audioCtx = new (window.AudioContext ||window.webkitAudioContext)();
} catch (err) {
  alert('!Your browser does not support Web Audio API!');
};

2、創建音源

創建音源的方法有以下幾種: 

1) 直接從HTML的video/audio元素中獲取,使用的方法為AudioContext.createMediaElementSource().

var source = audioCtx.createMediaElementSource(myMediaElement); //myMediaElement可以是頁面中的元素,也可以是用new創建的audio/video對象 

注意:由於audio標簽在移動端還有相當多的坑,大家可以搜索一下,避免踩坑哈!如果不考慮移動端的話,最簡單的辦法就是這種啦。

2) 從原始的PCM數據中創建音源,方法有AudioContext.createBuffer(),AudioContext.createBufferSource(), 和AudioContext.decodeAudioData(). 這里可以分成從網絡中獲取或者從本地文件中選擇兩種方式,關鍵代碼如下。

var source = audioCtx.createBufferSource();  //創建一個空的音源,一般使用該方式,后續將解碼的緩沖數據放入source中,直接對source操作。
// var buffer = audioCtx.createBuffer(2, 22050, 44100);  //創建一個雙通道、22050幀,44.1k采樣率的緩沖數據。
audioCtx.decodeAudioData(audioData, function(buffer) {
    source.buffer = buffer; //將解碼出來的數據放入source中
    //其他操作
}, function(err){ 
  alert('!Fail to decode the file!');   //解碼出錯處理
});

 網絡獲取方式,需要開一個Ajax異步請求來獲取文件,並且設置請求返回類型為'ArrayBuffer',代碼如下:

// use XHR to load an audio track, and
// decodeAudioData to decode it and stick it in a buffer.
// Then we put the buffer into the source
function getData() {
var request = new XMLHttpRequest();  //開一個請求
    request.open('GET', url, true);    //往url請求數據
    request.responseType = 'arraybuffer';  //設置返回數據類型
    request.onload = function() {
        var audioData = request.response;
        //數據緩沖完成之后,進行解碼
        audioCtx.decodeAudioData(audioData, function(buffer) {
            source.buffer = buffer;  //將解碼出來的數據放入source中
            //進行數據處理
        }, function(err) {
       alert(‘!Fail to decode the file!’);   //解碼出錯處理
     });
    };
    request.send();
}

 本地獲取的話需要通過文件類型的input標簽來進行文件選擇,監聽input的onchnage事件,一擔文件選中便開始在代碼中進行文件讀入,此處用到file reader來讀取文件,同樣的讀取結果的數據類型也設置為'ArrayBuffer'。我的項目使用了file reader本地讀取的辦法,兼顧移動端。

var audioInput = document.getElementById("uploader"); //HTML語句:<input type="file" id="uploader" />
audioInput.onchange = function() {
  //文件長度不為0則真的選中了文件,因為用戶點擊取消也會觸發onchange事件。
  if (audioInput.files.length !== 0) {
    files = audioInput.files[0]; //得到用戶選取的文件
    //文件選定之后,馬上用FileReader進行讀入
    fr = new FileReader();
    fr.onload = function(e) {
      var fileResult = e.target.result;
      //文件讀入完成,進行解碼
      audioCtx.decodeAudioData(fileResult, function(buffer) {
        source.buffer = buffer;//將解碼出來的數據放入source中
        //轉到播放和分析環節          

          }, function(err) {
        alert('!Fail to decode the file'); //解碼出錯
      });
    };
    fr.onerror = function(err) {
      alert('!Fail to read the file');    //文件讀入出錯
    };
    fr.readAsArrayBuffer(rfile); //同樣的,ArrayBuffer方式讀取
  }
};

3、創建效果節點,選擇輸出並將源、效果器和輸出連接起來。

前面也說過了,效果有非常多種,本文是建立分析節點。這里的連接是音源>>分析節點>>輸出,為什么不直接將音源和輸出連接起來呢?其實也可以直連,但因為分析節點需要做一定的處理,如果直接將音源和輸出連接的話會有一定的延遲。另外,這里定義了一個status參數,用來指示狀態值。最后的啟動有兩種寫法,有興趣的同學再去MDN上查查吧。

var status = 0,   //狀態,播放中:1,停止:0
  arraySize = 128,    //可以得到128組頻率值
    analyser = audioCtx.createAnalyser();     //創建分析節點
source.connect(analyser);        //將音源和分析節點連接在一起
analyser.connect(audioCtx.destination);        //將分析節點和輸出連接在一起
source.start(0);  //啟動音源
status = 1;  //更改音頻狀態

4、可視化繪圖

為了得到可視化效果,還需要對分析節點做一個傅里葉變換將信號從時域轉到頻域中,此處原理省略一萬字。。。有興趣的同學可以去看看信號處理相關的書籍。但是,這么難得的裝逼機會,我舍不得呀!看不慣的同學可以跳過,也可以微信轉賬給我(什么鬼。。。)。

項目中getByteFrequencyData(array)這個函數來獲取所需頻率的能量值,其中array數組的長度為頻率的個數。有看過資料的同學會發現這里很多時候用的是analyser.frequencyBinCount和analyser.fftsize兩個值,其中analyser.fftsize是快速傅立葉變換(FFT)用於頻域分析的尺寸,默認為2048。analyser.frequencyBinCount是fftsize的一半,這里我不需要那么多組數據,所以自定義了一個長度為128的8位無符號整形數組(譚浩強C語言后遺症,勿怪)。另外需要注意的是頻率值的范圍,是0到255,如果需要精度更高的值,可以使用AnalyserNode.getFloatFrequencyData()得到32位浮點數。經過上面的得到了這些值之后,我們就可以用它來跟某些視覺元素關聯起來,比如說常見的柱狀頻譜的高度、圓的半徑、線條的密度等等。我的項目里面采用的是能量球的方式。

var canvas = document.getElementById('drawCanvas'),
    ctx = canvas.getContext('2d'),
    cwidth = canvas.width,
    cheight = canvas.height,
    visualizer = [],    //可視化形狀
    animationId = null;
var random = function(m, n) {
    return Math.round(Math.random() * (n - m) + m);  //返回m~n之間的隨機數
};
for (var i = 0; i < num; i++) {
    var x = random(0, cwidth),
        y = random(0, cheight),
        color = "rgba(" + random(0, 255) + "," + random(0, 255) + "," + random(0, 255) + ",0)";  //隨機化顏色
    visualizer.push({
        x: x,
        y: y,
        dy: Math.random() + 0.1,   //保證dy>0.1
        color: color,
        radius: 30  //能量球初始化半徑
    });
}

var draw = function() {
    var array = new Uint8Array(128);  //創建頻率數組
    analyser.getByteFrequencyData(array);  //分析頻率信息,結果返回到array數組中,頻率值范圍:0~255
    if (status === 0) {
        //array數組歸零,有時候音頻播完了但是頻率值還殘留着,這時候需要強制清零
        for (var i = array.length - 1; i >= 0; i--) {
            array[i] = 0;
        };
        var allBallstoZero = true;  //能量球歸零
        for (var i = that.visualizer.length - 1; i >= 0; i--) {
            allBallstoZero = allBallstoZero && (visualizer[i].radius < 1);
        };
        if (allBallstoZero) {
            cancelAnimationFrame(animationId);   //結束動畫
            return;
        };
    };
    ctx.clearRect(0, 0, cwidth, cheight);
    for (var n = 0; n < array.length; n++) {
        var s = visualizer[n];
        s.radius = Math.round(array[n] / 256 * (cwidth > cheight ? cwidth / 25 : cheight / 18));  //控制能量球大小與畫布大小的比例
        var gradient = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.radius);  //創建能量球的漸變樣式
        gradient.addColorStop(0, "#fff");
        gradient.addColorStop(0.5, "#D2BEC0");
        gradient.addColorStop(0.75, s.color.substring(0, s.color.lastIndexOf(",")) + ",0.4)");
        gradient.addColorStop(1, s.color);
        ctx.fillStyle = gradient;
        ctx.beginPath();
        ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2, true);  //畫一個能量球
        ctx.fill();
        s.y = s.y - 2 * s.dy;  //能量球向上移動
        if ((s.y <= 0) && (status != 0)) {  //到畫布頂端的時候重置s.y,隨機化s.x
            s.y = cheight;  
            s.x = random(0, cwidth);
        }
    }
    animationId = requestAnimationFrame(draw);  //動畫
};

5、其他

音頻播完:

source.onended = function() {
    status = 0;  //更改播放狀態
};

自適應方面,在window的onload和onresize調整canvas尺寸,但重點不在這里。重點是能量球的大小如何跟隨畫布大小調節,因為動畫在進行中,所以最好的方案也是在動畫中動態改變,所以上段代碼中的cwidth和cheight的賦值應該放在繪圖動畫中。

var canvas = document.getElementById('drawCanvas');
canvas.width = window.clientWidth
           || document.documentElement.clientWidth
               || document.body.clientWidth;
canvas.height = window.clientHeight
               || document.documentElement.clientHeight
               || document.body.clientHeight;

增加點小玩法:鼠標捕捉能量球,鼠標進入能量球內的時候就捕捉到能量球,並要保持移動才能持續抓緊能量球

//鼠標捕捉能量球
canvas.onmousemove = function (e) {
    if (status != 0) {
        for (var n = 0; n < visualizer.length; n++) {
            var s = visualizer[n];
            if (Math.sqrt(Math.pow(s.x-e.pageX,2) + Math.pow(s.y-e.pageY,2)) < s.radius) {
                s.x = e.pageX;
                s.y = e.pageY;
            }
        }
    }
};

最后說說非常重要的代碼規范!

上述代碼只是為了簡化關系突出流程而改寫的代碼,並不符合代碼規范。為了更好地進行編碼,我們應該創建一個全局對象,把上述所有相關屬性及方法寫到其中。全局對象不但可以方便管理,而且在chrome中調試的時候,可以直接在控制台中查看並編輯,調試起來非常方便。按照慣例,對象的屬性直接寫在構造器里,對象的方法寫到原型中方便日后擴展繼承。對象內部使用的私有方法還有私有屬性以短橫線開頭,也是從封裝的角度考慮的。

 

這篇文章主要是參考了劉哇勇的博文和代碼(下2),后來查看MDN的時候發現了下1的更通俗簡單的版本,都是大力推薦!寫文章真不容易,首先是思路重新整理,然后是材料的收集,代碼的增減,工作量真心大。寫作不易,趕緊點贊哈~

Reference:

1、Visualizations with Web Audio API(官方原版,強力推薦)   https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API

2、開大你的音響,感受HTML5 Audio API帶來的視聽盛宴  http://www.cnblogs.com/Wayou/p/html5_audio_api_visualizer.html


免責聲明!

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



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