使用 Web Audio API 進行視覺化

Web Audio API 最有趣的功能之一是能夠從音訊源中提取頻率、波形和其他資料,然後可以使用這些資料建立視覺化。本文將解釋如何做到這一點,並提供幾個基本用例。

注意:您可以在我們的 Voice-change-O-matic 演示中找到所有程式碼片段的工作示例。

基本概念

要從音訊源中提取資料,您需要一個 AnalyserNode,它是使用 BaseAudioContext.createAnalyser 方法建立的,例如:

js
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();

然後,該節點會連線到您的音訊源,位於您的源和目標之間的某個點,例如:

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

注意:只要輸入連線到源(直接或透過另一個節點),您就不需要將分析器的輸出連線到另一個節點即可使其工作。

然後,分析器節點將使用快速傅立葉變換 (fft) 在特定頻率域中捕獲音訊資料,具體取決於您為 AnalyserNode.fftSize 屬性指定的值(如果未指定值,則預設為 2048)。

注意:您還可以使用 AnalyserNode.minDecibelsAnalyserNode.maxDecibels 為 fft 資料縮放範圍指定最小和最大功率值,並使用 AnalyserNode.smoothingTimeConstant 指定不同的資料平均常量。請閱讀這些頁面以獲取有關如何使用它們的更多資訊。

要捕獲資料,您需要使用 AnalyserNode.getFloatFrequencyData()AnalyserNode.getByteFrequencyData() 方法來捕獲頻率資料,並使用 AnalyserNode.getByteTimeDomainData()AnalyserNode.getFloatTimeDomainData() 來捕獲波形資料。

這些方法會將資料複製到指定的陣列中,因此在呼叫其中一個方法之前,您需要建立一個新陣列來接收資料。第一個方法生成 32 位浮點數,第二個和第三個方法生成 8 位無符號整數,因此標準的 JavaScript 陣列不行——您需要根據您處理的資料使用 Float32ArrayUint8Array 陣列。

因此,例如,假設我們處理的 fft 大小為 2048。我們返回 AnalyserNode.frequencyBinCount 值,該值是 fft 的一半,然後使用 frequencyBinCount 作為其長度引數呼叫 Uint8Array()——這就是對於該 fft 大小,我們將收集多少資料點。

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

要實際檢索資料並將其複製到我們的陣列中,我們然後呼叫我們想要的資料收集方法,並將陣列作為其引數。例如:

js
analyser.getByteTimeDomainData(dataArray);

此時,我們已經捕獲了該時刻的音訊資料並將其儲存在我們的陣列中,然後可以根據需要對其進行視覺化,例如將其繪製到 HTML <canvas> 上。

讓我們繼續看一些具體的例子。

建立波形/示波器

為了建立示波器視覺化(感謝 Soledad PenadésVoice-change-O-matic 中提供的原始程式碼),我們首先按照上一節中描述的標準模式設定緩衝區:

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

接下來,我們清除之前在畫布上繪製的內容,為新的視覺化顯示做好準備。

js
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

現在我們定義 draw() 函式。在這裡,我們執行以下操作:

  1. 使用 requestAnimationFrame() 在繪圖函式啟動後持續迴圈。
  2. 獲取時域資料並將其複製到我們的陣列中。
  3. 用純色填充畫布以開始。
  4. 設定要繪製的波形的線寬和描邊顏色,然後開始繪製路徑。
  5. 透過將畫布寬度除以陣列長度(等於前面定義的 FrequencyBinCount)來確定要繪製的線的每個段的寬度,然後定義一個 x 變數來定義繪製每段線的移動位置。
  6. 現在我們透過一個迴圈,為緩衝區中的每個點定義波形小段的位置,其高度基於陣列中的資料點值,然後將線條移動到下一個波形段應繪製的位置。
  7. 最後,我們在畫布右側中間完成線條,然後繪製我們定義的描邊。
js
function draw() {
  const drawVisual = requestAnimationFrame(draw);
  analyser.getByteTimeDomainData(dataArray);
  // Fill solid color
  canvasCtx.fillStyle = "rgb(200 200 200)";
  canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
  // Begin the path
  canvasCtx.lineWidth = 2;
  canvasCtx.strokeStyle = "rgb(0 0 0)";
  canvasCtx.beginPath();
  // Draw each point in the waveform
  const sliceWidth = WIDTH / bufferLength;
  let x = 0;
  for (let i = 0; i < bufferLength; i++) {
    const v = dataArray[i] / 128.0;
    const y = v * (HEIGHT / 2);

    if (i === 0) {
      canvasCtx.moveTo(x, y);
    } else {
      canvasCtx.lineTo(x, y);
    }

    x += sliceWidth;
  }

  // Finish the line
  canvasCtx.lineTo(WIDTH, HEIGHT / 2);
  canvasCtx.stroke();
}

在此程式碼段的末尾,我們呼叫 draw() 函式來啟動整個過程。

js
draw();

這為我們提供了每秒更新幾次的良好波形顯示。

a black oscilloscope line, showing the waveform of an audio signal

建立頻率條形圖

另一個很棒的小聲音視覺化是建立那些類似 Winamp 的頻率條形圖。我們在 Voice-change-O-matic 中有一個可用的,讓我們看看它是如何實現的。

首先,我們再次設定分析器和資料陣列,然後使用 clearRect() 清除當前畫布顯示。與之前唯一的區別是我們將 fft 大小設定得更小;這是為了讓圖中的每個條形足夠大,看起來像一個條形而不是一條細線。

js
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);

canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

接下來,我們再次使用 requestAnimationFrame() 啟動我們的 draw() 函式,設定一個迴圈,以便顯示的資料持續更新,並在每個動畫幀中清除顯示。

js
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 變數來記錄要在畫布上繪製當前條形的距離。

js
function draw() {
  // ...
  const barWidth = (WIDTH / bufferLength) * 2.5;
  let barHeight;
  let x = 0;
  // ...
}

和以前一樣,我們現在開始一個 for 迴圈,遍歷 dataArray 中的每個值。對於每個值,我們將 barHeight 設定為等於陣列值,根據 barHeight 設定填充顏色(較高的條形更亮),並在距畫布 x 畫素處繪製一個條形,該條形寬 barWidth,高 barHeight / 2(我們最終決定將每個條形分成兩半,以便它們都能更好地適應畫布)。

需要解釋的一個值是繪製每個條形的垂直偏移位置:HEIGHT - barHeight / 2。我這樣做是因為我希望每個條形從畫布底部向上延伸,而不是從頂部向下延伸,就像如果我們設定垂直位置為 0 那樣。因此,我們每次都改為將垂直位置設定為畫布高度減去 barHeight / 2,這樣每個條形都會從畫布中間向下繪製到底部。

js
function draw() {
  // ...
  for (let 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() 函式來啟動整個過程。

js
draw();

這段程式碼的結果如下:

a series of red bars in a bar graph, showing intensity of different frequencies in an audio signal

注意:本文中的示例展示了 AnalyserNode.getByteFrequencyData()AnalyserNode.getByteTimeDomainData() 的用法。有關展示 AnalyserNode.getFloatFrequencyData()AnalyserNode.getFloatTimeDomainData() 的工作示例,請參考我們的 Voice-change-O-matic-float-data 演示——這與原始的 Voice-change-O-matic 完全相同,只是它使用了 Float 資料,而不是無符號位元組資料。有關詳細資訊,請參閱原始碼的 此部分