HTML5 隨音樂節奏變化的頻譜圖動畫

這裏將要介紹的HTML5 音頻處理接口與Audio標籤是不同的。頁面上的Audio標籤只是HTML5更語義化的一個表現,而HTML5提供給JavaScript編程用的Audio API則讓咱們有能力在代碼中直接操做原始的音頻流數據,對其進行任意加工再造。html

   展現HTML5 Audio API 最典型直觀的一個例子就是跟隨音樂節奏變化的頻譜圖,也稱之爲可視化效果。本文即是以此爲例子展現JavaScript中操做音頻數據的。程序員

文中代碼僅供參考,實際代碼如下載的源碼爲準。

瞭解Audio API
  一段音頻到達揚聲器進行播放以前,半路對其進行攔截,因而咱們就獲得了音頻數據了,這個攔截工做是由window.AudioContext來作的,咱們全部對音頻的操做都基於這個對象。
web

 
   經過AudioContext能夠建立不一樣各種的 AudioNode,即音頻節點,不一樣節點做用不一樣,有的對音頻加上濾鏡好比提升音色(好比BiquadFilterNode),改變單調,有的音頻進行 分割,好比將音源中的聲道分割出來獲得左右聲道的聲音(ChannelSplitterNode),有的對音頻數據進行頻譜分析即本文要用到的 (AnalyserNode)。





瀏覽器中的Audio API
統一前綴
   JavaScript中處理音頻首先須要實例化一個音頻上下文類型window.AudioContext。目前Chrome和Firefox對其提供了 支持,但須要相應前綴,Chrome中爲window.webkitAudioContext,Firefox中爲mozAudioContext。因此 爲了讓代碼更通用,可以同時工做在兩種瀏覽器中,只須要一句代碼將前綴進行統一便可。
編程

window.AudioContext =
    window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;canvas

   這是一種常見的用法,或者操做符'||' 鏈接起來的表達式中,遇到真值即返回。好比在Chrome中,window.AudioContext爲undefined,接着往下走,碰到 window.webkitAudioContext不爲undefined,表達式在此判斷爲真值,因此將其返回,因而此時 window.AudioContext =window.webkitAudioContext ,因此代碼中咱們就能夠直接使用window.AudioContext 而不用擔憂具體Chrome仍是Firefox了。數組

  var audioContext=new window.AudioContext();瀏覽器

考慮瀏覽器不支持的狀況
  但這還只是保證了在支持AudioContext的瀏覽器中能正常工做,若是是在IE中,上面實例化對象的操做會失敗,因此有必要加個try catch語句來避免報錯。
  try {
   var audioContext = new window.AudioContext();
 } catch (e) {
    Console.log('!Your browser does not support AudioContext');
 }
安全

這樣就安全多啦,媽媽再不擔憂瀏覽器報錯了。

組織代碼
   爲了更好地進行編碼,咱們建立一個Visualizer對象,把全部相關屬性及方法寫到其中。按照慣例,對象的屬性直接寫在構造器裏面,對象的方法寫到原型中。對象內部使用的私有方法以短橫線開頭,不是必要可是種好的命名習慣。其中設置了一些基本的屬性將在後續代碼中使用,詳細的還請參見源碼,這裏只簡單展現。
服務器

 var Visualizer = function() {
    this.file = null, //要處理的文件,後面會講解如何獲取文件
    this.fileName = null, //要處理的文件的名,文件名
    this.audioContext = null, //進行音頻處理的上下文,稍後會進行初始化
    this.source = null, //保存音頻
 };
 Visualizer.prototype = {
   _prepareAPI: function() {
     //統一前綴,方便調用
     window.AudioContext =
       window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
    //這裏順便也將requestAnimationFrame也打個補丁,後面用來寫動畫要用
    window.requestAnimationFrame = 
           window.requestAnimationFrame || window.webkitRequestAnimationFrame || 
        window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
   //安全地實例化一個AudioContext並賦值到Visualizer的audioContext屬性上,方便後面處理音頻使用
   try {
       this.audioContext = new AudioContext();
    } catch (e) {
        console.log('!妳的瀏覽器不支持AudioContext:(');
        console.log(e);
    }
  },
 }
加載音頻文件
不用說,你確定得先在代碼中獲取到音頻文件,纔可以對其進一步加工。

文件獲取的方法: 讀取文件到JavaScript能夠有如下三種方法:
   1.新開一個Ajax異步請求來獲取文件,若是是本地測試須要關掉瀏覽器的同源安全策略才能獲取成功,否則只能把網頁放到服務器上才能正常工做。
   具體說來,就是先開一個XMLHttpRequest請求,將文件路徑做爲請求的URL,而且設置請求返回類型爲'ArrayBuffer',這種格式方便咱們後續的處理。下面是一個例子。
 loadSound("sample.mp3"); //調用
 // 定義加載音頻文件的函數
 function loadSound(url) {
   var request = new XMLHttpRequest(); //創建一個請求
   request.open('GET', url, true); //配置好請求類型,文件路徑等
   request.responseType = 'arraybuffer'; //配置數據返回類型
   // 一旦獲取完成,對音頻進行進一步操做,好比解碼
   request.onload = function() {
     var arraybuffer = request.response;
  }
  request.send();
 }

2.經過文件類型的input來進行文件選擇,監聽input的onchnage事件,一擔文件選中便開始在代碼中進行獲取處理,此法方便,且不須要工做在服務器上app

3.經過拖拽的形式把文件拖放到頁面進行獲取,比前面一種方法稍微繁雜一點(要監聽'dragenter','dragover','drop'等事件)但一樣能夠很好地在本地環境下工做,無需服務器支持。

不用說,方法2和3方便本地開發與測試,因此咱們兩種方法都實現,既支持選擇文件,也支持文件拖拽。

(1)經過選擇獲取
  在頁面放一個file類型的input。而後在JavaScript中監聽它的onchange事件。此事件在input的值發生變化時觸發。
對於onchange事件,在Chrome與Firefox中還有一點小的區別,若是你已經選擇了一個文件,此時Input就有值了,若是你再次選擇同一文件,onchange事件不會觸發,但在Firefox中該事件會觸發。這裏只是說起一下,關係不大。
 <label for="uploadedFile">Drag&drop or select a file to play:</label>
 <input type="file" id="uploadedFile"></input>

  固然,這裏同時也把最後咱們要畫圖用的canvas也一塊兒放上去吧,後面就不用多話了。因此下面就是最終的HTML了,頁面基本不會變,大量的工做是在JavaScript的編寫上。
 <div id="wrapper">
  <div id="fileWrapper" class="file_wrapper">
   <div id="info">
     HTML5 Audio API showcase | An Audio Viusalizer
   </div>
   <label for="uploadedFile">Drag&drop or select a file to play:</label>
   <input type="file" id="uploadedFile"></input>
  </div>
 <div id="visualizer_wrapper">
  <canvas id='canvas' width="800" height="350"></canvas>
 </div>
</div>
再稍微寫一點樣式:
  #fileWrapper {
   transition: all 0.5s ease;
  }
  #fileWrapper: hover {
   opacity: 1!important;
  }
  #visualizer_wrapper {
   text-align: center;
  }

向Visualizer對象的原型中新加一個方法,用於監聽文件選擇既前面討論的onchange事件,並在事件中獲取選擇的文件。

_addEventListner: function() {
    var that = this,
    audioInput = document.getElementById('uploadedFile'),
    dropContainer = document.getElementsByTagName("canvas")[0];
    //監聽是否有文件被選中
    audioInput.onchange = function() {
      //這裏判斷一下文件長度能夠肯定用戶是否真的選擇了文件,若是點了取消則文件長度爲0
      if (audioInput.files.length !== 0) {
          that.file = audioInput.files[0]; //將文件賦值到Visualizer對象的屬性上
          that.fileName = that.file.name;
          that._start(); //獲取到文件後,開始程序,這個方法會在後面定義並實現
      };
   };
 }
上面代碼中,咱們假設已經寫好了一個進一步處理文件的方法_start(),在獲取到文件後賦值給Visualizer對象的file屬性,以後在 _start()方法裏咱們就能夠經過訪問this.file來獲得該文件了,固然你也能夠直接讓_start()方法接收一個file參數,但將文件賦 值到Visualizer的屬性上的好處之一是咱們能夠在對象的任何方法中都能獲取該文件 ,不用想怎麼用參數傳來傳去。一樣,將文件名賦值到Visualizer的fileName屬性當中進行保存,也是爲了方便以後在音樂播放過程當中顯示當前 播放的文件。

(2)經過拖拽獲取
咱們把頁面中的canvas做爲放置文件的目標,在它身上監聽拖拽事件'dragenter','dragover','drop'等。
仍是在上面已經添加好的_ addEventListner方法裏,接着寫三個事件監聽的代碼。
dropContainer.addEventListener("dragenter", function() {
    that._updateInfo('Drop it on the page', true);
}, false);
dropContainer.addEventListener("dragover", function(e) {
    e.stopPropagation();
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy'; //設置文件放置類型爲拷貝
}, false);
dropContainer.addEventListener("dragleave", function() {
    that._updateInfo(that.info, false);
}, false);
dropContainer.addEventListener("drop", function(e) {
    e.stopPropagation();
    e.preventDefault();
    that.file = e.dataTransfer.files[0]; //獲取文件並賦值到Visualizer對象
    that.fileName = that.file.name;
    that._start();
}, false);

注意到上面代碼中咱們在'dragover'時設置文件拖放模式爲'copy',既以複製的形式獲取文件,若是不進行設置沒法正確獲取文件

而後在'drop'事件裏,咱們得到文件以進行一下步操做。

用FileReader讀取文件爲ArrayBuffer

   下面來看這個_start()方法,如今文件獲得了,但首先須要將獲取的文件轉換爲ArrayBuffer格式,纔可以傳遞給 AudioContext進行解碼,因此接下來_start()方法中要乾的事情就是實例化一個FileReader來將文件讀取爲 ArrayBuffer格式。

_start: function() {
    //read and decode the file into audio array buffer
    var that = this, //當前this指代Visualizer對象,賦值給that以以便在其餘地方使用
    file = this.file, //從Visualizer對象上獲取前面獲得的文件
    fr = new FileReader(); //實例化一個FileReader用於讀取文件
    fr.onload = function(e) { //文件讀取完後調用此函數
      var fileResult = e.target.result; //這是讀取成功獲得的結果ArrayBuffer數據
      var audioContext = that.audioContext; //從Visualizer獲得最開始實例化的AudioContext用來作解碼ArrayBuffer
      audioContext.decodeAudioData(fileResult,function(buffer){//解碼成功則調用此函數,參數buffer爲解碼後獲得的結果
         that._visualize(audioContext, buffer); //調用_visualize進行下一步處理,此方法在後面定義並實現
      }, function(e) { //這個是解碼失敗會調用的函數
           console.log("!哎瑪,文件解碼失敗:(");
      });
    };
    //將上一步獲取的文件傳遞給FileReader從而將其讀取爲ArrayBuffer格式
    fr.readAsArrayBuffer(file);
}

   注意這裏咱們把this賦值給了that,而後再 audioContext.decodeAudioData的回調函數中使用that來指代咱們的Visualizer對象。這是由於做用域的緣由。咱們 知道JavaScript中沒法經過花括號來建立代碼塊級做用域,而惟一能夠建立做用域的即是函數。一個函數就是一個做用域。函數內部的this指向的對 象要視狀況而定,就上面的代碼來講,它是audioContext。因此若是想要在這個回調函數中調用Visualizer身上方法或屬性,則須要經過另 一個變量來傳遞,這裏是that,咱們經過將外層this(指向的是咱們的Viusalizer對象)賦值給新建的局部變量that,此時that即可以 傳遞到內層做用域中,而不會與內層做用域裏面原來的this相沖突。像這樣的用法在源碼的其餘地方也有使用,細心的你能夠下載本文的源碼慢慢研究。

   因此,在 audioContext.decodeAudioData的回調函數裏,當解碼完成獲得audiobuffer文件(buffer參數)後,再把 audioContext和buffer傳遞給Visualizer的_visualize()方法進一步處理:播放音樂和繪製頻譜圖。固然此時 _visualize()方法尚未下,下面便開始實現它。

建立Analyser分析器及播放音頻

  上面已經將獲取的文件進行解碼,獲得了audio buffer數據。接下來是設置咱們的AudioContext以及獲取頻譜能量信息的Analyser節點。向Visualizer對象添加_visualize方法,咱們在這個方法裏完成這些工做。

播放音頻
   首先將buffer賦值給audioContext。AudioContext只至關於一個容器,要讓它真正豐富起來須要將實際的音樂信息傳遞給它的。也就是將audio buffer數據傳遞給它的BufferSource屬性。

其實到了這裏你應該有點暈了,不過不要緊,看代碼就會更明白一些,程序員是理解代碼優於文字的一種生物。
  var audioBufferSouceNode = audioContext.createBufferSource();
  audioBufferSouceNode.buffer = buffer;

就這麼兩名,把音頻文件的內容裝進了AudioContext。這時已經能夠開始播放咱們的音頻了。
   audioBufferSouceNode.start(0);

這裏參數是時間,表示從這段音頻的哪一個時刻開始播放。注意:在舊版本的瀏覽器裏是使用onteOn()來進行播放的,參數同樣,指開始時刻。

   但此時是聽不到聲音的,由於還差一步,須要將audioBufferSouceNode鏈接到audioContext.destination,這個AudioContext的destination也就相關於speaker(揚聲器)。
  audioBufferSouceNode.connect(audioContext.destination);
  audioBufferSouceNode.start(0);

此刻就可以聽到揚聲器傳過來動聽的聲音了。
  _visualize: function(audioContext, buffer) {
    var audioBufferSouceNode = audioContext.createBufferSource();
    audioBufferSouceNode.connect(audioContext.destination);
    audioBufferSouceNode.buffer = buffer;
    audioBufferSouceNode.start(0);
  }


建立分析器
建立獲取頻譜能量值的analyser節點。
  var analyser = audioContext.createAnalyser();
上面一步咱們是直接將audioBufferSouceNode與audioContext.destination相連的,音頻就直接輸出到揚聲器開始 播放了,如今爲了將音頻在播放前截取,因此要把analyser插在audioBufferSouceNode與 audioContext.destination之間。明白了這個道理,代碼也就很簡單了,audioBufferSouceNode鏈接到 analyser,analyser鏈接destination。
  audioBufferSouceNode.connect(analyser);
  analyser.connect(audioContext.destination);

而後再開始播放,此刻全部音頻數據都會通過analyser,咱們再從analyser中獲取頻譜的能量信息,將其畫出到Canvas便可。

假設咱們已經寫好了畫頻譜圖的方法_drawSpectrum(analyser);
 _visualize: function(audioContext, buffer) {
   var audioBufferSouceNode = audioContext.createBufferSource(),
   analyser = audioContext.createAnalyser();
   //將source與分析器鏈接
   audioBufferSouceNode.connect(analyser);
  //將分析器與destination鏈接,這樣才能造成到達揚聲器的通路
   analyser.connect(audioContext.destination);
   //將上一步解碼獲得的buffer數據賦值給source
   audioBufferSouceNode.buffer = buffer;
  //播放
   audioBufferSouceNode.start(0);
   //音樂響起後,把analyser傳遞到另外一個方法開始繪製頻譜圖了,由於繪圖須要的信息要從analyser裏面獲取
   this._drawSpectrum(analyser);
  }

繪製精美的頻譜圖

  接下來的工做,也是最後一步,也就是實現_drawSpectrum()方法,將跟隨音樂而靈動的柱狀頻譜圖畫出到頁面。
繪製柱狀能量槽
   首先你要對數字信號處理有必定了解,嚇人的,不瞭解也沒多大關係。頻譜反應的是聲音各頻率上能量的分佈,因此叫能量槽也沒有硬要跟遊戲聯繫起來的嫌疑,是 將輸入的信號通過傅里葉變化獲得的(大學裏的知識終於仍是能夠派得上用場了)。但特麼我知道這些又怎樣呢,僅僅爲了裝逼顯擺而以。真實的頻譜圖是頻率上連 續的,不是咱們看到的最終效果那樣均勻分開鋸齒狀的。

經過下面的代碼咱們能夠從analyser中獲得此刻的音頻中各頻率的能量值。
  var array = new Uint8Array(analyser.frequencyBinCount);
  analyser.getByteFrequencyData(array);

此刻array中存儲了從低頻0Hz到高頻~Hz的全部數據。頻率作爲X軸,能量值作爲Y軸,咱們能夠獲得相似下面的圖形。



因此,好比array[0]=100,咱們就知道在x=0處畫一個高爲100單位長度的長條,array[1]=50,而後在x=1畫一個高爲50單位長度的柱條,今後類推,若是用一個for循環遍歷array將其所有畫出的話,即是你看到的上圖。
採樣

但咱們要的不是那樣的效果,咱們只需在全部數據中進行抽樣,好比設定一個步長100,進度抽取,來畫出整個頻譜圖中的部分柱狀條。

   或者先根據畫面的大小,設計好每根柱條的寬度,以及他們的間隔,從而計算出畫面中一共須要共多少根,再來推算出這個採樣步長該取多少,本例即是這樣實現的。說來仍是有點暈,下面看簡單的代碼:

 var canvas = document.getElementById('canvas'),
 meterWidth = 10, //能量條的寬度
 gap = 2, //能量條間的間距
 meterNum = 800 / (10 + 2); //計算當前畫布上能畫多少條
 var step = Math.round(array.length / meterNum); //計算從analyser中的採樣步長

  咱們的畫布即Canvas寬800px,同時咱們設定柱條寬10px , 柱與柱間間隔爲2px,因此獲得meterNum爲總共能夠畫的柱條數。再用數組總長度除以這個數目就獲得採樣的步長,即在遍歷array時每隔step 這麼長一段咱們從數組中取一個值出來畫,這個值爲array[i*step]。這樣就均勻地取出meterNum個值,從而正確地反應了原來頻譜圖的形 狀。

 var canvas = document.getElementById('canvas'),
 cwidth = canvas.width,
 cheight = canvas.height - 2,
 meterWidth = 10, //能量條的寬度
 gap = 2, //能量條間的間距
 meterNum = 800 / (10 + 2), //計算當前畫布上能畫多少條
 ctx = canvas.getContext('2d'),
 array = new Uint8Array(analyser.frequencyBinCount);
 analyser.getByteFrequencyData(array);
 var step = Math.round(array.length / meterNum);計算從analyser中的採樣步長
 ctx.clearRect(0, 0, cwidth, cheight); //清理畫布準備畫畫
 //定義一個漸變樣式用於畫圖
 gradient = ctx.createLinearGradient(0, 0, 0, 300);
 gradient.addColorStop(1, '#0f0');
 gradient.addColorStop(0.5, '#ff0');
 gradient.addColorStop(0, '#f00');
 ctx.fillStyle = gradient;
 //對信源數組進行抽樣遍歷,畫出每一個頻譜條
 for (var i = 0; i < meterNum; i++) {
   var value = array[i * step];
   ctx.fillRect(i * 12 /*頻譜條的寬度+條間間距*/ , cheight - value + capHeight, meterWidth, cheight);
 }

使用requestAnimationFrame讓柱條動起來

  但上面繪製的僅僅是某一刻的頻譜,要讓整個畫面動起來,咱們須要不斷更新畫面,window.requestAnimationFrame()正好提供了更新畫面獲得動畫效果的功能,這裏直接給出簡單改造後的代碼,即獲得咱們要的效果了:跟隨音樂而靈動的頻譜柱狀圖。

var canvas = document.getElementById('canvas'),
    cwidth = canvas.width,
    cheight = canvas.height - 2,
    meterWidth = 10, //能量條的寬度
    gap = 2, //能量條間的間距
    meterNum = 800 / (10 + 2), //計算當前畫布上能畫多少條
    ctx = canvas.getContext('2d');
    //定義一個漸變樣式用於畫圖
    gradient = ctx.createLinearGradient(0, 0, 0, 300);
    gradient.addColorStop(1, '#0f0');
    gradient.addColorStop(0.5, '#ff0');
    gradient.addColorStop(0, '#f00');
    ctx.fillStyle = gradient;
    var drawMeter = function() {
      var array = new Uint8Array(analyser.frequencyBinCount);
      analyser.getByteFrequencyData(array);
      var step = Math.round(array.length / meterNum); //計算採樣步長
      ctx.clearRect(0, 0, cwidth, cheight); //清理畫布準備畫畫
      for (var i = 0; i < meterNum; i++) {
        var value = array[i * step];
        ctx.fillRect(i * 12 /*頻譜條的寬度+條間間距*/ , cheight - value + capHeight, meterWidth, cheight);
     }
     requestAnimationFrame(drawMeter);
  }
  requestAnimationFrame(drawMeter);

繪製緩慢降落的帽頭
  到上面一步,主要工做已經完成。最後爲了美觀,再實現一下柱條上方緩慢降落的帽頭。
原理也很簡單,就是在繪製柱條的同時在同一X軸的位置再繪製一個短的柱條,而且其開始和結束位置都要比頻譜中的柱條高。難的地方即是如何實現緩慢降落。

   首先要搞清楚的一點是,咱們拿一根柱條來講明問題,當此刻柱條高度高於前一時刻時,咱們看到的是往上衝的一根頻譜,因此這時帽頭是緊貼着正文 柱條的,這個好畫。考慮相反的狀況,當此刻高度要低於前一時刻的高度時,下方柱條是當即縮下去的,同時咱們須要記住上一時刻帽頭的高度位置,此刻畫的時候 就按照前一時刻的位置將Y-1來畫。若是下一時刻頻譜柱條仍是沒有超過帽頭的位置,繼續讓它降低,Y-1畫出帽頭。

   經過上面的分析,因此咱們在每次畫頻譜的時刻,須要將此刻頻譜及帽頭的Y值(即垂直方向的位置)記到一個循環外的變量中,在下次繪製的時刻從這個變量中讀取,將此刻的值與變量中保存的上一刻的值進行比較,而後按照上面的分析做圖。

最後給出實現的代碼:

_drawSpectrum: function(analyser) {
    var canvas = document.getElementById('canvas'),
        cwidth = canvas.width,
        cheight = canvas.height - 2,
        meterWidth = 10, //頻譜條寬度
        gap = 2, //頻譜條間距
        capHeight = 2,
        capStyle = '#fff',
        meterNum = 800 / (10 + 2), //頻譜條數量
        capYPositionArray = []; //將上一畫面各帽頭的位置保存到這個數組
    ctx = canvas.getContext('2d'),
    gradient = ctx.createLinearGradient(0, 0, 0, 300);
    gradient.addColorStop(1, '#0f0');
    gradient.addColorStop(0.5, '#ff0');
    gradient.addColorStop(0, '#f00');
    var drawMeter = function() {
        var array = new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(array);
        var step = Math.round(array.length / meterNum); //計算採樣步長
        ctx.clearRect(0, 0, cwidth, cheight);
        for (var i = 0; i < meterNum; i++) {
            var value = array[i * step]; //獲取當前能量值
            if (capYPositionArray.length < Math.round(meterNum)) {
                capYPositionArray.push(value); //初始化保存帽頭位置的數組,將第一個畫面的數據壓入其中
            };
            ctx.fillStyle = capStyle;
            //開始繪製帽頭
            if (value < capYPositionArray[i]) { //若是當前值小於以前值
    ctx.fillRect(i *12,cheight-(--capYPositionArray[i]),meterWidth,capHeight);//則使用前一次保存的值來繪製帽頭
            } else {
                ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); //不然使用當前值直接繪製
                capYPositionArray[i] = value;
            };
            //開始繪製頻譜條
            ctx.fillStyle = gradient;
            ctx.fillRect(i * 12, cheight - value + capHeight, meterWidth, cheight);
        }
        requestAnimationFrame(drawMeter);
    }
    requestAnimationFrame(drawMeter);
}

轉載:http://www.108js.com/article/article7/70196.html?id=983

相關文章
相關標籤/搜索