web-audio-api可視化音樂播放器,實現暫停切換歌曲功能,粉色系專場~

可視化的音樂播放器,可戳我觀看效果

瞭解Web-Audio-Api

  • 基礎知識

<audio>標籤是HTML5的新標籤,經過添加src屬性實現音樂播放。javascript

AudioContext是音頻播放環境,原理與canvas的繪製環境相似,都是須要建立環境上下文,經過上下文的調用相關的建立音頻節點,控制音頻流播放暫停操做等操做,這一些操做都須要發生在這個環境之中。html

try{
    var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 
}catch(e){
    alert('Web Audio API is not supported in this browser');
}
複製代碼

AudioNode接口是一個處理音頻的通用模塊,它能夠是音頻音源模塊,音頻播放設備模塊,也能夠是中間音頻處理模塊。不一樣的音頻節點的鏈接(經過AudioContext.connect()),以及終點鏈接AudioContext.destination(能夠看做是鏈接到耳機或揚聲器設備)完成後,才能輸出音樂。java

常見的音頻節點:
AudioBufferSourceNode: 播放和處理音頻數據
AnalyserNode: 顯示音頻時間和頻率數據 (經過分析頻率數據能夠繪製出波形圖之類的視圖,可視化的主要途徑)
GainNode: 音量節點,控制音頻的總音量
MediaElementAudioSourceNode: 關聯HTMLMediaElement,播放和處理來自<video>和<audio>元素的音頻
OscillatorNode: 一個週期性波形,只建立一個音調
...
複製代碼
  • 運行模式
  1. 建立音頻上下文
  2. 在上下文中,建立音頻源
  3. 建立音頻節點,處理音頻數據並鏈接
  4. 輸出設備

建立音頻上下文

try{
    var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 
}catch(e){
    alert('Web Audio API is not supported in this browser');
}
複製代碼

建立音頻源

因爲音頻文件的數據是二進制(非文本),因此要設置請求頭的responseTypearraybuffer,將.mp3音頻文件轉換成數組緩衝區ArrayBuffergit

AudioContext.decodeAudioData解碼成功以後獲取buffer,執行回調函數,將數據放入AudioBufferSourceNodegithub

方法一採用流式加載音樂文件,簡單易懂,缺點是經過createMediaElementSource加載的src文件必須是同源,不容許跨域web

下面步驟主要根據方法2。canvas

  • 方法一:經過HTMLMediaElement流式加載
<audio src="1.mp3"></audio>
  <script>
    let audio = document.querySelector('audio');
    let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    audio.addEventListener('canplay', function () {
      let source = audioCtx.createMediaElementSource(audio);
      source.connect(audioCtx.destination);
      audio.play()
    })
  </script>
複製代碼
  • 方法二:經過XMLHttpRequest獲取資源
let xhr = new XMLHttpRequest();
    xhr.open('GET', '1.mp3', true);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function () {
      audioCtx.decodeAudioData(xhr.response, function (buffer) {
        getBufferSuccess(buffer)
      })
    }
複製代碼
  • 方法三:經過input file獲取
let input = document.querySelector('input');
    input.addEventListener('change', function () {
      if (this.files.length !== 0) {
        let file = this.files[0];
        let fr = new FileReader();
        fr.onload = function () {
          let fileRet = e.target.result;
          audioCtx.decodeAudioData(fileRet, function (buffer) {
            getBufferSuccess(buffer);
          }, function (err) {
            console.log(err)
          })
        }
        fr.readAsArrayBuffer(file);
      }
    })
複製代碼

處理音頻數據

function getBufferSuccess(buffer) {
      // 建立頻率分析節點
      let analyser = audioCtx.createAnalyser();
      // 肯定頻域的快速傅里葉變換大小
      analyser.fftSize = 2048;
      // 這個屬性可讓最後一個分析幀的數據隨時間使值之間的過渡更平滑。
      analyser.smoothingTimeConstant = 0.6;
      // 建立播放對象節點
      let source = audioCtx.createBufferSource();
      // 填充音頻buffer數據
      source.buffer = buffer;
      // 建立音量節點(若是你須要用調整音量大小的話)
      let gainNode = audioCtx.createGain();
      
      // 鏈接節點對象
      source.connect(gainNode);
      gainNode.connect(analyser);
      analyser.connect(audioCtx.destination);
    }
複製代碼

獲取音頻頻率

  • 方法一:用js的方法獲取(經過監聽audioprocess事件,因爲性能問題,將會被棄用,不作詳細說明,感興趣的能夠了解一下)
// 此方法須要補充節點的鏈接
      let javascriptNode = audioCtx.createScriptProcessor(2048, 1, 1);
      javascriptNode.connect(audioCtx.destination);
      analyser.connect(javascriptNode);
      
        this.javascriptNode.onaudioprocess = function () {
            currData = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(currData);
        }

複製代碼
  • 方法二:用AnalyserNode獲取

獲取AnalyserNode節點裏的頻率長度frequencyBinCount,實例化長度爲8位的整型數組,經過AnalyserNode.getByteFrequencyData將節點中的頻率數據拷貝到數組中去,值的大小在0 - 256之間,數值越高代表頻率越高;AnalyserNode.getByteTimeDomainData原理同樣,不過獲取的是頻率大小,兩種方法根據需求選一種便可。api

function getData () {
      // analyser.frequencyBinCount 可視化值的數量,是前面fftSize的一半
      let currData = new Uint8Array(analyser.frequencyBinCount);
      analyser.getByteFrequencyData(currData);
      analyser.getByteTimeDomainData(currData);
    }
複製代碼

輸出設備

AudioBufferSourceNode.start(n) n表示開始的時間,默認爲0,開始播放音頻 AudioBufferSourceNode.stop(n) 音頻在第n秒時間中止,若沒有傳值表示當即中止跨域

其餘api

AudioContext.resume() 控制音頻的播放
AudioContext.suspend() 控制音頻的暫停
AudioContext.currentTime 獲取當前音頻播放時間
AudioBufferSourceNode.buffer.duration 獲取音頻的播放總時長
GainNode.gain.value 控制音量大小 [0, 1]
GainNode.gain.linearRampToValueAtTime 實現音量的漸入漸出數組

Canvas繪製可視化效果

瞭解上面的api,就能夠來着手繪製啦~,你想繪啥就繪啥,頻繁的調用canvas的api很耗性能問題,這裏講下我在測試中提升性能的小技巧。

  • 多分層canvas,一些不須要頻繁改動的繪製,例如背景,固定的裝飾繪製,能夠採用另外一個canvas的上下文來繪製
  • 離屏繪製,原理是生成一個沒有出如今頁面的canvas,在這個緩存的canvas中繪製,而真正展現的canvas只須要經過drawImage這個api將畫面繪製出來便可,參考此博文
  • 固定好lineWidth的長度,而不是每繪製一個就設定一次lineWidth
  • 繪製區域提早計算好,不要讓canvas邊繪製同時還要計算位置(canvas:好累哦~) 總而言之,少調用canvas api,但是也不要爲了提升性能而拋棄你的一些天馬星空的想法哦

遇到的問題

在切換歌曲中,遇到了這個報錯Failed to set the 'buffer' property on 'AudioBufferSourceNode': Cannot set buffer to non-null after it has been already been set to a non-null buffer at AudioContext,大體是講AudioBufferSourceNode的buffer屬性在以前我已經設置過了,不能被從新設置新的buffer值,因爲播放歌曲主要是經過其數組緩衝區ArrayBuffer來進行,可看看issue,解決辦法就是當須要切換歌曲狀況下,將當前的AudioBufferSourceNode銷燬,從新建立上下文環境,音頻節點,鏈接等操做。

源碼在這,交互部分寫得有點亂,由於當時原來只是想練練可視化,以後想到啥功能就加,因此致使代碼看起來冗餘繁瑣,你們能夠參考看看audio實現,主要在MusicPlay對象。

小白第一次發表博文,發現寫博文比寫一個demo還要時間長,怕寫出來的東西有錯誤會誤導你們(有錯誤請你們評論指出~),因此會去查不少相關資料,這個過程也是學習的過程,之後會常常寫寫博文滴!最後,但願你們經過這篇文章也能學會本身作這種可視化的效果,配合一些可視化庫還能作出很酷炫的效果呢,一塊兒互相學習進步吧,加油!(。・д・。)