使用 Web Audio API 進行視覺化
Web Audio API 最有趣的功能之一是能夠從音訊源中提取頻率、波形和其他資料,然後可以使用這些資料建立視覺化。本文將解釋如何做到這一點,並提供幾個基本用例。
注意:您可以在我們的 Voice-change-O-matic 演示中找到所有程式碼片段的工作示例。
基本概念
要從音訊源中提取資料,您需要一個 AnalyserNode,它是使用 BaseAudioContext.createAnalyser 方法建立的,例如:
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();
然後,該節點會連線到您的音訊源,位於您的源和目標之間的某個點,例如:
const source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);
distortion.connect(audioCtx.destination);
注意:只要輸入連線到源(直接或透過另一個節點),您就不需要將分析器的輸出連線到另一個節點即可使其工作。
然後,分析器節點將使用快速傅立葉變換 (fft) 在特定頻率域中捕獲音訊資料,具體取決於您為 AnalyserNode.fftSize 屬性指定的值(如果未指定值,則預設為 2048)。
注意:您還可以使用 AnalyserNode.minDecibels 和 AnalyserNode.maxDecibels 為 fft 資料縮放範圍指定最小和最大功率值,並使用 AnalyserNode.smoothingTimeConstant 指定不同的資料平均常量。請閱讀這些頁面以獲取有關如何使用它們的更多資訊。
要捕獲資料,您需要使用 AnalyserNode.getFloatFrequencyData() 和 AnalyserNode.getByteFrequencyData() 方法來捕獲頻率資料,並使用 AnalyserNode.getByteTimeDomainData() 和 AnalyserNode.getFloatTimeDomainData() 來捕獲波形資料。
這些方法會將資料複製到指定的陣列中,因此在呼叫其中一個方法之前,您需要建立一個新陣列來接收資料。第一個方法生成 32 位浮點數,第二個和第三個方法生成 8 位無符號整數,因此標準的 JavaScript 陣列不行——您需要根據您處理的資料使用 Float32Array 或 Uint8Array 陣列。
因此,例如,假設我們處理的 fft 大小為 2048。我們返回 AnalyserNode.frequencyBinCount 值,該值是 fft 的一半,然後使用 frequencyBinCount 作為其長度引數呼叫 Uint8Array()——這就是對於該 fft 大小,我們將收集多少資料點。
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
要實際檢索資料並將其複製到我們的陣列中,我們然後呼叫我們想要的資料收集方法,並將陣列作為其引數。例如:
analyser.getByteTimeDomainData(dataArray);
此時,我們已經捕獲了該時刻的音訊資料並將其儲存在我們的陣列中,然後可以根據需要對其進行視覺化,例如將其繪製到 HTML <canvas> 上。
讓我們繼續看一些具體的例子。
建立波形/示波器
為了建立示波器視覺化(感謝 Soledad Penadés 在 Voice-change-O-matic 中提供的原始程式碼),我們首先按照上一節中描述的標準模式設定緩衝區:
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
接下來,我們清除之前在畫布上繪製的內容,為新的視覺化顯示做好準備。
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
現在我們定義 draw() 函式。在這裡,我們執行以下操作:
- 使用
requestAnimationFrame()在繪圖函式啟動後持續迴圈。 - 獲取時域資料並將其複製到我們的陣列中。
- 用純色填充畫布以開始。
- 設定要繪製的波形的線寬和描邊顏色,然後開始繪製路徑。
- 透過將畫布寬度除以陣列長度(等於前面定義的 FrequencyBinCount)來確定要繪製的線的每個段的寬度,然後定義一個 x 變數來定義繪製每段線的移動位置。
- 現在我們透過一個迴圈,為緩衝區中的每個點定義波形小段的位置,其高度基於陣列中的資料點值,然後將線條移動到下一個波形段應繪製的位置。
- 最後,我們在畫布右側中間完成線條,然後繪製我們定義的描邊。
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() 函式來啟動整個過程。
draw();
這為我們提供了每秒更新幾次的良好波形顯示。

建立頻率條形圖
另一個很棒的小聲音視覺化是建立那些類似 Winamp 的頻率條形圖。我們在 Voice-change-O-matic 中有一個可用的,讓我們看看它是如何實現的。
首先,我們再次設定分析器和資料陣列,然後使用 clearRect() 清除當前畫布顯示。與之前唯一的區別是我們將 fft 大小設定得更小;這是為了讓圖中的每個條形足夠大,看起來像一個條形而不是一條細線。
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
接下來,我們再次使用 requestAnimationFrame() 啟動我們的 draw() 函式,設定一個迴圈,以便顯示的資料持續更新,並在每個動畫幀中清除顯示。
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 變數來記錄要在畫布上繪製當前條形的距離。
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,這樣每個條形都會從畫布中間向下繪製到底部。
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() 函式來啟動整個過程。
draw();
這段程式碼的結果如下:

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