1. 程式人生 > >基於Web Audio API實現音訊視覺化效果

基於Web Audio API實現音訊視覺化效果

網頁音訊介面最有趣的特性之一它就是可以獲取頻率、波形和其它來自聲源的資料,這些資料可以被用作音訊視覺化。這篇文章將解釋如何做到視覺化,並提供了一些基礎使用案例。
基本概念節
要從你的音訊源獲取資料,你需要一個 AnalyserNode節點,它可以用 AudioContext.createAnalyser() 方法建立,比如:

var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var analyser = audioCtx.createAnalyser();

然後把這個節點(node)

連線到你的聲源:

source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);

// etc.
注意: 分析器節點(Analyser Node) 不一定輸出到另一個節點,不輸出時也可以正常使用。但前提是它必須與一個聲源相連(直接或者通過其他節點間接相連都可以)。

分析器節點(Analyser Node) 將在一個特定的頻率域裡使用快速傅立葉變換(Fast Fourier Transform (FFT) )來捕獲音訊資料,這取決於你給 AnalyserNode.fftSize

屬性賦的值(如果沒有賦值,預設值為2048)。

注意: 你也可以為FFT資料縮放範圍指定一個最小值和最大值,使用AnalyserNode.minDecibels 和AnalyserNode.maxDecibels進行設定,要獲得不同資料的平均常量,使用 AnalyserNode.smoothingTimeConstant。閱讀這些頁面以獲得更多如何使用它們的資訊。

要捕獲資料,你需要使用 AnalyserNode.getFloatFrequencyData()AnalyserNode.getByteFrequencyData() 方法來獲取頻率資料,用 AnalyserNode.getByteTimeDomainData() 或 AnalyserNode.getFloatTimeDomainData()

來獲取波形資料。

這些方法把資料複製進了一個特定的陣列當中,所以你在呼叫它們之前要先建立一個新陣列。第一個方法會產生一個32位浮點陣列,第二個和第三個方法會產生8位無符號整型陣列,因此一個標準的JavaScript陣列就不能使用 —— 你需要用一個 Float32Array 或者 Uint8Array 陣列,具體需要哪個視情況而定。

那麼讓我們來看看例子,比如我們正在處理一個2048尺寸的FFT。我們返回 AnalyserNode.frequencyBinCount 值,它是FFT的一半,然後呼叫Uint8Array(),把frequencyBinCount作為它的長度引數 —— 這代表我們將對這個尺寸的FFT收集多少資料點。

analyser.fftSize = 2048;
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);

要正確檢索資料並把它複製到我們的數組裡,就要呼叫我們想要的資料收集方法,把陣列作為引數傳遞給它,例如:

analyser.getByteTimeDomainData(dataArray);

現在我們就獲取了那時的音訊資料,並存到了我們的數組裡,而且可以把它做成我們喜歡的視覺化效果了,比如把它畫在一個HTML5 <canvas> 畫布上。

建立一個頻率條形圖節
另一種小巧的視覺化方法是建立頻率條形圖,
現在讓我們來看看它是如何實現的。

首先,我們設定好解析器和空陣列,之後用 clearRect() 清空畫布。與之前的唯一區別是我們這次大大減小了FFT的大小,這樣做的原因是為了使得每個頻率條足夠寬,讓它們看著像“條”而不是“細杆”。

    analyser.fftSize = 256;
    var bufferLength = analyser.frequencyBinCount;
    console.log(bufferLength);
    var dataArray = new Uint8Array(bufferLength);
    canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

接下來我們寫好 draw() 函式,再一次用 requestAnimationFrame() 設定一個迴圈,這樣顯示的資料就可以保持重新整理,並且每一幀都清空一次畫布。

function draw() {
      drawVisual = requestAnimationFrame(draw);
      analyser.getByteFrequencyData(dataArray);
      canvasCtx.fillStyle = 'rgb(0, 0, 0)';
      canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
      }

現在我們來設定一個 barWidth 變數,它等於每一個條形的寬度。理論上用花布寬度除以條的個數就可以得到它,但是在這裡我們還要乘以2.5。這是因為有很多返回的頻率區域中是沒有聲音的,我們每天聽到的大多數聲音也只是在一個很小的頻率區域當中。在條形圖中我們肯定不想看到大片的空白條,所以我們就把一些能正常顯示的條形拉寬來填充這些空白區域。

我們還要設定一個條高度變數 barHeight,還有一個 x 變數來記錄當前條形的位置。

var barWidth = (WIDTH / bufferLength) * 2.5;
var barHeight;
var x = 0;

像之前一樣,我們進入迴圈來遍歷 dataArray 陣列中的資料。在每一次迴圈過程中,我們讓條形的高度 barHeight 等於陣列的數值,之後根據高度設定條形的填充色(條形越高,填充色越亮),然後在橫座標 x 處按照設定的寬度和高度的一半把條形畫出來(我們最後決定只畫高度的一半因為這樣條形看起來更美觀)。

需要多加解釋的一點是每個條形豎直方向的位置,我們在 HEIGHT-barHeight/2 的位置畫每一條,這是因為我想讓每個條形從底部向上伸出,而不是從頂部向下(如果我們把豎直位置設定為0它就會這樣畫)。所以,我們把豎直位置設定為畫布高度減去條形高度的一半,這樣每個條形就會從中間向下畫,直到畫布最底部。

for(var i = 0; i < bufferLength; i++) {
        barHeight = dataArray[i]/2;
        canvasCtx.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';
        canvasCtx.fillRect(x,HEIGHT-barHeight/2,barWidth,barHeight);
        x += barWidth + 1;
      }
    };

和剛才一樣,我們在最後呼叫 draw() 函式來開啟整個視覺化過程。

draw();

這些程式碼會帶來下面的效果:
在這裡插入圖片描述
原始碼:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title>視覺化音樂播放器</title>
</head>
<body>
<input type="file" name="" value="" id="musicFile">
<p id="tip"></p>
<canvas id="casvased" width="500" height="500"></canvas>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
//隨機變顏色
function randomRgbColor() { //隨機生成RGB顏色
 var r = Math.floor(Math.random() * 256); //隨機生成256以內r值
 var g = Math.floor(Math.random() * 256); //隨機生成256以內g值
 var b = Math.floor(Math.random() * 256); //隨機生成256以內b值
 return `rgb(${r},${g},${b})`; //返回rgb(r,g,b)格式顏色
}
//隨機數 0-255
function sum (m,n){
  var num = Math.floor(Math.random()*(m - n) + n);
   
}
console.log(sum(0,100));
console.log(sum(100,255));
//展示音訊視覺化
var canvas = document.getElementById("casvased");
var canvasCtx = canvas.getContext("2d");
//首先例項化AudioContext物件 很遺憾瀏覽器不相容,只能用相容性寫法;audioContext用於音訊處理的介面,並且工作原理是將AudioContext創建出來的各種節點(AudioNode)相互連線,音訊資料流經這些節點並作出相應處理。
//總結就一句話 AudioContext 是音訊物件,就像 new Date()是一個時間物件一樣
var AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext;
if (!AudioContext) {
  alert("您的瀏覽器不支援audio API,請更換瀏覽器(chrome、firefox)再嘗試,另外本人強烈建議使用谷歌瀏覽器!")
}
var audioContext = new AudioContext();//例項化
// 總結一下接下來的步驟
// 1 先獲取音訊檔案(目前只支援單個上傳)
// 2 讀取音訊檔案,讀取後,獲得二進位制型別的音訊檔案
// 3 對讀取後的二進位制檔案進行解碼
$('#musicFile').change(function(){
  if (this.files.length == 0) return;
  var file = $('#musicFile')[0].files[0];//通過input上傳的音訊檔案
  var fileReader = new FileReader();//使用FileReader非同步讀取檔案
  fileReader.readAsArrayBuffer(file);//開始讀取音訊檔案
  fileReader.onload = function(e) {//讀取檔案完成的回撥
    //e.target.result 即為讀取的音訊檔案(此檔案為二進位制檔案)
    //下面開始解碼操作 解碼需要一定時間,這個時間應該讓使用者感知到
    var count = 0;
    $('#tip').text('開始解碼')
    var timer = setInterval(function(){
      count++;
      $('#tip').text('解碼中,已用時'+count+'秒')
    },1000)
    //開始解碼,解碼成功後執行回撥函式
    audioContext.decodeAudioData(e.target.result, function(buffer) {
      clearInterval(timer)
      $('#tip').text('解碼成功,用時共計:'+count+'秒')
      // 建立AudioBufferSourceNode 用於播放解碼出來的buffer的節點
      var audioBufferSourceNode = audioContext.createBufferSource();
      // 建立AnalyserNode 用於分析音訊頻譜的節點
      var analyser = audioContext.createAnalyser();
      //fftSize (Fast Fourier Transform) 是快速傅立葉變換,一般情況下是固定值2048。具體作用是什麼我也不太清除,但是經過研究,這個值可以決定音訊頻譜的密集程度。值大了,頻譜就鬆散,值小就密集。
      analyser.fftSize = 256;
      // 連線節點,audioContext.destination是音訊要最終輸出的目標,
      // 我們可以把它理解為音效卡。所以所有節點中的最後一個節點應該再
      // 連線到audioContext.destination才能聽到聲音。
      audioBufferSourceNode.connect(analyser);
      analyser.connect(audioContext.destination);
      console.log(audioContext.destination)
      // 播放音訊
      audioBufferSourceNode.buffer = buffer; //回撥函式傳入的引數
      audioBufferSourceNode.start(); //部分瀏覽器是noteOn()函式,用法相同
      //視覺化 建立資料
      // var dataArray = new Uint8Array(analyser.fftSize);
      // analyser.getByteFrequencyData(dataArray)//將資料放入陣列,用來進行頻譜的視覺化繪製
      // console.log(analyser.getByteFrequencyData)
      var bufferLength = analyser.frequencyBinCount;
      console.log(bufferLength);
      var dataArray = new Uint8Array(bufferLength);
      console.log(dataArray)
      canvasCtx.clearRect(0, 0, 500, 500);
      function draw() {
        drawVisual = requestAnimationFrame(draw);
        analyser.getByteFrequencyData(dataArray);
        canvasCtx.fillStyle = 'rgb(0, 0, 0)';
		//canvasCtx.fillStyle = ;
        canvasCtx.fillRect(0, 0, 500, 500);
        var barWidth = (500 / bufferLength) * 2.5;
        var barHeight;
        var x = 0;
        for(var i = 0; i < bufferLength; i++) {
          barHeight = dataArray[i];
		  //隨機數0-255   Math.floor(Math.random()*255)  
		  // 隨機數  10*Math.random()
          canvasCtx.fillStyle = 'rgb(' + (barHeight+100) + ','+Math.floor(Math.random()*(20- 120) + 120)+','+Math.floor(Math.random()*(10 - 50) + 50)+')';
          canvasCtx.fillRect(x,500-barHeight/2,barWidth,barHeight/2);
          x += barWidth + 1;
        }
      };
      draw();
    });
  }
})
</script>
</html>

注意: 本文中的案例展現了 AnalyserNode.getByteFrequencyData() 和 AnalyserNode.getByteTimeDomainData() 的用法。如果想要檢視AnalyserNode.getFloatFrequencyData() 和 AnalyserNode.getFloatTimeDomainData() 的用法,請參考我們的 Voice-change-O-matic-float-data 演示(也能看到 原始碼 )——它和本文中出現的 Voice-change-O-matic 功能完全相同,唯一區別就是它使用的是浮點數作資料,而不是本文中的無符號整型數。

《參考:https://github.com/mdn/voice-change-o-matic
《案例一:https://mdn.github.io/voice-change-o-matic/
《案例二:http://margox.github.io/vudio.js/